com.threewks.thundr.gae.objectify.repository.AbstractRepository.java Source code

Java tutorial

Introduction

Here is the source code for com.threewks.thundr.gae.objectify.repository.AbstractRepository.java

Source

/*
 * 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;
    }
}