/**
* Copyright 2011-2015 John Ericksen
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.androidtransfuse.processor;
import org.androidtransfuse.TransfuseAnalysisException;
import org.androidtransfuse.model.Identified;
import org.androidtransfuse.model.Mergeable;
import org.apache.commons.beanutils.PropertyUtils;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
/**
* @author John Ericksen
*/
public class Merger {
public <T> T merge(Class<? extends T> targetClass, T target, T source) throws MergerException {
if (target == null) {
return source;
} else if (source == null) {
return target;
}
if (!Mergeable.class.isAssignableFrom(targetClass)) {
return target;
}
return (T) mergeMergeable((Class<? extends Mergeable>) targetClass, (Mergeable) target, (Mergeable) source);
}
private <T extends Mergeable> T mergeMergeable(Class<? extends T> targetClass, T target, T source) throws MergerException {
try {
BeanInfo beanInfo = Introspector.getBeanInfo(targetClass);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
Method getter = propertyDescriptor.getReadMethod();
Method setter = propertyDescriptor.getWriteMethod();
String propertyName = propertyDescriptor.getDisplayName();
if (PropertyUtils.isWriteable(target, propertyName)) {
//check for mergeCollection
MergeCollection mergeCollection = findAnnotation(MergeCollection.class, getter, setter);
if (Collection.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
PropertyUtils.setProperty(target, propertyName, mergeList(mergeCollection, propertyName, target, source));
}
//check for merge
Merge mergeAnnotation = findAnnotation(Merge.class, getter, setter);
PropertyUtils.setProperty(target, propertyName, mergeProperties(mergeAnnotation, propertyName, target, source));
}
}
} catch (NoSuchMethodException e) {
throw new MergerException("NoSuchMethodException while trying to merge", e);
} catch (IntrospectionException e) {
throw new MergerException("IntrospectionException while trying to merge", e);
} catch (IllegalAccessException e) {
throw new MergerException("IllegalAccessException while trying to merge", e);
} catch (InvocationTargetException e) {
throw new MergerException("InvocationTargetException while trying to merge", e);
}
return target;
}
private <T extends Annotation> T findAnnotation(Class<T> annotationClass, Method... methods) {
T annotation = null;
if (methods != null) {
for (Method method : methods) {
if (annotation == null && method != null && method.isAnnotationPresent(annotationClass)) {
annotation = method.getAnnotation(annotationClass);
}
}
}
return annotation;
}
private <T extends Mergeable> Object mergeProperties(Merge mergeAnnotation, String propertyName, T target, T source) throws MergerException {
try {
String tag = null;
if (mergeAnnotation != null) {
tag = mergeAnnotation.value();
}
Object targetProperty = PropertyUtils.getProperty(target, propertyName);
Object sourceProperty = PropertyUtils.getProperty(source, propertyName);
Class propertyType = PropertyUtils.getPropertyType(target, propertyName);
Object merged;
if (tag != null && target.isGenerated() && target.containsTag(tag)) {
merged = sourceProperty;
} else {
merged = merge(propertyType, targetProperty, sourceProperty);
}
updateTag(target, tag, merged == null);
return merged;
} catch (NoSuchMethodException e) {
throw new MergerException("NoSuchMethodException while trying to merge", e);
} catch (IllegalAccessException e) {
throw new MergerException("IllegalAccessException while trying to merge", e);
} catch (InvocationTargetException e) {
throw new MergerException("InvocationTargetException while trying to merge", e);
}
}
private <T extends Mergeable> void updateTag(T target, String tag, boolean remove) {
if(tag != null){
if(remove){
target.removeMergeTag(tag);
}
else{
target.addMergeTag(tag);
}
}
}
private <T extends Mergeable> List mergeList(MergeCollection mergeCollectionAnnotation, String propertyName, T target, T source) throws MergerException {
try {
List targetCollection = (List) PropertyUtils.getProperty(target, propertyName);
List sourceCollection = (List) PropertyUtils.getProperty(source, propertyName);
if (mergeCollectionAnnotation == null) {
return (List) merge(PropertyUtils.getPropertyType(target, propertyName), targetCollection, sourceCollection);
}
//update collection from source
Collection<Mergeable> merged = updateFromSource(targetCollection, sourceCollection, mergeCollectionAnnotation.type());
List targetResult = makeCollection(targetCollection, mergeCollectionAnnotation.collectionType(), target, propertyName);
targetResult.clear();
targetResult.addAll(merged);
return targetResult;
} catch (NoSuchMethodException e) {
throw new MergerException("NoSuchMethodException while trying to merge", e);
} catch (IllegalAccessException e) {
throw new MergerException("IllegalAccessException while trying to merge", e);
} catch (InvocationTargetException e) {
throw new MergerException("InvocationTargetException while trying to merge", e);
}
}
private <T extends Mergeable> List makeCollection(List targetList, Class<? extends List> listType, T target, String propertyName) throws MergerException {
try {
//merger only supports Lists
if (targetList == null) {
//first look for specific impl in annotation
if (listType != List.class) {
return listType.newInstance();
} else {
//try to instantiate field type
return (List) PropertyUtils.getPropertyType(target, propertyName).newInstance();
}
}
return targetList;
} catch (NoSuchMethodException e) {
throw new MergerException("NoSuchMethodException while trying to merge", e);
} catch (IllegalAccessException e) {
throw new MergerException("IllegalAccessException while trying to merge", e);
} catch (InstantiationException e) {
throw new MergerException("InstantiationException while trying to merge", e);
} catch (InvocationTargetException e) {
throw new MergerException("InvocationTargetException while trying to merge", e);
}
}
private List<Mergeable> updateFromSource(List targetList, List sourceList, Class<? extends Mergeable> type) throws MergerException {
Map<Object, Mergeable> targetMap = buildIdentifierMap(targetList);
Map<Object, Mergeable> sourceMap = buildIdentifierMap(sourceList);
Set<Object> originalTargetKeys = new HashSet<Object>(targetMap.keySet());
try {
//update
for (Map.Entry<Object, Mergeable> mergeableSourceEntry : sourceMap.entrySet()) {
Object sourceKey = mergeableSourceEntry.getKey();
if (targetMap.containsKey(sourceKey)) {
//replace
Mergeable targetValue = targetMap.get(sourceKey);
if (targetValue.isGenerated()) {
targetMap.put(sourceKey, merge(type, targetValue, mergeableSourceEntry.getValue()));
}
} else {
targetMap.put(sourceKey, merge(type, type.newInstance(), mergeableSourceEntry.getValue()));
}
originalTargetKeys.remove(sourceKey);
}
//remove the targets were not updated
for (Object targetKey : originalTargetKeys) {
Mergeable mergeable = targetMap.get(targetKey);
if (mergeable.isGenerated()) {
targetMap.remove(targetKey);
}
}
} catch (IllegalAccessException e) {
throw new MergerException("IllegalAccessException while trying to merge", e);
} catch (InstantiationException e) {
throw new MergerException("InstantiationException while trying to merge!", e);
}
//order should e preserved by LinkedHashMap
return new ArrayList<Mergeable>(targetMap.values());
}
private Map<Object, Mergeable> buildIdentifierMap(List input) {
Map<Object, Mergeable> mergeable = new LinkedHashMap<Object, Mergeable>();
if (input != null) {
int i = 0;
for (Object o : input) {
//validate all instance are of type Mergeable
if (!(o instanceof Mergeable)) {
throw new TransfuseAnalysisException("Merge collection failed on type: " + o.getClass().getName());
}
Mergeable t = (Mergeable) o;
Object key;
if(t instanceof Identified){
key = ((Identified)t).getIdentifier();
}
else{
key = i;
}
mergeable.put(key, t);
i++;
}
}
return mergeable;
}
}