All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.threewks.thundr.gae.objectify.repository.AbstractRepository Maven / Gradle / Ivy

The newest version!
/*
 * This file is a component of thundr, a software library from 3wks.
 * Read more: http://3wks.github.io/thundr/
 * Copyright (C) 2015 3wks, 
 *
 * 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 org.apache.commons.lang3.reflect.FieldUtils;

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.ObjectifyService;
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 implements AsyncRepository {
	protected IdGaeSearchService> searchService;
	protected Class entityType;
	protected Field idField;
	protected boolean isSearchable;
	protected ETransformer, Map, E>> toKeyLookup;
	protected ETransformer> toOfyKey;
	protected CollectionTransformer> toOfyKeys;
	protected ETransformer toId;
	protected CollectionTransformer toIds;
	protected ETransformer> toKey;
	protected CollectionTransformer> toKeys;
	protected ETransformer> toKeyFromEntity;
	protected CollectionTransformer> toKeysFromEntities;
	protected ETransformer, K> fromKey;
	protected CollectionTransformer, K> fromKeys;

	public AbstractRepository(Class entityType, ETransformer> toKey, ETransformer, K> fromKey, SearchConfig searchConfig) {
		this.entityType = entityType;
		this.searchService = createIdGaeSearchService(searchConfig);
		this.isSearchable = searchService != null && searchService.hasIndexableFields();
		this.idField = idField(entityType);
		this.toKey = toKey;
		this.toKeys = new CollectionTransformer>(toKey);
		this.fromKey = fromKey;
		this.fromKeys = new CollectionTransformer, K>(fromKey);

		this.toId = new ETransformer() {
			@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, Map, E>>() {
			@Override
			public Map, E> from(Iterable from) {
				Map, E> lookup = new LinkedHashMap, E>();
				for (E e : from) {
					lookup.put(key(e), e);
				}
				return lookup;
			}
		};
		this.toKeyFromEntity = new ETransformer>() {
			@Override
			public Key from(E from) {
				return Key.create(from);
			}
		};
		this.toKeysFromEntities = Expressive.Transformers.transformAllUsing(toKeyFromEntity);
	}

	@Override
	public E put(E entity) {
		return putAsync(entity).complete();
	}

	@Override
	public AsyncResult putAsync(final E entity) {
		boolean hasId = hasId(entity);
		final Result> 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() {
			@Override
			public E complete() {
				ofyFuture.now();
				if (searchFuture != null) {
					searchFuture.complete();
				}
				return entity;
			}
		};
	}

	@Override
	public List put(@SuppressWarnings("unchecked") E... entities) {
		return putAsync(entities).complete();
	}

	@Override
	@SuppressWarnings("unchecked")
	public AsyncResult> putAsync(E... entities) {
		return putAsync(Arrays.asList(entities));
	}

	@Override
	public List put(List entities) {
		return putAsync(entities).complete();
	}

	@Override
	public AsyncResult> putAsync(final List entities) {
		List ids = toIds.from(entities);
		final Result, 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>() {
			@Override
			public List complete() {
				ofyFuture.now();
				if (searchFuture != null) {
					searchFuture.complete();
				}
				return entities;
			}
		};
	}

	protected E loadInternal(Key keys) {
		return ofy().load().key(keys).now();
	}

	protected List loadInternal(Iterable> keys) {
		if (Expressive.isEmpty(keys)) {
			return Collections. emptyList();
		}
		Map, E> results = ofy().load().keys(keys);
		return Expressive.Transformers.transformAllUsing(Expressive.Transformers.usingLookup(results)).from(keys);
	}

	@Override
	public E get(K key) {
		Key ofyKey = toKey.from(key);
		return loadInternal(ofyKey);
	}

	@Override
	public List get(Iterable keys) {
		EList> ofyKeys = toKeys.from(keys);
		return loadInternal(ofyKeys);
	}

	@SuppressWarnings("unchecked")
	@Override
	public List get(K... keys) {
		return get(Arrays.asList(keys));
	}

	@Override
	public List list(int count) {
		return ofy().load().type(entityType).limit(count).list();
	}

	@Override
	public List getByField(String field, Object value) {
		return ofy().load().type(entityType).filter(field, value).list();
	}

	@Override
	public List getByField(String field, List values) {
		return ofy().load().type(entityType).filter(field + " in", values).list();
	}

	@Override
	public void deleteByKey(K key) {
		deleteByKeyAsync(key).complete();
	}

	@Override
	public AsyncResult deleteByKeyAsync(K key) {
		Key ofyKey = toKey.from(key);
		final Result ofyDelete = deleteInternal(ofyKey);
		final IndexOperation searchDelete = shouldSearch() ? searchService.removeById(ofyKey) : null;
		return new AsyncResult() {
			@Override
			public Void complete() {
				ofyDelete.now();
				if (searchDelete != null) {
					searchDelete.complete();
				}
				return null;
			}
		};
	}

	@SuppressWarnings("unchecked")
	@Override
	public void deleteByKey(K... keys) {
		deleteByKeyAsync(keys).complete();
	}

	@SuppressWarnings("unchecked")
	@Override
	public AsyncResult deleteByKeyAsync(K... keys) {
		return deleteByKeyAsync(Arrays.asList(keys));
	}

	@Override
	public void deleteByKey(Iterable ids) {
		deleteByKeyAsync(ids).complete();
	}

	@Override
	public AsyncResult deleteByKeyAsync(Iterable keys) {
		EList> ofyKeys = toKeys.from(keys);
		final Result ofyDelete = deleteInternal(ofyKeys);
		final IndexOperation searchDelete = shouldSearch() ? searchService.removeById(ofyKeys) : null;
		return new AsyncResult() {
			@Override
			public Void complete() {
				ofyDelete.now();
				if (searchDelete != null) {
					searchDelete.complete();
				}
				return null;
			}
		};
	}

	@Override
	public void delete(E entity) {
		deleteAsync(entity).complete();
	}

	@Override
	public AsyncResult deleteAsync(E e) {
		final Result ofyDelete = ofy().delete().entity(e);
		final IndexOperation searchDelete = shouldSearch() ? searchService.removeById(Key.create(e)) : null;
		return new AsyncResult() {
			@Override
			public Void complete() {
				ofyDelete.now();
				if (searchDelete != null) {
					searchDelete.complete();
				}
				return null;
			}
		};
	}

	@Override
	public void delete(Iterable entities) {
		deleteAsync(entities).complete();
	}

	@Override
	public AsyncResult deleteAsync(Iterable entities) {
		final Result ofyDelete = ofy().delete().entities(entities);
		final IndexOperation searchDelete = shouldSearch() ? searchService.removeById(toKeysFromEntities.from(entities)) : null;
		return new AsyncResult() {
			@Override
			public Void complete() {
				ofyDelete.now();
				if (searchDelete != null) {
					searchDelete.complete();
				}
				return null;
			}
		};
	}

	@SuppressWarnings("unchecked")
	@Override
	public void delete(E... entities) {
		deleteAsync(entities).complete();

	}

	@SuppressWarnings("unchecked")
	@Override
	public AsyncResult deleteAsync(E... entities) {
		return deleteAsync(Arrays.asList(entities));
	}

	@Override
	public Search 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(this, searchService.search());
	}

	@Override
	public int reindex(List keys, int batchSize, ReindexOperation reindexOperation) {
		int count = 0;
		List> batches = Lists.partition(keys, batchSize);
		for (List batchKeys : batches) {
			List batch = get(batchKeys);
			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();
			ofy().clear(); // Clear the Objectify cache to free memory for next batch
			Logger.info("Reindexed %d entities of type %s, %d of %d", batch.size(), entityType.getSimpleName(), count, keys.size());
		}
		return count;
	}

	protected IndexOperation index(final E entity) {
		return searchService.index(entity, key(entity));
	}

	protected IndexOperation index(List batch) {
		Map, E> keyedLookup = toKeyLookup.from(batch);
		return searchService.index(keyedLookup);
	}

	protected Result deleteInternal(Key key) {
		return ofy().delete().key(key);
	}

	protected Result deleteInternal(Iterable> keys) {
		return ofy().delete().keys(keys);
	}

	protected Key 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 entityType) {
		try {
			String idFieldName = ObjectifyService.factory().getMetadata(entityType).getKeyMetadata().getIdFieldName();
			return FieldUtils.getField(entityType, idFieldName, true);
		} catch (IllegalArgumentException | SecurityException e) {
			throw new RepositoryException(e, "Unable to determine id field for type %s: %s", entityType.getClass().getName(), e.getMessage());
		}
	}

	public SearchExecutor> getSearchExecutor() {
		return this.searchExecutor;
	}

	protected SearchExecutor> searchExecutor = new SearchExecutor>() {
		@Override
		public List getResultsAsIds(List results) {
			return fromKeys.from(searchService.getResultsAsIds(results));
		}

		@Override
		public List getResults(java.util.List results) {
			return loadInternal(searchService.getResultsAsIds(results));
		};

		@Override
		public com.threewks.thundr.search.Result createSearchResult(SearchImpl searchRequest) {
			Search> delegate = searchRequest.getSearchRequest();
			final com.threewks.thundr.search.Result> delegateResults = delegate.run();
			return new com.threewks.thundr.search.Result() {
				@Override
				public List getResults() throws SearchException {
					return loadInternal(delegateResults.getResultIds());
				}

				@Override
				public List 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  Class> keyClass() {
		Class keyClass = Key.class;
		return (Class>) keyClass;
	}

	/**
	 * Extension point allowing the IdGaeSearchService implementation to be modified
	 */
	protected IdGaeSearchService> createIdGaeSearchService(SearchConfig searchConfig) {
		return searchConfig == null ? null : new IdGaeSearchService>(entityType, AbstractRepository. keyClass(), searchConfig);
	}

}