ReferenceSystem terms and merge framework
[cdmlib.git] / cdmlib-model / src / main / java / eu / etaxonomy / cdm / strategy / merge / DefaultMergeStrategy.java
1 // $Id$
2 /**
3 * Copyright (C) 2007 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
11 package eu.etaxonomy.cdm.strategy.merge;
12
13 import java.lang.annotation.Annotation;
14 import java.lang.reflect.Field;
15 import java.lang.reflect.GenericDeclaration;
16 import java.lang.reflect.Method;
17 import java.lang.reflect.Modifier;
18 import java.lang.reflect.Type;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.UUID;
27
28 import javax.persistence.Transient;
29
30 import org.apache.log4j.Logger;
31 import org.joda.time.DateTime;
32
33 import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
34 import sun.reflect.generics.reflectiveObjects.TypeVariableImpl;
35 import eu.etaxonomy.cdm.model.agent.Contact;
36 import eu.etaxonomy.cdm.model.common.CdmBase;
37 import eu.etaxonomy.cdm.model.common.ICdmBase;
38 import eu.etaxonomy.cdm.model.common.IRelated;
39 import eu.etaxonomy.cdm.model.common.LSID;
40 import eu.etaxonomy.cdm.model.common.RelationshipBase;
41 import eu.etaxonomy.cdm.model.common.TimePeriod;
42 import eu.etaxonomy.cdm.strategy.StrategyBase;
43
44 /**
45 * @author a.mueller
46 * @created 31.07.2009
47 * @version 1.0
48 */
49 public class DefaultMergeStrategy extends StrategyBase implements IMergeStrategy {
50 private static final long serialVersionUID = -8513956338156791995L;
51 private static final Logger logger = Logger.getLogger(DefaultMergeStrategy.class);
52 final static UUID uuid = UUID.fromString("d85cd6c3-0147-452c-8fed-bbfb82f392f6");
53
54 public static DefaultMergeStrategy NewInstance(Class<? extends CdmBase> mergeClazz){
55 return new DefaultMergeStrategy(mergeClazz);
56 }
57
58 protected Map<String, MergeMode> mergeModeMap = new HashMap<String, MergeMode>();
59 protected MergeMode defaultMergeMode = MergeMode.FIRST;
60 protected MergeMode defaultCollectionMergeMode = MergeMode.ADD;
61
62 protected Class<? extends CdmBase> mergeClass;
63 protected Map<String, Field> mergeFields;
64
65 protected DefaultMergeStrategy(Class<? extends CdmBase> mergeClazz) {
66 super();
67 if (mergeClazz == null){
68 throw new IllegalArgumentException("Merge class must not be null");
69 }
70 this.mergeClass = mergeClazz;
71 boolean includeStatic = false;
72 boolean includeTransient = false;
73 boolean makeAccessible = true;
74 this.mergeFields = getAllNonStaticNonTransientFields(mergeClazz, includeStatic, includeTransient, makeAccessible);
75 initMergeModeMap();
76 }
77
78
79
80 /**
81 *
82 */
83 private void initMergeModeMap() {
84 for (Field field: mergeFields.values()){
85 for (Annotation annotation : field.getAnnotations()){
86 if (annotation.annotationType() == Merge.class){
87 MergeMode mergeMode = ((Merge)annotation).value();
88 mergeModeMap.put(field.getName(), mergeMode);
89 }
90 }
91 }
92 }
93
94
95
96 /* (non-Javadoc)
97 * @see eu.etaxonomy.cdm.strategy.StrategyBase#getUuid()
98 */
99 @Override
100 protected UUID getUuid() {
101 return uuid;
102 }
103
104
105
106 /**
107 * @return the merge class
108 */
109 public Class<? extends CdmBase> getMergeClass() {
110 return mergeClass;
111 }
112
113
114
115 /**
116 * @param mergeClazz the mergeClazz to set
117 */
118 public void setMergeClazz(Class<? extends CdmBase> mergeClazz) {
119 this.mergeClass = mergeClazz;
120 }
121
122
123
124 /* (non-Javadoc)
125 * @see eu.etaxonomy.cdm.strategy.merge.IMergeStragegy#getMergeMode(java.lang.String)
126 */
127 public MergeMode getMergeMode(String propertyName){
128 MergeMode result = mergeModeMap.get(propertyName);
129 if (result == null){
130 Field field = mergeFields.get(propertyName);
131 if (isCollection(field.getType())){
132 return defaultCollectionMergeMode;
133 }else{
134 return defaultMergeMode;
135 }
136 }else{
137 return result;
138 }
139 }
140
141 public void setMergeMode(String propertyName, MergeMode mergeMode) throws MergeException{
142 if (mergeFields.containsKey(propertyName)){
143 checkIdentifier(propertyName, mergeMode);
144 mergeModeMap.put(propertyName, mergeMode);
145 }else{
146 throw new MergeException("The class " + mergeClass.getName() + " does not contain a field with name " + propertyName);
147 }
148 }
149
150 /**
151 * Tests if a property is an identifier property
152 * @param propertyName
153 * @param mergeMode
154 * @throws MergeException
155 */
156 private void checkIdentifier(String propertyName, MergeMode mergeMode) throws MergeException {
157 if (mergeMode != MergeMode.FIRST){
158 if ("id".equalsIgnoreCase(propertyName) || "uuid".equalsIgnoreCase(propertyName)){
159 throw new MergeException("Identifier must always have merge mode MergeMode.FIRST");
160 }
161 }
162
163 }
164
165 public <T extends IMergable> Set<ICdmBase> invoke(T mergeFirst, T mergeSecond) throws MergeException {
166 return this.invoke(mergeFirst, mergeSecond, null);
167 }
168
169 /* (non-Javadoc)
170 * @see eu.etaxonomy.cdm.strategy.merge.IMergeStragegy#invoke(eu.etaxonomy.cdm.strategy.merge.IMergable, eu.etaxonomy.cdm.strategy.merge.IMergable)
171 */
172 public <T extends IMergable> Set<ICdmBase> invoke(T mergeFirst, T mergeSecond, Set<ICdmBase> clonedObjects) throws MergeException {
173 Set<ICdmBase> deleteSet = new HashSet<ICdmBase>();
174 if (clonedObjects == null){
175 clonedObjects = new HashSet<ICdmBase>();
176 }
177 deleteSet.add(mergeSecond);
178 try {
179 for (Field field : mergeFields.values()){
180 Class<?> fieldType = field.getType();
181 if (isIdentifier(field)){
182 //do nothing (id and uuid stay with first object)
183 }else if (isPrimitive(fieldType)){
184 mergePrimitiveField(mergeFirst, mergeSecond, field);
185 }else if (fieldType == String.class ){
186 mergeStringField(mergeFirst, mergeSecond, field);
187 }else if (isCollection(fieldType)){
188 mergeCollectionField(mergeFirst, mergeSecond, field, deleteSet, clonedObjects);
189 }else if(isUserType(fieldType)){
190 mergeUserTypeField(mergeFirst, mergeSecond, field);
191 }else if(isSingleCdmBaseObject(fieldType)){
192 mergeSingleCdmBaseField(mergeFirst, mergeSecond, field, deleteSet);
193 }else if(fieldType.isInterface()){
194 mergeInterfaceField(mergeFirst, mergeSecond, field, deleteSet);
195 }else{
196 throw new RuntimeException("Unknown Object type for merging: " + fieldType);
197 }
198 }
199 return deleteSet;
200 } catch (Exception e) {
201 throw new MergeException("Merge Exception in invoke", e);
202 }
203 }
204
205
206 /**
207 * @throws Exception
208 *
209 */
210 private <T extends IMergable> void mergeInterfaceField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
211 String propertyName = field.getName();
212 MergeMode mergeMode = this.getMergeMode(propertyName);
213 if (mergeMode != MergeMode.FIRST){
214 mergeCdmBaseValue(mergeFirst, mergeSecond, field, deleteSet);
215 }
216 System.out.println(propertyName + ": " + mergeMode + ", " + field.getType().getName());
217
218 }
219
220
221 /**
222 * @throws Exception
223 *
224 */
225 private <T extends IMergable> void mergeSingleCdmBaseField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
226 String propertyName = field.getName();
227 MergeMode mergeMode = this.getMergeMode(propertyName);
228 if (mergeMode != MergeMode.FIRST){
229 mergeCdmBaseValue(mergeFirst, mergeSecond, field, deleteSet);
230 }
231 System.out.println(propertyName + ": " + mergeMode + ", " + field.getType().getName());
232
233 }
234
235 private <T extends IMergable> void mergeCdmBaseValue(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
236 if (true){
237 Object value = getMergeValue(mergeFirst, mergeSecond, field);
238 if (value instanceof ICdmBase || value == null){
239 field.set(mergeFirst, (ICdmBase)value);
240 }else{
241 throw new MergeException("Merged value must be of type CdmBase but is not: " + value.getClass());
242 }
243 }else{
244 throw new MergeException("Not supported mode");
245 }
246 }
247
248 /**
249 * @throws Exception
250 *
251 */
252 private <T extends IMergable> void mergeUserTypeField(T mergeFirst, T mergeSecond, Field field) throws Exception {
253 String propertyName = field.getName();
254 Class<?> fieldType = field.getType();
255 MergeMode mergeMode = this.getMergeMode(propertyName);
256 if (mergeMode == MergeMode.MERGE){
257 Method mergeMethod = getMergeMethod(fieldType);
258 Object firstObject = field.get(mergeFirst);
259 if (firstObject == null){
260 firstObject = fieldType.newInstance();
261 }
262 Object secondObject = field.get(mergeSecond);
263 mergeMethod.invoke(firstObject, secondObject);
264 }else if (mergeMode != MergeMode.FIRST){
265 Object value = getMergeValue(mergeFirst, mergeSecond, field);
266 field.set(mergeFirst, value);
267 }
268 System.out.println(propertyName + ": " + mergeMode + ", " + fieldType.getName());
269 }
270
271 /**
272 * @return
273 * @throws NoSuchMethodException
274 * @throws SecurityException
275 */
276 private Method getMergeMethod(Class<?> fieldType) throws SecurityException, NoSuchMethodException {
277 Method mergeMethod = fieldType.getDeclaredMethod("merge", fieldType);
278 return mergeMethod;
279 }
280
281
282
283 /**
284 * @throws Exception
285 *
286 */
287 private <T extends IMergable> void mergeCollectionField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet, Set<ICdmBase> clonedObjects) throws Exception {
288 String propertyName = field.getName();
289 Class<?> fieldType = field.getType();
290 MergeMode mergeMode = this.getMergeMode(propertyName);
291 if (mergeMode != MergeMode.FIRST){
292 mergeCollectionFieldNoFirst(mergeFirst, mergeSecond, field, mergeMode, deleteSet, clonedObjects);
293 }
294 System.out.println(propertyName + ": " + mergeMode + ", " + fieldType.getName());
295
296 }
297
298 private <T extends IMergable> void mergeCollectionFieldNoFirst(T mergeFirst, T mergeSecond, Field field, MergeMode mergeMode, Set<ICdmBase> deleteSet, Set<ICdmBase> clonedObjects) throws Exception{
299 Class<?> fieldType = field.getType();
300 if (mergeMode == MergeMode.ADD || mergeMode == MergeMode.ADD_CLONE){
301 //FIXME
302 Method addMethod = getAddMethod(field);
303 Method removeMethod = getAddMethod(field, true);
304
305 if (Set.class.isAssignableFrom(fieldType) || List.class.isAssignableFrom(fieldType)){
306 Collection<ICdmBase> secondCollection = (Collection<ICdmBase>)field.get(mergeSecond);
307 List<ICdmBase> removeList = new ArrayList<ICdmBase>();
308 for (ICdmBase obj : secondCollection){
309 Object objectToAdd;
310 if (mergeMode == MergeMode.ADD){
311 objectToAdd = obj;
312 }else if(mergeMode == MergeMode.ADD_CLONE){
313 Method cloneMethod = obj.getClass().getDeclaredMethod("clone");
314 objectToAdd = cloneMethod.invoke(obj);
315 clonedObjects.add(obj);
316 }else{
317 throw new MergeException("Unknown collection merge mode: " + mergeMode);
318 }
319 addMethod.invoke(mergeFirst, objectToAdd);
320 removeList.add(obj);
321 }
322 for (ICdmBase removeObj : removeList ){
323 //removeMethod.invoke(mergeSecond, removeObj);
324 if ((removeObj instanceof CdmBase)&& mergeMode == MergeMode.ADD_CLONE) {
325 deleteSet.add(removeObj);
326 }
327 }
328 }else{
329 throw new MergeException("Merge for collections other than sets and lists not yet implemented");
330 }
331 }else if (mergeMode == MergeMode.RELATION){
332 if (Set.class.isAssignableFrom(fieldType) || List.class.isAssignableFrom(fieldType)){
333 Collection<RelationshipBase> secondCollection = (Collection<RelationshipBase>)field.get(mergeSecond);
334 List<ICdmBase> removeList = new ArrayList<ICdmBase>();
335 for (RelationshipBase relation : secondCollection){
336 Method relatedFromMethod = RelationshipBase.class.getDeclaredMethod("getRelatedFrom");
337 relatedFromMethod.setAccessible(true);
338 Object relatedFrom = relatedFromMethod.invoke(relation);
339
340 Method relatedToMethod = RelationshipBase.class.getDeclaredMethod("getRelatedTo");
341 relatedToMethod.setAccessible(true);
342 Object relatedTo = relatedToMethod.invoke(relation);
343
344 if (relatedFrom.equals(mergeSecond)){
345 Method setRelatedMethod = RelationshipBase.class.getDeclaredMethod("setRelatedFrom", IRelated.class);
346 setRelatedMethod.setAccessible(true);
347 setRelatedMethod.invoke(relation, mergeFirst);
348 }
349 if (relatedTo.equals(mergeSecond)){
350 Method setRelatedMethod = RelationshipBase.class.getDeclaredMethod("setRelatedTo", IRelated.class);
351 setRelatedMethod.setAccessible(true);
352 setRelatedMethod.invoke(relation, mergeFirst);
353 }
354 ((IRelated)mergeFirst).addRelationship(relation);
355 removeList.add(relation);
356 }
357 for (ICdmBase removeObj : removeList){
358 //removeMethod.invoke(mergeSecond, removeObj);
359 if (removeObj instanceof CdmBase){
360 deleteSet.add(removeObj);
361 }
362 }
363 }else{
364 throw new MergeException("Merge for collections other than sets and lists not yet implemented");
365 }
366 }else{
367 throw new MergeException("Other merge modes for collections not yet implemented");
368 }
369 }
370
371
372 private Method getAddMethod(Field field) throws MergeException{
373 return getAddMethod(field, false);
374 }
375
376 private Method getAddMethod(Field field, boolean remove) throws MergeException{
377 Method result;
378 Class parameterClass = getCollectionType(field);
379 String fieldName = field.getName();
380 String firstCapital = fieldName.substring(0, 1).toUpperCase();
381 String rest = fieldName.substring(1);
382 String prefix = remove? "remove": "add";
383 String methodName = prefix + firstCapital + rest;
384 boolean endsWithS = parameterClass.getSimpleName().endsWith("s");
385 if (! endsWithS && ! fieldName.equals("media")){
386 methodName = methodName.substring(0, methodName.length() -1); //remove 's' at end
387 }
388 Class<?> methodClass = field.getDeclaringClass();
389 try {
390 result = methodClass.getMethod(methodName, parameterClass);
391 }catch (NoSuchMethodException e1) {
392 try {
393 result = methodClass.getDeclaredMethod(methodName, parameterClass);
394 result.setAccessible(true);
395 } catch (NoSuchMethodException e) {
396 logger.warn(methodName);
397 throw new IllegalArgumentException("Default adding method for collection field ("+field.getName()+") does not exist");
398 }
399 } catch (SecurityException e) {
400 throw e;
401 }
402 return result;
403 }
404
405 /**
406 * @throws Exception
407 *
408 */
409 private <T extends IMergable> void mergePrimitiveField(T mergeFirst, T mergeSecond, Field field) throws Exception {
410 String propertyName = field.getName();
411 Class<?> fieldType = field.getType();
412 MergeMode mergeMode = this.getMergeMode(propertyName);
413 if (mergeMode != MergeMode.FIRST){
414 Object value = getMergeValue(mergeFirst, mergeSecond, field);
415 field.set(mergeFirst, value);
416 }
417 System.out.println(propertyName + ": " + mergeMode + ", " + fieldType.getName());
418
419 }
420
421 /**
422 * @throws Exception
423 *
424 */
425 private <T extends IMergable> void mergeStringField(T mergeFirst, T mergeSecond, Field field) throws Exception {
426 String propertyName = field.getName();
427 Class<?> fieldType = field.getType();
428 MergeMode mergeMode = this.getMergeMode(propertyName);
429 if (mergeMode != MergeMode.FIRST){
430 Object value = getMergeValue(mergeFirst, mergeSecond, field);
431 field.set(mergeFirst, value);
432 }
433 System.out.println(propertyName + ": " + mergeMode + ", " + fieldType.getName());
434
435 }
436
437 /**
438 * @param fieldType
439 * @return
440 */
441 private boolean isIdentifier(Field field) {
442 Class<?> fieldType = field.getType();
443 if ("id".equals(field.getName()) && fieldType == int.class ){
444 return true;
445 }else if ("uuid".equals(field.getName()) && fieldType == UUID.class ){
446 return true;
447 }else{
448 return false;
449 }
450 }
451
452 /**
453 * @param fieldType
454 * @return
455 */
456 private boolean isPrimitive(Class<?> fieldType) {
457 if (fieldType.isPrimitive()){
458 return true;
459 }else{
460 return false;
461 }
462 }
463
464 /**
465 * @param fieldType
466 * @return
467 */
468 private boolean isSingleCdmBaseObject(Class<?> fieldType) {
469 if (CdmBase.class.isAssignableFrom(fieldType)){
470 return true;
471 }else{
472 return false;
473 }
474 }
475
476
477
478 /**
479 * @param fieldType
480 * @return
481 */
482 private boolean isCollection(Class<?> fieldType) {
483 if (Collection.class.isAssignableFrom(fieldType) ){
484 return true;
485 }else{
486 return false;
487 }
488 }
489
490
491 /**
492 * @param fieldType
493 * @return
494 */
495 private boolean isUserType(Class<?> fieldType) {
496 if ( fieldType == TimePeriod.class ||
497 fieldType == DateTime.class ||
498 fieldType == LSID.class ||
499 fieldType == Contact.class
500 ){
501 return true;
502 }else{
503 return false;
504 }
505 }
506
507
508 /**
509 * @param cdmBase
510 * @param toMerge
511 * @param field
512 * @param mergeMode
513 * @throws Exception
514 */
515 protected <T extends IMergable> Object getMergeValue(T mergeFirst, T mergeSecond,
516 Field field) throws Exception {
517 MergeMode mergeMode = this.getMergeMode(field.getName());
518 try {
519 if (mergeMode == MergeMode.FIRST){
520 return field.get(mergeFirst);
521 }else if (mergeMode == MergeMode.SECOND){
522 return field.get(mergeSecond);
523 }else if (mergeMode == MergeMode.NULL){
524 return null;
525 }else if (mergeMode == MergeMode.CONCAT){
526 return ((String)field.get(mergeFirst) + (String)field.get(mergeSecond));
527 }else if (mergeMode == MergeMode.AND){
528 return ((Boolean)field.get(mergeFirst) && (Boolean)field.get(mergeSecond));
529 }else if (mergeMode == MergeMode.OR){
530 return ((Boolean)field.get(mergeFirst) || (Boolean)field.get(mergeSecond));
531 }else{
532 throw new IllegalStateException("Unknown MergeMode");
533 }
534 } catch (IllegalArgumentException e) {
535 throw new Exception(e);
536 }
537 }
538
539 /**
540 * Computes all fields recursively
541 * @param clazz
542 * @return
543 */
544 protected Map<String, Field> getAllNonStaticNonTransientFields(Class clazz, boolean includeStatic, boolean includeTransient, boolean makeAccessible) {
545 Map<String, Field> result = new HashMap<String, Field>();
546 //exclude static
547 for (Field field: clazz.getDeclaredFields()){
548 if (includeStatic || ! Modifier.isStatic(field.getModifiers())){
549 if (includeTransient || ! isTransient(field)){
550 field.setAccessible(makeAccessible);
551 result.put(field.getName(), field);
552 }
553 }
554 }
555
556 //include superclass fields
557 Class superclass = clazz.getSuperclass();
558 if (CdmBase.class.isAssignableFrom(superclass)){
559 result.putAll(getAllNonStaticNonTransientFields(superclass, includeStatic, includeTransient, makeAccessible));
560 }
561 return result;
562 }
563
564
565
566 /**
567 * Returns true, if field has an annotation of type javax.persistence.Annotation
568 * @param field
569 * @return
570 */
571 private boolean isTransient(Field field) {
572 for (Annotation annotation : field.getAnnotations()){
573 //if (Transient.class.isAssignableFrom(annotation.annotationType())){
574 if (annotation.annotationType() == Transient.class){
575 return true;
576 }
577 }
578 return false;
579 }
580
581
582 private Class getCollectionType(Field field) throws MergeException{
583 Type genericType = (ParameterizedTypeImpl)field.getGenericType();
584 if (genericType instanceof ParameterizedTypeImpl){
585 ParameterizedTypeImpl paraType = (ParameterizedTypeImpl)genericType;
586 Class<?> rawType = paraType.getRawType();
587 Type[] arguments = paraType.getActualTypeArguments();
588 //System.out.println(arguments.length);
589 if (arguments.length == 1){
590 Class collectionClass;
591 if (arguments[0] instanceof Class){
592 collectionClass = (Class)arguments[0];
593 }else if(arguments[0] instanceof TypeVariableImpl){
594 TypeVariableImpl typeVariable = (TypeVariableImpl)arguments[0];
595 GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
596 collectionClass = (Class)genericDeclaration;
597 }else{
598 throw new MergeException("Collection with other types than TypeVariableImpl are not yet supported");
599 }
600 return collectionClass;
601 }else{
602 throw new MergeException("Collection with multiple types not supported");
603 }
604 }else{
605 throw new MergeException("Collection has no generic type of type ParameterizedTypeImpl. Unsupport case.");
606 }
607 }
608
609
610
611 }