Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.vaadin.addon.jpacontainer.provider.CachingSupport Maven / Gradle / Ivy
/*
* JPAContainer
* Copyright (C) 2010 Oy IT Mill Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package com.vaadin.addon.jpacontainer.provider;
import com.vaadin.addon.jpacontainer.EntityProvider;
import com.vaadin.addon.jpacontainer.Filter;
import com.vaadin.addon.jpacontainer.Filter.PropertyIdPreprocessor;
import com.vaadin.addon.jpacontainer.SortBy;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
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.Query;
/**
* 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 (IT Mill)
* @since 1.0
*/
class CachingSupport implements Serializable {
private final LocalEntityProvider entityProvider;
private int maxCacheSize = 1000;
private boolean cacheInUse = 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 String toQLString() {
return "";
}
public String toQLString(PropertyIdPreprocessor propertyIdPreprocessor) {
return "";
}
};
/**
* 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 MAX_FILTER_CACHE_SIZE * MAX_SORTBY_CACHE_SIZE
.
*/
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 CachingSupport
for the specified
* entity provider.
*
* @param entityProvider
* the entity provider (never null).
*/
public CachingSupport(LocalEntityProvider 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 (IT Mill)
* @since 1.0
*/
static class IdListEntry implements Serializable {
private static final long serialVersionUID = -3552793234160831297L;
public ArrayList 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 (IT Mill)
* @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, IdListEntry> idListMap = new CacheMap, IdListEntry>(
MAX_SORTBY_CACHE_SIZE);
public Set idSet = new CacheSet(getMaxCacheSize());
/**
* Creates a new FilterCacheEntry
.
*
* @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() {
if (entityCount == null) {
entityCount = entityProvider.doGetEntityCount(
getFilter());
}
return entityCount;
}
/**
* @see EntityProvider#containsEntity(java.lang.Object, com.vaadin.addons.jpacontainer.Filter)
*/
public synchronized boolean containsId(Object entityId) {
if (!idSet.contains(entityId)) {
if (entityProvider.doContainsEntity(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(List sortBy) {
return getIdAt(sortBy, 0);
}
/**
* @see EntityProvider#getNextEntityIdentifier(java.lang.Object, com.vaadin.addons.jpacontainer.Filter, java.util.List)
*/
public synchronized Object getNextId(Object entityId,
List sortBy) {
IdListEntry entry = idListMap.get(sortBy);
if (entry == null) {
entry = new IdListEntry();
entry.idList = new ArrayList();
entry.listOffset = -1;
idListMap.put(sortBy, entry);
}
int index = entry.idList.indexOf(entityId);
if (index == -1) {
entry.idList = new ArrayList(getNextIds(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(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(Object entityId,
List sortBy) {
IdListEntry entry = idListMap.get(sortBy);
if (entry == null) {
entry = new IdListEntry();
entry.idList = new ArrayList();
entry.listOffset = -1;
idListMap.put(sortBy, entry);
}
int index = entry.idList.indexOf(entityId);
if (index == -1) {
List objects = getPreviousIds(getFilter(), sortBy,
entityId,
CHUNK_SIZE);
// We have to reverse the list
entry.idList = new ArrayList(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 objects = getPreviousIds(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 l = new ArrayList();
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(List sortBy) {
return getIdAt(sortBy, getEntityCount() - 1);
}
/**
* Informs the cache that entityId
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) {
if (containsId(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(List sortBy, int index) {
IdListEntry entry = idListMap.get(sortBy);
if (entry == null) {
entry = new IdListEntry();
entry.idList = new ArrayList(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 l = new ArrayList(CHUNK_SIZE + entry.idList.size());
int startFrom = index - CHUNK_SIZE;
if (startFrom < 0) {
startFrom = 0;
}
l.addAll(getIds(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(getFilter(), sortBy, index,
CHUNK_SIZE));
} else {
entry.idList.clear();
entry.idList.addAll(getIds(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 getAllIds(List sortBy) {
IdListEntry entry = idListMap.get(sortBy);
if (entry == null) {
entry = new IdListEntry();
idListMap.put(sortBy, entry);
}
if (!entry.containsAll) {
entry.idList = new ArrayList(getIds(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();
}
/**
* Gets all the identifiers that match filter
, sorted by sortBy
,
* starting with the identifier at position startFrom
and retrieving a maximum
* number of fetchMax
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.
*/
@SuppressWarnings("unchecked")
protected List getIds(Filter filter, List sortBy,
int startFrom, int fetchMax) {
Query query = entityProvider.createFilteredQuery("obj."
+ entityProvider.getEntityClassMetadata().getIdentifierProperty().getName(),
"obj", filter, entityProvider.addPrimaryKeyToSortList(sortBy), false, null);
query.setFirstResult(startFrom);
if (fetchMax > 0) {
query.setMaxResults(fetchMax);
}
return query.getResultList();
}
/**
* Gets all the identifiers that match filter
, sorted by sortBy
,
* starting with the identifier next to startFrom
and retrieving a maximum
* number of fetchMax
items. If startFrom
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 getNextIds(Filter filter, List sortBy,
Object startFrom, int fetchMax) {
Query query = entityProvider.createSiblingQuery(startFrom, filter, sortBy, false);
if (fetchMax > 0) {
query.setMaxResults(fetchMax);
}
return query.getResultList();
}
/**
* Gets all the identifiers that match filter
, sorted backwards by sortBy
,
* starting with the identifier prior to startFrom
and retrieving a maximum number
* of fetchMax
items. If startFrom
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 getPreviousIds(Filter filter, List sortBy,
Object startFrom, int fetchMax) {
Query query = entityProvider.createSiblingQuery(startFrom, filter, sortBy, true);
if (fetchMax > 0) {
query.setMaxResults(fetchMax);
}
return query.getResultList();
}
private Map entityCache;
private Map filterCache;
/**
* A hash map that will remove the oldest items once
* its size reaches a specified max size.
*
* @author Petter Holmström (IT Mill)
* @since 1.0
*/
protected static class CacheMap extends HashMap {
private static final long serialVersionUID = 2900939583997256189L;
private LinkedList addOrder = new LinkedList();
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 (IT Mill)
* @since 1.0
*/
protected static class CacheSet extends HashSet {
private static final long serialVersionUID = 2900939583997256189L;
private LinkedList addOrder = new LinkedList();
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 getEntityCache() {
if (entityCache == null) {
entityCache = new CacheMap(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 getFilterCache() {
if (filterCache == null) {
filterCache = new CacheMap(
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;
}
public boolean isCacheInUse() {
return cacheInUse;
}
/**
* Turns the cache on or off.
* @param cacheInUse true to turn on the cache, false to turn it off.
*/
public void setCacheInUse(boolean cacheInUse) {
this.cacheInUse = cacheInUse;
if (!cacheInUse) {
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(Object entityId, Filter filter) {
if (!isCacheInUse()) {
return entityProvider.doContainsEntity(entityId, filter);
} else {
return getFilterCacheEntry(filter).containsId(entityId);
}
}
public List getAllEntityIdentifiers(Filter filter,
List sortBy) {
if (sortBy == null) {
sortBy = Collections.emptyList();
}
if (!isCacheInUse()) {
return entityProvider.doGetAllEntityIdentifiers(filter, sortBy);
} else {
return getFilterCacheEntry(filter).getAllIds(sortBy);
}
}
public synchronized T getEntity(Object entityId) {
if (!isCacheInUse()) {
return entityProvider.doGetEntity(entityId);
} else {
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);
}
}
/**
* Returns a clone of entity
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 isCacheInUse() || 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(Filter filter) {
if (!isCacheInUse()) {
return entityProvider.doGetEntityCount(filter);
} else {
return getFilterCacheEntry(filter).getEntityCount();
}
}
public Object getEntityIdentifierAt(Filter filter, List sortBy,
int index) {
if (sortBy == null) {
sortBy = Collections.emptyList();
}
if (!isCacheInUse()) {
return entityProvider.doGetEntityIdentifierAt(filter, sortBy, index);
} else {
return getFilterCacheEntry(filter).getIdAt(sortBy, index);
}
}
public Object getFirstEntityIdentifier(Filter filter, List sortBy) {
if (sortBy == null) {
sortBy = Collections.emptyList();
}
if (!isCacheInUse()) {
return entityProvider.doGetFirstEntityIdentifier(filter, sortBy);
} else {
return getFilterCacheEntry(filter).getFirstId(sortBy);
}
}
public Object getLastEntityIdentifier(Filter filter, List sortBy) {
if (sortBy == null) {
sortBy = Collections.emptyList();
}
if (!isCacheInUse()) {
return entityProvider.doGetLastEntityIdentifier(filter, sortBy);
} else {
return getFilterCacheEntry(filter).getLastId(sortBy);
}
}
public Object getNextEntityIdentifier(Object entityId, Filter filter,
List sortBy) {
if (sortBy == null) {
sortBy = Collections.emptyList();
}
if (!isCacheInUse()) {
return entityProvider.doGetNextEntityIdentifier(entityId, filter, sortBy);
} else {
return getFilterCacheEntry(filter).getNextId(entityId, sortBy);
}
}
public Object getPreviousEntityIdentifier(Object entityId, Filter filter,
List sortBy) {
if (sortBy == null) {
sortBy = Collections.emptyList();
}
if (!isCacheInUse()) {
return entityProvider.doGetPreviousEntityIdentifier(entityId, filter, sortBy);
} else {
return getFilterCacheEntry(filter).getPreviousId(entityId, sortBy);
}
}
}