jenkins merging release branch into master (strategy: theirs)
[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.lang3.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 protected String analyzeMore(CdmBase bean, EntityKey entityKey, String flags, CdmBase mappedEntity) {
205 return flags;
206 }
207
208 public void printEntityGraph(PrintStream printStream){
209 printLegend(printStream);
210 for(String path : entityPathList) {
211 printStream.println(path);
212 }
213 }
214
215 public void printCopyEntities(PrintStream printStream){
216 printStream.println("-------------- Copy Entities --------------");
217 printLegend(printStream);
218 for(EntityKey key : copyEntitiyKeys){
219 for(String path : entityPathsMap.get(key)) {
220 printStream.println(path);
221 }
222 }
223 }
224
225 /**
226 * @param printStream
227 */
228 protected void printLegend(PrintStream printStream) {
229 printStream.println(this.getClass().getSimpleName() + " legend: ");
230 printStream.println(" - '!{objectHash}': detected copy entity, followed by object hash");
231 }
232
233 public class EntityKey {
234
235 Class type;
236 int id;
237
238 public EntityKey(Class type, int id){
239 this.type = type;
240 this.id = id;
241 }
242
243 public EntityKey(CdmBase entity){
244 type = entity.getClass();
245 id = entity.getId();
246 }
247
248 public Class getType() {
249 return type;
250 }
251
252 public int getId() {
253 return id;
254 }
255
256 @Override
257 public int hashCode() {
258 return new HashCodeBuilder(15, 33)
259 .append(type)
260 .append(id)
261 .toHashCode();
262 }
263
264 @Override
265 public boolean equals(Object obj) {
266 EntityKey other = (EntityKey)obj;
267 return this.id == other.id && this.type == other.type;
268
269 }
270
271 @Override
272 public String toString() {
273 return type.getSimpleName() + "#" + getId();
274 }
275
276 }
277
278 /**
279 *
280 * @return the entities in this cache
281 */
282 public Set<CdmBase> getEntities(){
283 return entities;
284 }
285
286 /**
287 * {@inheritDoc}
288 * <p>
289 * In case the cached bean is a HibernateProxy it will be unproxied
290 * before returning it.
291 */
292 @Override
293 public <CDM extends CdmBase> CDM find(CDM value) {
294 if(value != null){
295 EntityKey entityKey = new EntityKey(HibernateProxyHelper.deproxy(value));
296 CDM cachedBean = (CDM) entityyMap.get(entityKey);
297 if(cachedBean != null){
298 return (CDM) HibernateProxyHelper.deproxy(cachedBean, CdmBase.class);
299 }
300 }
301 return null;
302 }
303
304 private <CDM extends CdmBase> CDM findProxy(CDM value) {
305 if(value != null){
306 EntityKey entityKey = new EntityKey(HibernateProxyHelper.deproxy(value));
307 return (CDM) entityyMap.get(entityKey);
308 }
309 return null;
310 }
311
312 /**
313 * {@inheritDoc}
314 * <p>
315 * In case the cached bean is a HibernateProxy it will be unproxied
316 * before returning it.
317 */
318 @Override
319 public <CDM extends CdmBase> CDM find(Class<CDM> type, int id) {
320 EntityKey entityKey = new EntityKey(type, id);
321 CDM cachedBean = (CDM) entityyMap.get(entityKey);
322 if(cachedBean != null){
323 return (CDM) HibernateProxyHelper.deproxy(cachedBean, CdmBase.class);
324 }
325 return null;
326 }
327
328 @Override
329 public <CDM extends CdmBase> CDM findAndUpdate(CDM value){
330 CDM cachedBean = findProxy(value);
331 if(cachedBean != null && VersionableEntity.class.isAssignableFrom(cachedBean.getClass())){
332 updatedCachedIfEarlier((VersionableEntity)cachedBean, (VersionableEntity)value);
333 }
334 if(cachedBean != null){
335 return (CDM) HibernateProxyHelper.deproxy(cachedBean, CdmBase.class);
336 }
337 return null;
338
339 }
340
341 /**
342 * @param cachedValue
343 * @param value
344 */
345 private <CDM extends VersionableEntity> void updatedCachedIfEarlier(CDM cachedValue, CDM value) {
346 if(cachedValue != null && value != null && value.getUpdated() != null){
347 if(cachedValue.getUpdated() == null || value.getUpdated().isAfter(cachedValue.getUpdated())){
348 try {
349 copyProperties(cachedValue, value);
350 } catch (IllegalAccessException e) {
351 /* should never happen */
352 e.printStackTrace();
353 } catch (InvocationTargetException e) {
354 /* critical! re-throw as runtime exception, which is ok in the context of a vaadin app */
355 throw new RuntimeException(e);
356 }
357 }
358 }
359
360 }
361
362 /**
363 * partially copy of {@link BeanUtilsBean#copyProperties(Object, Object)}
364 */
365 private <CDM extends VersionableEntity> void copyProperties(CDM dest, CDM orig) throws IllegalAccessException, InvocationTargetException {
366
367 PropertyUtilsBean propertyUtils = BeanUtilsBean.getInstance().getPropertyUtils();
368 PropertyDescriptor[] origDescriptors =
369 propertyUtils.getPropertyDescriptors(orig);
370 for (int i = 0; i < origDescriptors.length; i++) {
371 String name = origDescriptors[i].getName();
372 if ("class".equals(name)) {
373 continue; // No point in trying to set an object's class
374 }
375 if (propertyUtils.isReadable(orig, name) &&
376 propertyUtils.isWriteable(dest, name)) {
377 try {
378 if(CdmBase.class.isAssignableFrom(propertyUtils.getPropertyType(dest, name))){
379 CdmBase origValue = (CdmBase)propertyUtils.getSimpleProperty(orig, name);
380 if(!Hibernate.isInitialized(origValue)){
381 // ignore uninitialized entities
382 continue;
383 }
384 // only copy entities if origValue either is a completely different entity (A)
385 // or if origValue is updated (B).
386 CdmBase destValue = (CdmBase)propertyUtils.getSimpleProperty(dest, name);
387 if(destValue == null && origValue == null){
388 continue;
389 }
390 if(
391 origValue != null && destValue == null ||
392 origValue == null && destValue != null ||
393
394 origValue.getId() != destValue.getId() ||
395 origValue.getClass() != destValue.getClass() ||
396 origValue.getUuid() != destValue.getUuid()){
397 // (A)
398 BeanUtilsBean.getInstance().copyProperty(dest, name, origValue);
399
400 } else {
401 // (B) recurse into findAndUpdate
402 findAndUpdate(origValue);
403 }
404 } else {
405 Object value = propertyUtils.getSimpleProperty(orig, name);
406 BeanUtilsBean.getInstance().copyProperty(dest, name, value);
407 }
408
409 } catch (NoSuchMethodException e) {
410 // Should not happen
411 }
412 }
413 }
414
415 }
416
417 @Override
418 public <CDM extends CdmBase> void add(CDM value) {
419 entities.add(value);
420 analyzeEntity(value, "");
421 }
422 }