Java tutorial
/* * This file is a component of thundr, a software library from 3wks. * Read more: http://3wks.github.io/thundr/ * Copyright (C) 2014 3wks, <thundr@3wks.com.au> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.threewks.thundr.gae.objectify.repository; import static com.googlecode.objectify.ObjectifyService.ofy; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.atomicleopard.expressive.EList; import com.atomicleopard.expressive.ETransformer; import com.atomicleopard.expressive.Expressive; import com.atomicleopard.expressive.transform.CollectionTransformer; import com.google.appengine.api.search.ScoredDocument; import com.google.common.collect.Lists; import com.googlecode.objectify.Key; import com.googlecode.objectify.Result; import com.threewks.thundr.exception.BaseException; import com.threewks.thundr.logger.Logger; import com.threewks.thundr.search.IndexOperation; import com.threewks.thundr.search.Search; import com.threewks.thundr.search.SearchException; import com.threewks.thundr.search.gae.IdGaeSearchService; import com.threewks.thundr.search.gae.SearchConfig; import com.threewks.thundr.search.gae.SearchExecutor; public abstract class AbstractRepository<E, K> implements Repository<E, K> { protected IdGaeSearchService<E, Key<E>> searchService; protected Class<E> entityType; protected List<String> fieldsToIndex; protected Field idField; protected boolean isSearchable; protected ETransformer<Iterable<E>, Map<Key<E>, E>> toKeyLookup; protected ETransformer<com.google.appengine.api.datastore.Key, Key<E>> toOfyKey; protected CollectionTransformer<com.google.appengine.api.datastore.Key, Key<E>> toOfyKeys; protected ETransformer<E, Object> toId; protected CollectionTransformer<E, Object> toIds; protected ETransformer<K, Key<E>> toKey; protected CollectionTransformer<K, Key<E>> toKeys; protected ETransformer<E, Key<E>> toKeyFromEntity; protected CollectionTransformer<E, Key<E>> toKeysFromEntities; protected ETransformer<Key<E>, K> fromKey; protected CollectionTransformer<Key<E>, K> fromKeys; public AbstractRepository(Class<E> entityType, ETransformer<K, Key<E>> toKey, ETransformer<Key<E>, K> fromKey, SearchConfig searchConfig) { this.searchService = searchConfig == null ? null : new IdGaeSearchService<E, Key<E>>(entityType, AbstractRepository.<E>keyClass(), searchConfig); this.isSearchable = searchService != null && searchService.hasIndexableFields(); this.entityType = entityType; this.idField = idField(entityType); this.toKey = toKey; this.toKeys = new CollectionTransformer<K, Key<E>>(toKey); this.fromKey = fromKey; this.fromKeys = new CollectionTransformer<Key<E>, K>(fromKey); this.toId = new ETransformer<E, Object>() { @Override public Object from(E from) { try { return idField.get(from); } catch (IllegalArgumentException | IllegalAccessException e) { throw new RepositoryException(e, "Unable to access '%s.%s' - cannot extract an id: %s", AbstractRepository.this.entityType.getSimpleName(), idField.getName(), e.getMessage()); } } }; this.toIds = Expressive.Transformers.transformAllUsing(toId); this.toKeyLookup = new ETransformer<Iterable<E>, Map<Key<E>, E>>() { @Override public Map<Key<E>, E> from(Iterable<E> from) { Map<Key<E>, E> lookup = new LinkedHashMap<Key<E>, E>(); for (E e : from) { lookup.put(key(e), e); } return lookup; } }; this.toKeyFromEntity = new ETransformer<E, Key<E>>() { @Override public Key<E> from(E from) { return Key.create(from); } }; this.toKeysFromEntities = Expressive.Transformers.transformAllUsing(toKeyFromEntity); } @Override public AsyncResult<E> save(final E entity) { boolean hasId = hasId(entity); final Result<Key<E>> ofyFuture = ofy().save().entity(entity); if (!hasId) { // if no id exists - we need objectify to complete so that the id can be used in indexing the record. ofyFuture.now(); } final IndexOperation searchFuture = shouldSearch() ? index(entity) : null; return new AsyncResult<E>() { @Override public E complete() { ofyFuture.now(); if (searchFuture != null) { searchFuture.complete(); } return entity; } }; } @Override @SuppressWarnings("unchecked") public AsyncResult<List<E>> save(E... entities) { return save(Arrays.asList(entities)); } @Override public AsyncResult<List<E>> save(final List<E> entities) { List<Object> ids = toIds.from(entities); final Result<Map<Key<E>, E>> ofyFuture = ofy().save().entities(entities); if (ids.contains(null)) { ofyFuture.now(); // force sync save } final IndexOperation searchFuture = shouldSearch() ? index(entities) : null; return new AsyncResult<List<E>>() { @Override public List<E> complete() { ofyFuture.now(); if (searchFuture != null) { searchFuture.complete(); } return entities; } }; } protected E loadInternal(Key<E> keys) { return ofy().load().key(keys).now(); } protected List<E> loadInternal(Iterable<Key<E>> keys) { if (Expressive.isEmpty(keys)) { return Collections.<E>emptyList(); } Map<Key<E>, E> results = ofy().load().keys(keys); return Expressive.Transformers.transformAllUsing(Expressive.Transformers.usingLookup(results)).from(keys); } @Override public E load(K key) { Key<E> ofyKey = toKey.from(key); return loadInternal(ofyKey); } @Override public List<E> load(Iterable<K> keys) { EList<Key<E>> ofyKeys = toKeys.from(keys); return loadInternal(ofyKeys); } @SuppressWarnings("unchecked") @Override public List<E> load(K... keys) { return load(Arrays.asList(keys)); } @Override public List<E> list(int count) { return ofy().load().type(entityType).limit(count).list(); } @Override public List<E> loadByField(String field, Object value) { return ofy().load().type(entityType).filter(field, value).list(); } @Override public List<E> loadByField(String field, List<Object> values) { return ofy().load().type(entityType).filter(field + " in", values).list(); } @Override public AsyncResult<Void> deleteByKey(K key) { Key<E> ofyKey = toKey.from(key); final Result<Void> ofyDelete = deleteInternal(ofyKey); final IndexOperation searchDelete = shouldSearch() ? searchService.removeById(ofyKey) : null; return new AsyncResult<Void>() { @Override public Void complete() { ofyDelete.now(); if (searchDelete != null) { searchDelete.complete(); } return null; } }; } @SuppressWarnings("unchecked") @Override public AsyncResult<Void> deleteByKey(K... keys) { return deleteByKey(Arrays.asList(keys)); } @Override public AsyncResult<Void> deleteByKey(Iterable<K> keys) { EList<Key<E>> ofyKeys = toKeys.from(keys); final Result<Void> ofyDelete = deleteInternal(ofyKeys); final IndexOperation searchDelete = shouldSearch() ? searchService.removeById(ofyKeys) : null; return new AsyncResult<Void>() { @Override public Void complete() { ofyDelete.now(); if (searchDelete != null) { searchDelete.complete(); } return null; } }; } @Override public AsyncResult<Void> delete(E e) { final Result<Void> ofyDelete = ofy().delete().entity(e); final IndexOperation searchDelete = shouldSearch() ? searchService.removeById(Key.create(e)) : null; return new AsyncResult<Void>() { @Override public Void complete() { ofyDelete.now(); if (searchDelete != null) { searchDelete.complete(); } return null; } }; } @Override public AsyncResult<Void> delete(Iterable<E> entities) { final Result<Void> ofyDelete = ofy().delete().entities(entities); final IndexOperation searchDelete = shouldSearch() ? searchService.removeById(toKeysFromEntities.from(entities)) : null; return new AsyncResult<Void>() { @Override public Void complete() { ofyDelete.now(); if (searchDelete != null) { searchDelete.complete(); } return null; } }; } @SuppressWarnings("unchecked") @Override public AsyncResult<Void> delete(E... entities) { return delete(Arrays.asList(entities)); } @Override public Search<E, K> search() { if (!isSearchable) { throw new BaseException( "Unable to search on type %s - there is no search service available to this repository", entityType.getSimpleName()); } return new SearchImpl<E, K>(this, searchService.search()); } /** * Reindexes all the entities matching the given search operation. The given {@link ReindexOperation}, if present will be applied to each batch of entities. * * @param search * @param batchSize * @param reindexOperation * @return the overall count of re-indexed entities. */ @Override public int reindex(Search<E, K> search, int batchSize, ReindexOperation<E> reindexOperation) { int count = 0; List<E> results = search.run().getResults(); List<List<E>> batches = Lists.partition(results, batchSize); for (List<E> batch : batches) { batch = reindexOperation == null ? batch : reindexOperation.apply(batch); if (reindexOperation != null) { // we only re-save the batch when a re-index op is supplied, otherwise the data can't have changed. ofy().save().entities(batch).now(); } if (shouldSearch()) { index(batch).complete(); } count += batch.size(); Logger.info("Reindexed %d entities of type %s, %d of %d", batch.size(), entityType.getSimpleName(), count, results.size()); } return count; } protected IndexOperation index(final E entity) { return searchService.index(entity, key(entity)); } protected IndexOperation index(List<E> batch) { Map<Key<E>, E> keyedLookup = toKeyLookup.from(batch); return searchService.index(keyedLookup); } protected Result<Void> deleteInternal(Key<E> key) { return ofy().delete().key(key); } protected Result<Void> deleteInternal(Iterable<Key<E>> keys) { return ofy().delete().keys(keys); } protected Key<E> key(E entity) { return Key.create(entity); } protected boolean hasId(E entity) { try { return idField.get(entity) != null; } catch (IllegalArgumentException | IllegalAccessException e) { throw new RepositoryException(e, "Unable to determine if an id exists for a %s - %s: %s", entityType.getSimpleName(), entity, e.getMessage()); } } protected boolean shouldSearch() { return isSearchable; } protected Field idField(Class<E> entityType) { try { String idFieldName = ofy().factory().getMetadata(entityType).getKeyMetadata().getIdFieldName(); Field idField = entityType.getDeclaredField(idFieldName); idField.setAccessible(true); return idField; } catch (NoSuchFieldException | SecurityException e) { throw new RepositoryException(e, "Unable to determine id field for type %s: %s", entityType.getClass().getName(), e.getMessage()); } } public SearchExecutor<E, K, SearchImpl<E, K>> getSearchExecutor() { return this.searchExecutor; } protected SearchExecutor<E, K, SearchImpl<E, K>> searchExecutor = new SearchExecutor<E, K, SearchImpl<E, K>>() { @Override public List<K> getResultsAsIds(List<ScoredDocument> results) { return fromKeys.from(searchService.getResultsAsIds(results)); } @Override public List<E> getResults(java.util.List<ScoredDocument> results) { return loadInternal(searchService.getResultsAsIds(results)); }; @Override public com.threewks.thundr.search.Result<E, K> createSearchResult(SearchImpl<E, K> searchRequest) { Search<E, Key<E>> delegate = searchRequest.getSearchRequest(); final com.threewks.thundr.search.Result<E, Key<E>> delegateResults = delegate.run(); return new com.threewks.thundr.search.Result<E, K>() { @Override public List<E> getResults() throws SearchException { return loadInternal(delegateResults.getResultIds()); } @Override public List<K> getResultIds() throws SearchException { return fromKeys.from(delegateResults.getResultIds()); } @Override public long getMatchingRecordCount() { return delegateResults.getMatchingRecordCount(); } @Override public long getReturnedRecordCount() { return delegateResults.getReturnedRecordCount(); } @Override public String cursor() { return delegateResults.cursor(); } }; } }; @SuppressWarnings({ "unchecked", "rawtypes" }) private static <T> Class<Key<T>> keyClass() { Class keyClass = Key.class; return (Class<Key<T>>) keyClass; } }