Intensive model change for referenceBase and subclass, smaller model changes for...
[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.common.CdmUtils;
36 import eu.etaxonomy.cdm.model.agent.Contact;
37 import eu.etaxonomy.cdm.model.common.CdmBase;
38 import eu.etaxonomy.cdm.model.common.ICdmBase;
39 import eu.etaxonomy.cdm.model.common.IRelated;
40 import eu.etaxonomy.cdm.model.common.LSID;
41 import eu.etaxonomy.cdm.model.common.RelationshipBase;
42 import eu.etaxonomy.cdm.model.common.TimePeriod;
43 import eu.etaxonomy.cdm.strategy.StrategyBase;
44
45 /**
46 * @author a.mueller
47 * @created 31.07.2009
48 * @version 1.0
49 */
50 public class DefaultMergeStrategy extends StrategyBase implements IMergeStrategy {
51 private static final long serialVersionUID = -8513956338156791995L;
52 private static final Logger logger = Logger.getLogger(DefaultMergeStrategy.class);
53 final static UUID uuid = UUID.fromString("d85cd6c3-0147-452c-8fed-bbfb82f392f6");
54
55 public static DefaultMergeStrategy NewInstance(Class<? extends CdmBase> mergeClazz){
56 return new DefaultMergeStrategy(mergeClazz);
57 }
58
59 protected Map<String, MergeMode> mergeModeMap = new HashMap<String, MergeMode>();
60 protected MergeMode defaultMergeMode = MergeMode.FIRST;
61 protected MergeMode defaultCollectionMergeMode = MergeMode.ADD;
62
63 protected Class<? extends CdmBase> mergeClass;
64 protected Map<String, Field> mergeFields;
65
66 protected DefaultMergeStrategy(Class<? extends CdmBase> mergeClazz) {
67 super();
68 if (mergeClazz == null){
69 throw new IllegalArgumentException("Merge class must not be null");
70 }
71 this.mergeClass = mergeClazz;
72 boolean includeStatic = false;
73 boolean includeTransient = false;
74 boolean makeAccessible = true;
75 this.mergeFields = CdmUtils.getAllFields(mergeClass, CdmBase.class, includeStatic, includeTransient, makeAccessible, true);
76 initMergeModeMap();
77 }
78
79
80
81 /**
82 *
83 */
84 private void initMergeModeMap() {
85 for (Field field: mergeFields.values()){
86 for (Annotation annotation : field.getAnnotations()){
87 if (annotation.annotationType() == Merge.class){
88 MergeMode mergeMode = ((Merge)annotation).value();
89 mergeModeMap.put(field.getName(), mergeMode);
90 }
91 }
92 }
93 }
94
95
96
97 /* (non-Javadoc)
98 * @see eu.etaxonomy.cdm.strategy.StrategyBase#getUuid()
99 */
100 @Override
101 protected UUID getUuid() {
102 return uuid;
103 }
104
105
106
107 /**
108 * @return the merge class
109 */
110 public Class<? extends CdmBase> getMergeClass() {
111 return mergeClass;
112 }
113
114
115
116 /**
117 * @param mergeClazz the mergeClazz to set
118 */
119 public void setMergeClazz(Class<? extends CdmBase> mergeClazz) {
120 this.mergeClass = mergeClazz;
121 }
122
123
124
125 /* (non-Javadoc)
126 * @see eu.etaxonomy.cdm.strategy.merge.IMergeStragegy#getMergeMode(java.lang.String)
127 */
128 public MergeMode getMergeMode(String propertyName){
129 MergeMode result = mergeModeMap.get(propertyName);
130 if (result == null){
131 Field field = mergeFields.get(propertyName);
132 if (isCollection(field.getType())){
133 return defaultCollectionMergeMode;
134 }else{
135 return defaultMergeMode;
136 }
137 }else{
138 return result;
139 }
140 }
141
142 public void setMergeMode(String propertyName, MergeMode mergeMode) throws MergeException{
143 if (mergeFields.containsKey(propertyName)){
144 checkIdentifier(propertyName, mergeMode);
145 mergeModeMap.put(propertyName, mergeMode);
146 }else{
147 throw new MergeException("The class " + mergeClass.getName() + " does not contain a field with name " + propertyName);
148 }
149 }
150
151 /**
152 * Tests if a property is an identifier property
153 * @param propertyName
154 * @param mergeMode
155 * @throws MergeException
156 */
157 private void checkIdentifier(String propertyName, MergeMode mergeMode) throws MergeException {
158 if (mergeMode != MergeMode.FIRST){
159 if ("id".equalsIgnoreCase(propertyName) || "uuid".equalsIgnoreCase(propertyName)){
160 throw new MergeException("Identifier must always have merge mode MergeMode.FIRST");
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 if(fieldType.isEnum()){
196 mergeEnumField(mergeFirst, mergeSecond, field, deleteSet);
197 }else{
198 throw new RuntimeException("Unknown Object type for merging: " + fieldType);
199 }
200 }
201 return deleteSet;
202 } catch (Exception e) {
203 throw new MergeException("Merge Exception in invoke", e);
204 }
205 }
206
207
208 /**
209 * @throws Exception
210 *
211 */
212 private <T extends IMergable> void mergeInterfaceField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
213 String propertyName = field.getName();
214 MergeMode mergeMode = this.getMergeMode(propertyName);
215 if (mergeMode != MergeMode.FIRST){
216 mergeCdmBaseValue(mergeFirst, mergeSecond, field, deleteSet);
217 }
218 logger.debug(propertyName + ": " + mergeMode + ", " + field.getType().getName());
219
220 }
221
222 /**
223 * @throws Exception
224 *
225 */
226 private <T extends IMergable> void mergeEnumField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
227 String propertyName = field.getName();
228 MergeMode mergeMode = this.getMergeMode(propertyName);
229 if (mergeMode != MergeMode.FIRST){
230 mergeCdmBaseValue(mergeFirst, mergeSecond, field, deleteSet);
231 }
232 logger.debug(propertyName + ": " + mergeMode + ", " + field.getType().getName());
233
234 }
235
236 /**
237 * @throws Exception
238 *
239 */
240 private <T extends IMergable> void mergeSingleCdmBaseField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
241 String propertyName = field.getName();
242 MergeMode mergeMode = this.getMergeMode(propertyName);
243 if (mergeMode != MergeMode.FIRST){
244 mergeCdmBaseValue(mergeFirst, mergeSecond, field, deleteSet);
245 }
246 logger.debug(propertyName + ": " + mergeMode + ", " + field.getType().getName());
247
248 }
249
250 private <T extends IMergable> void mergeCdmBaseValue(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
251 if (true){
252 Object value = getMergeValue(mergeFirst, mergeSecond, field);
253 if (value instanceof ICdmBase || value == null){
254 field.set(mergeFirst, (ICdmBase)value);
255 }else{
256 throw new MergeException("Merged value must be of type CdmBase but is not: " + value.getClass());
257 }
258 }else{
259 throw new MergeException("Not supported mode");
260 }
261 }
262
263 /**
264 * @throws Exception
265 *
266 */
267 private <T extends IMergable> void mergeUserTypeField(T mergeFirst, T mergeSecond, Field field) throws Exception {
268 String propertyName = field.getName();
269 Class<?> fieldType = field.getType();
270 MergeMode mergeMode = this.getMergeMode(propertyName);
271 if (mergeMode == MergeMode.MERGE){
272 Method mergeMethod = getMergeMethod(fieldType);
273 Object firstObject = field.get(mergeFirst);
274 if (firstObject == null){
275 firstObject = fieldType.newInstance();
276 }
277 Object secondObject = field.get(mergeSecond);
278 mergeMethod.invoke(firstObject, secondObject);
279 }else if (mergeMode != MergeMode.FIRST){
280 Object value = getMergeValue(mergeFirst, mergeSecond, field);
281 field.set(mergeFirst, value);
282 }
283 logger.debug(propertyName + ": " + mergeMode + ", " + fieldType.getName());
284 }
285
286 /**
287 * @return
288 * @throws NoSuchMethodException
289 * @throws SecurityException
290 */
291 private Method getMergeMethod(Class<?> fieldType) throws SecurityException, NoSuchMethodException {
292 Method mergeMethod = fieldType.getDeclaredMethod("merge", fieldType);
293 return mergeMethod;
294 }
295
296
297
298 /**
299 * @throws Exception
300 *
301 */
302 private <T extends IMergable> void mergeCollectionField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet, Set<ICdmBase> clonedObjects) throws Exception {
303 String propertyName = field.getName();
304 Class<?> fieldType = field.getType();
305 MergeMode mergeMode = this.getMergeMode(propertyName);
306 if (mergeMode != MergeMode.FIRST){
307 mergeCollectionFieldNoFirst(mergeFirst, mergeSecond, field, mergeMode, deleteSet, clonedObjects);
308 }
309 logger.debug(propertyName + ": " + mergeMode + ", " + fieldType.getName());
310
311 }
312
313 private <T extends IMergable> void mergeCollectionFieldNoFirst(T mergeFirst, T mergeSecond, Field field, MergeMode mergeMode, Set<ICdmBase> deleteSet, Set<ICdmBase> clonedObjects) throws Exception{
314 Class<?> fieldType = field.getType();
315 if (mergeMode == MergeMode.ADD || mergeMode == MergeMode.ADD_CLONE){
316 //FIXME
317 Method addMethod = getAddMethod(field);
318 Method removeMethod = getAddMethod(field, true);
319
320 if (Set.class.isAssignableFrom(fieldType) || List.class.isAssignableFrom(fieldType)){
321 Collection<ICdmBase> secondCollection = (Collection<ICdmBase>)field.get(mergeSecond);
322 List<ICdmBase> removeList = new ArrayList<ICdmBase>();
323 for (ICdmBase obj : secondCollection){
324 Object objectToAdd;
325 if (mergeMode == MergeMode.ADD){
326 objectToAdd = obj;
327 }else if(mergeMode == MergeMode.ADD_CLONE){
328 Method cloneMethod = obj.getClass().getDeclaredMethod("clone");
329 objectToAdd = cloneMethod.invoke(obj);
330 clonedObjects.add(obj);
331 }else{
332 throw new MergeException("Unknown collection merge mode: " + mergeMode);
333 }
334 addMethod.invoke(mergeFirst, objectToAdd);
335 removeList.add(obj);
336 }
337 for (ICdmBase removeObj : removeList ){
338 //removeMethod.invoke(mergeSecond, removeObj);
339 if ((removeObj instanceof CdmBase)&& mergeMode == MergeMode.ADD_CLONE) {
340 deleteSet.add(removeObj);
341 }
342 }
343 }else{
344 throw new MergeException("Merge for collections other than sets and lists not yet implemented");
345 }
346 }else if (mergeMode == MergeMode.RELATION){
347 if (Set.class.isAssignableFrom(fieldType) || List.class.isAssignableFrom(fieldType)){
348 Collection<RelationshipBase<?,?,?>> secondCollection = (Collection<RelationshipBase<?,?,?>>)field.get(mergeSecond);
349 List<ICdmBase> removeList = new ArrayList<ICdmBase>();
350 for (RelationshipBase<?,?,?> relation : secondCollection){
351 Method relatedFromMethod = RelationshipBase.class.getDeclaredMethod("getRelatedFrom");
352 relatedFromMethod.setAccessible(true);
353 Object relatedFrom = relatedFromMethod.invoke(relation);
354
355 Method relatedToMethod = RelationshipBase.class.getDeclaredMethod("getRelatedTo");
356 relatedToMethod.setAccessible(true);
357 Object relatedTo = relatedToMethod.invoke(relation);
358
359 if (relatedFrom.equals(mergeSecond)){
360 Method setRelatedMethod = RelationshipBase.class.getDeclaredMethod("setRelatedFrom", IRelated.class);
361 setRelatedMethod.setAccessible(true);
362 setRelatedMethod.invoke(relation, mergeFirst);
363 }
364 if (relatedTo.equals(mergeSecond)){
365 Method setRelatedMethod = RelationshipBase.class.getDeclaredMethod("setRelatedTo", IRelated.class);
366 setRelatedMethod.setAccessible(true);
367 setRelatedMethod.invoke(relation, mergeFirst);
368 }
369 ((IRelated)mergeFirst).addRelationship(relation);
370 removeList.add(relation);
371 }
372 for (ICdmBase removeObj : removeList){
373 //removeMethod.invoke(mergeSecond, removeObj);
374 if (removeObj instanceof CdmBase){
375 deleteSet.add(removeObj);
376 }
377 }
378 }else{
379 throw new MergeException("Merge for collections other than sets and lists not yet implemented");
380 }
381 }else{
382 throw new MergeException("Other merge modes for collections not yet implemented");
383 }
384 }
385
386
387 private Method getAddMethod(Field field) throws MergeException{
388 return getAddMethod(field, false);
389 }
390
391 public static Method getAddMethod(Field field, boolean remove) throws MergeException{
392 Method result;
393 Class parameterClass = getCollectionType(field);
394 String fieldName = field.getName();
395 String firstCapital = fieldName.substring(0, 1).toUpperCase();
396 String rest = fieldName.substring(1);
397 String prefix = remove? "remove": "add";
398 String methodName = prefix + firstCapital + rest;
399 boolean endsWithS = parameterClass.getSimpleName().endsWith("s");
400 if (! endsWithS && ! fieldName.equals("media")){
401 methodName = methodName.substring(0, methodName.length() -1); //remove 's' at end
402 }
403 Class<?> methodClass = field.getDeclaringClass();
404 try {
405 result = methodClass.getMethod(methodName, parameterClass);
406 }catch (NoSuchMethodException e1) {
407 try {
408 result = methodClass.getDeclaredMethod(methodName, parameterClass);
409 result.setAccessible(true);
410 } catch (NoSuchMethodException e) {
411 logger.warn(methodName);
412 throw new IllegalArgumentException("Default adding method for collection field ("+field.getName()+") does not exist");
413 }
414 } catch (SecurityException e) {
415 throw e;
416 }
417 return result;
418 }
419
420 /**
421 * @throws Exception
422 *
423 */
424 private <T extends IMergable> void mergePrimitiveField(T mergeFirst, T mergeSecond, Field field) throws Exception {
425 String propertyName = field.getName();
426 Class<?> fieldType = field.getType();
427 MergeMode mergeMode = this.getMergeMode(propertyName);
428 if (mergeMode != MergeMode.FIRST){
429 Object value = getMergeValue(mergeFirst, mergeSecond, field);
430 field.set(mergeFirst, value);
431 }
432 logger.debug(propertyName + ": " + mergeMode + ", " + fieldType.getName());
433
434 }
435
436 /**
437 * @throws Exception
438 *
439 */
440 private <T extends IMergable> void mergeStringField(T mergeFirst, T mergeSecond, Field field) throws Exception {
441 String propertyName = field.getName();
442 Class<?> fieldType = field.getType();
443 MergeMode mergeMode = this.getMergeMode(propertyName);
444 if (mergeMode != MergeMode.FIRST){
445 Object value = getMergeValue(mergeFirst, mergeSecond, field);
446 field.set(mergeFirst, value);
447 }
448 logger.debug(propertyName + ": " + mergeMode + ", " + fieldType.getName());
449
450 }
451
452 /**
453 * @param fieldType
454 * @return
455 */
456 private boolean isIdentifier(Field field) {
457 Class<?> fieldType = field.getType();
458 if ("id".equals(field.getName()) && fieldType == int.class ){
459 return true;
460 }else if ("uuid".equals(field.getName()) && fieldType == UUID.class ){
461 return true;
462 }else{
463 return false;
464 }
465 }
466
467
468 /**
469 * @param cdmBase
470 * @param toMerge
471 * @param field
472 * @param mergeMode
473 * @throws Exception
474 */
475 protected <T extends IMergable> Object getMergeValue(T mergeFirst, T mergeSecond,
476 Field field) throws Exception {
477 MergeMode mergeMode = this.getMergeMode(field.getName());
478 try {
479 if (mergeMode == MergeMode.FIRST){
480 return field.get(mergeFirst);
481 }else if (mergeMode == MergeMode.SECOND){
482 return field.get(mergeSecond);
483 }else if (mergeMode == MergeMode.NULL){
484 return null;
485 }else if (mergeMode == MergeMode.CONCAT){
486 return ((String)field.get(mergeFirst) + (String)field.get(mergeSecond));
487 }else if (mergeMode == MergeMode.AND){
488 return ((Boolean)field.get(mergeFirst) && (Boolean)field.get(mergeSecond));
489 }else if (mergeMode == MergeMode.OR){
490 return ((Boolean)field.get(mergeFirst) || (Boolean)field.get(mergeSecond));
491 }else{
492 throw new IllegalStateException("Unknown MergeMode");
493 }
494 } catch (IllegalArgumentException e) {
495 throw new Exception(e);
496 }
497 }
498
499
500 private static Class getCollectionType(Field field) throws MergeException{
501 Type genericType = (ParameterizedTypeImpl)field.getGenericType();
502 if (genericType instanceof ParameterizedTypeImpl){
503 ParameterizedTypeImpl paraType = (ParameterizedTypeImpl)genericType;
504 Class<?> rawType = paraType.getRawType();
505 Type[] arguments = paraType.getActualTypeArguments();
506
507 if (arguments.length == 1){
508 Class collectionClass;
509 if (arguments[0] instanceof Class){
510 collectionClass = (Class)arguments[0];
511 }else if(arguments[0] instanceof TypeVariableImpl){
512 TypeVariableImpl typeVariable = (TypeVariableImpl)arguments[0];
513 GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
514 collectionClass = (Class)genericDeclaration;
515 }else{
516 throw new MergeException("Collection with other types than TypeVariableImpl are not yet supported");
517 }
518 return collectionClass;
519 }else{
520 throw new MergeException("Collection with multiple types not supported");
521 }
522 }else{
523 throw new MergeException("Collection has no generic type of type ParameterizedTypeImpl. Unsupport case.");
524 }
525 }
526
527
528
529 }