Project

General

Profile

Download (18.1 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
 */
42
public class DefaultMergeStrategy extends StrategyBase implements IMergeStrategy  {
43
	private static final long serialVersionUID = -8513956338156791995L;
44
	private static final Logger logger = Logger.getLogger(DefaultMergeStrategy.class);
45
	final static UUID uuid = UUID.fromString("d85cd6c3-0147-452c-8fed-bbfb82f392f6");
46

    
47
	public static DefaultMergeStrategy NewInstance(Class<? extends CdmBase> mergeClazz){
48
		return new DefaultMergeStrategy(mergeClazz);
49
	}
50

    
51
	private final boolean onlyReallocateReferences = false;
52

    
53
	protected MergeMode defaultMergeMode = MergeMode.FIRST;
54
	protected MergeMode defaultCollectionMergeMode = MergeMode.ADD;
55

    
56
	protected Map<String, MergeMode> mergeModeMap = new HashMap<String, MergeMode>();
57
	protected Class<? extends CdmBase> mergeClass;
58
	protected Map<String, Field> mergeFields;
59

    
60
	protected DefaultMergeStrategy(Class<? extends CdmBase> mergeClazz) {
61
		super();
62
		if (mergeClazz == null){
63
			throw new IllegalArgumentException("Merge class must not be null");
64
		}
65
		this.mergeClass = mergeClazz;
66
		boolean includeStatic = false;
67
		boolean includeTransient = false;
68
		boolean makeAccessible = true;
69
		this.mergeFields = CdmUtils.getAllFields(mergeClass, CdmBase.class, includeStatic, includeTransient, makeAccessible, true);
70
		initMergeModeMap();
71
	}
72

    
73

    
74
	@Override
75
	public boolean isOnlyReallocateReferences() {
76
		return this.onlyReallocateReferences;
77
	}
78

    
79
//	@Override
80
//	public void setOnlyReallocateLinks(boolean onlyReallocateReferences) {
81
//		this.onlyReallocateReferences = onlyReallocateReferences;
82
//	}
83

    
84

    
85
	/**
86
	 *
87
	 */
88
	private void initMergeModeMap() {
89
		for (Field field: mergeFields.values()){
90
			for (Annotation annotation : field.getAnnotations()){
91
				if (annotation.annotationType() == Merge.class){
92
					MergeMode mergeMode = ((Merge)annotation).value();
93
					mergeModeMap.put(field.getName(), mergeMode);
94
				}
95
			}
96
		}
97
	}
98

    
99
	@Override
100
	protected UUID getUuid() {
101
		return uuid;
102
	}
103

    
104
	/**
105
	 * @return the merge class
106
	 */
107
	public Class<? extends CdmBase> getMergeClass() {
108
		return mergeClass;
109
	}
110

    
111

    
112

    
113
	/**
114
	 * @param mergeClazz the mergeClazz to set
115
	 */
116
	public void setMergeClazz(Class<? extends CdmBase> mergeClazz) {
117
		this.mergeClass = mergeClazz;
118
	}
119

    
120

    
121
	@Override
122
	public MergeMode getMergeMode(String propertyName){
123
		MergeMode result = mergeModeMap.get(propertyName);
124
		if (result == null){
125
			Field field = mergeFields.get(propertyName);
126
			if (isCollection(field.getType())){
127
				return defaultCollectionMergeMode;
128
			}else{
129
				return defaultMergeMode;
130
			}
131
		}else{
132
			return result;
133
		}
134
	}
135

    
136
	@Override
137
	public void setMergeMode(String propertyName, MergeMode mergeMode) throws MergeException{
138
		if (mergeFields.containsKey(propertyName)){
139
			checkIdentifier(propertyName, mergeMode);
140
			mergeModeMap.put(propertyName, mergeMode);
141
		}else{
142
			throw new MergeException("The class " + mergeClass.getName() + " does not contain a field with name " + propertyName);
143
		}
144
	}
145

    
146

    
147
	@Override
148
	public void setDefaultMergeMode(MergeMode defaultMergeMode) {
149
		this.defaultMergeMode = defaultMergeMode;
150
	}
151

    
152

    
153
	@Override
154
	public void setDefaultCollectionMergeMode(MergeMode defaultCollectionMergeMode) {
155
		this.defaultCollectionMergeMode = defaultCollectionMergeMode;
156
	}
157

    
158

    
159
	/**
160
	 * Tests if a property is an identifier property
161
	 * @param propertyName
162
	 * @param mergeMode
163
	 * @throws MergeException
164
	 */
165
	private void checkIdentifier(String propertyName, MergeMode mergeMode) throws MergeException {
166
		if (mergeMode != MergeMode.FIRST){
167
			if ("id".equalsIgnoreCase(propertyName) || "uuid".equalsIgnoreCase(propertyName)){
168
				throw new MergeException("Identifier must always have merge mode MergeMode.FIRST");
169
			}
170
		}
171
	}
172

    
173
	@Override
174
	public <T extends IMergable> Set<ICdmBase> invoke(T mergeFirst, T mergeSecond) throws MergeException {
175
		return this.invoke(mergeFirst, mergeSecond, null);
176
	}
177

    
178
	@Override
179
	public <T extends IMergable> Set<ICdmBase> invoke(T mergeFirst, T mergeSecond, Set<ICdmBase> clonedObjects) throws MergeException {
180
		Set<ICdmBase> deleteSet = new HashSet<ICdmBase>();
181
		if (clonedObjects == null){
182
			clonedObjects = new HashSet<ICdmBase>();
183
		}
184
		deleteSet.add(mergeSecond);
185
		try {
186
 			for (Field field : mergeFields.values()){
187
				Class<?> fieldType = field.getType();
188
				if (isIdentifier(field)){
189
					//do nothing (id and uuid stay with first object)
190
				}else if (isPrimitive(fieldType)){
191
					mergePrimitiveField(mergeFirst, mergeSecond, field);
192
				}else if (fieldType == String.class ){
193
					mergeStringField(mergeFirst, mergeSecond, field);
194
				}else if (isCollection(fieldType)){
195
					mergeCollectionField(mergeFirst, mergeSecond, field, deleteSet, clonedObjects);
196
				}else if(isUserType(fieldType)){
197
					mergeUserTypeField(mergeFirst, mergeSecond, field);
198
				}else if(isSingleCdmBaseObject(fieldType)){
199
					mergeSingleCdmBaseField(mergeFirst, mergeSecond, field, deleteSet);
200
				}else if(fieldType.isInterface()){
201
					mergeInterfaceField(mergeFirst, mergeSecond, field, deleteSet);
202
				}else if(fieldType.isEnum()){
203
					mergeEnumField(mergeFirst, mergeSecond, field, deleteSet);
204
				}else{
205
					throw new RuntimeException("Unknown Object type for merging: " + fieldType);
206
				}
207
			}
208
 			return deleteSet;
209
		} catch (Exception e) {
210
			throw new MergeException("Merge Exception in invoke", e);
211
		}
212
	}
213

    
214

    
215
	/**
216
	 * @throws Exception
217
	 *
218
	 */
219
	private <T extends IMergable> void mergeInterfaceField(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 mergeEnumField(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
	/**
244
	 * @throws Exception
245
	 *
246
	 */
247
	private <T extends IMergable> void mergeSingleCdmBaseField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
248
		String propertyName = field.getName();
249
		MergeMode mergeMode =  this.getMergeMode(propertyName);
250
		if (mergeMode != MergeMode.FIRST){
251
			mergeCdmBaseValue(mergeFirst, mergeSecond, field, deleteSet);
252
		}
253
		logger.debug(propertyName + ": " + mergeMode + ", " + field.getType().getName());
254

    
255
	}
256

    
257
	private <T extends IMergable> void mergeCdmBaseValue(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
258
		if (true){
259
			Object value = getMergeValue(mergeFirst, mergeSecond, field);
260
			if (value instanceof ICdmBase || value == null){
261
				field.set(mergeFirst, value);
262
			}else{
263
				throw new MergeException("Merged value must be of type CdmBase but is not: " + value.getClass());
264
			}
265
		}else{
266
			throw new MergeException("Not supported mode");
267
		}
268
	}
269

    
270
	/**
271
	 * @throws Exception
272
	 *
273
	 */
274
	private <T extends IMergable> void mergeUserTypeField(T mergeFirst, T mergeSecond, Field field) throws Exception {
275
		String propertyName = field.getName();
276
		Class<?> fieldType = field.getType();
277
		MergeMode mergeMode =  this.getMergeMode(propertyName);
278
		if (mergeMode == MergeMode.MERGE){
279
			Method mergeMethod = getMergeMethod(fieldType);
280
			Object firstObject = field.get(mergeFirst);
281
			if (firstObject == null){
282
				firstObject = fieldType.newInstance();
283
			}
284
			Object secondObject = field.get(mergeSecond);
285
			mergeMethod.invoke(firstObject, secondObject);
286
		}else if (mergeMode != MergeMode.FIRST){
287
			Object value = getMergeValue(mergeFirst, mergeSecond, field);
288
			field.set(mergeFirst, value);
289
		}
290
		logger.debug(propertyName + ": " + mergeMode + ", " + fieldType.getName());
291
	}
292

    
293
	/**
294
	 * @return
295
	 * @throws NoSuchMethodException
296
	 * @throws SecurityException
297
	 */
298
	private Method getMergeMethod(Class<?> fieldType) throws SecurityException, NoSuchMethodException {
299
		Method mergeMethod = fieldType.getDeclaredMethod("merge", fieldType);
300
		return mergeMethod;
301
	}
302

    
303

    
304

    
305
	/**
306
	 * @throws Exception
307
	 *
308
	 */
309
	private <T extends IMergable> void mergeCollectionField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet, Set<ICdmBase> clonedObjects) throws Exception {
310
		String propertyName = field.getName();
311
		Class<?> fieldType = field.getType();
312
		MergeMode mergeMode =  this.getMergeMode(propertyName);
313
		if (mergeMode != MergeMode.FIRST){
314
			mergeCollectionFieldNoFirst(mergeFirst, mergeSecond, field, mergeMode, deleteSet, clonedObjects);
315
		}
316
		logger.debug(propertyName + ": " + mergeMode + ", " + fieldType.getName());
317
	}
318

    
319
	protected <T extends IMergable> void mergeCollectionFieldNoFirst(T mergeFirst, T mergeSecond, Field field, MergeMode mergeMode, Set<ICdmBase> deleteSet, Set<ICdmBase> clonedObjects) throws Exception{
320
		Class<?> fieldType = field.getType();
321
		if (mergeMode == MergeMode.ADD || mergeMode == MergeMode.ADD_CLONE){
322
			//FIXME
323
			Method addMethod = getAddMethod(field);
324
			Method removeMethod = getAddMethod(field, true);
325

    
326
			if (Set.class.isAssignableFrom(fieldType) || List.class.isAssignableFrom(fieldType)){
327
				Collection<ICdmBase> secondCollection = (Collection<ICdmBase>)field.get(mergeSecond);
328
				List<ICdmBase> removeList = new ArrayList<ICdmBase>();
329
				if(secondCollection != null) {
330
    				for (ICdmBase obj : secondCollection){
331
    					Object objectToAdd;
332
    					if (mergeMode == MergeMode.ADD){
333
    						objectToAdd = obj;
334
    					}else if(mergeMode == MergeMode.ADD_CLONE){
335
    						Method cloneMethod = obj.getClass().getDeclaredMethod("clone");
336
    						objectToAdd = cloneMethod.invoke(obj);
337
    						clonedObjects.add(obj);
338
    					}else{
339
    						throw new MergeException("Unknown collection merge mode: " + mergeMode);
340
    					}
341
    					addMethod.invoke(mergeFirst, objectToAdd);
342
    					removeList.add(obj);
343
    				}
344
				}
345
				for (ICdmBase removeObj : removeList ){
346
					//removeMethod.invoke(mergeSecond, removeObj);
347
					if ((removeObj instanceof CdmBase)&& mergeMode == MergeMode.ADD_CLONE) {
348
						deleteSet.add(removeObj);
349
					}
350
				}
351
			}else{
352
				throw new MergeException("Merge for collections other than sets and lists not yet implemented");
353
			}
354
		}else if (mergeMode == MergeMode.RELATION){
355
			if (Set.class.isAssignableFrom(fieldType) || List.class.isAssignableFrom(fieldType)){
356
				Collection<RelationshipBase<?,?,?>> secondCollection = (Collection<RelationshipBase<?,?,?>>)field.get(mergeSecond);
357
				List<ICdmBase> removeList = new ArrayList<ICdmBase>();
358
				for (RelationshipBase<?,?,?> relation : secondCollection){
359
					Method relatedFromMethod = RelationshipBase.class.getDeclaredMethod("getRelatedFrom");
360
					relatedFromMethod.setAccessible(true);
361
					Object relatedFrom = relatedFromMethod.invoke(relation);
362

    
363
					Method relatedToMethod = RelationshipBase.class.getDeclaredMethod("getRelatedTo");
364
					relatedToMethod.setAccessible(true);
365
					Object relatedTo = relatedToMethod.invoke(relation);
366

    
367
					if (relatedFrom.equals(mergeSecond)){
368
						Method setRelatedMethod = RelationshipBase.class.getDeclaredMethod("setRelatedFrom", IRelated.class);
369
						setRelatedMethod.setAccessible(true);
370
						setRelatedMethod.invoke(relation, mergeFirst);
371
					}
372
					if (relatedTo.equals(mergeSecond)){
373
						Method setRelatedMethod = RelationshipBase.class.getDeclaredMethod("setRelatedTo", IRelated.class);
374
						setRelatedMethod.setAccessible(true);
375
						setRelatedMethod.invoke(relation, mergeFirst);
376
					}
377
					((IRelated)mergeFirst).addRelationship(relation);
378
					removeList.add(relation);
379
				}
380
				for (ICdmBase removeObj : removeList){
381
					//removeMethod.invoke(mergeSecond, removeObj);
382
					if (removeObj instanceof CdmBase){
383
						deleteSet.add(removeObj);
384
					}
385
				}
386
			}else{
387
				throw new MergeException("Merge for collections other than sets and lists not yet implemented");
388
			}
389
		}else{
390
			throw new MergeException("Other merge modes for collections not yet implemented");
391
		}
392
	}
393

    
394
	public static Method getReplaceMethod(Field field) throws MergeException{
395
	    return getAddMethod(field, false, true);
396
	}
397

    
398
	private Method getAddMethod(Field field) throws MergeException{
399
		return getAddMethod(field, false, false);
400
	}
401

    
402
	public static Method getAddMethod(Field field, boolean remove) throws MergeException{
403
	    return getAddMethod(field, remove, false);
404
	}
405

    
406
	private static Method getAddMethod(Field field, boolean remove, boolean replace) throws MergeException{
407
		Method result;
408
		Class<?> parameterClass = getCollectionType(field);
409
		Class<?>[] parameterClasses = replace ?
410
		        new Class[]{parameterClass,parameterClass}:
411
		        new Class[]{parameterClass};
412
		String fieldName = field.getName();
413
		String firstCapital = fieldName.substring(0, 1).toUpperCase();
414
		String rest = fieldName.substring(1);
415
		String prefix = replace? "replace" : remove? "remove": "add";
416
		String methodName = prefix + firstCapital + rest;
417
		boolean endsWithS = parameterClass.getSimpleName().endsWith("s");
418
		if (! endsWithS && ! fieldName.equals("media")){
419
			methodName = methodName.substring(0, methodName.length() -1); //remove 's' at end
420
		}
421
		Class<?> methodClass = field.getDeclaringClass();
422
		try {
423
			result = methodClass.getMethod(methodName, parameterClasses);
424
		}catch (NoSuchMethodException e1) {
425
			try {
426
				result = methodClass.getDeclaredMethod(methodName, parameterClasses);
427
				result.setAccessible(true);
428
			} catch (NoSuchMethodException e) {
429
				logger.warn(methodName);
430
				throw new IllegalArgumentException("Default '" +prefix+"' method for collection field ("+field.getName()+") does not exist");
431
			}
432
		} catch (SecurityException e) {
433
			throw e;
434
		}
435
		return result;
436
	}
437

    
438
	/**
439
	 * @throws Exception
440
	 *
441
	 */
442
	private <T extends IMergable> void mergePrimitiveField(T mergeFirst, T mergeSecond, Field field) throws Exception {
443
		String propertyName = field.getName();
444
		Class<?> fieldType = field.getType();
445
		MergeMode mergeMode =  this.getMergeMode(propertyName);
446
		if (mergeMode != MergeMode.FIRST){
447
			Object value = getMergeValue(mergeFirst, mergeSecond, field);
448
			field.set(mergeFirst, value);
449
		}
450
		logger.debug(propertyName + ": " + mergeMode + ", " + fieldType.getName());
451

    
452
	}
453

    
454
	/**
455
	 * @throws Exception
456
	 *
457
	 */
458
	private <T extends IMergable> void mergeStringField(T mergeFirst, T mergeSecond, Field field) throws Exception {
459
		String propertyName = field.getName();
460
		Class<?> fieldType = field.getType();
461
		MergeMode mergeMode =  this.getMergeMode(propertyName);
462
		if (mergeMode != MergeMode.FIRST){
463
			Object value = getMergeValue(mergeFirst, mergeSecond, field);
464
			field.set(mergeFirst, value);
465
		}
466
		logger.debug(propertyName + ": " + mergeMode + ", " + fieldType.getName());
467

    
468
	}
469

    
470
	/**
471
	 * @param fieldType
472
	 * @return
473
	 */
474
	private boolean isIdentifier(Field field) {
475
		Class<?> fieldType = field.getType();
476
		if ("id".equals(field.getName()) && fieldType == int.class ){
477
			return true;
478
		}else if ("uuid".equals(field.getName()) && fieldType == UUID.class ){
479
			return true;
480
		}else{
481
			return false;
482
		}
483
	}
484

    
485

    
486
	/**
487
	 * @param cdmBase
488
	 * @param toMerge
489
	 * @param field
490
	 * @param mergeMode
491
	 * @throws Exception
492
	 */
493
	protected <T extends IMergable> Object getMergeValue(T mergeFirst, T mergeSecond,
494
			Field field) throws Exception {
495
		MergeMode mergeMode =  this.getMergeMode(field.getName());
496
		try {
497
			if (mergeMode == MergeMode.FIRST){
498
				return field.get(mergeFirst);
499
			}else if (mergeMode == MergeMode.SECOND){
500
				return field.get(mergeSecond);
501
			}else if (mergeMode == MergeMode.NULL){
502
				return null;
503
			}else if (mergeMode == MergeMode.CONCAT){
504
				return ((String)field.get(mergeFirst) + (String)field.get(mergeSecond));
505
			}else if (mergeMode == MergeMode.AND){
506
				return ((Boolean)field.get(mergeFirst) && (Boolean)field.get(mergeSecond));
507
			}else if (mergeMode == MergeMode.OR){
508
				return ((Boolean)field.get(mergeFirst) || (Boolean)field.get(mergeSecond));
509
			}else{
510
				throw new IllegalStateException("Unknown MergeMode");
511
			}
512
		} catch (IllegalArgumentException e) {
513
			throw new Exception(e);
514
		}
515
	}
516

    
517

    
518
	private static Class getCollectionType(Field field) throws MergeException{
519
		Type genericType = field.getGenericType();
520
		if (genericType instanceof ParameterizedType/*Impl*/){
521
			ParameterizedType paraType = (ParameterizedType)genericType;
522
			Type rawType = paraType.getRawType();
523
			Type[] arguments = paraType.getActualTypeArguments();
524

    
525
			if (arguments.length == 1){
526
				Class collectionClass;
527
				if (arguments[0] instanceof Class){
528
					collectionClass = (Class)arguments[0];
529
				}else if(arguments[0] instanceof TypeVariable/*Impl*/){
530
					TypeVariable typeVariable = (TypeVariable)arguments[0];
531
					GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
532
					collectionClass = (Class)genericDeclaration;
533
				}else{
534
					throw new MergeException("Collection with other types than TypeVariableImpl are not yet supported");
535
				}
536
				return collectionClass;
537
			}else{
538
				throw new MergeException("Collection with multiple types not supported");
539
			}
540
		}else{
541
			throw new MergeException("Collection has no generic type of type ParameterizedTypeImpl. Unsupport case.");
542
		}
543
	}
544

    
545
}
(2-2/7)