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

com.techempower.cache.EntityStore Maven / Gradle / Ivy

There is a newer version: 3.3.14
Show newest version
/*******************************************************************************
 * Copyright (c) 2018, TechEmpower, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name TechEmpower, Inc. nor the names of its
 *       contributors may be used to endorse or promote products derived from
 *       this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

package com.techempower.cache;

import com.google.common.primitives.*;
import gnu.trove.map.*;
import gnu.trove.map.hash.*;

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;

import org.reflections.*;

import com.techempower.*;
import com.techempower.cache.annotation.*;
import com.techempower.classloader.*;
import com.techempower.collection.*;
import com.techempower.data.*;
import com.techempower.data.EntityGroup.Builder;
import com.techempower.data.annotation.*;
import com.techempower.gemini.cluster.*;
import com.techempower.gemini.configuration.*;
import com.techempower.helper.*;
import com.techempower.reflect.*;
import com.techempower.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class serves as a storage manager for several entity groups and the
 * relationships between those entity groups.  It also provides some extra 
 * functionality such as sorting.
 *   

* Although this class has evolved into a role of controlling storage for both * cached and non-cached entities, it remains in the com.techempower.cache * package because most applications use caching to some degree (there is no * parent class without caching from which this class derives functionality) * and also for compatibility reasons. *

* When used as part of a Gemini application, Gemini's InfrastructureServlet * will automatically call the initialize method when the Servlet is * initialized. *

* Configuration options: *

    *
  • EntityStore.CacheMethodValues - Cache the values of objects * queried by reflection (in getObjectByMethod, etc.), which can improve the * performance of future lookups at the cost of memory? Default is no.
  • *
*/ public class EntityStore implements Configurable { // // Constants. // private static final Class[] NO_PARAMETERS = new Class[0]; private static final Object[] NO_VALUES = new Object[0]; private static final int INITIAL_GROUPS_SIZE = 20; private static final String ERROR_METHOD_ACCESS = "Method cannot be accessed: "; private static final String ERROR_FIELD_ACCESS = "Field cannot be accessed: "; // // Member variables. // private final TechEmpowerApplication application; private final Map,EntityGroup> groups; private final ConnectorFactory connectorFactory; private final Logger log = LoggerFactory.getLogger(getClass()); private volatile CacheListener[] listeners; private ExecutorService preinitializationTasks = Executors.newSingleThreadExecutor(); private Reflections reflections = null; private boolean initialized = false; private boolean cacheMethodValues = false; /** * Whether to expect the return value from Statement.executeUpdate() to indicate * whether the row was actually changed. If using MySQL and the connect string * has useAffectedRows=true set on it, then this can be used. It defaults to * false because it is MySQL-specific and requires explicitly enabling this in * the connect string. */ private boolean useAffectedRows = false; /** * The registered method value caches. These allow you to quickly find * entities by the value of a given field. */ private Map, MethodValueCache> methodValueCaches = new HashMap<>(); private final TLongObjectMap> entityGroupClassesById = new TLongObjectHashMap<>(); /** * The list of all relations managed by this entity store. This is a * superset of the {@link #cachedRelations} list. */ private final List> relations = new ArrayList<>(); /** * The list of all cached relations managed by this entity store. This is a * subset of the {@link #relations} list. Some operations, such as * communicating about cache resets, only affect cached relations, so this * list is provided (in addition to the other) as a convenience. */ private final List> cachedRelations = new ArrayList<>(); /** * A map from definition classes (in practice, any arbitrary class) to * relations. */ private final ConcurrentMap>, EntityRelation> relationsMap = new ConcurrentHashMap<>(); private ConcurrentMap cachedResponses = new ConcurrentHashMap<>(); /** * Keeps track of the methods that are annotated with either @Indexed or * @NotIndexed. */ private final Map, Map> indexedAnnotatedMethods = new HashMap<>(); /** * Keeps track of the classes that are annotated with either @Indexed or * @NotIndexed. */ private final Map, Boolean> indexedAnnotatedClasses = new HashMap<>(); /** * The type adapters used by entity groups in this cache. These allow * non-standard data entity field types to be stored in the database. */ private final List> typeAdapters = new ArrayList<>(); // // Member methods. // /** * Constructor. * * @param application a reference to the application using this * EntityStore. * @param connectorFactory A ConnectorFactory to use to * communicate with the database. */ public EntityStore(TechEmpowerApplication application, final ConnectorFactory connectorFactory) { this.application = application; this.connectorFactory = connectorFactory; this.groups = new HashMap<>(INITIAL_GROUPS_SIZE); this.listeners = new CacheListener[0]; // Start constructing Reflections on a new thread since it takes a // bit of time. preinitializationTasks.submit(new Runnable() { @Override public void run() { try { log.info("Instantiating Reflections component."); reflections = PackageClassLoader.getReflectionClassLoader(application); log.info("Reflections component instantiated: {}", reflections); } catch (Exception exc) { log.error("Exception while instantiating Reflections component.", exc); } } }); } /** * Configure the cache. */ @Override public void configure(EnhancedProperties props) { // First read the legacy name for this property and then the new name. cacheMethodValues = props.getBoolean("CacheController.CacheMethodValues", false); cacheMethodValues = props.getBoolean("EntityStore.CacheMethodValues", cacheMethodValues); // Whether to expect the return value from Statement.executeUpdate() to indicate // whether the row was actually changed. useAffectedRows = props.getBoolean("EntityStore.UseAffectedRows", useAffectedRows); if (useAffectedRows) { log.warn("EntityStore.UseAffectedRows is enabled, which REQUIRES that the database connection be configured so update statements return the count of affected rows. If unsure, disable this."); } methodValueCaches = new HashMap<>(); // This should only happen when the application is reconfigured. if (CollectionHelper.isNonEmpty(groups)) { for (EntityGroup group : groups.values()) { methodValueCaches.put(group.type(), new MethodValueCache<>(this, group.type())); } } } /** * Are we caching this method's values? This is dependent on the value of * {@code cacheMethodValues} and also whether or not the method has been * annotated with {@code @Indexed} or {@code @NotIndexed} annotations. * *

{@code forceIndexedMethods} and {@code forceNotIndexedMethods} must be * initialized for this class type before calling this. This should be * done when registering a cache group. * * @return true if this method has its values cached. Returns false if this * method does not have its values cached or this class type is not registered * in the cache group. */ protected boolean isIndexed(Class type, String methodName) { // This is not a cached data entity, so immediately return false. if (!groups.containsKey(type)) { return false; } Boolean classIndexed = indexedAnnotatedClasses.get(type); // The class wasn't specifically annotated, so use the global cacheMethodValues // value. if (classIndexed == null) { classIndexed = cacheMethodValues; } // If we are caching method values by default, then we only need to check // if this method was explicitly marked as @NotIndexed. if (classIndexed) { Boolean methodIndexed = indexedAnnotatedMethods.get(type).get(methodName); // This will always return true unless the method was specifically // annotated and explicitly marked as @NotIndexed. return methodIndexed == null || methodIndexed; } // If we are not caching method values by default, then we only need to check // if this method was explicitly marked as @Indexed. else { Boolean methodIndexed = indexedAnnotatedMethods.get(type).get(methodName); // This method was specifically annotated and explicitly marked as // @Indexed. return methodIndexed != null && methodIndexed; } } /** * Whether the provided class uses the @Indexed annotation and therefore uses * the method value cache. */ public boolean usesMethodValueCache(Class type) { return methodValueCaches.get(type) != null; } /** * Update the specified method value cache. */ public void methodValueCacheUpdate(Class type, long... ids) { final MethodValueCache methodValueCache = methodValueCaches.get(type); if (methodValueCache != null) { for (long id : ids) { methodValueCache.update(id); } } } /** * Delete from the specified method value cache. */ public void methodValueCacheDelete(Class type, long... ids) { final MethodValueCache methodValueCache = methodValueCaches.get(type); if (methodValueCache != null) { for (long id : ids) { methodValueCache.delete(id); } } } /** * Reset all entity groups controlled by this controller. */ public void reset() { reset(true, true); } /** * Reset each entity group controlled by this controller. Note that * listeners are only notified of the full reset as a coarse-grained * notification. That is, the listeners will not receive individual * calls to cacheTypeReset for all entity types during a full reset. * * @param notifyListeners Should CacheListeners be notified of this reset? */ public void reset(boolean notifyListeners, boolean notifyDistributionListeners) { log.debug("Full reset."); // Reset entity groups. for (EntityGroup group : groups.values()) { group.reset(); } // Reset relations. for (CachedRelation relation : cachedRelations) { relation.reset(notifyListeners, notifyDistributionListeners); } // Reset method value cache. for (MethodValueCache methodValueCache : methodValueCaches.values()) { methodValueCache.reset(); } // Notify the listeners of the full reset only. We do not call // cacheTypeReset for each entity type. if (notifyListeners) { final CacheListener[] toNotify = listeners; for (CacheListener listener : toNotify) { listener.cacheFullReset(); } } } /** * Reset the entity group specified by the type id. */ public void reset(Class type) { reset(type, true, true); } /** * Reset the entity group specified by the type id. * * @param type the object type of the entity group to be reset. * @param notifyListeners Should CacheListeners be notified of this reset? * @param notifyDistributionListeners Whether to notify any * DistributionListeners; only used When notifyListeners is true. */ public void reset(Class type, boolean notifyListeners, boolean notifyDistributionListeners) { for (CachedRelation relation : cachedRelations) { relation.reset(type, notifyListeners, notifyListeners); } final MethodValueCache methodValueCache = methodValueCaches.get(type); if (methodValueCache != null) { methodValueCache.reset(); } final EntityGroup group = groups.get(type); if (group != null) { group.reset(); if (notifyListeners) { // Notify the listeners. final CacheListener[] toNotify = listeners; for (CacheListener listener : toNotify) { if (!(listener instanceof DistributionListener) || notifyDistributionListeners) { listener.cacheTypeReset(group.type()); } } } } } /** * Adds a CacheListener to be notified of cache events. */ public synchronized void addListener(CacheListener listener) { // Do nothing if we already have this listener in our list. if (CollectionHelper.arrayContains(listeners, listener)) { return; } // Copy existing listeners. final CacheListener[] newArray = new CacheListener[listeners.length + 1]; if (listeners.length > 0) { System.arraycopy(listeners, 0, newArray, 0, listeners.length); } // Add our new listener. newArray[newArray.length - 1] = listener; // Replace the member variable. listeners = newArray; } /** * Removes a CacheListener. */ public synchronized void removeListener(CacheListener listener) { if (CollectionHelper.arrayContains(listeners, listener)) { final CacheListener[] newArray = new CacheListener[listeners.length - 1]; int index = 0; for (CacheListener l : listeners) { if (l != listener) { newArray[index++] = l; } } listeners = newArray; } } /** * Gets an entity group by type. Returns null if no such entity group * is found. */ @SuppressWarnings("unchecked") public EntityGroup getGroup(Class type) { return (EntityGroup)groups.get(type); } /** * Gets an entity group by type, throwing a ControllerError if no such * group is found. */ protected EntityGroup getGroupSafe(Class type) { if (type == null) { throw new ControllerError("Invalid parameter: type is null."); } EntityGroup toReturn = getGroup(type); if (toReturn == null) { throw new ControllerError(type.getSimpleName() + " is not registered with the EntityStore."); } else { return toReturn; } } /** * Gets a cache group by its unique group number. *

* This method is intended for internal use within the Gemini core, not * broad use within Gemini applications. */ public EntityGroup getGroup(int groupNumber) { return getGroup(entityGroupClassesById.get(groupNumber)); } /** * Gets a cached relation by its unique id. *

* This method is intended for internal use within the Gemini core, not broad * use within Gemini applications. */ public CachedRelation getCachedRelation(long relationId) { return cachedRelations.get(Ints.saturatedCast(relationId - 1)); } /** * Collects @Indexed and @NotIndexed annotations for data entity methods. * This should be called before adding the entity to the cache group. */ protected void collectIndexedMethodAnnotations(EntityGroup group) { indexedAnnotatedMethods.put(group.type(), new HashMap()); for (Method method : group.type().getMethods()) { if (method.isAnnotationPresent(Indexed.class)) { indexedAnnotatedMethods.get(group.type()).put(method.getName(), true); } else if (method.getAnnotation(NotIndexed.class) != null) { indexedAnnotatedMethods.get(group.type()).put(method.getName(), false); } } if (group.type().isAnnotationPresent(Indexed.class)) { indexedAnnotatedClasses.put(group.type(), true); } else if (group.type().isAnnotationPresent(NotIndexed.class)) { indexedAnnotatedClasses.put(group.type(), false); } } /** * Add a new cache group to this Controller. * * @param group The entities to be cached. */ public void register(EntityGroup.Builder group) { register(group.build(this)); } /** * Add a new cache group to this Controller. * * @param group The entities to be cached. */ public EntityGroup register(EntityGroup group) { // Collect @Indexed and @NotIndexed annotations collectIndexedMethodAnnotations(group); // Add the group. groups.put(group.type(), group); // Give the group a unique ID. group.setGroupNumber(groups.size()); entityGroupClassesById.put(group.getGroupNumber(), group.type()); methodValueCaches.put(group.type(), new MethodValueCache<>(this, group.type())); log.info("Registered {} with id {}", group, group.getGroupNumber()); return group; } /** * Returns a list of registered entity groups. */ public List> getGroupList() { return new ArrayList<>(groups.values()); } /** * Get the connector factory for this CC. */ public ConnectorFactory getConnectorFactory() { return connectorFactory; } /** * Gets the Application reference. */ public TechEmpowerApplication getApplication() { return application; } /** * Initialize the EntityStore. The basic implementation provided here * will attempt to find any classes annotated with the Entity and * CachedEntity annotations within the application's package hierarchy. */ public void initialize() { // Wait for pre-initialization tasks to complete. try { log.info("Completing preinitialization tasks."); preinitializationTasks.shutdown(); log.info("Awaiting termination of preinitialization tasks."); preinitializationTasks.awaitTermination(5L, TimeUnit.MINUTES); log.info("Preinitialization tasks complete."); log.info("Reflections component: {}", reflections); } catch (InterruptedException iexc) { log.error("Preinitialization interrupted.", iexc); } // Throw an exception if Reflections is not ready. if (reflections == null) { throw new ConfigurationError("Reflections not ready; application cannot start."); } // The default behavior is to search com.techempower and the package of // the main application for any cache annotations and auto register any // entity types found. register(); initialized = true; } /** * Return the number of the objects contained in the entity group specified * by type. Throws ControllerError if no such group is registered. */ public int size(Class type) { return getGroupSafe(type).size(); } /** * Return all the objects contained in the entity group specified by * type. Throws ControllerError if no such group is registered. */ public List list(Class type) { return getGroupSafe(type).list(); } /** * Return all the objects contained in the entity group specified by * type, mapped by id. Throws ControllerError if no such group is registered. */ public TLongObjectMap map(Class type) { return getGroupSafe(type).map(); } /** * Return a collection of objects contained in the entity group based * on a method name value and object type. Returns empty collection in the * event of an error or if no objects cannot be found. */ @SuppressWarnings("unchecked") public List list(Class type, String methodName, Object value) { if (isIndexed(type, methodName)) { MethodValueCache methodValueCache = (MethodValueCache)methodValueCaches.get(type); if (methodValueCache != null) { return methodValueCache.getObjects(methodName, value); } } return list(type, methodName, NO_PARAMETERS, value); } /** * Simplified list convenience method. */ public List list(Class type, String methodName, Object[] args, Object value) { return list(type, methodName, NO_PARAMETERS, args, value); } /** * Simplified list convenience method. */ public List list(Class type, String methodName, Class[] paramTypes, Object[] args, Object value) { return list(type, methodName, paramTypes, args, value, false); } /** * Simplified list convenience method. */ public List list(Class type, String methodName, Object value, boolean loose) { return list(type, methodName, NO_PARAMETERS, NO_VALUES, value, loose); } /** * Return a collection of objects contained in the entity group based * on a method name value and object type. Returns empty collection in the * event of an error or if no IdentifiableObjects cannot be found. * * @param methodName a method from which the return value is compared to the * "value" parameter. * @param paramTypes the types of the parameters this method takes, * required if you are also providing an "args" parameter. * @param args method parameters passed to "methodName" method when invoked. * @param value the value for which to search. * @param loose if true, the value returned by the method will be matched * both literally and after a call to "toString" */ public List list(Class type, String methodName, Class[] paramTypes, Object[] args, Object value, boolean loose) { List toReturn = new ArrayList<>(); List objects = list(type); if (CollectionHelper.isNonEmpty(objects)) { // Try to get the method. Method method; try { method = type.getDeclaredMethod(methodName, paramTypes); } catch (NoSuchMethodException exc) { try { method = type.getMethod(methodName, paramTypes); } catch (NoSuchMethodException etexc) { throw new ControllerError("No method found: " + methodName, etexc); } } // Continue if we have a method. if (method != null) { try { Iterator iterator = objects.iterator(); T object; while (iterator.hasNext()) { object = iterator.next(); // Check the value returned by the method. Object objValue = method.invoke(object, args); if (value.equals(objValue)) { toReturn.add(object); } else if ((loose) && (value.equals(objValue.toString()))) { toReturn.add(object); } } } catch (InvocationTargetException etexc) { throw new ControllerError("Unable to invoke method: " + methodName, etexc); } catch (SecurityException | IllegalAccessException e) { throw new ControllerError(ERROR_METHOD_ACCESS + methodName, e); } } } return toReturn; } /** * Return a particular IdentifiableObject contained in the entity group * based on ID and object type. Returns null if no such entity group * is found. * * @param identifier identifier for the object */ public T get(Class type, long identifier) { return getGroupSafe(type).get(identifier); } /** * Return a particular IdentifiableObject contained in the entity group * based on ID and object type. Returns null if no such entity group * is found. * * @param identifier A string representation of an integer identity for * the object; must be a number. If the parse fails, id -1 will be used * (which should return null). * * @return The object of the given type and with the given identifier if * found, null if the type and/or identifier was null or invalid * or if the entity could not be found */ public T get(Class type, String identifier) { return get(type, NumberHelper.parseIntPermissive(identifier, -1)); } /** * Finds the object in a given entity group with the highest identity. */ public Identifiable getHighestIdentity(Class type) { long highestID = getGroupSafe(type).highest(); return get(type, highestID); } /** * Finds the object in a given entity group with the lowest identity. */ public Identifiable getLowestIdentity(Class type) { long lowestID = getGroupSafe(type).lowest(); return get(type, lowestID); } /** * Return the set of IdentifiableObjects contained in a entity group sorted * by a field. * * @param unsorted The original unsorted collection of objects. * @param sortField the field or method by which to sort the * IdentifiableObjects. * @param comparisonType the comparison type as specified by * ReflectiveComparator. * @param ordering either ASCENDING or DESCENDING, as specified by * ReflectiveComparator. * @param ignoreCase If comparing Strings, should case be ignored? * Has no effect if comparing anything other than Strings. */ public List sortedList(Collection unsorted, String sortField, int comparisonType, int ordering, boolean ignoreCase) { if (unsorted != null) { final List results = new ArrayList<>(unsorted); final ReflectiveComparator comparator = new ReflectiveComparator<>( sortField, comparisonType, ordering); comparator.setIgnoreCase(ignoreCase); // Sort the results. Collections.sort(results, comparator); return results; } else { return Collections.emptyList(); } } /** * Return the set of IdentifiableObjects contained in a entity group sorted * by a field. * * @param unsorted The original unsorted collection of objects. * @param sortField the field or method by which to sort the * IdentifiableObjects. * @param comparisonType the comparison type as specified by * ReflectiveComparator. * @param ordering either ASCENDING or DESCENDING, as specified by * ReflectiveComparator. */ public List sortedList(Collection unsorted, String sortField, int comparisonType, int ordering) { return sortedList(unsorted, sortField, comparisonType, ordering, false); } /** * Return the set of IdentifiableObjects contained in a entity group sorted * by a field. Assumes an ASCENDING ordering. * * @param unsorted The original unsorted collection of objects. * @param sortField the field or method by which to sort the * IdentifiableObjects. * @param comparisonType the comparison type as specified by * ReflectiveComparator. */ public List sortedList(Collection unsorted, String sortField, int comparisonType) { return sortedList(unsorted, sortField, comparisonType, ReflectiveComparator.ASCENDING, false); } /** * Return the set of IdentifiableObjects contained in a entity group sorted * by a field. Assumes an ASCENDING ordering. * * @param sortField the field or method by which to sort the * IdentifiableObjects. * @param comparisonType the comparison type as specified by * ReflectiveComparator. */ public List sortedList(Class type, String sortField, int comparisonType) { return sortedList(list(type), sortField, comparisonType); } /** * Return the set of IdentifiableObjects contained in a entity group sorted * by a field. * * @param sortField the field or method by which to sort the * IdentifiableObjects. * @param comparisonType the comparison type as specified by * ReflectiveComparator. * @param ordering either ASCENDING or DESCENDING, as specified by * ReflectiveComparator. * @param ignoreCase If comparing Strings, should case be ignored? * Has no effect if comparing anything other than Strings. */ public List sortedList(Class type, String sortField, int comparisonType, int ordering, boolean ignoreCase) { return sortedList(list(type), sortField, comparisonType, ordering, ignoreCase); } /** * Return the set of IdentifiableObjects contained in a entity group sorted * by a field. * * @param sortField the field or method by which to sort the * IdentifiableObjects. * @param comparisonType the comparison type as specified by * ReflectiveComparator. * @param ordering either ASCENDING or DESCENDING, as specified by * ReflectiveComparator. */ public List sortedList(Class type, String sortField, int comparisonType, int ordering) { return sortedList(list(type), sortField, comparisonType, ordering); } /** * Return a particular IdentifiableObject contained in the entity group * based on a field value and object type. Returns null in the event of an * error or if the entity group cannot be found. * * @param fieldName the field to match the value to * @param value the value on which to search */ public T getByField(Class type, String fieldName, Object value) { Field field = null; final List list = list(type); try { for (T object : list) { // Cache a reference to the Field if not yet done so. if (field == null) { field = object.getClass().getDeclaredField(fieldName); } // Check the value of the field within this object. Object objValue = field.get(object); if (value.equals(objValue)) { return object; } } } catch (NoSuchFieldException | SecurityException | IllegalAccessException e) { throw new ControllerError(ERROR_FIELD_ACCESS + fieldName, e); } // If we get here, return null. return null; } /** * Return a particular IdentifiableObject contained in the entity group * based on a method name and object type. Returns null in the event of an * error or if the entity group cannot be found. * * @param methodName the method to call * @param value the value on which to search */ @SuppressWarnings("unchecked") public T get(Class type, String methodName, Object value) { if (isIndexed(type, methodName)) { MethodValueCache methodValueCache = (MethodValueCache)methodValueCaches.get(type); if (methodValueCache != null) { return methodValueCache.getObject(methodName, value); } } Method method = null; final List list = list(type); try { for (T object : list) { // Cache a reference to the Method if not yet done so. if (method == null) { method = object.getClass().getMethod(methodName, NO_PARAMETERS); } // Check the value of the field within this object. final Object objValue = method.invoke(object, NO_VALUES); if (Objects.equals(value, objValue)) { return object; } } } catch (NoSuchMethodException | SecurityException | IllegalAccessException | InvocationTargetException e) { throw new ControllerError(ERROR_METHOD_ACCESS + methodName, e); } // If we get here, return null. return null; } /** * Refreshes a single object in the cache. * * @param ids the ids of the objects */ public void refresh(Class type, long... ids) { getGroupSafe(type).refresh(ids); // Update index/methodValueCache. methodValueCacheUpdate(type, ids); // Notify the listeners. notifyListenersCacheObjectExpired(true, type, ids); } /** * CacheMessageManager needs this in order to notify listeners about specific * objects expiring. * * @param notifyDistributionListeners Whether to notify distribution listeners. * CacheMessageManager would pass false to * this. * @param type * @param ids */ public void notifyListenersCacheObjectExpired(boolean notifyDistributionListeners, Class type, long... ids) { final CacheListener[] toNotify = listeners; for (CacheListener listener : toNotify) { if (!(listener instanceof DistributionListener) || notifyDistributionListeners) { for (long id : ids) { listener.cacheObjectExpired(type, id); } } } } /** * Puts a data entity into the database/data-store. This will also cache * the entity if a cache is in use. If the entity is new and is assigned * a new Identity during the put operation, the entity will be affected * directly. */ @SuppressWarnings("unchecked") public void put(T entity) { if (entity == null) { throw new ControllerError("Cannot put null entity."); } int rowsUpdated = getGroupSafe((Class)entity.getClass()).put(entity); // If useAffectedRows is enabled, then only update the methodValueCache and // notify the listeners if an actual change was persisted. if (!useAffectedRows || rowsUpdated > 0) { // Update method value caches. methodValueCacheUpdate(entity.getClass(), entity.getId()); // Notify the listeners. final CacheListener[] toNotify = listeners; for (CacheListener listener : toNotify) { listener.cacheObjectExpired(entity.getClass(), entity.getId()); } } } /** * Remove a data entity from the database/data-store. This will also * remove the entity from the cache if a cache is in use. */ public void remove(Class type, long id) { T entity = get(type, id); if (entity != null) { remove(entity); } } /** * Remove a data entity from the database/data-store. This will also * remove the entity from the cache if a cache is in use. */ public void remove(T entity) { if (entity == null) { throw new ControllerError("Cannot remove null entity."); } getGroupSafe(entity.getClass()).remove(entity.getId()); // Update relations. for (EntityRelation relation : relations) { relation.removeEntity(entity); } // Update method value cache. methodValueCacheDelete(entity.getClass(), entity.getId()); // Notify the listeners. final CacheListener[] toNotify = listeners; for (CacheListener listener : toNotify) { listener.removeFromCache(entity.getClass(), entity.getId()); } } // // Custom Caching // // The following methods allow for additional custom caching to be defined // /** * Registers the given relation with the cache and notifies the listeners. * * @param relation a relation between Identifiable objects */ public > E register(EntityRelation.Builder relation) { return register(relation.build(this), null); } /** * Registers the given relation with the cache and notifies the listeners. Once registered, * you will be able to get this relation from the cache via the provided Class. */ public , D extends EntityRelationDescriptor> E register(EntityRelation.Builder relation, Class definition) { return register(relation.build(this), definition); } /** * Registers the given relation with the cache and notifies the listeners. * * @param relation a relation between Identifiable objects */ public > E register(E relation) { return register(relation, null); } /** * Registers the given relation with the cache and notifies the listeners. * If definition is not null, then once registered, you will be able to get * this relation from the cache via the provided definition Class. Useful if * you have two CachedRelations on the same two classes. * * @param definition a Class that will be used later on to look up this * relation */ @SuppressWarnings("unchecked") public , D extends EntityRelationDescriptor> E register(E relation, Class definition) { relations.add(relation); if (relation instanceof CachedRelation) { final CachedRelation cr = (CachedRelation) relation; cachedRelations.add(cr); // Give the relation a unique ID. cr.setId(cachedRelations.size()); log.debug("Registered {} with id {}", cr, cr.getId()); } // if we're provided with a definition Class, we need to add this relation // to our relationMap for quick lookup via the definition class if (definition != null) { relationsMap.put((Class>)definition, relation); } return relation; } /** * Registers all entities, cached entities and cached relations in a given * package. Uses the annotations in com.techempower.data.annotation. */ @SuppressWarnings("unchecked") public void register() { log.info("Registering annotated entities, relations, and type adapters."); try { final ExecutorService service = Executors.newFixedThreadPool(5); // @Entity-annoted classes. service.submit(new Runnable() { @Override public void run() { for (Class clazz : reflections.getTypesAnnotatedWith(Entity.class)) { // The annotation can contain some additional information on how to // build this EntityGroup. final Entity annotation = clazz.getAnnotation(Entity.class); final Builder builder = EntityGroup.of( (Class)clazz); if (!annotation.table().isEmpty()) { builder.table(annotation.table()); } if (!annotation.id().isEmpty()) { builder.id(annotation.id()); } if (!annotation.comparator().isEmpty()) { builder.comparator(annotation.comparator()); } // finally register the EntityGroup register(builder); } } }); // @CachedEntity-annotated classes. service.submit(new Runnable() { @Override public void run() { for (Class clazz : reflections.getTypesAnnotatedWith(CachedEntity.class)) { // The annotation can contain some additional information on how to // build this CacheGroup. final CachedEntity annotation = clazz.getAnnotation(CachedEntity.class); final Builder builder; if (annotation.lruSize() > 0) { final LruCacheGroup.Builder lruBuilder = LruCacheGroup.of((Class)clazz); lruBuilder.maximumSize(annotation.lruSize()); builder = lruBuilder; } else { builder = CacheGroup.of((Class)clazz); } if (!annotation.table().isEmpty()) { builder.table(annotation.table()); } if (!annotation.id().isEmpty()) { builder.id(annotation.id()); } if (!annotation.comparator().isEmpty()) { builder.comparator(annotation.comparator()); } // finally register the CacheGroup register(builder); } } }); // @PureMemoryEntity-annotated classes. service.submit(new Runnable() { @Override public void run() { for (Class clazz : reflections.getTypesAnnotatedWith(PureMemoryEntity.class)) { register(PureMemoryGroup.of((Class)clazz)); } } }); // Relations. service.submit(new Runnable() { @Override public void run() { for (@SuppressWarnings("rawtypes") Class clazz : reflections.getSubTypesOf(EntityRelationDescriptor.class)) { // We check for the existence of any @Relation annotations. if (clazz.isAnnotationPresent(Relation.class)) { final Relation annotation = clazz.getAnnotation(Relation.class); // A class uses the @Relation annotation must specify 2 fields, one // with @Left and one with @Right. To help us determine which two // Identifiable classes make up this relation. final Field[] fields = clazz.getDeclaredFields(); Field left = null; Field right = null; for (Field field : fields) { if (field.isAnnotationPresent(Left.class)) { left = field; } else if (field.isAnnotationPresent(Right.class)) { right = field; } } // We need to have both a left column and right column specified via // the @Left and @Right annotations. TODO The classes defined by // these annotations must also be of subclasses of Identifiable. if (left == null || right == null) { // If anything here fails I don't want to start up, I want to // be notified immediately. throw new RuntimeException( "Cannot create CachedRelation from @Relation definition class without specifying @Left and @Right Identifiables."); } final Left leftAnnotation = left.getAnnotation(Left.class); final Right rightAnnotation = right.getAnnotation(Right.class); // We're ready to register this CachedRelation. The table name will // be inferred from the class name. The left and right column names // will use the name of the parameters. // Don't register it more than once. if (getRelation((Class>)clazz) == null) { final CachedRelation.Builder builder = CachedRelation.of( (Class) left.getType(), (Class) right.getType()); if (!annotation.table().isEmpty()) { builder.table(annotation.table()); } if (!leftAnnotation.column().isEmpty()) { builder.leftColumn(leftAnnotation.column()); } if (!rightAnnotation.column().isEmpty()) { builder.rightColumn(rightAnnotation.column()); } // Finally register the Relation register(builder, clazz); } } } } }); // Relations. service.submit(new Runnable() { @Override public void run() { // Finally, look for any TypeAdapter classes that are annotated for (@SuppressWarnings("rawtypes") Class clazz : reflections.getSubTypesOf(TypeAdapter.class)) { // We check for the existence of any known annotations, including // @Entity, @Cached, @Relation if (clazz.isAnnotationPresent(EntityTypeAdapter.class)) { try { register((TypeAdapter) clazz.getConstructor(NO_PARAMETERS).newInstance()); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { throw new RuntimeException("Warn: Could not register TypeAdapter", e); } } } } }); try { service.shutdown(); service.awaitTermination(1L, TimeUnit.HOURS); } catch (InterruptedException iexc) { log.error("Unable to register all entities in 1 hour!"); } log.info("Done registering annotated items."); } catch (ReflectionsException e) { throw new RuntimeException("Warn: problem registering class with reflection", e); } } /** * Returns the relation based on the definition class. This method requires * that you provided a definition class when registering this cache group. */ @SuppressWarnings("unchecked") public EntityRelation getRelation(Class> definition) { return (EntityRelation) relationsMap.get(definition); } /** * Returns the relation whose table name matches the given table name. * * @param tableName The name of the table in the relation to be returned. * @return The relation whose table name matches the given table name. */ public EntityRelation getRelation(String tableName) { for (EntityRelation relation : relations) { if (relation.tableName().equals(tableName)) { return relation; } } return null; } /** * Returns a copy of the list of all relations in the cache. This is a * superset of the relations returned by {@link #getCachedRelations()}. */ public List> getRelations() { return new ArrayList<>(relations); } /** * Returns a copy of the list of all cached relations in the cache. * This is a subset of the relations returned by {@link #getRelations()}. */ public List> getCachedRelations() { return new ArrayList<>(cachedRelations); } /** * Returns the cached response text associated with the given parameter keys. * @param parameterKeys The key of expected cached response. * @return The cached response payload for the given parameterKeys or * null if there is no cached response for the given * request. */ public String getCachedResponse(String parameterKeys) { return cachedResponses.get(parameterKeys); } /** * Caches the response text for the parameters associated with the given * request. * @param parameterKeys The key for the given responseText string. * @param responseText The responseText to cache for the given request. */ public void setCachedResponse(String parameterKeys, String responseText) { // Remove the previous. cachedResponses.remove(parameterKeys); // Try to put it in. cachedResponses.putIfAbsent(parameterKeys, responseText); // This is inherently NOT thread-safe, but in cases where this // comes up, we don't really care: some other thread put a response // value in at or around the same time we removed and tried to put. // If out put returns their value, then w/e. If it returns null, then // we can assume ours was there first. } /** * Clears all cached responses (in memory and on disk). */ public void clearCachedResponses() { // Brand new one, let the old one get garbage collected. cachedResponses = new ConcurrentHashMap<>(); } /** * Returns a list of objects with the given ids. The objects are in the * order specified by the given ids. The returned list will not include * nulls. */ public List list(Class type, long... ids) { return list(type, CollectionHelper.toList(ids)); } /** * Returns a list of objects with the given ids. The objects are in the * order specified by the given ids. The returned list will not include * nulls. */ public List list(Class type, Collection ids) { return getGroupSafe(type).list(ids); } /** * Returns a map of objects with the given ids. */ public TLongObjectMap map(Class type, long... ids) { return map(type, CollectionHelper.toList(ids)); } /** * Returns a map of objects with the given ids. */ public TLongObjectMap map(Class type, Collection ids) { return getGroupSafe(type).map(ids); } /** * Remove the given entities from the database (and cache if applicable). */ public void removeAll(Class type, long... ids) { removeAll(type, CollectionHelper.toList(ids)); } /** * Remove the given entities from the database (and cache if applicable). */ public void removeAll(Class type, Collection ids) { getGroupSafe(type).removeAll(ids); // Update relations. for (EntityRelation relation : relations) { for (long id : ids) { relation.removeEntity(type, id); } } // Update method value caches. MethodValueCache methodValueCache = methodValueCaches.get(type); if (methodValueCache != null) { for (long id : ids) { methodValueCache.delete(id); } } // Notify the listeners. final CacheListener[] toNotify = listeners; for (CacheListener listener : toNotify) { for (long id : ids) { listener.removeFromCache(type, id); } } } /** * Put objects into the data store (and cache if applicable). This will * always persist the objects to the data store; caching is only applicable * when using the CacheGroup subclass or something similar. * * @param objects The objects to put ("put" means persist to disk and update * in cache if this is a cache group.) */ @SafeVarargs public final void putAll(T... objects) { putAll(CollectionHelper.toList(objects)); } /** * Put objects into the data store (and cache if applicable). This will * always persist the objects to the data store; caching is only applicable * when using the CacheGroup subclass or something similar. * * @param objects The objects to put ("put" means persist to disk and update * in cache if this is a cache group.) */ @SuppressWarnings("unchecked") public void putAll(Collection objects) { if (objects == null) { throw new ControllerError("Cannot putAll of a null collection."); } // Group the objects by type. Map, Collection> map = new HashMap<>(); for (T object : objects) { Collection collection = map.get(object.getClass()); if (collection == null) { collection = new ArrayList<>(); map.put((Class)object.getClass(), collection); } collection.add(object); } // For each type, update the cache. for (Map.Entry, Collection> entry : map.entrySet()) { Class type = entry.getKey(); Collection collection = entry.getValue(); // Update the group. int rowsUpdated = getGroupSafe(type).putAll(collection); // If useAffectedRows is enabled, then only update the methodValueCache and // notify the listeners if an actual change was persisted. if (!useAffectedRows || rowsUpdated > 0) { // Update method value caches. MethodValueCache methodValueCache = (MethodValueCache)methodValueCaches.get(type); if (methodValueCache != null) { for (T object : collection) { methodValueCache.update(object.getId()); } } // Notify the listeners. final CacheListener[] toNotify = listeners; for (CacheListener listener : toNotify) { for (T object : collection) { listener.cacheObjectExpired(object.getClass(), object.getId()); } } } } } /** * Remove the given entities from the database (and cache if applicable). */ @SafeVarargs public final void removeAll(T... objects) { removeAll(CollectionHelper.toList(objects)); } /** * Remove the given entities from the database (and cache if applicable). */ @SuppressWarnings("unchecked") public void removeAll(Collection objects) { if (objects == null) { throw new ControllerError("Cannot removeAll of a null collection."); } // Group the objects by type. Map, Collection> map = new HashMap<>(); for (T object : objects) { Collection collection = map.get(object.getClass()); if (collection == null) { collection = new ArrayList<>(); map.put((Class)object.getClass(), collection); } collection.add(object.getId()); } // For each type, update the cache. for (Map.Entry, Collection> entry : map.entrySet()) { Class type = entry.getKey(); Collection collection = entry.getValue(); // Update the group. getGroupSafe(type).removeAll(collection); // Update relations. for (EntityRelation relation : relations) { for (long id : collection) { relation.removeEntity(type, id); } } // Update method value caches. MethodValueCache methodValueCache = (MethodValueCache)methodValueCaches.get(type); if (methodValueCache != null) { for (long id : collection) { methodValueCache.delete(id); } } // Notify the listeners. final CacheListener[] toNotify = listeners; for (CacheListener listener : toNotify) { for (long id : collection) { listener.removeFromCache(type, id); } } } } /** * Registers the given type adapter with this controller. The adapter will be * used to translate all entity fields of one type to another type when * persisting entities to the database. This is useful when you would like to * persist custom or non-standard fields to the database. For example: *

   * // Allows all data entities to have DateMidnight (from Joda Time) fields
   * // that get persisted like java.util.Date in the database.
   * register(
   *     new TypeAdapter<DateMidnight, Date>() {
   *       public Date write(DateMidnight value)
   *       {
   *         return (value == null)
   *             ? null
   *             : value.toDate();
   *       }
   *       public DateMidnight read(Date value)
   *       {
   *         return (value == null)
   *             ? null
   *             : new DateMidnight(value);
   *       }
   *     });
   * 
* * @param The adapter converts values from this type. * @param The adapter converts values to this type. */ public void register(TypeAdapter adapter) { if (adapter == null) { throw new ControllerError("Cannot register a null type adapter."); } typeAdapters.add(adapter); } /** * Returns an unmodifiable view of the type adapters registered with this * controller. This method is intended for use in {@link EntityGroup} only. */ public List> getTypeAdapters() { return Collections.unmodifiableList(typeAdapters); } /** * Returns whether this EntityStore has completed initialization. */ public boolean isInitialized() { return initialized; } @Override public String toString() { return "EntityStore " + (initialized ? "[" + groups.size() + " group" + StringHelper.pluralize(groups.size()) + "; " + listeners.length + " listener" + StringHelper.pluralize(listeners.length) + "]" : "[Not yet initialized]" ); } } // End EntityStore.




© 2015 - 2024 Weber Informatics LLC | Privacy Policy