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