001/** 002 * Copyright 2013 John Ericksen 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.androidtransfuse.processor; 017 018import org.androidtransfuse.TransfuseAnalysisException; 019import org.androidtransfuse.model.Identified; 020import org.androidtransfuse.model.Mergeable; 021import org.apache.commons.beanutils.PropertyUtils; 022 023import java.beans.BeanInfo; 024import java.beans.IntrospectionException; 025import java.beans.Introspector; 026import java.beans.PropertyDescriptor; 027import java.lang.annotation.Annotation; 028import java.lang.reflect.InvocationTargetException; 029import java.lang.reflect.Method; 030import java.util.*; 031 032/** 033 * @author John Ericksen 034 */ 035public class Merger { 036 037 public <T> T merge(Class<? extends T> targetClass, T target, T source) throws MergerException { 038 039 if (target == null) { 040 return source; 041 } else if (source == null) { 042 return target; 043 } 044 045 if (!Mergeable.class.isAssignableFrom(targetClass)) { 046 return target; 047 } 048 049 return (T) mergeMergeable((Class<? extends Mergeable>) targetClass, (Mergeable) target, (Mergeable) source); 050 } 051 052 private <T extends Mergeable> T mergeMergeable(Class<? extends T> targetClass, T target, T source) throws MergerException { 053 054 try { 055 056 BeanInfo beanInfo = Introspector.getBeanInfo(targetClass); 057 058 PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); 059 060 for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { 061 Method getter = propertyDescriptor.getReadMethod(); 062 Method setter = propertyDescriptor.getWriteMethod(); 063 064 String propertyName = propertyDescriptor.getDisplayName(); 065 066 if (PropertyUtils.isWriteable(target, propertyName)) { 067 068 //check for mergeCollection 069 MergeCollection mergeCollection = findAnnotation(MergeCollection.class, getter, setter); 070 if (Collection.class.isAssignableFrom(propertyDescriptor.getPropertyType())) { 071 PropertyUtils.setProperty(target, propertyName, mergeList(mergeCollection, propertyName, target, source)); 072 } 073 074 //check for merge 075 Merge mergeAnnotation = findAnnotation(Merge.class, getter, setter); 076 PropertyUtils.setProperty(target, propertyName, mergeProperties(mergeAnnotation, propertyName, target, source)); 077 } 078 } 079 } catch (NoSuchMethodException e) { 080 throw new MergerException("NoSuchMethodException while trying to merge", e); 081 } catch (IntrospectionException e) { 082 throw new MergerException("IntrospectionException while trying to merge", e); 083 } catch (IllegalAccessException e) { 084 throw new MergerException("IllegalAccessException while trying to merge", e); 085 } catch (InvocationTargetException e) { 086 throw new MergerException("InvocationTargetException while trying to merge", e); 087 } 088 089 return target; 090 } 091 092 private <T extends Annotation> T findAnnotation(Class<T> annotationClass, Method... methods) { 093 T annotation = null; 094 if (methods != null) { 095 for (Method method : methods) { 096 if (annotation == null && method != null && method.isAnnotationPresent(annotationClass)) { 097 annotation = method.getAnnotation(annotationClass); 098 } 099 } 100 } 101 return annotation; 102 } 103 104 private <T extends Mergeable> Object mergeProperties(Merge mergeAnnotation, String propertyName, T target, T source) throws MergerException { 105 106 try { 107 108 String tag = null; 109 if (mergeAnnotation != null) { 110 tag = mergeAnnotation.value(); 111 } 112 113 Object targetProperty = PropertyUtils.getProperty(target, propertyName); 114 Object sourceProperty = PropertyUtils.getProperty(source, propertyName); 115 Class propertyType = PropertyUtils.getPropertyType(target, propertyName); 116 117 Object merged; 118 if (tag != null && target.isGenerated() && target.containsTag(tag)) { 119 merged = sourceProperty; 120 } else { 121 merged = merge(propertyType, targetProperty, sourceProperty); 122 } 123 124 updateTag(target, tag, merged == null); 125 return merged; 126 127 } catch (NoSuchMethodException e) { 128 throw new MergerException("NoSuchMethodException while trying to merge", e); 129 } catch (IllegalAccessException e) { 130 throw new MergerException("IllegalAccessException while trying to merge", e); 131 } catch (InvocationTargetException e) { 132 throw new MergerException("InvocationTargetException while trying to merge", e); 133 } 134 } 135 136 private <T extends Mergeable> void updateTag(T target, String tag, boolean remove) { 137 if(tag != null){ 138 if(remove){ 139 target.removeMergeTag(tag); 140 } 141 else{ 142 target.addMergeTag(tag); 143 } 144 } 145 } 146 147 private <T extends Mergeable> List mergeList(MergeCollection mergeCollectionAnnotation, String propertyName, T target, T source) throws MergerException { 148 149 try { 150 151 List targetCollection = (List) PropertyUtils.getProperty(target, propertyName); 152 List sourceCollection = (List) PropertyUtils.getProperty(source, propertyName); 153 154 if (mergeCollectionAnnotation == null) { 155 return (List) merge(PropertyUtils.getPropertyType(target, propertyName), targetCollection, sourceCollection); 156 } 157 158 //update collection from source 159 Collection<Mergeable> merged = updateFromSource(targetCollection, sourceCollection, mergeCollectionAnnotation.type()); 160 161 List targetResult = makeCollection(targetCollection, mergeCollectionAnnotation.collectionType(), target, propertyName); 162 163 targetResult.clear(); 164 targetResult.addAll(merged); 165 166 return targetResult; 167 168 } catch (NoSuchMethodException e) { 169 throw new MergerException("NoSuchMethodException while trying to merge", e); 170 } catch (IllegalAccessException e) { 171 throw new MergerException("IllegalAccessException while trying to merge", e); 172 } catch (InvocationTargetException e) { 173 throw new MergerException("InvocationTargetException while trying to merge", e); 174 } 175 } 176 177 private <T extends Mergeable> List makeCollection(List targetList, Class<? extends List> listType, T target, String propertyName) throws MergerException { 178 179 try { 180 //merger only supports Lists 181 if (targetList == null) { 182 //first look for specific impl in annotation 183 if (listType != List.class) { 184 return listType.newInstance(); 185 } else { 186 //try to instantiate field type 187 return (List) PropertyUtils.getPropertyType(target, propertyName).newInstance(); 188 } 189 } 190 191 return targetList; 192 } catch (NoSuchMethodException e) { 193 throw new MergerException("NoSuchMethodException while trying to merge", e); 194 } catch (IllegalAccessException e) { 195 throw new MergerException("IllegalAccessException while trying to merge", e); 196 } catch (InstantiationException e) { 197 throw new MergerException("InstantiationException while trying to merge", e); 198 } catch (InvocationTargetException e) { 199 throw new MergerException("InvocationTargetException while trying to merge", e); 200 } 201 } 202 203 private List<Mergeable> updateFromSource(List targetList, List sourceList, Class<? extends Mergeable> type) throws MergerException { 204 205 Map<Object, Mergeable> targetMap = buildIdentifierMap(targetList); 206 Map<Object, Mergeable> sourceMap = buildIdentifierMap(sourceList); 207 Set<Object> originalTargetKeys = new HashSet<Object>(targetMap.keySet()); 208 209 try { 210 //update 211 for (Map.Entry<Object, Mergeable> mergeableSourceEntry : sourceMap.entrySet()) { 212 213 Object sourceKey = mergeableSourceEntry.getKey(); 214 215 if (targetMap.containsKey(sourceKey)) { 216 //replace 217 Mergeable targetValue = targetMap.get(sourceKey); 218 if (targetValue.isGenerated()) { 219 targetMap.put(sourceKey, merge(type, targetValue, mergeableSourceEntry.getValue())); 220 } 221 } else { 222 targetMap.put(sourceKey, merge(type, type.newInstance(), mergeableSourceEntry.getValue())); 223 } 224 originalTargetKeys.remove(sourceKey); 225 } 226 227 //remove the targets were not updated 228 for (Object targetKey : originalTargetKeys) { 229 Mergeable mergeable = targetMap.get(targetKey); 230 231 if (mergeable.isGenerated()) { 232 targetMap.remove(targetKey); 233 } 234 } 235 } catch (IllegalAccessException e) { 236 throw new MergerException("IllegalAccessException while trying to merge", e); 237 } catch (InstantiationException e) { 238 throw new MergerException("InstantiationException while trying to merge!", e); 239 } 240 241 //order should e preserved by LinkedHashMap 242 return new ArrayList<Mergeable>(targetMap.values()); 243 } 244 245 private Map<Object, Mergeable> buildIdentifierMap(List input) { 246 Map<Object, Mergeable> mergeable = new LinkedHashMap<Object, Mergeable>(); 247 248 if (input != null) { 249 int i = 0; 250 for (Object o : input) { 251 //validate all instance are of type Mergeable 252 if (!(o instanceof Mergeable)) { 253 throw new TransfuseAnalysisException("Merge collection failed on type: " + o.getClass().getName()); 254 } 255 Mergeable t = (Mergeable) o; 256 Object key; 257 if(t instanceof Identified){ 258 key = ((Identified)t).getIdentifier(); 259 } 260 else{ 261 key = i; 262 } 263 mergeable.put(key, t); 264 i++; 265 } 266 } 267 268 return mergeable; 269 } 270}