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
.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
;
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");
55 public static DefaultMergeStrategy
NewInstance(Class
<?
extends CdmBase
> mergeClazz
){
56 return new DefaultMergeStrategy(mergeClazz
);
59 protected Map
<String
, MergeMode
> mergeModeMap
= new HashMap
<String
, MergeMode
>();
60 protected MergeMode defaultMergeMode
= MergeMode
.FIRST
;
61 protected MergeMode defaultCollectionMergeMode
= MergeMode
.ADD
;
63 protected Class
<?
extends CdmBase
> mergeClass
;
64 protected Map
<String
, Field
> mergeFields
;
66 protected DefaultMergeStrategy(Class
<?
extends CdmBase
> mergeClazz
) {
68 if (mergeClazz
== null){
69 throw new IllegalArgumentException("Merge class must not be null");
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);
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
);
98 * @see eu.etaxonomy.cdm.strategy.StrategyBase#getUuid()
101 protected UUID
getUuid() {
108 * @return the merge class
110 public Class
<?
extends CdmBase
> getMergeClass() {
117 * @param mergeClazz the mergeClazz to set
119 public void setMergeClazz(Class
<?
extends CdmBase
> mergeClazz
) {
120 this.mergeClass
= mergeClazz
;
126 * @see eu.etaxonomy.cdm.strategy.merge.IMergeStragegy#getMergeMode(java.lang.String)
128 public MergeMode
getMergeMode(String propertyName
){
129 MergeMode result
= mergeModeMap
.get(propertyName
);
131 Field field
= mergeFields
.get(propertyName
);
132 if (isCollection(field
.getType())){
133 return defaultCollectionMergeMode
;
135 return defaultMergeMode
;
142 public void setMergeMode(String propertyName
, MergeMode mergeMode
) throws MergeException
{
143 if (mergeFields
.containsKey(propertyName
)){
144 checkIdentifier(propertyName
, mergeMode
);
145 mergeModeMap
.put(propertyName
, mergeMode
);
147 throw new MergeException("The class " + mergeClass
.getName() + " does not contain a field with name " + propertyName
);
152 * Tests if a property is an identifier property
153 * @param propertyName
155 * @throws MergeException
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");
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
);
195 }else if(fieldType
.isEnum()){
196 mergeEnumField(mergeFirst
, mergeSecond
, field
, deleteSet
);
198 throw new RuntimeException("Unknown Object type for merging: " + fieldType
);
202 } catch (Exception e
) {
203 throw new MergeException("Merge Exception in invoke", e
);
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
);
218 logger
.debug(propertyName
+ ": " + mergeMode
+ ", " + field
.getType().getName());
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
);
232 logger
.debug(propertyName
+ ": " + mergeMode
+ ", " + field
.getType().getName());
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
);
246 logger
.debug(propertyName
+ ": " + mergeMode
+ ", " + field
.getType().getName());
250 private <T
extends IMergable
> void mergeCdmBaseValue(T mergeFirst
, T mergeSecond
, Field field
, Set
<ICdmBase
> deleteSet
) throws Exception
{
252 Object value
= getMergeValue(mergeFirst
, mergeSecond
, field
);
253 if (value
instanceof ICdmBase
|| value
== null){
254 field
.set(mergeFirst
, (ICdmBase
)value
);
256 throw new MergeException("Merged value must be of type CdmBase but is not: " + value
.getClass());
259 throw new MergeException("Not supported mode");
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();
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
);
283 logger
.debug(propertyName
+ ": " + mergeMode
+ ", " + fieldType
.getName());
288 * @throws NoSuchMethodException
289 * @throws SecurityException
291 private Method
getMergeMethod(Class
<?
> fieldType
) throws SecurityException
, NoSuchMethodException
{
292 Method mergeMethod
= fieldType
.getDeclaredMethod("merge", fieldType
);
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
);
309 logger
.debug(propertyName
+ ": " + mergeMode
+ ", " + fieldType
.getName());
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
){
317 Method addMethod
= getAddMethod(field
);
318 Method removeMethod
= getAddMethod(field
, true);
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
){
325 if (mergeMode
== MergeMode
.ADD
){
327 }else if(mergeMode
== MergeMode
.ADD_CLONE
){
328 Method cloneMethod
= obj
.getClass().getDeclaredMethod("clone");
329 objectToAdd
= cloneMethod
.invoke(obj
);
330 clonedObjects
.add(obj
);
332 throw new MergeException("Unknown collection merge mode: " + mergeMode
);
334 addMethod
.invoke(mergeFirst
, objectToAdd
);
337 for (ICdmBase removeObj
: removeList
){
338 //removeMethod.invoke(mergeSecond, removeObj);
339 if ((removeObj
instanceof CdmBase
)&& mergeMode
== MergeMode
.ADD_CLONE
) {
340 deleteSet
.add(removeObj
);
344 throw new MergeException("Merge for collections other than sets and lists not yet implemented");
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
);
355 Method relatedToMethod
= RelationshipBase
.class.getDeclaredMethod("getRelatedTo");
356 relatedToMethod
.setAccessible(true);
357 Object relatedTo
= relatedToMethod
.invoke(relation
);
359 if (relatedFrom
.equals(mergeSecond
)){
360 Method setRelatedMethod
= RelationshipBase
.class.getDeclaredMethod("setRelatedFrom", IRelated
.class);
361 setRelatedMethod
.setAccessible(true);
362 setRelatedMethod
.invoke(relation
, mergeFirst
);
364 if (relatedTo
.equals(mergeSecond
)){
365 Method setRelatedMethod
= RelationshipBase
.class.getDeclaredMethod("setRelatedTo", IRelated
.class);
366 setRelatedMethod
.setAccessible(true);
367 setRelatedMethod
.invoke(relation
, mergeFirst
);
369 ((IRelated
)mergeFirst
).addRelationship(relation
);
370 removeList
.add(relation
);
372 for (ICdmBase removeObj
: removeList
){
373 //removeMethod.invoke(mergeSecond, removeObj);
374 if (removeObj
instanceof CdmBase
){
375 deleteSet
.add(removeObj
);
379 throw new MergeException("Merge for collections other than sets and lists not yet implemented");
382 throw new MergeException("Other merge modes for collections not yet implemented");
387 private Method
getAddMethod(Field field
) throws MergeException
{
388 return getAddMethod(field
, false);
391 public static Method
getAddMethod(Field field
, boolean remove
) throws MergeException
{
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
403 Class
<?
> methodClass
= field
.getDeclaringClass();
405 result
= methodClass
.getMethod(methodName
, parameterClass
);
406 }catch (NoSuchMethodException e1
) {
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");
414 } catch (SecurityException e
) {
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
);
432 logger
.debug(propertyName
+ ": " + mergeMode
+ ", " + fieldType
.getName());
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
);
448 logger
.debug(propertyName
+ ": " + mergeMode
+ ", " + fieldType
.getName());
456 private boolean isIdentifier(Field field
) {
457 Class
<?
> fieldType
= field
.getType();
458 if ("id".equals(field
.getName()) && fieldType
== int.class ){
460 }else if ("uuid".equals(field
.getName()) && fieldType
== UUID
.class ){
475 protected <T
extends IMergable
> Object
getMergeValue(T mergeFirst
, T mergeSecond
,
476 Field field
) throws Exception
{
477 MergeMode mergeMode
= this.getMergeMode(field
.getName());
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
){
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
));
492 throw new IllegalStateException("Unknown MergeMode");
494 } catch (IllegalArgumentException e
) {
495 throw new Exception(e
);
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();
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
;
516 throw new MergeException("Collection with other types than TypeVariableImpl are not yet supported");
518 return collectionClass
;
520 throw new MergeException("Collection with multiple types not supported");
523 throw new MergeException("Collection has no generic type of type ParameterizedTypeImpl. Unsupport case.");