ref #7709 rename putMethods in cacher
[cdmlib.git] / cdmlib-cache / src / main / java / eu / etaxonomy / cdm / cache / CacheLoader.java
1 /**
2 * Copyright (C) 2015 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
8 */
9 package eu.etaxonomy.cdm.cache;
10
11 import java.lang.reflect.Field;
12 import java.util.ArrayList;
13 import java.util.Collection;
14 import java.util.Iterator;
15 import java.util.List;
16 import java.util.Map;
17
18 import org.apache.log4j.Logger;
19 import org.springframework.util.ReflectionUtils;
20
21 import eu.etaxonomy.cdm.api.service.pager.Pager;
22 import eu.etaxonomy.cdm.model.ICdmCacher;
23 import eu.etaxonomy.cdm.model.common.CdmBase;
24 import eu.etaxonomy.cdm.persistence.dto.MergeResult;
25 import net.sf.ehcache.Cache;
26 import net.sf.ehcache.Element;
27
28 /**
29 * @author cmathew
30 * @since 19 Feb 2015
31 */
32 public class CacheLoader {
33
34 private static final Logger logger = Logger.getLogger(CacheLoader.class);
35
36 private static boolean isRecursiveEnabled = true;
37
38 protected final ICdmCacher cdmCacher;
39
40 private final Cache cdmlibModelCache;
41
42 public CacheLoader(ICdmCacher cdmCacher) {
43 this.cdmCacher = cdmCacher;
44 this.cdmlibModelCache = CdmRemoteCacheManager.getInstance().getCdmModelGetMethodsCache();
45 }
46
47 public CdmModelFieldPropertyFromClass getFromCdmlibModelCache(String className) {
48 Element e = cdmlibModelCache.get(className);
49 if (e == null) {
50 return null;
51 } else {
52 return (CdmModelFieldPropertyFromClass) e.getObjectValue();
53 }
54 }
55
56 @SuppressWarnings("unchecked")
57 public <T extends Object> T load(T obj, boolean recursive, boolean update) {
58 if(obj == null) {
59 return null;
60 }
61 if(obj instanceof CdmBase) {
62 return (T) load((CdmBase)obj, recursive, update);
63 } else if (obj instanceof Map) {
64 return (T) load((Map<T,T>)obj, recursive, update);
65 } else if (obj instanceof Collection) {
66 return (T) load((Collection<T>)obj, recursive, update);
67 } else if(obj instanceof Pager) {
68 load(((Pager<?>)obj).getRecords(), recursive, update);
69 return obj;
70 } else if(obj instanceof MergeResult) {
71 return (T) load((MergeResult<CdmBase>)obj, recursive, update);
72 }
73
74 return obj;
75 }
76
77 @SuppressWarnings("unchecked")
78 private <T extends Object> T loadRecursive(T obj, List<Object> alreadyVisitedEntities, boolean update) {
79 if(obj == null) {
80 return null;
81 }
82 if(obj instanceof CdmBase) {
83 return (T) loadRecursive((CdmBase)obj, alreadyVisitedEntities, update);
84 } else if (obj instanceof Map) {
85 return (T) load((Map<T,T>)obj, alreadyVisitedEntities, update);
86 } else if (obj instanceof Collection) {
87 return (T) load((Collection<T>)obj, alreadyVisitedEntities, update);
88 } else if (obj instanceof MergeResult) {
89 return (T) loadRecursive((MergeResult<CdmBase>)obj, alreadyVisitedEntities, update);
90 }
91
92 if (logger.isInfoEnabled()){logger.info("No caching yet for type " + obj.getClass().getName());}
93
94 return obj;
95 }
96
97 public <T extends Object> Map<T,T> load(Map<T,T> map, boolean recursive, boolean update){
98
99 if(isRecursiveEnabled && recursive) {
100 if (logger.isDebugEnabled()){logger.debug("---- starting recursive load for cdm entity map");}
101 List<Object> alreadyVisitedEntities = new ArrayList<>();
102 Map<T,T> cachedMap = load(map, alreadyVisitedEntities, update);
103 alreadyVisitedEntities.clear();
104 if (logger.isDebugEnabled()){logger.debug("---- ending recursive load for cdm entity map \n");}
105 return cachedMap;
106 } else {
107 return load(map, null, update);
108 }
109 }
110
111 private <T extends Object> Map<T,T> load(Map<T,T> map, List<Object> alreadyVisitedEntities, boolean update){
112
113 if(map == null || map.isEmpty()) {
114 return map;
115 }
116
117 Object[] result = new Object[ map.size() * 2 ];
118 Iterator<Map.Entry<T,T>> iter = map.entrySet().iterator();
119 int i=0;
120 // to avoid ConcurrentModificationException
121 if (alreadyVisitedEntities != null){
122 alreadyVisitedEntities.add(map);
123 }
124 while ( iter.hasNext() ) {
125 Map.Entry<T,T> e = iter.next();
126 result[i++] = e.getKey();
127 result[i++] = e.getValue();
128 }
129
130 for(i=0; i<result.length;i++) {
131 if(alreadyVisitedEntities == null) {
132 result[i] = load(result[i], false, update);
133 } else {
134 result[i] = loadRecursive(result[i], alreadyVisitedEntities, update);
135 }
136 }
137 map.clear();
138 for(i = 0; i < result.length; i+=2 ) {
139 map.put(
140 (T)result[i],
141 (T)result[i+1]
142 );
143 }
144 return map;
145 }
146
147 public <T extends Object> Collection<T> load(Collection<T> collection, boolean recursive, boolean update){
148
149 Collection<T> loadedCollection;
150 if(isRecursiveEnabled && recursive) {
151 if (logger.isDebugEnabled()){logger.debug("---- starting recursive load for cdm entity collection");}
152 List<Object> alreadyVisitedEntities = new ArrayList<>();
153 Collection<T> cachedCollection = load(collection, alreadyVisitedEntities, update);
154 alreadyVisitedEntities.clear();
155 if (logger.isDebugEnabled()){logger.debug("---- ending recursive load for cdm entity collection \n");}
156 loadedCollection = cachedCollection;
157 } else {
158 loadedCollection = load(collection, null, update);
159 }
160 return loadedCollection;
161 }
162
163 @SuppressWarnings("unchecked")
164 private <T extends Object> Collection<T> load(Collection<T> collection, List<Object> alreadyVisitedEntities, boolean update) {
165
166 if(collection == null || collection.isEmpty()) {
167 return collection;
168 }
169 int length = collection.size();
170 Object[] result = new Object[length];
171 Iterator<T> collectionItr = collection.iterator();
172 int count = 0;
173 // to avoid ConcurrentModificationException
174 if (alreadyVisitedEntities != null){
175 alreadyVisitedEntities.add(collection);
176 }
177 while(collectionItr.hasNext()) {
178 Object obj = collectionItr.next();
179 if(alreadyVisitedEntities == null) {
180 result[count] = load(obj, false, update);
181 } else {
182 result[count] = loadRecursive(obj, alreadyVisitedEntities, update);
183 }
184
185 count++;
186 }
187
188 collection.clear();
189
190 for ( int i = 0; i < length; i++ ) {
191 collection.add((T)result[i]);
192 }
193
194 return collection;
195 }
196
197 public MergeResult<CdmBase> load(MergeResult<CdmBase> mergeResult, boolean recursive, boolean update) {
198 CdmBase cdmBase = load(mergeResult.getMergedEntity(), recursive, update);
199 load(mergeResult.getNewEntities(), recursive, update);
200 return new MergeResult<>(cdmBase, mergeResult.getNewEntities());
201 }
202
203 public MergeResult<CdmBase> loadRecursive(MergeResult<CdmBase> mergeResult, List<Object> alreadyVisitedEntities, boolean update) {
204 CdmBase cdmBase = loadRecursive(mergeResult.getMergedEntity(), alreadyVisitedEntities, update);
205 loadRecursive(mergeResult.getNewEntities(), alreadyVisitedEntities, update);
206 return new MergeResult<>(cdmBase, mergeResult.getNewEntities());
207 }
208
209 /**
210 * Loads the {@link eu.etaxonomy.cdm.model.common.CdmBase cdmEntity}) in the
211 * cache.
212 * <p>
213 * <b>WARNING: Recursive updating of the cached entity will not take place
214 * in case there is a cached entity which is the same/identical object as
215 * <code>cdmEntity</code>.</b>
216 *
217 * For in depth details on the mechanism see
218 * {@link #loadRecursive(CdmBase, List, boolean)} and
219 * {@link #getCdmBaseTypeFieldValue(CdmBase, CdmBase, String, List, boolean)}
220 *
221 * @param cdmEntity
222 * the entity to be put into the cache
223 * @param recursive
224 * if <code>true</code>, the cache loader will load the whole
225 * entity graph recursively into the cache
226 * @param update
227 * all fields of the cached entity will be overwritten by setting
228 * them to the value of the cdm entity being loaded
229 */
230 public <T extends CdmBase> T load(T cdmEntity, boolean recursive, boolean update) {
231 if(cdmEntity == null) {
232 return null;
233 }
234
235 // start by looking up the cdm entity in the cache
236 T cachedCdmEntity = cdmCacher.getFromCache(cdmEntity);
237
238 if(cachedCdmEntity != null) {
239 // if cdm entity was found in cache then
240 logger.debug(" - object of type " + cdmEntity.getClass().getName() + " with id " + cdmEntity.getId() + " already exists");
241 // .. return if the cached and input objects are identical, else (this is a newly loaded object so) continue
242 if(cachedCdmEntity == cdmEntity) {
243 return cachedCdmEntity;
244 }
245 }
246
247 T loadedCdmBase;
248 if(isRecursiveEnabled && recursive) {
249 logger.debug("---- starting recursive load for cdm entity " + cdmEntity.getClass().getName() + " with id " + cdmEntity.getId());
250 List<Object> alreadyVisitedEntities = new ArrayList<>();
251 T cb = loadRecursive(cdmEntity, alreadyVisitedEntities, update);
252 alreadyVisitedEntities.clear();
253 logger.debug("---- ending recursive load for cdm entity " + cdmEntity.getClass().getName() + " with id " + cdmEntity.getId() + "\n");
254 loadedCdmBase = cb;
255 } else {
256 loadedCdmBase = put(cdmEntity);
257 }
258 return loadedCdmBase;
259
260 }
261
262 protected <T extends CdmBase> T put(T cdmEntity) {
263 logger.debug("loading object of type " + cdmEntity.getClass().getName() + " with id " + cdmEntity.getId());
264 cdmCacher.putToCache((CdmBase)ProxyUtils.deproxy(cdmEntity));
265 return cdmCacher.getFromCache(cdmEntity);
266 }
267
268 /**
269 * Load the <code>cdmEntity</code> graph recursively into the cache and
270 * updates entities which are already in the cache depending on the value of
271 * <code>update</code>, for more in depth details on this mechanism see
272 * {@link #getCdmBaseTypeFieldValue(CdmBase, CdmBase, String, List, boolean)}.
273 *
274 * @param cdmEntity
275 * the entity to be loaded into the cache
276 * @param alreadyVisitedEntities
277 * protocol list of entities already visited during loading an
278 * entity graph recursively into the cache.
279 *
280 * @param update
281 * all fields of the cached entity will be overwritten by setting
282 * them to the value of the cdm entity being loaded
283 * @return
284 * The cached object which is identical with the input entity in case
285 * the object did not yet exist in the cache
286 */
287 private <T extends CdmBase> T loadRecursive(T cdmEntity, List<Object> alreadyVisitedEntities, boolean update) {
288
289 T cachedCdmEntity = put(cdmEntity);
290
291 // we want to recurse through the cdmEntity (and not the cachedCdmEntity)
292 // since there could be new or deleted objects in the cdmEntity sub-graph
293
294 // start by getting the fields from the cdm entity
295 //TODO improve generics for deproxyOrNull, probably need to split the method
296 @SuppressWarnings("unchecked")
297 T deproxiedEntity = (T)ProxyUtils.deproxyOrNull(cdmEntity);
298 if(deproxiedEntity != null){
299 String className = deproxiedEntity.getClass().getName();
300 CdmModelFieldPropertyFromClass cmfpfc = getFromCdmlibModelCache(className);
301 if(cmfpfc != null) {
302 alreadyVisitedEntities.add(cdmEntity);
303 List<String> fields = cmfpfc.getFields();
304 for(String field : fields) {
305 // retrieve the actual object corresponding to the field.
306 // this object will be either a CdmBase or a Collection / Map
307 // with CdmBase as the generic type
308
309 CdmBase cdmEntityInSubGraph = getCdmBaseTypeFieldValue(deproxiedEntity, cachedCdmEntity, field, alreadyVisitedEntities, update);
310 if(cdmEntityInSubGraph != null) {
311 //checkForIdenticalCdmEntity(alreadyVisitedEntities, cdmEntityInSubGraph);
312 if(!checkForIdenticalCdmEntity(alreadyVisitedEntities, cdmEntityInSubGraph)) {
313 logger.debug("recursive loading object of type " + cdmEntityInSubGraph.getClass().getName() + " with id " + cdmEntityInSubGraph.getId());
314 loadRecursive(cdmEntityInSubGraph, alreadyVisitedEntities, update);
315 } else {
316 logger.debug("object of type " + cdmEntityInSubGraph.getClass().getName() + " with id " + cdmEntityInSubGraph.getId() + " already visited");
317 }
318 }
319 }
320 } else {
321 throw new CdmClientCacheException("CdmEntity with class " + cdmEntity.getClass().getName() + " is not found in the cdmlib model cache. " +
322 "The cache may be corrupted or not in sync with the latest model version" );
323 }
324 } else { //deproxiedEntity == null
325 logger.debug("ignoring uninitlialized proxy " + cdmEntity.getClass() + "#" + cdmEntity.getId());
326 }
327
328 return cachedCdmEntity;
329 }
330
331 /**
332 * All fields of the <code>cdmEntity</code> containing proxy objects will be
333 * set to the un-proxied field value. If <code>update</code> is enabled the
334 * value of the cached entity will be overwritten by the value of the
335 * <code>cdmEntity</code>. In case the cached field value contains a proxy
336 * object the value will always be overwritten (Q: This might only occur in
337 * case of uninitialized proxies, since initialized proxies are expected to
338 * be replaced by the target entity.)
339 *
340 * @param cdmEntity
341 * the entity to be loaded into the cache
342 * @param cachedCdmEntity
343 * the entity which resides in the cache
344 * @param fieldName
345 * the field name to operate on
346 * @param alreadyVisitedEntities
347 * protocol list of entities already visited during loading an
348 * entity graph recursively into the cache.
349 * @param update
350 * all fields of the cached entity will be overwritten by setting
351 * them to the value of the cdm entity being loaded
352 * @return
353 */
354 private CdmBase getCdmBaseTypeFieldValue(CdmBase cdmEntity,
355 CdmBase cachedCdmEntity,
356 String fieldName,
357 List<Object> alreadyVisitedEntities,
358 boolean update) {
359
360 // this method attempts to make sure that for any two objects found in
361 // the object graph, if they are equal then they should also be the same,
362 // which is crucial for the merge to work
363 if(cachedCdmEntity == null) {
364 throw new CdmClientCacheException("When trying to set field value, the cached cdm entity cannot be null");
365 }
366
367 Class<?> clazz = cdmEntity.getClass();
368 try {
369 // this call will search in the provided class as well as
370 // the super classes until it finds the field
371 Field field = ReflectionUtils.findField(clazz, fieldName);
372
373 if(field == null) {
374 throw new CdmClientCacheException("Field '" + fieldName
375 + "' not found when searching in class '" + clazz.getName() + "' and its superclasses");
376 }
377 field.setAccessible(true);
378 Object obj = field.get(cdmEntity);
379 // resetting the value in cdm entity to the deproxied object
380 obj = ProxyUtils.deproxy(obj);
381 field.set(cdmEntity, obj);
382 Object cachedObj = field.get(cachedCdmEntity);
383 CdmBase cdmEntityInSubGraph = null;
384
385 if(!ProxyUtils.isUninitializedProxy(obj) && (update || ProxyUtils.isUninitializedProxy(cachedObj))) {
386 // if we are in update mode we have to make the field of the cached entity
387 // up-to-date by setting it to the value of the cdm entity being loaded
388 //
389 // if the cdm entity is a proxy then we always update to make sure that
390 // newly created entities are always up-to-date
391 //
392 // NOTE : the field is overridden in the case of the exception
393 // found below
394 field.set(cachedCdmEntity, obj);
395 }
396
397 if(obj != null && !ProxyUtils.isUninitializedProxy(obj)) {
398 if(CdmBase.class.isAssignableFrom(obj.getClass())) {
399 logger.debug("found initialised cdm entity '" + fieldName + "' in object of type " + clazz.getName() + " with id " + cdmEntity.getId());
400
401 cdmEntityInSubGraph = (CdmBase)obj;
402 CdmBase cachedCdmEntityInSubGraph = cdmCacher.getFromCache(cdmEntityInSubGraph);
403
404 if(cachedCdmEntityInSubGraph != null) {
405 if(cachedCdmEntityInSubGraph != cdmEntityInSubGraph) {
406 // exception : is the case where
407 // the field has been already initialized, cached and
408 // is not the same as the one in the cache, in which case we set the value
409 // of the field to the one found in the cache
410 logger.debug("setting cached + real value to '" + fieldName + "' in object of type " + clazz.getName() + " with id " + cdmEntity.getId());
411 field.set(cachedCdmEntity, cachedCdmEntityInSubGraph);
412 field.set(cdmEntity, cachedCdmEntityInSubGraph);
413 } else {
414 // since the field value object in cdmEntity
415 // is the same as the field value object in cachedCdmEntity
416 // we are sure that the subgraph is also correctly loaded,
417 // so we can exit the recursion
418 return null;
419 }
420 }
421 } else if(obj instanceof Map && !checkForIdenticalCdmEntity(alreadyVisitedEntities, obj)) {
422 loadRecursive((Map<?,?>)obj, alreadyVisitedEntities, update);
423 } else if(obj instanceof Collection && !checkForIdenticalCdmEntity(alreadyVisitedEntities, obj)) {
424 loadRecursive((Collection<?>)obj, alreadyVisitedEntities, update);
425 }
426 }
427 // we return the original cdm entity in the sub graph because we
428 // want to continue to recurse on the input cdm entity graph
429 // and not the one in the cache
430 return cdmEntityInSubGraph;
431 } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
432 throw new CdmClientCacheException(e);
433 }
434 }
435
436 private boolean checkForIdenticalCdmEntity(List<Object> objList, Object objToCompare) {
437 if(objToCompare != null) {
438 for(Object obj : objList) {
439 if(obj == objToCompare) {
440 return true;
441 }
442 }
443 }
444 return false;
445 }
446
447 public static boolean isRecursiveEnabled() {
448 return isRecursiveEnabled;
449 }
450
451 public static void setRecursiveEnabled(boolean recursiveEnabled) {
452 isRecursiveEnabled = recursiveEnabled;
453 }
454 }