/* * Copyright 2016 Sai Pullabhotla. * * 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.jmethods.catatumbo.impl; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumMap; import java.util.List; import java.util.Map; import com.google.cloud.datastore.Datastore; import com.google.cloud.datastore.DatastoreException; import com.google.cloud.datastore.GqlQuery; import com.google.cloud.datastore.IncompleteKey; import com.google.cloud.datastore.Key; import com.google.cloud.datastore.KeyFactory; import com.google.cloud.datastore.Query; import com.google.cloud.datastore.QueryResults; import com.jmethods.catatumbo.DatastoreBatch; import com.jmethods.catatumbo.DatastoreKey; import com.jmethods.catatumbo.DatastoreMetadata; import com.jmethods.catatumbo.DatastoreStats; import com.jmethods.catatumbo.DatastoreTransaction; import com.jmethods.catatumbo.EntityManager; import com.jmethods.catatumbo.EntityManagerException; import com.jmethods.catatumbo.EntityQueryRequest; import com.jmethods.catatumbo.KeyQueryRequest; import com.jmethods.catatumbo.ProjectionQueryRequest; import com.jmethods.catatumbo.QueryResponse; import com.jmethods.catatumbo.Tenant; import com.jmethods.catatumbo.TransactionMode; import com.jmethods.catatumbo.TransactionalTask; import com.jmethods.catatumbo.Utility; import com.jmethods.catatumbo.impl.IdentifierMetadata.DataType; /** * Default implementation of {@link EntityManager} interface. Manages entities in the Cloud * Datastore such as inserting entities, updating, deleting, retrieving, etc. In addition to the * standard CRUD operations, the EntityManager allows running GQL queries to retrieve multiple * entities that match the specified criteria. * * @author Sai Pullabhotla */ public class DefaultEntityManager implements EntityManager { /** * Batch size for sending delete requests when using the deleteAll method */ private static final int DEFAULT_DELETE_ALL_BATCH_SIZE = 100; /** * Reference to the native Datastore object */ private Datastore datastore; /** * Datastore writer */ private DefaultDatastoreWriter writer; /** * Datastore reader */ private DefaultDatastoreReader reader; /** * Metadata of global callbacks */ private Map<CallbackType, List<CallbackMetadata>> globalCallbacks; /** * Creates a new instance of <code>DefaultEntityManager</code>. * * @param datastore * the Datastore object */ public DefaultEntityManager(Datastore datastore) { this.datastore = datastore; writer = new DefaultDatastoreWriter(this); reader = new DefaultDatastoreReader(this); } /** * Returns the underlying Datastore object. * * @return the underlying Datastore object. */ public Datastore getDatastore() { return datastore; } @Override public <E> long deleteAll(Class<E> entityClass) { EntityMetadata entityMetadata = EntityIntrospector.introspect(entityClass); return deleteAll(entityMetadata.getKind()); } @Override public long deleteAll(String kind) { if (Utility.isNullOrEmpty(kind)) { throw new IllegalArgumentException("kind cannot be null or blank"); } String query = "SELECT __key__ FROM " + kind; try { GqlQuery<Key> gqlQuery = Query.newGqlQueryBuilder(Query.ResultType.KEY, query) .setNamespace(getEffectiveNamespace()).build(); QueryResults<Key> keys = datastore.run(gqlQuery); Key[] nativeKeys = new Key[DEFAULT_DELETE_ALL_BATCH_SIZE]; long deleteCount = 0; int i = 0; while (keys.hasNext()) { nativeKeys[i++] = keys.next(); if (i % DEFAULT_DELETE_ALL_BATCH_SIZE == 0) { datastore.delete(nativeKeys); deleteCount += DEFAULT_DELETE_ALL_BATCH_SIZE; i = 0; } } if (i > 0) { datastore.delete(Arrays.copyOfRange(nativeKeys, 0, i)); deleteCount += i; } return deleteCount; } catch (DatastoreException exp) { throw new EntityManagerException(exp); } } @Override public DefaultDatastoreTransaction newTransaction() { return newTransaction(TransactionMode.READ_WRITE); } @Override public DefaultDatastoreTransaction newTransaction(TransactionMode transactionMode) { if (transactionMode == null) { throw new IllegalArgumentException("transactionMode cannot be null"); } return new DefaultDatastoreTransaction(this, transactionMode); } @Override public DatastoreBatch newBatch() { return new DefaultDatastoreBatch(this); } @Override public <T> T executeInTransaction(TransactionalTask<T> task) { return executeInTransaction(task, TransactionMode.READ_WRITE); } @Override public <T> T executeInTransaction(TransactionalTask<T> task, TransactionMode transactionMode) { if (transactionMode == null) { throw new IllegalArgumentException("transactionMode cannot be null"); } DatastoreTransaction transaction = null; try { transaction = new DefaultDatastoreTransaction(this, transactionMode); T returnValue = task.execute(transaction); transaction.commit(); return returnValue; } catch (Exception exp) { if (transaction != null) { transaction.rollback(); } throw new EntityManagerException(exp); } finally { if (transaction != null && transaction.isActive()) { transaction.rollback(); } } } @Override public <E> E insert(E entity) { return writer.insert(entity); } @Override public <E> List<E> insert(List<E> entities) { return writer.insert(entities); } @Override public <E> E update(E entity) { return writer.updateWithOptimisticLock(entity); } @Override public <E> List<E> update(List<E> entities) { return writer.updateWithOptimisticLock(entities); } @Override public <E> E upsert(E entity) { return writer.upsert(entity); } @Override public <E> List<E> upsert(List<E> entities) { return writer.upsert(entities); } @Override public void delete(Object entity) { writer.delete(entity); } @Override public void delete(List<?> entities) { writer.delete(entities); } @Override public <E> void delete(Class<E> entityClass, long id) { writer.delete(entityClass, id); } @Override public <E> void delete(Class<E> entityClass, String id) { writer.delete(entityClass, id); } @Override public <E> void delete(Class<E> entityClass, DatastoreKey parentKey, long id) { writer.delete(entityClass, parentKey, id); } @Override public <E> void delete(Class<E> entityClass, DatastoreKey parentKey, String id) { writer.delete(entityClass, parentKey, id); } @Override public void deleteByKey(DatastoreKey key) { writer.deleteByKey(key); } @Override public void deleteByKey(List<DatastoreKey> keys) { writer.deleteByKey(keys); } @Override public <E> E load(Class<E> entityClass, long id) { return reader.load(entityClass, id); } @Override public <E> E load(Class<E> entityClass, String id) { return reader.load(entityClass, id); } @Override public <E> E load(Class<E> entityClass, DatastoreKey parentKey, long id) { return reader.load(entityClass, parentKey, id); } @Override public <E> E load(Class<E> entityClass, DatastoreKey parentKey, String id) { return reader.load(entityClass, parentKey, id); } @Override public <E> E load(Class<E> entityClass, DatastoreKey key) { return reader.load(entityClass, key); } @Override public <E> List<E> loadById(Class<E> entityClass, List<Long> identifiers) { return reader.loadById(entityClass, identifiers); } @Override public <E> List<E> loadByName(Class<E> entityClass, List<String> identifiers) { return reader.loadByName(entityClass, identifiers); } @Override public <E> List<E> loadByKey(Class<E> entityClass, List<DatastoreKey> keys) { return reader.loadByKey(entityClass, keys); } @Override public EntityQueryRequest createEntityQueryRequest(String query) { return reader.createEntityQueryRequest(query); } @Override public ProjectionQueryRequest createProjectionQueryRequest(String query) { return reader.createProjectionQueryRequest(query); } @Override public KeyQueryRequest createKeyQueryRequest(String query) { return reader.createKeyQueryRequest(query); } @Override public <E> QueryResponse<E> executeEntityQueryRequest(Class<E> expectedResultType, EntityQueryRequest request) { return reader.executeEntityQueryRequest(expectedResultType, request); } @Override public <E> QueryResponse<E> executeProjectionQueryRequest(Class<E> expectedResultType, ProjectionQueryRequest request) { return reader.executeProjectionQueryRequest(expectedResultType, request); } @Override public QueryResponse<DatastoreKey> executeKeyQueryRequest(KeyQueryRequest request) { return reader.executeKeyQueryRequest(request); } @Override public DatastoreMetadata getDatastoreMetadata() { return new DefaultDatastoreMetadata(this); } @Override public DatastoreStats getDatastoreStats() { return new DefaultDatastoreStats(this); } @Override public DatastoreKey allocateId(Object entity) { List<DatastoreKey> keys = allocateId(Arrays.asList(new Object[] { entity })); return keys.get(0); } @Override public List<DatastoreKey> allocateId(List<Object> entities) { for (Object entity : entities) { IdentifierMetadata identifierMetadata = EntityIntrospector.getIdentifierMetadata(entity); if (DataType.STRING == identifierMetadata.getDataType()) { throw new IllegalArgumentException( "ID allocation is only valid for entities with numeric identifiers"); } Object id = IntrospectionUtils.getFieldValue(identifierMetadata, entity); if (!(id == null || ((long) id) == 0)) { throw new IllegalArgumentException( "ID allocation is only valid for entities with a null or zero ID"); } } IncompleteKey[] incompleteKeys = new IncompleteKey[entities.size()]; int i = 0; for (Object entity : entities) { incompleteKeys[i++] = getIncompleteKey(entity); } List<Key> nativeKeys = datastore.allocateId(incompleteKeys); return DatastoreUtils.toDatastoreKeys(nativeKeys); } /** * Returns an IncompleteKey of the given entity. * * @param entity * the entity * @return the incomplete key */ private IncompleteKey getIncompleteKey(Object entity) { EntityMetadata entityMetadata = EntityIntrospector.introspect(entity.getClass()); String kind = entityMetadata.getKind(); ParentKeyMetadata parentKeyMetadata = entityMetadata.getParentKeyMetadata(); DatastoreKey parentKey = null; IncompleteKey incompleteKey = null; if (parentKeyMetadata != null) { parentKey = (DatastoreKey) IntrospectionUtils.getFieldValue(parentKeyMetadata, entity); } if (parentKey != null) { incompleteKey = IncompleteKey.newBuilder(parentKey.nativeKey(), kind).build(); } else { incompleteKey = IncompleteKey.newBuilder(datastore.getOptions().getProjectId(), kind) .setNamespace(getEffectiveNamespace()).build(); } return incompleteKey; } @Override public void setDefaultListeners(Class<?>... entityListeners) { globalCallbacks = new EnumMap<>(CallbackType.class); for (Class<?> listenerClass : entityListeners) { ExternalListenerMetadata listenerMetadata = ExternalListenerIntrospector .introspect(listenerClass); Map<CallbackType, Method> callbacks = listenerMetadata.getCallbacks(); if (callbacks != null) { for (Map.Entry<CallbackType, Method> entry : callbacks.entrySet()) { CallbackType callbackType = entry.getKey(); Method callbackMethod = entry.getValue(); CallbackMetadata callbackMetadata = new CallbackMetadata(EntityListenerType.DEFAULT, callbackType, callbackMethod); putDefaultCallback(callbackType, callbackMetadata); } } } } /** * Puts/adds the given callback type and its metadata to the list of default listeners. * * @param callbackType * the event type * @param metadata * the callback metadata */ private void putDefaultCallback(CallbackType callbackType, CallbackMetadata metadata) { List<CallbackMetadata> metadataList = globalCallbacks.get(callbackType); if (metadataList == null) { metadataList = new ArrayList<>(); globalCallbacks.put(callbackType, metadataList); } metadataList.add(metadata); } /** * Executes the entity listeners associated with the given entity. * * @param callbackType * the event type * @param entity * the entity that produced the event */ public void executeEntityListeners(CallbackType callbackType, Object entity) { // We may get null entities here. For example loading a nonexistent ID // or IDs. if (entity == null) { return; } EntityListenersMetadata entityListenersMetadata = EntityIntrospector .getEntityListenersMetadata(entity); List<CallbackMetadata> callbacks = entityListenersMetadata.getCallbacks(callbackType); if (!entityListenersMetadata.isExcludeDefaultListeners()) { executeGlobalListeners(callbackType, entity); } if (callbacks == null) { return; } for (CallbackMetadata callback : callbacks) { switch (callback.getListenerType()) { case EXTERNAL: Object listener = ListenerFactory.getInstance().getListener(callback.getListenerClass()); invokeCallbackMethod(callback.getCallbackMethod(), listener, entity); break; case INTERNAL: invokeCallbackMethod(callback.getCallbackMethod(), entity); break; default: String message = String.format("Unknown or unimplemented callback listener type: %s", callback.getListenerType()); throw new EntityManagerException(message); } } } /** * Executes the entity listeners associated with the given list of entities. * * @param callbackType * the callback type * @param entities * the entities */ public void executeEntityListeners(CallbackType callbackType, List<?> entities) { for (Object entity : entities) { executeEntityListeners(callbackType, entity); } } /** * Executes the global listeners for the given event type for the given entity. * * @param callbackType * the event type * @param entity * the entity */ private void executeGlobalListeners(CallbackType callbackType, Object entity) { if (globalCallbacks == null) { return; } List<CallbackMetadata> callbacks = globalCallbacks.get(callbackType); if (callbacks == null) { return; } for (CallbackMetadata callback : callbacks) { Object listener = ListenerFactory.getInstance().getListener(callback.getListenerClass()); invokeCallbackMethod(callback.getCallbackMethod(), listener, entity); } } /** * Invokes the given callback method on the given target object. * * @param callbackMethod * the callback method * @param listener * the listener object on which to invoke the method * @param entity * the entity for which the callback is being invoked. */ private static void invokeCallbackMethod(Method callbackMethod, Object listener, Object entity) { try { callbackMethod.invoke(listener, entity); } catch (Exception exp) { String message = String.format("Failed to execute callback method %s of class %s", callbackMethod.getName(), callbackMethod.getDeclaringClass().getName()); throw new EntityManagerException(message, exp); } } /** * Invokes the given callback method on the given target object. * * @param callbackMethod * the callback method * @param entity * the entity for which the callback is being invoked */ private static void invokeCallbackMethod(Method callbackMethod, Object entity) { try { callbackMethod.invoke(entity); } catch (Exception exp) { String message = String.format("Failed to execute callback method %s of class %s", callbackMethod.getName(), callbackMethod.getDeclaringClass().getName()); throw new EntityManagerException(message, exp); } } /** * Creates and returns a new native KeyFactory. If a namespace was specified using {@link Tenant}, * the returned KeyFactory will have the specified namespace. * * @return a {@link KeyFactory} */ KeyFactory newNativeKeyFactory() { KeyFactory keyFactory = datastore.newKeyFactory(); String namespace = Tenant.getNamespace(); if (namespace != null) { keyFactory.setNamespace(namespace); } return keyFactory; } /** * Returns the effective namespace. If a namespace was specified using {@link Tenant}, it will be * returned. Otherwise, the namespace of this EntityManager is returned. * * @return the effective namespace. */ String getEffectiveNamespace() { String namespace = Tenant.getNamespace(); if (namespace == null) { namespace = datastore.getOptions().getNamespace(); } return namespace; } }