3 * Copyright (C) 2007 EDIT
4 * European Distributed Institute of Taxonomy
5 * http://www.e-taxonomy.eu
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.
11 package eu
.etaxonomy
.cdm
.strategy
.merge
;
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
;
26 import java
.util
.UUID
;
28 import javax
.persistence
.Transient
;
30 import org
.apache
.log4j
.Logger
;
31 import org
.joda
.time
.DateTime
;
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
;
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");
54 public static DefaultMergeStrategy
NewInstance(Class
<?
extends CdmBase
> mergeClazz
){
55 return new DefaultMergeStrategy(mergeClazz
);
58 protected Map
<String
, MergeMode
> mergeModeMap
= new HashMap
<String
, MergeMode
>();
59 protected MergeMode defaultMergeMode
= MergeMode
.FIRST
;
60 protected MergeMode defaultCollectionMergeMode
= MergeMode
.ADD
;
62 protected Class
<?
extends CdmBase
> mergeClass
;
63 protected Map
<String
, Field
> mergeFields
;
65 protected DefaultMergeStrategy(Class
<?
extends CdmBase
> mergeClazz
) {
67 if (mergeClazz
== null){
68 throw new IllegalArgumentException("Merge class must not be null");
70 this.mergeClass
= mergeClazz
;
71 boolean includeStatic
= false;
72 boolean includeTransient
= false;
73 boolean makeAccessible
= true;
74 this.mergeFields
= getAllNonStaticNonTransientFields(mergeClazz
, includeStatic
, includeTransient
, makeAccessible
);
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
);
97 * @see eu.etaxonomy.cdm.strategy.StrategyBase#getUuid()
100 protected UUID
getUuid() {
107 * @return the merge class
109 public Class
<?
extends CdmBase
> getMergeClass() {
116 * @param mergeClazz the mergeClazz to set
118 public void setMergeClazz(Class
<?
extends CdmBase
> mergeClazz
) {
119 this.mergeClass
= mergeClazz
;
125 * @see eu.etaxonomy.cdm.strategy.merge.IMergeStragegy#getMergeMode(java.lang.String)
127 public MergeMode
getMergeMode(String propertyName
){
128 MergeMode result
= mergeModeMap
.get(propertyName
);
130 Field field
= mergeFields
.get(propertyName
);
131 if (isCollection(field
.getType())){
132 return defaultCollectionMergeMode
;
134 return defaultMergeMode
;
141 public void setMergeMode(String propertyName
, MergeMode mergeMode
) throws MergeException
{
142 if (mergeFields
.containsKey(propertyName
)){
143 checkIdentifier(propertyName
, mergeMode
);
144 mergeModeMap
.put(propertyName
, mergeMode
);
146 throw new MergeException("The class " + mergeClass
.getName() + " does not contain a field with name " + propertyName
);
151 * Tests if a property is an identifier property
152 * @param propertyName
154 * @throws MergeException
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");
165 public <T
extends IMergable
> Set
<ICdmBase
> invoke(T mergeFirst
, T mergeSecond
) throws MergeException
{
166 return this.invoke(mergeFirst
, mergeSecond
, null);
170 * @see eu.etaxonomy.cdm.strategy.merge.IMergeStragegy#invoke(eu.etaxonomy.cdm.strategy.merge.IMergable, eu.etaxonomy.cdm.strategy.merge.IMergable)
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
>();
177 deleteSet
.add(mergeSecond
);
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
);
196 throw new RuntimeException("Unknown Object type for merging: " + fieldType
);
200 } catch (Exception e
) {
201 throw new MergeException("Merge Exception in invoke", e
);
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
);
216 System
.out
.println(propertyName
+ ": " + mergeMode
+ ", " + field
.getType().getName());
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
);
231 System
.out
.println(propertyName
+ ": " + mergeMode
+ ", " + field
.getType().getName());
235 private <T
extends IMergable
> void mergeCdmBaseValue(T mergeFirst
, T mergeSecond
, Field field
, Set
<ICdmBase
> deleteSet
) throws Exception
{
237 Object value
= getMergeValue(mergeFirst
, mergeSecond
, field
);
238 if (value
instanceof ICdmBase
|| value
== null){
239 field
.set(mergeFirst
, (ICdmBase
)value
);
241 throw new MergeException("Merged value must be of type CdmBase but is not: " + value
.getClass());
244 throw new MergeException("Not supported mode");
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();
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
);
268 System
.out
.println(propertyName
+ ": " + mergeMode
+ ", " + fieldType
.getName());
273 * @throws NoSuchMethodException
274 * @throws SecurityException
276 private Method
getMergeMethod(Class
<?
> fieldType
) throws SecurityException
, NoSuchMethodException
{
277 Method mergeMethod
= fieldType
.getDeclaredMethod("merge", fieldType
);
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
);
294 System
.out
.println(propertyName
+ ": " + mergeMode
+ ", " + fieldType
.getName());
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
){
302 Method addMethod
= getAddMethod(field
);
303 Method removeMethod
= getAddMethod(field
, true);
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
){
310 if (mergeMode
== MergeMode
.ADD
){
312 }else if(mergeMode
== MergeMode
.ADD_CLONE
){
313 Method cloneMethod
= obj
.getClass().getDeclaredMethod("clone");
314 objectToAdd
= cloneMethod
.invoke(obj
);
315 clonedObjects
.add(obj
);
317 throw new MergeException("Unknown collection merge mode: " + mergeMode
);
319 addMethod
.invoke(mergeFirst
, objectToAdd
);
322 for (ICdmBase removeObj
: removeList
){
323 //removeMethod.invoke(mergeSecond, removeObj);
324 if ((removeObj
instanceof CdmBase
)&& mergeMode
== MergeMode
.ADD_CLONE
) {
325 deleteSet
.add(removeObj
);
329 throw new MergeException("Merge for collections other than sets and lists not yet implemented");
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
);
340 Method relatedToMethod
= RelationshipBase
.class.getDeclaredMethod("getRelatedTo");
341 relatedToMethod
.setAccessible(true);
342 Object relatedTo
= relatedToMethod
.invoke(relation
);
344 if (relatedFrom
.equals(mergeSecond
)){
345 Method setRelatedMethod
= RelationshipBase
.class.getDeclaredMethod("setRelatedFrom", IRelated
.class);
346 setRelatedMethod
.setAccessible(true);
347 setRelatedMethod
.invoke(relation
, mergeFirst
);
349 if (relatedTo
.equals(mergeSecond
)){
350 Method setRelatedMethod
= RelationshipBase
.class.getDeclaredMethod("setRelatedTo", IRelated
.class);
351 setRelatedMethod
.setAccessible(true);
352 setRelatedMethod
.invoke(relation
, mergeFirst
);
354 ((IRelated
)mergeFirst
).addRelationship(relation
);
355 removeList
.add(relation
);
357 for (ICdmBase removeObj
: removeList
){
358 //removeMethod.invoke(mergeSecond, removeObj);
359 if (removeObj
instanceof CdmBase
){
360 deleteSet
.add(removeObj
);
364 throw new MergeException("Merge for collections other than sets and lists not yet implemented");
367 throw new MergeException("Other merge modes for collections not yet implemented");
372 private Method
getAddMethod(Field field
) throws MergeException
{
373 return getAddMethod(field
, false);
376 private Method
getAddMethod(Field field
, boolean remove
) throws MergeException
{
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
388 Class
<?
> methodClass
= field
.getDeclaringClass();
390 result
= methodClass
.getMethod(methodName
, parameterClass
);
391 }catch (NoSuchMethodException e1
) {
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");
399 } catch (SecurityException e
) {
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
);
417 System
.out
.println(propertyName
+ ": " + mergeMode
+ ", " + fieldType
.getName());
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
);
433 System
.out
.println(propertyName
+ ": " + mergeMode
+ ", " + fieldType
.getName());
441 private boolean isIdentifier(Field field
) {
442 Class
<?
> fieldType
= field
.getType();
443 if ("id".equals(field
.getName()) && fieldType
== int.class ){
445 }else if ("uuid".equals(field
.getName()) && fieldType
== UUID
.class ){
456 private boolean isPrimitive(Class
<?
> fieldType
) {
457 if (fieldType
.isPrimitive()){
468 private boolean isSingleCdmBaseObject(Class
<?
> fieldType
) {
469 if (CdmBase
.class.isAssignableFrom(fieldType
)){
482 private boolean isCollection(Class
<?
> fieldType
) {
483 if (Collection
.class.isAssignableFrom(fieldType
) ){
495 private boolean isUserType(Class
<?
> fieldType
) {
496 if ( fieldType
== TimePeriod
.class ||
497 fieldType
== DateTime
.class ||
498 fieldType
== LSID
.class ||
499 fieldType
== Contact
.class
515 protected <T
extends IMergable
> Object
getMergeValue(T mergeFirst
, T mergeSecond
,
516 Field field
) throws Exception
{
517 MergeMode mergeMode
= this.getMergeMode(field
.getName());
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
){
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
));
532 throw new IllegalStateException("Unknown MergeMode");
534 } catch (IllegalArgumentException e
) {
535 throw new Exception(e
);
540 * Computes all fields recursively
544 protected Map
<String
, Field
> getAllNonStaticNonTransientFields(Class clazz
, boolean includeStatic
, boolean includeTransient
, boolean makeAccessible
) {
545 Map
<String
, Field
> result
= new HashMap
<String
, Field
>();
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
);
556 //include superclass fields
557 Class superclass
= clazz
.getSuperclass();
558 if (CdmBase
.class.isAssignableFrom(superclass
)){
559 result
.putAll(getAllNonStaticNonTransientFields(superclass
, includeStatic
, includeTransient
, makeAccessible
));
567 * Returns true, if field has an annotation of type javax.persistence.Annotation
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){
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
;
598 throw new MergeException("Collection with other types than TypeVariableImpl are not yet supported");
600 return collectionClass
;
602 throw new MergeException("Collection with multiple types not supported");
605 throw new MergeException("Collection has no generic type of type ParameterizedTypeImpl. Unsupport case.");