added tests for recursive cdm entity loading and updated corresponding code
[taxeditor.git] / eu.etaxonomy.taxeditor.remoting / src / main / java / eu / etaxonomy / cdm / api / cache / CdmTransientEntityCacher.java
1 // $Id$
2 /**
3 * Copyright (C) 2014 EDIT
4 * European Distributed Institute of Taxonomy
5 * http://www.e-taxonomy.eu
6 *
7 * The contents of this file are subject to the Mozilla Public License Version 1.1
8 * See LICENSE.TXT at the top of this package for the full license terms.
9 */
10 package eu.etaxonomy.cdm.api.cache;
11
12 import java.lang.reflect.Field;
13 import java.lang.reflect.InvocationTargetException;
14 import java.lang.reflect.Method;
15 import java.util.ArrayList;
16 import java.util.HashSet;
17 import java.util.Iterator;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Set;
21 import java.util.UUID;
22
23 import javassist.util.proxy.ProxyFactory;
24
25 import net.sf.ehcache.Cache;
26 import net.sf.ehcache.Element;
27 import net.sf.ehcache.config.CacheConfiguration;
28 import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
29
30 import org.apache.log4j.Logger;
31 import org.hibernate.collection.spi.PersistentCollection;
32 import org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer;
33 import org.springframework.beans.factory.annotation.Autowired;
34 import org.springframework.util.ReflectionUtils;
35
36 import eu.etaxonomy.cdm.api.service.ICommonService;
37 import eu.etaxonomy.cdm.model.common.CdmBase;
38
39 /**
40 *
41 * This cache guarantees that
42 * - all objects put will be ancestors of CdmBase
43 * - all CdmBase objects in the cache will be already de-proxied
44 * - after any CdmBase object is put in the cache,
45 * all non-null / non-proxy CdmBase objects in the sub-graph
46 * will also be present in the cache.
47 *
48 * @author cmathew
49 * @date 14 Oct 2014
50 *
51 */
52
53 public class CdmTransientEntityCacher extends CdmServiceCacher {
54
55 private static final Logger logger = Logger.getLogger(CdmTransientEntityCacher.class);
56
57 @Autowired
58 private CdmEntityCachingUtils cdmEntityCachingUtils;
59
60 @Autowired
61 private ICommonService commonService;
62
63 private String cacheId;
64
65 private Cache cache;
66
67 private Cache cdmlibModelCache;
68
69 private static boolean isRecursiveEnabled = true;
70
71
72 public CdmTransientEntityCacher(String cacheId) {
73 this.cacheId = cacheId;
74
75 cache = new Cache(getEntityCacheConfiguration(cacheId));
76 getDefaultCacheManager().addCache(cache);
77
78 cdmlibModelCache = CdmRemoteCacheManager.getInstance().getCdmModelGetMethodsCache();
79
80 }
81
82 public CdmTransientEntityCacher(Object obj) {
83 this(obj.getClass().getName() + String.valueOf(obj.hashCode()));
84 }
85
86 /**
87 * Returns the default cache configuration.
88 *
89 * @return
90 */
91 private CacheConfiguration getEntityCacheConfiguration(String cacheId) {
92 // For a better understanding on how to size caches, refer to
93 // http://ehcache.org/documentation/configuration/cache-size
94 return new CacheConfiguration(cacheId, 500)
95 .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU)
96 .eternal(false)
97 // default ttl and tti set to 2 hours
98 .timeToLiveSeconds(60*60*2)
99 .timeToIdleSeconds(60*60*2);
100
101 }
102
103
104 /**
105 * Returns the cache corresponding to the cache id
106 *
107 * @param cacheId
108 * @return
109 */
110 private Cache getCache() {
111 return getDefaultCacheManager().getCache(cacheId);
112 }
113
114 public <T extends Object> T load(T obj, boolean recursive) {
115 if(obj instanceof CdmBase) {
116 return load(obj, recursive);
117 } else if (obj instanceof Map) {
118 return load(obj, recursive);
119 }
120
121 return obj;
122 }
123
124 @SuppressWarnings("unchecked")
125 private <T extends Object> T loadRecursive(T obj, Set<CdmBase> alreadyVisitedEntities) {
126 if(obj instanceof CdmBase) {
127 return loadRecursive(obj, alreadyVisitedEntities);
128 } else if (obj instanceof Map) {
129 return (T) load((Map<T,T>)obj, alreadyVisitedEntities);
130 }
131 logger.info("No caching yet for type " + obj.getClass().getName());
132
133 return obj;
134 }
135
136 public <T extends Object> Map<T,T> load(Map<T,T> map, boolean recursive){
137 if(isRecursiveEnabled && recursive) {
138 logger.info("---- starting recursive load for map");
139 Set<CdmBase> alreadyVisitedEntities = new HashSet<CdmBase>();
140 Map<T,T> cachedMap = load(map, alreadyVisitedEntities);
141 alreadyVisitedEntities.clear();
142 logger.info("---- ending recursive load for cdm entity map \n");
143 return cachedMap;
144 } else {
145 return load(map, null);
146 }
147 }
148
149 private <T extends Object> Map<T,T> load(Map<T,T> map, Set<CdmBase> alreadyVisitedEntities){
150 if(map == null || map.isEmpty()) {
151 return map;
152 }
153
154 int originalMapSize = map.size();
155 Object[] result = new Object[ map.size() * 2 ];
156 Iterator<Map.Entry<T,T>> iter = map.entrySet().iterator();
157 int i=0;
158 while ( iter.hasNext() ) {
159 Map.Entry<T,T> e = (Map.Entry<T,T>) iter.next();
160 result[i++] = e.getKey();
161 result[i++] = e.getValue();
162 }
163
164 for(i=0; i<result.length;i++) {
165 if(alreadyVisitedEntities == null) {
166 result[i] = load(result[i], false);
167 } else {
168 result[i] = loadRecursive(result[i], alreadyVisitedEntities);
169 }
170 }
171 map.clear();
172 for(i = 0; i < originalMapSize; i+=2 ) {
173 map.put(
174 (T)result[i],
175 (T)result[i+1]
176 );
177 }
178 return map;
179 }
180 /**
181 * Puts the (Key,Value) pair of ({@link java.util.UUID}, {@link eu.etaxonomy.cdm.model.common.CdmBase}),
182 * in the cache corresponding to the given cache id
183 *
184 * @param cacheId
185 * @param uuid
186 * @param cdmEntity
187 */
188 public CdmBase load(CdmBase cdmEntity, boolean recursive) {
189
190 if(isRecursiveEnabled && recursive) {
191 logger.info("---- starting recursive load for cdm entity " + cdmEntity.getClass().getName() + " with id " + cdmEntity.getId());
192 Set<CdmBase> alreadyVisitedEntities = new HashSet<CdmBase>();
193 CdmBase cb = loadRecursive(cdmEntity, alreadyVisitedEntities);
194 alreadyVisitedEntities.clear();
195 logger.info("---- ending recursive load for cdm entity " + cdmEntity.getClass().getName() + " with id " + cdmEntity.getId() + "\n");
196 return cb;
197 } else {
198 return load(cdmEntity);
199 }
200
201 // split into two methods
202 // - load(CdmBase cdmBase)
203 // - recursiveLoad(CdmBase cdmbase, List<CdmBase> alreadyVisitedEntities)
204 // : the entity to be visted should added before the recursive call
205 // and deleted after the call to end finally with a list
206 // of size zero at the end
207 // : entity to recurse should be the original entity and
208 // not the cached one
209
210 }
211
212
213 private CdmBase load(CdmBase cdmEntity) {
214 logger.info("loading object of type " + cdmEntity.getClass().getName() + " with id " + cdmEntity.getId());
215
216 // start by looking up the cdm entity in the cache
217 CdmBase cachedCdmEntity = getFromCache(cdmEntity);
218
219 if(cachedCdmEntity != null) {
220 // if cdm entity was found in cache then return ...
221 logger.info(" - object of type " + cdmEntity.getClass().getName() + " with id " + cdmEntity.getId() + " already exists");
222 return cachedCdmEntity;
223 } else {
224 // ... else save the entity in the cache
225 getCache().put(new Element(generateKey(cdmEntity), cdmEntity));
226 logger.info(" - object of type " + cdmEntity.getClass().getName() + " with id " + cdmEntity.getId() + " put in cache");
227 return cdmEntity;
228 }
229 }
230
231 private CdmBase loadRecursive(CdmBase cdmEntity, Set<CdmBase> alreadyVisitedEntities) {
232
233 CdmBase cachedCdmEntity = load(cdmEntity);
234
235 // we want to recursive through the cdmEntity (and not the cachedCdmEntity)
236 // since there could be new initialized object in the cdmEntity sub-graph
237
238 // start by getting the fields from the cdm entity
239 String className = cdmEntity.getClass().getName();
240 CdmModelFieldPropertyFromClass cmgmfc = getFromCdmlibModelCache(className);
241 if(cmgmfc != null) {
242 alreadyVisitedEntities.add(cdmEntity);
243 List<String> fields = cmgmfc.getFields();
244 for(String field : fields) {
245 // retrieve the actual object corresponding to the field.
246 // this object will be either a CdmBase or a Collection / Map
247 // with CdmBase as the generic type
248
249 // In the case that the returned is either a Collection or a Map
250 // the individual objects inside these also need to be loaded
251 // by calling the corresponding cachify method in the
252 // CdmEntityCachingUtils
253
254 CdmBase cdmEntityInSubGraph = getCdmBaseTypeFieldValue(cdmEntity, cachedCdmEntity, field);
255 if(cdmEntityInSubGraph != null && !alreadyVisitedEntities.contains(cdmEntityInSubGraph)) {
256 logger.info("recursive loading object of type " + cdmEntityInSubGraph.getClass().getName() + " with id " + cdmEntityInSubGraph.getId());
257 loadRecursive(cdmEntityInSubGraph, alreadyVisitedEntities);
258 }
259 }
260 }
261 return cachedCdmEntity;
262 }
263
264
265 private CdmBase getCdmBaseTypeFieldValue(CdmBase cdmEntity, CdmBase cachedCdmEntity, String fieldName) {
266
267
268 if(cachedCdmEntity == null) {
269 throw new CdmClientCacheException("When trying to set filed value, the cached cdm entity cannot be null");
270 }
271
272 Class<?> clazz = cdmEntity.getClass();
273 try {
274 // this call will search in the provided class as well as
275 // the super classes until it finds the field
276 Field field = ReflectionUtils.findField(clazz, fieldName);
277 // logger.info("Retrieving field " + field
278 // + " from cdm entity of type " + c.getName()
279 // + " with id " + cdmEntity.getId());
280
281
282 if(field == null) {
283 throw new CdmClientCacheException("Field '" + fieldName
284 + "' not found when searching in class '" + clazz.getName() + "' and its supercalsses");
285 }
286 field.setAccessible(true);
287 Object o = field.get(cdmEntity);
288 // if(o == null) {
289 // logger.info(" - resulting object is null");
290 // } else if(ProxyFactory.isProxyClass(o.getClass())) {
291 // logger.info(" - resulting object is a proxy object");
292 // } else if(o instanceof PersistentCollection) {
293 // logger.info(" - resulting object is a persistent collection object");
294 // } else {
295 // logger.info(" - resulting object is of type " + o.getClass().getName());
296 // }
297 CdmBase cdmEntityInSubGraph = null;
298 if(o != null
299 && !ProxyFactory.isProxyClass(o.getClass())
300 && !(o instanceof PersistentCollection)
301 && CdmBase.class.isAssignableFrom(o.getClass())) {
302 // logger.info("retrieving object of type " + o.getClass().getName()
303 // + " with id " + ((CdmBase)o).getId());
304
305 //FIXME:Remoting add case for collection / map
306 //FIXME:Remoting if cached entity != original entity then
307 // replace original with cache using
308 // field.set and return original entity
309 cdmEntityInSubGraph = (CdmBase)o;
310
311 CdmBase cachedCdmEntityInSubGraph = getFromCache(cdmEntityInSubGraph);
312
313 if(cachedCdmEntityInSubGraph != null) {
314 if(cachedCdmEntityInSubGraph != cdmEntityInSubGraph) {
315 field.set(cachedCdmEntity, cachedCdmEntityInSubGraph);
316 }
317 } else {
318 field.set(cachedCdmEntity, cdmEntityInSubGraph);
319 }
320 }
321 // we return the original cdm entity in the sub graph because we
322 // want to continue to recurse on the input cdm entity graph
323 // and not the one in the cache
324 return cdmEntityInSubGraph;
325 } catch (SecurityException e) {
326 throw new CdmClientCacheException(e);
327 } catch (IllegalArgumentException e) {
328 throw new CdmClientCacheException(e);
329 } catch (IllegalAccessException e) {
330 throw new CdmClientCacheException(e);
331 }
332 }
333
334 public void put(CdmBase cdmEntity) {
335 CdmEntityCacheKey id = new CdmEntityCacheKey(cdmEntity);
336 Element cachedCdmEntityElement = getCacheElement(id);
337
338 if(cachedCdmEntityElement == null) {
339 cachedCdmEntityElement = getCacheElement(cdmEntity.getUuid());
340 if(cachedCdmEntityElement != null) {
341 logger.info("Cdm Entity with id : " + cdmEntity.getId() + " already exists in permanent cache. Ignoring put.");
342 return;
343 }
344 }
345
346 getCache().put(new Element(id, cdmEntity));
347 }
348
349
350 private Element getCacheElement(CdmEntityCacheKey key) {
351 return getCache().get(key);
352 }
353
354 public CdmModelFieldPropertyFromClass getFromCdmlibModelCache(String className) {
355 Element e = cdmlibModelCache.get(className);
356 if (e == null) {
357 return null;
358 } else {
359 return (CdmModelFieldPropertyFromClass) e.getObjectValue();
360 }
361 }
362
363 public CdmBase getFromCache(CdmEntityCacheKey id) {
364 Element e = getCacheElement(id);
365 if (e == null) {
366 return null;
367 } else {
368 return (CdmBase) e.getObjectValue();
369 }
370 }
371
372 public CdmBase getFromCache(Class<? extends CdmBase> clazz, int id) {
373 CdmEntityCacheKey cacheId = generateKey(clazz,id);
374 return getFromCache(cacheId);
375 }
376
377 public CdmBase getFromCache(CdmBase cdmBase) {
378
379 CdmEntityCacheKey cacheId = generateKey(cdmBase);
380
381 CdmBase cachedCdmEntity = getFromCache(cacheId);
382
383 if(cachedCdmEntity == null) {
384 // ... then try the permanent (uuid-based) cache
385 cachedCdmEntity = getFromCache(cdmBase.getUuid());
386 }
387
388 return cachedCdmEntity;
389 }
390
391 public CdmBase getFromCache(CdmBase cdmBase, Class<? extends CdmBase> clazz) {
392
393 cdmBase = CdmBase.deproxy(cdmBase, clazz);
394
395 CdmEntityCacheKey cacheId = generateKey(cdmBase);
396
397 CdmBase cachedCdmEntity = getFromCache(cacheId);
398
399 if(cachedCdmEntity == null) {
400 // ... then try the permanent (uuid-based) cache
401 cachedCdmEntity = getFromCache(cdmBase.getUuid());
402 }
403
404 return cachedCdmEntity;
405 }
406
407 public List<CdmBase> getAllEntities() {
408 List<CdmBase> entities = new ArrayList<CdmBase>();
409 Map<String, CdmBase> elementsMap = getCache().getAllWithLoader(getCache().getKeys(), null);
410 for (Map.Entry<String, CdmBase> entry : elementsMap.entrySet()) {
411 entities.add(entry.getValue());
412 }
413 return entities;
414 }
415
416
417
418
419 /* (non-Javadoc)
420 * @see eu.etaxonomy.cdm.model.ICdmCacher#exists(java.util.UUID)
421 */
422 @Override
423 public boolean exists(UUID uuid) {
424 return (getCacheElement(uuid) != null || super.getCacheElement(uuid) != null);
425 }
426
427 public boolean exists(CdmEntityCacheKey key) {
428 return (getCacheElement(key) != null);
429 }
430
431
432 /* (non-Javadoc)
433 * @see eu.etaxonomy.cdm.model.ICdmCacher#existsAndIsNotNull(java.util.UUID)
434 */
435 @Override
436 public boolean existsAndIsNotNull(UUID uuid) {
437 return getFromCache(uuid) != null;
438 }
439
440 public boolean existsAndIsNotNull(CdmEntityCacheKey id) {
441 return getFromCache(id) != null;
442 }
443
444
445 public static CdmEntityCacheKey generateKey(Class<? extends CdmBase> clazz, int id) {
446 return new CdmEntityCacheKey(clazz, id);
447 }
448
449
450 public static CdmEntityCacheKey generateKey(CdmBase cdmBase) {
451 Class<? extends CdmBase> entityClass = cdmBase.getClass();
452 int id = cdmBase.getId();
453 return new CdmEntityCacheKey(entityClass, id);
454 }
455
456 public static boolean isRecursiveEnabled() {
457 return isRecursiveEnabled;
458 }
459
460 public static void setRecursiveEnabled(boolean ire) {
461 isRecursiveEnabled = ire;
462 }
463
464 }