cleanup
[cdm-vaadin.git] / src / main / java / eu / etaxonomy / cdm / cache / CdmEntityCache.java
1 /**
2 * Copyright (C) 2017 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.beans.PropertyDescriptor;
12 import java.io.PrintStream;
13 import java.lang.reflect.InvocationTargetException;
14 import java.util.ArrayList;
15 import java.util.Collection;
16 import java.util.HashMap;
17 import java.util.HashSet;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Set;
21
22 import org.apache.commons.beanutils.BeanUtilsBean;
23 import org.apache.commons.beanutils.PropertyUtils;
24 import org.apache.commons.beanutils.PropertyUtilsBean;
25 import org.apache.commons.lang.builder.HashCodeBuilder;
26 import org.apache.logging.log4j.LogManager;
27 import org.apache.logging.log4j.Logger;
28 import org.hibernate.Hibernate;
29 import org.hibernate.collection.internal.AbstractPersistentCollection;
30 import org.hibernate.envers.internal.entities.mapper.relation.lazy.proxy.CollectionProxy;
31 import org.hibernate.envers.internal.entities.mapper.relation.lazy.proxy.MapProxy;
32 import org.hibernate.envers.internal.entities.mapper.relation.lazy.proxy.SortedMapProxy;
33
34 import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
35 import eu.etaxonomy.cdm.model.common.CdmBase;
36 import eu.etaxonomy.cdm.model.common.VersionableEntity;
37 import eu.etaxonomy.cdm.model.reference.INomenclaturalReference;
38 import eu.etaxonomy.cdm.persistence.dao.initializer.AbstractBeanInitializer;
39
40 /**
41 * @author a.kohlbecker
42 * @since 08.11.2017
43 */
44 public class CdmEntityCache implements EntityCache {
45
46 private static final Logger logger = LogManager.getLogger();
47
48
49 protected static final String COPY_ENTITY = "!";
50
51 protected Set<CdmBase> entities = new HashSet<>();
52
53 private Map<EntityKey, CdmBase> entityyMap = new HashMap<>();
54
55 private List<String> entityPathList = new ArrayList<>();
56
57 private Map<EntityKey, List<String>> entityPathsMap = new HashMap<>();
58
59 private Set<EntityKey> copyEntitiyKeys = new HashSet<>();
60
61 private Set<Object> objectsSeen = new HashSet<>();
62
63 protected CdmEntityCache(){
64
65 }
66
67 /**
68 * @param entity the first entity to be cached. can be <code>null</code>.
69 * Further entities can be added to the cache with {@link CdmEntityCache#add(CdmBase)}
70 */
71 public CdmEntityCache(CdmBase entity){
72 if(entity != null){
73 this.entities.add(entity);
74 update();
75 }
76 }
77
78 @Override
79 public boolean update() {
80
81 entityPathList.clear();
82 entityPathsMap.clear();
83 objectsSeen.clear();
84 copyEntitiyKeys.clear();
85
86 for(CdmBase entity : entities){
87 analyzeEntity(entity, "");
88 }
89
90 return copyEntitiyKeys.isEmpty();
91 }
92
93 /**
94 *
95 */
96 protected void analyzeEntity(CdmBase bean, String propertyPath) {
97
98 if(bean == null){
99 return;
100 }
101
102 CdmBase proxyBean = bean;
103
104 bean = HibernateProxyHelper.deproxy(proxyBean, CdmBase.class);
105
106 EntityKey entityKey = new EntityKey(bean);
107
108 propertyPath += "[" + entityKey;
109 String flags = "";
110 CdmBase mappedEntity = entityyMap.put(entityKey, proxyBean);
111
112 if(mappedEntity != null && mappedEntity != bean) {
113 copyEntitiyKeys.add(entityKey);
114 flags += COPY_ENTITY + bean.hashCode();
115 }
116
117 flags = analyzeMore(bean, entityKey, flags, mappedEntity);
118
119 if(!flags.isEmpty()){
120 propertyPath += "(" + flags + ")";
121 }
122 propertyPath += "]";
123
124 logger.debug(propertyPath);
125
126 entityPathList.add(propertyPath);
127 if(!entityPathsMap.containsKey(entityKey)){
128 entityPathsMap.put(entityKey, new ArrayList<>());
129 }
130 entityPathsMap.get(entityKey).add(propertyPath);
131
132 if(!objectsSeen.add(bean)){
133 // avoid cycles, do not recurse into properties of objects that have been analyzed already
134 return;
135 }
136
137 Set<PropertyDescriptor> properties = AbstractBeanInitializer.getProperties(bean, null);
138 for(PropertyDescriptor prop : properties){
139
140 try {
141 Object propertyValue = PropertyUtils.getProperty(bean, prop.getName());
142
143 if(propertyValue == null){
144 continue;
145 }
146
147 String propertyPathSuffix = "." + prop.getName();
148 logger.debug("\t\tproperty:" + propertyPathSuffix);
149
150 if(Hibernate.isInitialized(propertyValue)) {
151
152 if(CdmBase.class.isAssignableFrom(prop.getPropertyType())
153 || INomenclaturalReference.class.equals(prop.getPropertyType())
154 ){
155 analyzeEntity(HibernateProxyHelper.deproxy(propertyValue, CdmBase.class), propertyPath + propertyPathSuffix);
156 continue;
157 }
158
159 Collection<CdmBase> collection = null;
160 if(propertyValue instanceof AbstractPersistentCollection){
161 if (propertyValue instanceof Collection) {
162 collection = (Collection<CdmBase>) propertyValue;
163 } else if (propertyValue instanceof Map) {
164 collection = ((Map<?,CdmBase>)propertyValue).values();
165 } else {
166 logger.error("unhandled subtype of AbstractPersistentCollection");
167 }
168 } else if (propertyValue instanceof CollectionProxy
169 || propertyValue instanceof MapProxy<?, ?>
170 || propertyValue instanceof SortedMapProxy<?, ?>){
171 //hibernate envers collections
172 // FIXME this won't work!!!!
173 collection = (Collection<CdmBase>)propertyValue;
174 }
175
176 if(collection != null){
177 for(CdmBase collectionItem : collection){
178 analyzeEntity(HibernateProxyHelper.deproxy(collectionItem, CdmBase.class), propertyPath + propertyPathSuffix);
179 }
180 } else {
181 // logger.error("Unhandled property type " + propertyValue.getClass().getName());
182 }
183 }
184
185 } catch (IllegalAccessException e) {
186 String message = "Illegal access on property " + prop;
187 logger.error(message);
188 throw new RuntimeException(message, e);
189 } catch (InvocationTargetException e) {
190 String message = "Cannot invoke property " + prop + " not found";
191 logger.error(message);
192 throw new RuntimeException(message, e);
193 } catch (NoSuchMethodException e) {
194 String message = "Property " + prop.getName() + " not found for class " + bean.getClass();
195 logger.error(message);
196 }
197
198 }
199 }
200
201 /**
202 * Empty method which can be implemented by subclasses which do further analysis.
203 *
204 * @param bean
205 * @param entityKey
206 * @param flags
207 * @param mappedEntity
208 * @return
209 */
210 protected String analyzeMore(CdmBase bean, EntityKey entityKey, String flags, CdmBase mappedEntity) {
211 return flags;
212 }
213
214
215 public void printEntityGraph(PrintStream printStream){
216 printLegend(printStream);
217 for(String path : entityPathList) {
218 printStream.println(path);
219 }
220 }
221
222 public void printCopyEntities(PrintStream printStream){
223 printStream.println("-------------- Copy Entities --------------");
224 printLegend(printStream);
225 for(EntityKey key : copyEntitiyKeys){
226 for(String path : entityPathsMap.get(key)) {
227 printStream.println(path);
228 }
229 }
230 }
231
232 /**
233 * @param printStream
234 */
235 protected void printLegend(PrintStream printStream) {
236 printStream.println(this.getClass().getSimpleName() + " legend: ");
237 printStream.println(" - '!{objectHash}': detected copy entity, followed by object hash");
238 }
239
240 public class EntityKey {
241
242 Class type;
243 int id;
244
245 public EntityKey(Class type, int id){
246 this.type = type;
247 this.id = id;
248 }
249
250 public EntityKey(CdmBase entity){
251 type = entity.getClass();
252 id = entity.getId();
253 }
254
255 /**
256 * @return the type
257 */
258 public Class getType() {
259 return type;
260 }
261
262 /**
263 * @return the id
264 */
265 public int getId() {
266 return id;
267 }
268
269 /**
270 * {@inheritDoc}
271 */
272 @Override
273 public int hashCode() {
274 return new HashCodeBuilder(15, 33)
275 .append(type)
276 .append(id)
277 .toHashCode();
278 }
279
280 @Override
281 public boolean equals(Object obj) {
282 EntityKey other = (EntityKey)obj;
283 return this.id == other.id && this.type == other.type;
284
285 }
286
287 @Override
288 public String toString() {
289 return type.getSimpleName() + "#" + getId();
290 }
291
292 }
293
294 /**
295 *
296 * @return the entities in this cache
297 */
298 public Set<CdmBase> getEntities(){
299 return entities;
300 }
301
302 /**
303 * {@inheritDoc}
304 * <p>
305 * In case the cached bean is a HibernateProxy it will be unproxied
306 * before returning it.
307 */
308 @Override
309 public <CDM extends CdmBase> CDM find(CDM value) {
310 if(value != null){
311 EntityKey entityKey = new EntityKey(HibernateProxyHelper.deproxy(value));
312 CDM cachedBean = (CDM) entityyMap.get(entityKey);
313 if(cachedBean != null){
314 return (CDM) HibernateProxyHelper.deproxy(cachedBean, CdmBase.class);
315 }
316 }
317 return null;
318 }
319
320 private <CDM extends CdmBase> CDM findProxy(CDM value) {
321 if(value != null){
322 EntityKey entityKey = new EntityKey(HibernateProxyHelper.deproxy(value));
323 return (CDM) entityyMap.get(entityKey);
324 }
325 return null;
326 }
327
328 /**
329 * {@inheritDoc}
330 * <p>
331 * In case the cached bean is a HibernateProxy it will be unproxied
332 * before returning it.
333 */
334 @Override
335 public <CDM extends CdmBase> CDM find(Class<CDM> type, int id) {
336 EntityKey entityKey = new EntityKey(type, id);
337 CDM cachedBean = (CDM) entityyMap.get(entityKey);
338 if(cachedBean != null){
339 return (CDM) HibernateProxyHelper.deproxy(cachedBean, CdmBase.class);
340 }
341 return null;
342 }
343
344 @Override
345 public <CDM extends CdmBase> CDM findAndUpdate(CDM value){
346 CDM cachedBean = findProxy(value);
347 if(cachedBean != null && VersionableEntity.class.isAssignableFrom(cachedBean.getClass())){
348 updatedCachedIfEarlier((VersionableEntity)cachedBean, (VersionableEntity)value);
349 }
350 if(cachedBean != null){
351 return (CDM) HibernateProxyHelper.deproxy(cachedBean, CdmBase.class);
352 }
353 return null;
354
355 }
356
357 /**
358 * @param cachedValue
359 * @param value
360 */
361 private <CDM extends VersionableEntity> void updatedCachedIfEarlier(CDM cachedValue, CDM value) {
362 if(cachedValue != null && value != null && value.getUpdated() != null){
363 if(cachedValue.getUpdated() == null || value.getUpdated().isAfter(cachedValue.getUpdated())){
364 try {
365 copyProperties(cachedValue, value);
366 } catch (IllegalAccessException e) {
367 /* should never happen */
368 e.printStackTrace();
369 } catch (InvocationTargetException e) {
370 /* critical! re-throw as runtime exception, which is ok in the context of a vaadin app */
371 throw new RuntimeException(e);
372 }
373 }
374 }
375
376 }
377
378 /**
379 * partially copy of {@link BeanUtilsBean#copyProperties(Object, Object)}
380 */
381 private <CDM extends VersionableEntity> void copyProperties(CDM dest, CDM orig) throws IllegalAccessException, InvocationTargetException {
382
383 PropertyUtilsBean propertyUtils = BeanUtilsBean.getInstance().getPropertyUtils();
384 PropertyDescriptor[] origDescriptors =
385 propertyUtils.getPropertyDescriptors(orig);
386 for (int i = 0; i < origDescriptors.length; i++) {
387 String name = origDescriptors[i].getName();
388 if ("class".equals(name)) {
389 continue; // No point in trying to set an object's class
390 }
391 if (propertyUtils.isReadable(orig, name) &&
392 propertyUtils.isWriteable(dest, name)) {
393 try {
394 if(CdmBase.class.isAssignableFrom(propertyUtils.getPropertyType(dest, name))){
395 CdmBase origValue = (CdmBase)propertyUtils.getSimpleProperty(orig, name);
396 if(!Hibernate.isInitialized(origValue)){
397 // ignore uninitialized entities
398 continue;
399 }
400 // only copy entities if origValue either is a completely different entity (A)
401 // or if origValue is updated (B).
402 CdmBase destValue = (CdmBase)propertyUtils.getSimpleProperty(dest, name);
403 if(destValue == null && origValue == null){
404 continue;
405 }
406 if(
407 origValue != null && destValue == null ||
408 origValue == null && destValue != null ||
409
410 origValue.getId() != destValue.getId() ||
411 origValue.getClass() != destValue.getClass() ||
412 origValue.getUuid() != destValue.getUuid()){
413 // (A)
414 BeanUtilsBean.getInstance().copyProperty(dest, name, origValue);
415
416 } else {
417 // (B) recurse into findAndUpdate
418 findAndUpdate(origValue);
419 }
420 } else {
421 Object value = propertyUtils.getSimpleProperty(orig, name);
422 BeanUtilsBean.getInstance().copyProperty(dest, name, value);
423 }
424
425 } catch (NoSuchMethodException e) {
426 // Should not happen
427 }
428 }
429 }
430
431 }
432
433 /**
434 * {@inheritDoc}
435 */
436 @Override
437 public <CDM extends CdmBase> void add(CDM value) {
438 entities.add(value);
439 analyzeEntity(value, "");
440 }
441
442
443 }