Project

General

Profile

Download (23.8 KB) Statistics
| Branch: | Tag: | Revision:
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.logging.log4j.LogManager;
19
import org.apache.logging.log4j.Logger;
20
import org.springframework.util.ReflectionUtils;
21

    
22
import eu.etaxonomy.cdm.api.service.pager.Pager;
23
import eu.etaxonomy.cdm.model.ICdmCacher;
24
import eu.etaxonomy.cdm.model.common.CdmBase;
25
import eu.etaxonomy.cdm.persistence.dto.MergeResult;
26
import net.sf.ehcache.Cache;
27
import net.sf.ehcache.Element;
28

    
29
/**
30
 * @author cmathew
31
 * @since 19 Feb 2015
32
 */
33
public class CacheLoader {
34

    
35
    private static final Logger logger = LogManager.getLogger(CacheLoader.class);
36

    
37
    private static boolean isRecursiveEnabled = true;
38

    
39
    protected final ICdmCacher cdmCacher;
40

    
41
    private final Cache cdmlibModelCache;
42

    
43
    public CacheLoader(ICdmCacher cdmCacher) {
44
        this.cdmCacher = cdmCacher;
45
        this.cdmlibModelCache = CdmRemoteCacheManager.getInstance().getCdmModelGetMethodsCache();
46
    }
47

    
48
    public CdmModelFieldPropertyFromClass getFromCdmlibModelCache(String className) {
49
        Element e = cdmlibModelCache.get(className);
50
        if (e == null) {
51
            return null;
52
        } else {
53
            return (CdmModelFieldPropertyFromClass) e.getObjectValue();
54
        }
55
    }
56

    
57
    @SuppressWarnings("unchecked")
58
    public <T extends Object> T load(T obj, boolean recursive, boolean update) {
59
        if(obj == null) {
60
            return null;
61
        }
62
        if(obj instanceof CdmBase) {
63
            return (T) load((CdmBase)obj, recursive, update);
64
        } else if (obj instanceof Map) {
65
            return (T) load((Map<T,T>)obj, recursive, update);
66
        } else if (obj instanceof Collection) {
67
            return (T) load((Collection<T>)obj, recursive, update);
68
        } else if(obj instanceof Pager) {
69
            load(((Pager<?>)obj).getRecords(), recursive, update);
70
            return obj;
71
        } else if(obj instanceof MergeResult) {
72
            return (T) load((MergeResult<CdmBase>)obj, recursive, update);
73
        }
74

    
75
        return obj;
76
    }
77

    
78
    @SuppressWarnings("unchecked")
79
    private <T extends Object> T loadRecursiveAny(T obj, List<Object> alreadyVisitedEntities, boolean update) {
80
        if(obj == null) {
81
            return null;
82
        }
83
        if(obj instanceof CdmBase) {
84
            return (T) loadRecursive((CdmBase)obj, alreadyVisitedEntities, update);
85
        } else if (obj instanceof Map) {
86
            return (T) loadRecursiveMap((Map<T,T>)obj, alreadyVisitedEntities, update);
87
        } else if (obj instanceof Collection) {
88
            return (T) loadRecursiveCollection((Collection<T>)obj, alreadyVisitedEntities, update);
89
        } else if (obj instanceof MergeResult) {
90
            return (T) loadRecursive((MergeResult<CdmBase>)obj, alreadyVisitedEntities, update);
91
        }
92

    
93
        if (logger.isInfoEnabled()){logger.info("No caching yet for type " + obj.getClass().getName());}
94

    
95
        return obj;
96
    }
97

    
98
    public <T extends Object> Map<T,T> load(Map<T,T> map, boolean recursive, boolean update){
99

    
100
        if(isRecursiveEnabled && recursive) {
101
            if (logger.isDebugEnabled()){logger.debug("---- starting recursive load for cdm entity map");}
102
            List<Object> alreadyVisitedEntities = new ArrayList<>();
103
            Map<T,T> cachedMap = loadRecursiveMap(map, alreadyVisitedEntities, update);
104
            alreadyVisitedEntities.clear();
105
            if (logger.isDebugEnabled()){logger.debug("---- ending recursive load for cdm entity map \n");}
106
            return cachedMap;
107
        } else {
108
            return loadRecursiveMap(map, null, update);
109
        }
110
    }
111

    
112
    /**
113
     * Loads the map recursively into the cache. If alreadyVisitedEntities is <code>null</code> only
114
     * the map elements are loaded without recursion.
115
     */
116
    private <T extends Object> Map<T,T> loadRecursiveMap(Map<T,T> map, List<Object> alreadyVisitedEntities, boolean update){
117

    
118
        if(map == null || map.isEmpty()) {
119
            return map;
120
        }
121

    
122
        Object[] result = new Object[ map.size() * 2 ];
123
        Iterator<Map.Entry<T,T>> iter = map.entrySet().iterator();
124
        int i=0;
125
        // to avoid ConcurrentModificationException
126
        if (alreadyVisitedEntities != null){
127
            alreadyVisitedEntities.add(map);
128
        }
129
        while ( iter.hasNext() ) {
130
            Map.Entry<T,T> e = iter.next();
131
            result[i++] = e.getKey();
132
            result[i++] = e.getValue();
133
        }
134

    
135
        for(i=0; i<result.length;i++) {
136
            if(alreadyVisitedEntities == null) {
137
                result[i] = load(result[i], false, update);
138
            } else {
139
                result[i] = loadRecursiveAny(result[i], alreadyVisitedEntities, update);
140
            }
141
        }
142
        map.clear();
143
        for(i = 0; i < result.length; i+=2 ) {
144
            map.put(
145
                    (T)result[i],
146
                    (T)result[i+1]
147
                    );
148
        }
149
        return map;
150
    }
151

    
152
    public <T extends Object> Collection<T> load(Collection<T> collection,  boolean recursive, boolean update){
153

    
154
        Collection<T> loadedCollection;
155
        if(isRecursiveEnabled && recursive) {
156
            if (logger.isDebugEnabled()){logger.debug("---- starting recursive load for cdm entity collection");}
157
            List<Object> alreadyVisitedEntities = new ArrayList<>();
158
            Collection<T> cachedCollection = loadRecursiveCollection(collection, alreadyVisitedEntities, update);
159
            alreadyVisitedEntities.clear();
160
            if (logger.isDebugEnabled()){logger.debug("---- ending recursive load for cdm entity collection \n");}
161
            loadedCollection = cachedCollection;
162
        } else {
163
            loadedCollection = loadRecursiveCollection(collection, null, update);
164
        }
165
        return loadedCollection;
166
    }
167

    
168
    /**
169
     * Loads the collection recursively into the cache. If alreadyVisitedEntities is <code>null</code> only
170
     * the collection elements are loaded without recursion.
171
     */
172
    @SuppressWarnings("unchecked")
173
    private <T extends Object> Collection<T> loadRecursiveCollection(Collection<T> collection, List<Object> alreadyVisitedEntities, boolean update) {
174

    
175
        if(collection == null || collection.isEmpty()) {
176
            return collection;
177
        }
178
        int length = collection.size();
179
        Object[] result = new Object[length];
180
        Iterator<T> collectionItr = collection.iterator();
181
        int count = 0;
182
        // to avoid ConcurrentModificationException
183
        if (alreadyVisitedEntities != null){
184
            alreadyVisitedEntities.add(collection);
185
        }
186
        while(collectionItr.hasNext()) {
187
            Object obj = collectionItr.next();
188
            if(alreadyVisitedEntities == null) {
189
                result[count] = load(obj, false, update);
190
            } else {
191
                result[count] = loadRecursiveAny(obj, alreadyVisitedEntities, update);
192
            }
193

    
194
            count++;
195
        }
196

    
197
        collection.clear();
198

    
199
        for ( int i = 0; i < length; i++ ) {
200
            collection.add((T)result[i]);
201
        }
202

    
203
        return collection;
204
    }
205

    
206
    public MergeResult<CdmBase> load(MergeResult<CdmBase> mergeResult, boolean recursive, boolean update) {
207
        CdmBase cdmBase = load(mergeResult.getMergedEntity(), recursive, update);
208
        load(mergeResult.getNewEntities(), recursive, update);
209
        return new MergeResult<>(cdmBase, mergeResult.getNewEntities());
210
    }
211

    
212
    /**
213
     * Loads the elements in the merge result into the cache. If alreadyVisitedEntities is <code>null</code> only
214
     * the merge result elements are loaded without recursion.
215
     */
216
    private MergeResult<CdmBase> loadRecursive(MergeResult<CdmBase> mergeResult, List<Object> alreadyVisitedEntities, boolean update) {
217
        CdmBase cdmBase = loadRecursive(mergeResult.getMergedEntity(), alreadyVisitedEntities, update);
218
        loadRecursiveCollection(mergeResult.getNewEntities(), alreadyVisitedEntities, update);
219
        return new MergeResult<>(cdmBase, mergeResult.getNewEntities());
220
    }
221

    
222
    /**
223
     * Loads the {@link eu.etaxonomy.cdm.model.common.CdmBase cdmEntity}) in the
224
     * cache.
225
     * <p>
226
     * <b>WARNING: Recursive updating of the cached entity will not take place
227
     * in case there is a cached entity which is the same/identical object as
228
     * <code>cdmEntity</code>.</b>
229
     *
230
     * For in depth details on the mechanism see
231
     * {@link #loadRecursive(CdmBase, List, boolean)} and
232
     * {@link #getAndUpdateFieldValue(CdmBase, CdmBase, String, List, boolean)}
233
     *
234
     * @param cdmEntity
235
     *            the entity to be put into the cache
236
     * @param recursive
237
     *            if <code>true</code>, the cache loader will load the whole
238
     *            entity graph recursively into the cache
239
     * @param update
240
     *            all fields of the cached entity will be overwritten by setting
241
     *            them to the value of the cdm entity being loaded
242
     */
243
    public <T extends CdmBase> T load(T cdmEntity, boolean recursive, boolean update) {
244
        if(cdmEntity == null) {
245
            return null;
246
        }
247

    
248
        // start by looking up the cdm entity in the cache
249
        T cachedCdmEntity = cdmCacher.getFromCache(cdmEntity);
250

    
251
        if(cachedCdmEntity != null) {
252
            // if cdm entity was found in cache then
253
            if (logger.isDebugEnabled()){logger.debug(" - object " + cdmEntity.getClass().getSimpleName() + "[" + cdmEntity.getId() + "|" + cdmEntity.getUuid() + "] already exists. ("+ System.identityHashCode(cdmEntity) +") already exists");}
254
            // .. return if the cached and input objects are identical, else (this is a newly loaded object so) continue
255
            if(cachedCdmEntity == cdmEntity) {
256
                return cachedCdmEntity;
257
            }
258
        }
259

    
260
        T loadedCdmBase;
261
        if(isRecursiveEnabled && recursive) {
262
            if (logger.isDebugEnabled()){logger.debug("---- starting recursive load for cdm entity " + cdmEntity.getClass().getSimpleName() + " with id " + cdmEntity.getId() + "("+ System.identityHashCode(cdmEntity) +")");}
263
            List<Object> alreadyVisitedEntities = new ArrayList<>();
264
            T cb = loadRecursive(cdmEntity, alreadyVisitedEntities, update);
265
            alreadyVisitedEntities.clear();
266
            if (logger.isDebugEnabled()){logger.debug("---- ending recursive load for cdm entity " + cdmEntity.getClass().getSimpleName() + " with id " + cdmEntity.getId() + "("+ System.identityHashCode(cdmEntity) +")\n");}
267
            loadedCdmBase =  cb;
268
        } else {
269
            loadedCdmBase = putToCache(cdmEntity);
270
        }
271
        return loadedCdmBase;
272

    
273
//        if(isRecursiveEnabled && recursive) {
274
//            if (logger.isDebugEnabled()){logger.debug("---- starting recursive load for cdm entity " + cdmEntity.getClass().getSimpleName() + " with id " + cdmEntity.getId() + "("+ System.identityHashCode(cdmEntity) +")" );}
275
//            List<Object> alreadyVisitedEntities = new ArrayList<>();
276
//            loadedCdmBase = loadRecursive(cdmEntity, alreadyVisitedEntities, update);
277
//            alreadyVisitedEntities.clear();
278
//            if (logger.isDebugEnabled()){logger.debug("---- ending recursive load for cdm entity " + cdmEntity.getClass().getSimpleName() + " with id " + cdmEntity.getId() + "("+ System.identityHashCode(cdmEntity) +")" + "\n");}
279
//        } else if (update){
280
//            if (logger.isDebugEnabled()){logger.debug("---- update cdm entity " + cdmEntity.getClass().getSimpleName() + " with id " + cdmEntity.getId() + "("+ System.identityHashCode(cdmEntity) +")" );}
281
//            loadedCdmBase = loadRecursive(cdmEntity, null, update);
282
//            if (logger.isDebugEnabled()){logger.debug("---- ending update for cdm entity " + cdmEntity.getClass().getSimpleName() + " with id " + cdmEntity.getId() + + "("+ System.identityHashCode(cdmEntity) +")\n");}
283
//        } else {
284
//            loadedCdmBase = putToCache(cdmEntity);
285
//        }
286
//        return loadedCdmBase;
287

    
288
    }
289

    
290
    /**
291
     * Puts the entity to the cache if it does not yet exist and returns the cached entity.
292
     */
293
    protected <T extends CdmBase> T putToCache(T cdmEntity) {
294
        if (logger.isDebugEnabled()){logger.debug("put object of type " + cdmEntity.getClass().getSimpleName() + " with id " + cdmEntity.getId() + "("+ System.identityHashCode(cdmEntity) +") to cache ");}
295
        cdmCacher.putToCache(ProxyUtils.deproxyIfInitialized(cdmEntity));
296
        return cdmCacher.getFromCache(cdmEntity);
297
    }
298

    
299
    /**
300
     * Load the <code>cdmEntity</code> graph recursively into the cache and
301
     * updates entities which are already in the cache depending on the value of
302
     * <code>update</code>, for more in depth details on this mechanism see
303
     * {@link #getAndUpdateFieldValue(CdmBase, CdmBase, String, List, boolean)}.
304
     *
305
     * @param cdmEntity
306
     *            the entity to be loaded into the cache
307
     * @param alreadyVisitedEntities
308
     *            protocol list of entities already visited during loading an
309
     *            entity graph recursively into the cache. If <code>null</code>
310
     *            only the entity itself is loaded without recursion.
311
     * @param update
312
     *            all fields of the cached entity will be overwritten by setting
313
     *            them to the value of the cdm entity being loaded
314
     * @return
315
     *            The cached object which is identical with the input entity in case
316
     *            the object did not yet exist in the cache
317
     */
318
    private <T extends CdmBase> T loadRecursive(T cdmEntity,  List<Object> alreadyVisitedEntities, boolean update) {
319
        if (cdmCacher.ignoreRecursiveLoad(cdmEntity)){
320
            if (logger.isDebugEnabled()){logger.debug("recursive load for " + cdmEntity.getClass() + "#" + cdmEntity.getId()+ "("+ System.identityHashCode(cdmEntity) +") which is usually ignored by the cache");}
321
        }
322
        T cachedCdmEntity = putToCache(cdmEntity);
323

    
324
        // we want to recurse through the cdmEntity (and not the cachedCdmEntity)
325
        // since there could be new or deleted objects in the cdmEntity sub-graph
326

    
327
        // start by getting the fields from the cdm entity
328
        T deproxiedEntity = ProxyUtils.deproxyOrNull(cdmEntity);
329
        if(deproxiedEntity == null){
330
            if (logger.isDebugEnabled()){logger.debug("ignoring uninitlialized proxy " + cdmEntity.getClass() + "#" + cdmEntity.getId()+ "("+ System.identityHashCode(cdmEntity) +")" );}
331
        }else{
332
            String className = deproxiedEntity.getClass().getName();
333
            CdmModelFieldPropertyFromClass classFields = getFromCdmlibModelCache(className);
334
            if(classFields != null) {
335
                if (alreadyVisitedEntities != null){
336
                    alreadyVisitedEntities.add(cdmEntity);
337
                }
338
                List<String> fields = classFields.getFields();
339
                for(String field : fields) {
340
                    loadField(alreadyVisitedEntities, update, cachedCdmEntity, deproxiedEntity, field);
341
                }
342
            } else {
343
                throw new CdmClientCacheException("CdmEntity with class " + cdmEntity.getClass().getName() + " is not found in the cdmlib model cache. " +
344
                        "The cache may be corrupted or not in sync with the latest model version" );
345
            }
346
        }
347

    
348
        return cachedCdmEntity;
349
    }
350

    
351
    /**
352
     * Updates and loads recursively the value of the given field in the entity. If alreadyVisitedEntities
353
     * is <code>null</code> no recursion will take place.
354
     */
355
    private <T extends CdmBase> void loadField(List<Object> alreadyVisitedEntities, boolean update, T cachedCdmEntity,
356
            T deproxiedEntity, String field) {
357
        // retrieve the actual object corresponding to the field.
358
        // this object will be either a CdmBase or a Collection / Map
359
        // with CdmBase as the generic type
360
        Object fieldValue = getAndUpdateFieldValue(deproxiedEntity, cachedCdmEntity, field, alreadyVisitedEntities, update);
361
        //Map and Collection are loaded recursivly in getAndUpdateFieldValue already
362
        if (fieldValue != null) {
363
            CdmBase cdmEntityInSubGraph = (CdmBase)fieldValue;
364
            if(!entityAlreadyVisisted(alreadyVisitedEntities, cdmEntityInSubGraph) ) {
365
                if(cdmCacher.ignoreRecursiveLoad(cdmEntityInSubGraph)){
366
                    if (logger.isDebugEnabled()){logger.debug("recursive load of type " + cdmEntityInSubGraph.getClass().getSimpleName() + " with id " + cdmEntityInSubGraph.getId() + "("+ System.identityHashCode(cdmEntityInSubGraph) +") ignored");}
367
                }else{
368
                    if (logger.isDebugEnabled()){logger.debug("recursive loading object of type " + cdmEntityInSubGraph.getClass().getSimpleName() + " with id " + cdmEntityInSubGraph.getId() + "("+ System.identityHashCode(cdmEntityInSubGraph) +")" );}
369
                    loadRecursive(cdmEntityInSubGraph, alreadyVisitedEntities, update);
370
                }
371
            } else {
372
                if (logger.isDebugEnabled()){logger.debug("object of type " + cdmEntityInSubGraph.getClass().getSimpleName() + " with id " + cdmEntityInSubGraph.getId() + "("+ System.identityHashCode(cdmEntityInSubGraph) +") already visited");}
373
            }
374
//        } else if (fieldValue != null){
375
//            throw new IllegalArgumentException("Unhandled field value of type " + fieldValue.getClass().getName() + " for field " + field);
376
        }
377
    }
378

    
379
    /**
380
     * All fields of the <code>cdmEntity</code> containing proxy objects will be
381
     * set to the un-proxied field value. If <code>update</code> is enabled the
382
     * value of the cached entity will be overwritten by the value of the
383
     * <code>cdmEntity</code>. In case the cached field value contains a proxy
384
     * object the value will always be overwritten (Q: This might only occur in
385
     * case of uninitialized proxies, since initialized proxies are expected to
386
     * be replaced by the target entity.)
387
     *
388
     * @param cdmEntity
389
     *            the entity to be loaded into the cache
390
     * @param cachedCdmEntity
391
     *            the entity which resides in the cache
392
     * @param fieldName
393
     *            the field name to operate on
394
     * @param alreadyVisitedEntities
395
     *            protocol list of entities already visited during loading an
396
     *            entity graph recursively into the cache.
397
     * @param update
398
     *            all fields of the cached entity will be overwritten by setting
399
     *            them to the value of the cdm entity being loaded
400
     * @return
401
     */
402
    private CdmBase getAndUpdateFieldValue(CdmBase cdmEntity,
403
            CdmBase cachedCdmEntity,
404
            String fieldName,
405
            List<Object> alreadyVisitedEntities,
406
            boolean update) {
407

    
408
        // this method attempts to make sure that for any two objects found in
409
        // the object graph, if they are equal then they should also be the same,
410
        // which is crucial for the merge to work
411
        if(cachedCdmEntity == null) {
412
            throw new CdmClientCacheException("When trying to set field value, the cached cdm entity cannot be null");
413
        }
414

    
415
        Class<?> clazz = cdmEntity.getClass();
416
        try {
417
            // this call will search in the provided class as well as
418
            // the super classes until it finds the field
419
            Field field = ReflectionUtils.findField(clazz, fieldName);
420

    
421
            if(field == null) {
422
                throw new CdmClientCacheException("Field '" + fieldName
423
                        + "' not found when searching in class '" + clazz.getName() + "' and its superclasses");
424
            }
425
            field.setAccessible(true);
426
            Object obj = field.get(cdmEntity);
427
            // resetting the value in cdm entity to the deproxied object
428
            obj = ProxyUtils.deproxyIfInitialized(obj);
429
            field.set(cdmEntity, obj);
430
            Object cachedObj = field.get(cachedCdmEntity);
431
            CdmBase cdmEntityInSubGraph = null;
432

    
433
            if(!ProxyUtils.isUninitializedProxy(obj) && (update || ProxyUtils.isUninitializedProxy(cachedObj))) {
434
                // if we are in update mode we have to make the field of the cached entity
435
                // up-to-date by setting it to the value of the cdm entity being loaded
436
                //
437
                // if the cdm entity is a proxy then we always update to make sure that
438
                // newly created entities are always up-to-date
439
                //
440
                // NOTE : the field is overridden in the case of the exception
441
                // found below
442
                field.set(cachedCdmEntity, obj);
443
            }
444

    
445
            if(obj != null && !ProxyUtils.isUninitializedProxy(obj)) {
446
                if(CdmBase.class.isAssignableFrom(obj.getClass())) {
447
                    if (logger.isDebugEnabled()){logger.debug("found initialised cdm entity '" + fieldName + "' in object of type " + clazz.getSimpleName() + " with id " + cdmEntity.getId() + "("+ System.identityHashCode(cdmEntity) +")" );}
448

    
449
                    cdmEntityInSubGraph = (CdmBase)obj;
450
                    CdmBase cachedCdmEntityInSubGraph = cdmCacher.getFromCache(cdmEntityInSubGraph);
451

    
452
                    if(cachedCdmEntityInSubGraph != null) {
453
                        if(cachedCdmEntityInSubGraph != cdmEntityInSubGraph) {
454
                            // exception : is the case where
455
                            // the field has been already initialized, cached and
456
                            // is not the same as the one in the cache, in which case we set the value
457
                            // of the field to the one found in the cache
458
                            if (logger.isDebugEnabled()){logger.debug("setting cached + real value to '" + fieldName + "' in object of type " + clazz.getSimpleName() + " with id " + cdmEntity.getId() + "("+ System.identityHashCode(cdmEntity) +")" );}
459
                            field.set(cachedCdmEntity, cachedCdmEntityInSubGraph);
460
                            field.set(cdmEntity, cachedCdmEntityInSubGraph);
461
                        } else {
462
                            // since the field value object in cdmEntity
463
                            // is the same as the field value object in cachedCdmEntity
464
                            // we are sure that the subgraph is also correctly loaded,
465
                            // so we can exit the recursion
466
                            return null;
467
                        }
468
                    }
469
                } else if(obj instanceof Map && !entityAlreadyVisisted(alreadyVisitedEntities, obj)) {
470
                    loadRecursiveMap((Map<Object,Object>)obj, alreadyVisitedEntities, update);
471
                } else if(obj instanceof Collection && !entityAlreadyVisisted(alreadyVisitedEntities, obj)) {
472
                    loadRecursiveCollection((Collection<?>)obj, alreadyVisitedEntities, update);
473
                }
474
            }
475
            // we return the original cdm entity in the sub graph because we
476
            // want to continue to recurse on the input cdm entity graph
477
            // and not the one in the cache
478
            return cdmEntityInSubGraph;
479
        } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
480
            throw new CdmClientCacheException(e);
481
        }
482
    }
483

    
484
    private boolean entityAlreadyVisisted(List<Object> objList, Object objToCompare) {
485
        if(objList != null && objToCompare != null) {
486
            for(Object obj : objList) {
487
                if(obj == objToCompare) {
488
                    return true;
489
                }
490
            }
491
        }
492
        return false;
493
    }
494

    
495
    public static boolean isRecursiveEnabled() {
496
        return isRecursiveEnabled;
497
    }
498

    
499
    public static void  setRecursiveEnabled(boolean recursiveEnabled) {
500
        isRecursiveEnabled = recursiveEnabled;
501
    }
502
}
(1-1/12)