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}