/** * Copyright 2009-2013 Oy Vaadin Ltd * * 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.vaadin.addon.jpacontainer.provider; import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.persistence.TypedQuery; import com.vaadin.addon.jpacontainer.EntityContainer; import com.vaadin.addon.jpacontainer.EntityProvider; import com.vaadin.addon.jpacontainer.QueryModifierDelegate; import com.vaadin.addon.jpacontainer.SortBy; import com.vaadin.v7.data.Container.Filter; import com.vaadin.v7.data.Item; /** * Delegate class that implements caching for {@link LocalEntityProvider}s and * their subclasses. This class is internal and should never be used outside of * JPAContainer. * * @author Petter Holmström (Vaadin Ltd) * @since 1.0 */ class CachingSupport<T> implements Serializable { private final LocalEntityProvider<T> entityProvider; private int maxCacheSize = 1000; private boolean cacheEnabled = true; private boolean cloneCachedEntities = false; /** * The number of entity IDs to fetch every time a query is made. */ protected static final int CHUNK_SIZE = 150; /** * A {@link Filter}-instance representing the null-filter (i.e. no filter * applied). */ protected static Filter NULL_FILTER = new Filter() { private static final long serialVersionUID = 6142104349424102387L; public boolean passesFilter(Object itemId, Item item) throws UnsupportedOperationException { return true; } public boolean appliesToProperty(Object propertyId) { return false; } }; /** * The max size of the filter cache (i.e. how many different filters to * cache). * * @see #getFilterCache() */ public static final int MAX_FILTER_CACHE_SIZE = 10; /** * The max size of the sort by cache for each filter. Thus, the maximum * number of cached filter-sortBy combinations is * <code>MAX_FILTER_CACHE_SIZE * MAX_SORTBY_CACHE_SIZE</code>. */ public static final int MAX_SORTBY_CACHE_SIZE = 10; // TODO Make chunk size, filter cache size and sortBy cache size user // configurable. /** * Creates a new <code>CachingSupport</code> for the specified entity * provider. * * @param entityProvider * the entity provider (never null). */ public CachingSupport(LocalEntityProvider<T> entityProvider) { assert entityProvider != null : "entityProvider should not be null"; this.entityProvider = entityProvider; } /** * Data structure used by {@link FilterCacheEntry} to store entityId lists * sorted in different ways. * * @author Petter Holmström (Vaadin Ltd) * @since 1.0 */ static class IdListEntry implements Serializable { private static final long serialVersionUID = -3552793234160831297L; public ArrayList<Object> idList; public int listOffset = 0; public boolean containsAll = false; } /** * This class represents a cache for a specific {@link Filter}. The class * contains counterparts of most of the methods defined in * {@link EntityProvider}. * * @author Petter Holmström (Vaadin Ltd) * @since 1.0 */ class FilterCacheEntry implements Serializable { // TODO Optimize the use of lists private static final long serialVersionUID = -2978864194978758736L; private Filter filter; private Integer entityCount; public Map<List<SortBy>, IdListEntry> idListMap = new CacheMap<List<SortBy>, IdListEntry>( MAX_SORTBY_CACHE_SIZE); public Set<Object> idSet = new CacheSet<Object>(getMaxCacheSize()); /** * Creates a new <code>FilterCacheEntry</code>. * * @param filter * the filter for which this cache should be created. */ public FilterCacheEntry(Filter filter) { this.filter = filter; } /** * Gets the number of entities that match this particular filter. * * @return the number of entities. */ public synchronized int getEntityCount(EntityContainer<T> container) { if(!isCachingPossible(container)) { return entityProvider.doGetEntityCount(container, getFilter()); } if (entityCount == null) { entityCount = entityProvider.doGetEntityCount(container, getFilter()); } return entityCount; } /** * @see EntityProvider#containsEntity(java.lang.Object, * com.vaadin.addons.jpacontainer.Filter) */ public synchronized boolean containsId(EntityContainer<T> container, Object entityId) { if (!idSet.contains(entityId)) { if (entityProvider.doContainsEntity(container, entityId, getFilter())) { idSet.add(entityId); return true; } else { return false; } } else { return true; } } /** * @see EntityProvider#getFirstEntityIdentifier(com.vaadin.addons.jpacontainer.Filter, * java.util.List) */ public Object getFirstId(EntityContainer<T> container, List<SortBy> sortBy) { return getIdAt(container, sortBy, 0); } /** * @see EntityProvider#getNextEntityIdentifier(java.lang.Object, * com.vaadin.addons.jpacontainer.Filter, java.util.List) */ public synchronized Object getNextId(EntityContainer<T> container, Object entityId, List<SortBy> sortBy) { IdListEntry entry = idListMap.get(sortBy); if (entry == null) { entry = new IdListEntry(); entry.idList = new ArrayList<Object>(); entry.listOffset = -1; idListMap.put(sortBy, entry); } int index = entry.idList.indexOf(entityId); if (index == -1) { entry.idList = new ArrayList<Object>(getNextIds(container, getFilter(), sortBy, entityId, CHUNK_SIZE)); if (entry.idList.isEmpty()) { return null; } else { return entry.idList.get(0); } } else { if (index == entry.idList.size() - 1) { if (getMaxCacheSize() > -1 && entry.idList.size() + CHUNK_SIZE > getMaxCacheSize()) { // Clean up the cache if (entry.idList.size() <= CHUNK_SIZE) { entry.idList.clear(); index = -1; } else { entry.idList.subList(0, CHUNK_SIZE).clear(); index -= CHUNK_SIZE; } } entry.idList.addAll(getNextIds(container, getFilter(), sortBy, entityId, CHUNK_SIZE)); } if (index + 1 == entry.idList.size()) { return null; } else { return entry.idList.get(index + 1); } } } /** * @see EntityProvider#getPreviousEntityIdentifier(java.lang.Object, * com.vaadin.addons.jpacontainer.Filter, java.util.List) */ public synchronized Object getPreviousId(EntityContainer<T> container, Object entityId, List<SortBy> sortBy) { IdListEntry entry = idListMap.get(sortBy); if (entry == null) { entry = new IdListEntry(); entry.idList = new ArrayList<Object>(); entry.listOffset = -1; idListMap.put(sortBy, entry); } int index = entry.idList.indexOf(entityId); if (index == -1) { List<Object> objects = getPreviousIds(container, getFilter(), sortBy, entityId, CHUNK_SIZE); // We have to reverse the list entry.idList = new ArrayList<Object>(objects.size()); for (int i = objects.size() - 1; i >= 0; i--) { entry.idList.add(objects.get(i)); } if (entry.idList.isEmpty()) { return null; } else { return entry.idList.get(entry.idList.size() - 1); } } else { if (index == 0) { List<Object> objects = getPreviousIds(container, getFilter(), sortBy, entityId, CHUNK_SIZE); if (objects.isEmpty()) { return null; } // Store the ID we are looking for Object theId = objects.get(0); // Save the rest of the IDs in the cache for future use ArrayList<Object> l = new ArrayList<Object>(); for (int i = objects.size() - 1; i >= 0; i--) { l.add(objects.get(i)); } if (getMaxCacheSize() > -1 && entry.idList.size() + CHUNK_SIZE > getMaxCacheSize()) { // Clean up the cache if (entry.idList.size() > CHUNK_SIZE) { l.addAll(entry.idList.subList(0, entry.idList.size() - CHUNK_SIZE)); } } else { l.addAll(entry.idList); } entry.idList = l; return theId; } else { return entry.idList.get(index - 1); } } } /** * @see EntityProvider#getLastEntityIdentifier(com.vaadin.addons.jpacontainer.Filter, * java.util.List) */ public Object getLastId(EntityContainer<T> container, List<SortBy> sortBy) { return getIdAt(container, sortBy, getEntityCount(container) - 1); } /** * Informs the cache that <code>entityId</code> has been invalidated * (changed or removed). If the entityId is currently in cache, the * cache is flushed, forcing the data to be fetched from the database * when requested the next time. * * @param entityId * the entityId to invalidate. */ public synchronized void invalidate(Object entityId) { // Clear the caches to force the data to be re-fetched from the // database // in case the ordering has changed idListMap.clear(); // Removing the entity Id from the Id cache should be enough idSet.remove(entityId); } /** * @see EntityProvider#getEntityIdentifierAt(com.vaadin.addons.jpacontainer.Filter, * java.util.List, int) */ public synchronized Object getIdAt(EntityContainer<T> container, List<SortBy> sortBy, int index) { IdListEntry entry = idListMap.get(sortBy); if (entry == null) { entry = new IdListEntry(); entry.idList = new ArrayList<Object>(CHUNK_SIZE * 2); idListMap.put(sortBy, entry); } // listOffset may be -1 if the list has been loaded by a call // to getNextId() or getPreviousId() if (!entry.containsAll && (entry.idList.isEmpty() || index < entry.listOffset || index >= entry.listOffset + entry.idList.size())) { // Check if we can concatenate the index lists if (entry.listOffset > -1 && index == entry.listOffset - 1) { if (getMaxCacheSize() > -1 && entry.idList.size() + CHUNK_SIZE > getMaxCacheSize()) { // Clean up the cache if (entry.idList.size() <= CHUNK_SIZE) { entry.idList.clear(); } else { entry.idList.subList( entry.idList.size() - CHUNK_SIZE, entry.idList.size()).clear(); } } ArrayList<Object> l = new ArrayList<Object>(CHUNK_SIZE + entry.idList.size()); int startFrom = index - CHUNK_SIZE; if (startFrom < 0) { startFrom = 0; } l.addAll(getIds(container, getFilter(), sortBy, startFrom, index - startFrom + 1)); l.addAll(entry.idList); entry.idList = l; entry.listOffset = startFrom; } else if (entry.listOffset > -1 && index == entry.listOffset + entry.idList.size()) { // It is possible that maxCacheSize < CHUNK_SIZE => we have // to make sure that the list is at least as big as // CHUNK_SIZE if (getMaxCacheSize() > -1 && entry.idList.size() + CHUNK_SIZE > getMaxCacheSize()) { // Clean up the cache if (entry.idList.size() <= CHUNK_SIZE) { entry.listOffset += entry.idList.size(); entry.idList.clear(); } else { entry.idList.subList(0, CHUNK_SIZE).clear(); entry.listOffset += CHUNK_SIZE; } } entry.idList.addAll(getIds(container, getFilter(), sortBy, index, CHUNK_SIZE)); } else { entry.idList.clear(); entry.idList.addAll(getIds(container, getFilter(), sortBy, index, CHUNK_SIZE)); entry.listOffset = index; } } int i = index - entry.listOffset; if (entry.idList.size() <= i) { return null; } return entry.idList.get(i); } /** * @see EntityProvider#getAllEntityIdentifiers(com.vaadin.addons.jpacontainer.Filter, * java.util.List) */ public synchronized List<Object> getAllIds(EntityContainer<T> container, List<SortBy> sortBy) { IdListEntry entry = idListMap.get(sortBy); if (entry == null) { entry = new IdListEntry(); idListMap.put(sortBy, entry); } if (!entry.containsAll) { entry.idList = new ArrayList<Object>(getIds(container, getFilter(), sortBy, 0, -1)); entry.listOffset = 0; entry.containsAll = true; } return Collections.unmodifiableList(entry.idList); } /** * Gets the filter for which this cache has been created. * * @return the filter (may be null). */ public Filter getFilter() { return filter == NULL_FILTER ? null : filter; } } /** * TODO Document me! * * @param entityId * @param updated */ public synchronized void invalidate(Object entityId, boolean updated) { getEntityCache().remove(entityId); if (updated) { // TODO Do something smarter than flushing the entire cache! getFilterCache().clear(); } else { for (FilterCacheEntry fce : getFilterCache().values()) { fce.invalidate(entityId); } } } /** * TODO Document me! * * @param entity */ public synchronized void entityAdded(T entity) { // TODO Do something smarter than flushing the entire cache! flush(); // This is currently obsolete, but when above todo is implemented, // uncomment this or somehow increment relevat filter caches. // invalidateSize(); } /** * Gets all the identifiers that match <code>filter</code>, sorted by * <code>sortBy</code>, starting with the identifier at position * <code>startFrom</code> and retrieving a maximum number of * <code>fetchMax</code> items. * * @param filter * the filter to apply, if any (may be null). * @param sortBy * the ordering information (may not be null). * @param startFrom * the index of the first identifier to retrieve. * @param fetchMax * the maximum number of identifiers to retrieve, or 0 to * retrieve all. * @return a list of identifiers. */ protected List<Object> getIds(EntityContainer<T> container, Filter filter, List<SortBy> sortBy, int startFrom, int fetchMax) { TypedQuery<Object> query = entityProvider.createFilteredQuery( container, Arrays.asList(entityProvider.getEntityClassMetadata() .getIdentifierProperty().getName()), filter, entityProvider.addPrimaryKeyToSortList(sortBy), false); query.setFirstResult(startFrom); if (fetchMax > 0) { query.setMaxResults(fetchMax); } return query.getResultList(); } /** * Gets all the identifiers that match <code>filter</code>, sorted by * <code>sortBy</code>, starting with the identifier next to * <code>startFrom</code> and retrieving a maximum number of * <code>fetchMax</code> items. If <code>startFrom</code> is at position n, * then item n+1 will be the first item in the returnde list, n+2 the * second, etc. * * @param filter * the filter to apply, if any (may be null). * @param sortBy * the ordering information (may not be null). * @param startFrom * the entityId prioir to the first identifier to retrieve. * @param fetchMax * the maximum number of identifiers to retrieve, or 0 to * retrieve all. * @return a list of identifiers. */ protected List<Object> getNextIds(EntityContainer<T> container, Filter filter, List<SortBy> sortBy, Object startFrom, int fetchMax) { TypedQuery<Object> query = entityProvider.createSiblingQuery(container, startFrom, filter, sortBy, false); if (fetchMax > 0) { query.setMaxResults(fetchMax); } return query.getResultList(); } /** * Gets all the identifiers that match <code>filter</code>, sorted backwards * by <code>sortBy</code>, starting with the identifier prior to * <code>startFrom</code> and retrieving a maximum number of * <code>fetchMax</code> items. If <code>startFrom</code> is at position n, * then item n-1 will be the first item in the returned list, n-2 the * second, etc. * * @param filter * the filter to apply, if any (may be null). * @param sortBy * the ordering information (may not be null). * @param startFrom * the entityId next to the first identifier to retrieve. * @param fetchMax * the maximum number of identifiers to retrieve, or 0 to * retrieve all. * @return a list of identifiers. */ protected List<Object> getPreviousIds(EntityContainer<T> container, Filter filter, List<SortBy> sortBy, Object startFrom, int fetchMax) { TypedQuery<Object> query = entityProvider.createSiblingQuery(container, startFrom, filter, sortBy, true); if (fetchMax > 0) { query.setMaxResults(fetchMax); } return query.getResultList(); } private Map<Object, T> entityCache; private Map<Filter, FilterCacheEntry> filterCache; /** * A hash map that will remove the oldest items once its size reaches a * specified max size. * * @author Petter Holmström (Vaadin Ltd) * @since 1.0 */ protected static class CacheMap<K, V> extends HashMap<K, V> { private static final long serialVersionUID = 2900939583997256189L; private LinkedList<K> addOrder = new LinkedList<K>(); private int maxSize; public CacheMap(int maxSize) { super(maxSize); this.maxSize = maxSize; } @Override public synchronized V put(K key, V value) { if (size() == maxSize) { // remove oldest item remove(addOrder.removeFirst()); } addOrder.add(key); return super.put(key, value); } } /** * A hash set that will remove the oldest items once its size reaches a * specified max size. * * @author Petter Holmström (Vaadin Ltd) * @since 1.0 */ protected static class CacheSet<V> extends HashSet<V> { private static final long serialVersionUID = 2900939583997256189L; private LinkedList<V> addOrder = new LinkedList<V>(); private int maxSize; public CacheSet(int maxSize) { super(maxSize); this.maxSize = maxSize; } @Override public synchronized boolean add(V e) { if (size() == maxSize) { // remove oldest item remove(addOrder.removeFirst()); } addOrder.add(e); return super.add(e); } } /** * Gets the cache for entity instances. If no cache exists, it will be * created. * * @return the entity cache (never null). */ synchronized Map<Object, T> getEntityCache() { if (entityCache == null) { entityCache = new CacheMap<Object, T>(getMaxCacheSize()); } return entityCache; } /** * Gets the cache for filter results. If no cache exists, it will be * created. * * @return the filter cache (never null). */ synchronized Map<Filter, FilterCacheEntry> getFilterCache() { if (filterCache == null) { filterCache = new CacheMap<Filter, FilterCacheEntry>( MAX_FILTER_CACHE_SIZE); } return filterCache; } /** * Gets the cache entry for the specified filter. If no cache entry exists, * it will be created. * * @param filter * the filter whose cache entry to fetch (may be null). * @return the filter cache entry (never null). */ synchronized FilterCacheEntry getFilterCacheEntry(Filter filter) { if (filter == null) { filter = NULL_FILTER; } FilterCacheEntry e = getFilterCache().get(filter); if (e == null) { e = new FilterCacheEntry(filter); getFilterCache().put(filter, e); } return e; } public synchronized void flush() { if (entityCache != null) { entityCache.clear(); } if (filterCache != null) { filterCache.clear(); } } public int getMaxCacheSize() { return maxCacheSize; } /** * Check whether caching is possible or not. Caching is not possible if * there is a {@link QueryModifierDelegate}, that modifies the filters * applied to queries, attached to the entity provider. * * @return true if caching is possible */ public boolean isCachingPossible(EntityContainer<T> container) { if (container != null && container.getQueryModifierDelegate() != null) { return false; } QueryModifierDelegate d = entityProvider.getQueryModifierDelegate(); if (d != null) { // Try to tell the delegate that filters will be added and pass in // all nulls. If the delegate throws an NPE it most probably // modifies the filters, which means that we cannot reliably cache // anything at this level. try { d.filtersWillBeAdded(null, null, null); } catch (NullPointerException npe) { // The delegate modifies filters return false; } } return true; } /** * Only returns true if both {@link #isCacheEnabled()} and * {@link #isCachingPossible()} are true. * * @return true if the caching mechanism is actually used. */ public boolean usesCache(EntityContainer<T> container) { return isCacheEnabled() && isCachingPossible(container); } public boolean isCacheEnabled() { return cacheEnabled; } /** * Turns the cache on or off. * * @param cacheEnabled * true to turn on the cache, false to turn it off. */ public void setCacheEnabled(boolean cacheEnabled) { this.cacheEnabled = cacheEnabled; if (!cacheEnabled) { flush(); } } /** * Sets the maximum number of items to keep in each cache. This method will * cause any existing caches to be flushed and re-created. * * @param maxSize * the maximum cache size to set. */ public void setMaxCacheSize(int maxSize) { this.maxCacheSize = maxSize; entityCache = null; filterCache = null; } public boolean containsEntity(EntityContainer<T> container, Object entityId, Filter filter) { if (usesCache(container)) { return getFilterCacheEntry(filter).containsId(container, entityId); } else { return entityProvider.doContainsEntity(container, entityId, filter); } } public List<Object> getAllEntityIdentifiers(EntityContainer<T> container, Filter filter, List<SortBy> sortBy) { if (sortBy == null) { sortBy = Collections.emptyList(); } if (usesCache(container)) { return getFilterCacheEntry(filter).getAllIds(container, sortBy); } else { return entityProvider.doGetAllEntityIdentifiers(container, filter, sortBy); } } public synchronized T getEntity(EntityContainer<T> container, Object entityId) { if (usesCache(container)) { T entity = getEntityCache().get(entityId); if (entity == null) { // TODO Should we fetch several entities at once? entity = entityProvider.doGetEntity(entityId); if (entity == null) { return null; } getEntityCache().put(entityId, entity); } return cloneEntityIfNeeded(entity); } else { return entityProvider.doGetEntity(entityId); } } /** * Returns a clone of <code>entity</code> if * {@link #isCloneCachedEntities() } is true. * * @param entity * the entity to clone (must not be null and must be an instance * of {@link Cloneable}). * @return the cloned entity. */ @SuppressWarnings("unchecked") protected T cloneEntityIfNeeded(T entity) { if (isCloneCachedEntities()) { assert entity instanceof Cloneable : "entity is not cloneable"; try { Method m = entity.getClass().getMethod("clone"); T copy = (T) m.invoke(entity); return copy; } catch (Exception e) { throw new UnsupportedOperationException( "Could not clone entity", e); } } else { return entity; } } public boolean isEntitiesDetached() { return usesCache(null) || entityProvider.isEntitiesDetached(); } public boolean isCloneCachedEntities() { return cloneCachedEntities; } public void setCloneCachedEntities(boolean clone) throws UnsupportedOperationException { if (!clone) { this.cloneCachedEntities = false; } else { if (Cloneable.class.isAssignableFrom(entityProvider .getEntityClassMetadata().getMappedClass())) { this.cloneCachedEntities = true; } else { throw new UnsupportedOperationException( "Entity class is not cloneable"); } } } public int getEntityCount(EntityContainer<T> container, Filter filter) { if (usesCache(container)) { return getFilterCacheEntry(filter).getEntityCount(container); } else { return entityProvider.doGetEntityCount(container, filter); } } public Object getEntityIdentifierAt(EntityContainer<T> container, Filter filter, List<SortBy> sortBy, int index) { if (sortBy == null) { sortBy = Collections.emptyList(); } if (usesCache(container)) { return getFilterCacheEntry(filter).getIdAt(container, sortBy, index); } else { return entityProvider.doGetEntityIdentifierAt(container, filter, sortBy, index); } } public Object getFirstEntityIdentifier(EntityContainer<T> container, Filter filter, List<SortBy> sortBy) { if (sortBy == null) { sortBy = Collections.emptyList(); } if (usesCache(container)) { return getFilterCacheEntry(filter).getFirstId(container, sortBy); } else { return entityProvider.doGetFirstEntityIdentifier(container, filter, sortBy); } } public Object getLastEntityIdentifier(EntityContainer<T> container, Filter filter, List<SortBy> sortBy) { if (sortBy == null) { sortBy = Collections.emptyList(); } if (usesCache(container)) { return getFilterCacheEntry(filter).getLastId(container, sortBy); } else { return entityProvider.doGetLastEntityIdentifier(container, filter, sortBy); } } public Object getNextEntityIdentifier(EntityContainer<T> container, Object entityId, Filter filter, List<SortBy> sortBy) { if (sortBy == null) { sortBy = Collections.emptyList(); } if (usesCache(container)) { return getFilterCacheEntry(filter).getNextId(container, entityId, sortBy); } else { return entityProvider.doGetNextEntityIdentifier(container, entityId, filter, sortBy); } } public Object getPreviousEntityIdentifier(EntityContainer<T> container, Object entityId, Filter filter, List<SortBy> sortBy) { if (sortBy == null) { sortBy = Collections.emptyList(); } if (usesCache(container)) { return getFilterCacheEntry(filter).getPreviousId(container, entityId, sortBy); } else { return entityProvider.doGetPreviousEntityIdentifier(container, entityId, filter, sortBy); } } public void invalidateSize() { // TODO review synchronization of this whole class Object[] array = filterCache.keySet().toArray(); for (Object filter : array) { FilterCacheEntry filterCacheEntry = filterCache.get(filter); if (filterCacheEntry.entityCount != null) { synchronized (filterCacheEntry.entityCount) { filterCacheEntry.entityCount = null; } } } } public void entityRemoved(Object entityId) { invalidate(entityId, false); invalidateSize(); } /** * Clears the cache. */ public void clear() { if (entityCache != null) { entityCache.clear(); } if (filterCache != null) { filterCache.clear(); } } }