Project

General

Profile

Download (18.1 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2007 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9

    
10
package eu.etaxonomy.cdm.strategy.merge;
11

    
12
import java.lang.annotation.Annotation;
13
import java.lang.reflect.Field;
14
import java.lang.reflect.GenericDeclaration;
15
import java.lang.reflect.Method;
16
import java.lang.reflect.ParameterizedType;
17
import java.lang.reflect.Type;
18
import java.lang.reflect.TypeVariable;
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;
24
import java.util.Map;
25
import java.util.Set;
26
import java.util.UUID;
27

    
28
import org.apache.log4j.Logger;
29

    
30
import eu.etaxonomy.cdm.common.CdmUtils;
31
import eu.etaxonomy.cdm.model.common.CdmBase;
32
import eu.etaxonomy.cdm.model.common.ICdmBase;
33
import eu.etaxonomy.cdm.model.common.IRelated;
34
import eu.etaxonomy.cdm.model.common.RelationshipBase;
35
import eu.etaxonomy.cdm.strategy.StrategyBase;
36

    
37
/**
38
 * @author a.mueller
39
 * @created 31.07.2009
40
 */
41
public class DefaultMergeStrategy extends StrategyBase implements IMergeStrategy  {
42
	private static final long serialVersionUID = -8513956338156791995L;
43
	private static final Logger logger = Logger.getLogger(DefaultMergeStrategy.class);
44
	final static UUID uuid = UUID.fromString("d85cd6c3-0147-452c-8fed-bbfb82f392f6");
45

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

    
50
	private final boolean onlyReallocateReferences = false;
51

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

    
55
	protected Map<String, MergeMode> mergeModeMap = new HashMap<String, MergeMode>();
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
	@Override
74
	public boolean isOnlyReallocateReferences() {
75
		return this.onlyReallocateReferences;
76
	}
77

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

    
83

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

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

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

    
110

    
111

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

    
119

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

    
145

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

    
151

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

    
157

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

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

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

    
213

    
214
	/**
215
	 * @throws Exception
216
	 *
217
	 */
218
	private <T extends IMergable> void mergeInterfaceField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
219
		String propertyName = field.getName();
220
		MergeMode mergeMode =  this.getMergeMode(propertyName);
221
		if (mergeMode != MergeMode.FIRST){
222
			mergeCdmBaseValue(mergeFirst, mergeSecond, field, deleteSet);
223
		}
224
		logger.debug(propertyName + ": " + mergeMode + ", " + field.getType().getName());
225

    
226
	}
227

    
228
	/**
229
	 * @throws Exception
230
	 *
231
	 */
232
	private <T extends IMergable> void mergeEnumField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
233
		String propertyName = field.getName();
234
		MergeMode mergeMode =  this.getMergeMode(propertyName);
235
		if (mergeMode != MergeMode.FIRST){
236
			mergeCdmBaseValue(mergeFirst, mergeSecond, field, deleteSet);
237
		}
238
		logger.debug(propertyName + ": " + mergeMode + ", " + field.getType().getName());
239

    
240
	}
241

    
242
	/**
243
	 * @throws Exception
244
	 *
245
	 */
246
	private <T extends IMergable> void mergeSingleCdmBaseField(T mergeFirst, T mergeSecond, Field field, Set<ICdmBase> deleteSet) throws Exception {
247
		String propertyName = field.getName();
248
		MergeMode mergeMode =  this.getMergeMode(propertyName);
249
		if (mergeMode != MergeMode.FIRST){
250
			mergeCdmBaseValue(mergeFirst, mergeSecond, field, deleteSet);
251
		}
252
		logger.debug(propertyName + ": " + mergeMode + ", " + field.getType().getName());
253

    
254
	}
255

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

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

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

    
302

    
303

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

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

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

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

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

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

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

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

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

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

    
451
	}
452

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

    
467
	}
468

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

    
484

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

    
516

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

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

    
544
}
(2-2/7)