Project

General

Profile

Download (18 KB) Statistics
| Branch: | Tag: | Revision:
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.ParameterizedType;
18
import java.lang.reflect.Type;
19
import java.lang.reflect.TypeVariable;
20
import java.util.ArrayList;
21
import java.util.Collection;
22
import java.util.HashMap;
23
import java.util.HashSet;
24
import java.util.List;
25
import java.util.Map;
26
import java.util.Set;
27
import java.util.UUID;
28

    
29
import org.apache.log4j.Logger;
30

    
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 = (ParameterizedType)field.getGenericType();
497
		if (genericType instanceof ParameterizedType/*Impl*/){
498
			ParameterizedType paraType = (ParameterizedType)genericType;
499
			Type 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 TypeVariable/*Impl*/){
507
					TypeVariable typeVariable = (TypeVariable)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
}
(1-1/6)