001 /*--------------------------------------------------------------------------+
002 $Id: DeepCloneTestUtils.java 27815 2010-05-20 16:11:32Z hummelb $
003 | |
004 | Copyright 2005-2010 Technische Universitaet Muenchen |
005 | |
006 | Licensed under the Apache License, Version 2.0 (the "License"); |
007 | you may not use this file except in compliance with the License. |
008 | You may obtain a copy of the License at |
009 | |
010 | http://www.apache.org/licenses/LICENSE-2.0 |
011 | |
012 | Unless required by applicable law or agreed to in writing, software |
013 | distributed under the License is distributed on an "AS IS" BASIS, |
014 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
015 | See the License for the specific language governing permissions and |
016 | limitations under the License. |
017 +--------------------------------------------------------------------------*/
018 package edu.tum.cs.commons.test;
019
020 import java.lang.reflect.InvocationTargetException;
021 import java.lang.reflect.Method;
022 import java.lang.reflect.ParameterizedType;
023 import java.lang.reflect.Type;
024 import java.util.ArrayList;
025 import java.util.Arrays;
026 import java.util.Collection;
027 import java.util.Collections;
028 import java.util.Comparator;
029 import java.util.IdentityHashMap;
030 import java.util.List;
031
032 import edu.tum.cs.commons.assertion.CCSMAssert;
033 import edu.tum.cs.commons.collections.AllEqualComparator;
034 import edu.tum.cs.commons.collections.CollectionUtils;
035 import edu.tum.cs.commons.collections.IIdProvider;
036 import edu.tum.cs.commons.collections.IdComparator;
037 import edu.tum.cs.commons.collections.IdentityHashSet;
038 import edu.tum.cs.commons.reflect.MethodNameComparator;
039 import edu.tum.cs.commons.string.StringUtils;
040
041 /**
042 * This class provides various utility methods used to test deep cloning
043 * implementations
044 *
045 *
046 * @author deissenb
047 * @author $Author: hummelb $
048 * @version $Rev: 27815 $
049 * @levd.rating GREEN Hash: ED526603069BA695A145272D022B02D1
050 */
051 public class DeepCloneTestUtils {
052
053 /** Name of the deep clone method. */
054 private static final String DEEP_CLONE_METHOD_NAME = "deepClone";
055
056 /** Name of the clone method. */
057 private static final String CLONE_METHOD_NAME = "clone";
058
059 /**
060 * This method is used to test deep cloning implementations. Provided with
061 * two object, the original and the clone, it automatically traverses the
062 * networks attached to both objects and establishes a mapping between the
063 * objects of both networks.
064 * <p>
065 * To achieve this, the method uses reflection to determine the methods that
066 * define the object networks, typically examples are
067 * <code>getChildren()</code> or <code>getParent()</code>. To limit the
068 * selection of methods, a list of package prefixes may specified. Only
069 * methods that do return a type matched by one of the prefixes are taken
070 * into account. Additionally all methods that return an implementation of
071 * {@link Collection} are considered. However, we currently do not consider
072 * nested collections like <code>Set<Set<K></code>.
073 * <p>
074 * To establish the mapping between the networks a comparator is needed to
075 * order members of object in both networks in the same way. This comparator
076 * must not be capable of comparing all possible types but must support
077 * ordering possible member type combinations of the object network under
078 * investigation. Usually, the object already provide some means of
079 * identification via IDs or full qualified names. These can be used to
080 * implement the comparator.
081 *
082 * @param orig
083 * point of entry for the original network
084 * @param clone
085 * point of entry for the clone network
086 * @param comparator
087 * the comparator is needed to compare the two networks
088 * @param packagePrefixes
089 * list of package prefixes to take into account
090 */
091 public static IdentityHashMap<Object, Object> buildCloneMap(Object orig,
092 Object clone, Comparator<Object> comparator,
093 String... packagePrefixes) {
094 IdentityHashMap<Object, Object> map = new IdentityHashMap<Object, Object>();
095 buildCloneMap(orig, clone, map, comparator, packagePrefixes);
096 return map;
097 }
098
099 /**
100 * This works analogous to
101 * {@link #getAllReferencedObjects(Object, String...)} but allows to limit
102 * the results to a certain type.
103 */
104 public static <T> IdentityHashSet<T> getAllReferencedObjects(Object root,
105 Class<T> type, String... packagePrefixes) {
106 IdentityHashSet<T> result = new IdentityHashSet<T>();
107 for (Object object : getAllReferencedObjects(root, packagePrefixes)) {
108 if (type.isAssignableFrom(object.getClass())) {
109 @SuppressWarnings("unchecked")
110 T object2 = (T) object;
111 result.add(object2);
112 }
113 }
114 return result;
115 }
116
117 /**
118 * Get all objects an object references. To to find the objects, this method
119 * uses reflection to determine the methods that define the object networks,
120 * typically examples are <code>getChildren()</code> or
121 * <code>getParent()</code>. To limit the selection of methods, a list of
122 * package prefixes must be specified. Only methods that do return a type
123 * matched by one of the prefixes are taken into account. Additionally all
124 * methods that return an implementation of {@link Collection} are
125 * considered.
126 *
127 * @param root
128 * root of the object network
129 * @param packagePrefixes
130 * list of package prefixes to take into account
131 */
132 public static IdentityHashSet<Object> getAllReferencedObjects(Object root,
133 String... packagePrefixes) {
134 IdentityHashSet<Object> result = new IdentityHashSet<Object>();
135 buildReferenceSet(root, result, packagePrefixes);
136 return result;
137 }
138
139 /**
140 * This method uses
141 * {@link #buildCloneMap(Object, Object, Comparator, String...)} to build a
142 * map between the original object network and clone network. Based on this
143 * map it performs the following checks:
144 * <ul>
145 * <li>Checks if objects are not same.</li>
146 * <li>Checks if objects are of same type.</li>
147 * <li>Checks if object and clone network are disjoint.</li>
148 * </ul>
149 *
150 * @param orig
151 * point of entry for the original network
152 * @param clone
153 * point of entry for the clone network
154 * @param idProvider
155 * an id provider that generates an id for all objects in the
156 * networks. This is necessary to establish comparable orderings.
157 * @param packagePrefixes
158 * list of package prefixes to take into account
159 * @return the map to allow further tests.
160 */
161 public static <I extends Comparable<I>> IdentityHashMap<Object, Object> testDeepCloning(
162 Object orig, Object clone, IIdProvider<I, Object> idProvider,
163 String... packagePrefixes) {
164
165 IdentityHashMap<Object, Object> map = buildCloneMap(orig, clone,
166 new IdComparator<I, Object>(idProvider), packagePrefixes);
167
168 for (Object origObject : map.keySet()) {
169 Object cloneObject = map.get(origObject);
170
171 CCSMAssert.isTrue(orig.getClass().equals(clone.getClass()),
172 "Objects " + origObject + " and " + cloneObject
173 + " have different types.");
174
175 // no need to check this for enums.
176 if (origObject.getClass().isEnum()) {
177 continue;
178 }
179
180 CCSMAssert.isFalse(origObject == cloneObject, "Objects "
181 + origObject + " and " + cloneObject + " are same.");
182
183 CCSMAssert.isFalse(map.values().contains(origObject),
184 "Clone network contains original object: " + origObject);
185 CCSMAssert.isFalse(map.keySet().contains(cloneObject),
186 "Orig network contains clone object: " + origObject);
187
188 }
189
190 return map;
191 }
192
193 /**
194 * Recursively build clone map. See
195 * {@link #buildCloneMap(Object, Object, Comparator, String...)} for
196 * details.
197 */
198 private static void buildCloneMap(Object orig, Object clone,
199 IdentityHashMap<Object, Object> map, Comparator<Object> comparator,
200 String... packagePrefixes) {
201
202 if (!orig.getClass().equals(clone.getClass())) {
203 throw new RuntimeException("Objects " + orig + " and " + clone
204 + " are of different tpye [orig: "
205 + orig.getClass().getName() + "][clone:"
206 + orig.getClass().getName() + "]");
207 }
208
209 map.put(orig, clone);
210
211 // get all objects referenced by the original
212 ArrayList<Object> origRefObjects = getReferencedObjects(orig,
213 comparator, packagePrefixes);
214
215 // get all objects referenced by the clone
216 ArrayList<Object> cloneRefObjects = getReferencedObjects(clone,
217 comparator, packagePrefixes);
218
219 if (origRefObjects.size() != cloneRefObjects.size()) {
220 throw new RuntimeException("Objects " + orig + " and " + clone
221 + " have unequal numbers of referenced objects [orig: "
222 + origRefObjects + "][clone:" + cloneRefObjects + "]");
223 }
224
225 // traverse recursively
226 for (int i = 0; i < origRefObjects.size(); i++) {
227 Object key = origRefObjects.get(i);
228 Object value = cloneRefObjects.get(i);
229
230 // do not traverse objects already visited, but ensure that we have
231 // an unambiguous mapping
232 if (map.containsKey(key)) {
233 if (!(map.get(key) == value)) {
234 throw new RuntimeException("Object " + key
235 + " appears to be cloned to " + map.get(key)
236 + " and to " + value);
237 }
238 } else if (key != null) {
239 buildCloneMap(key, value, map, comparator, packagePrefixes);
240 }
241 }
242 }
243
244 /** Recursively build set of all referenced objects. */
245 private static void buildReferenceSet(Object object,
246 IdentityHashSet<Object> set, String[] packagePrefixes) {
247
248 set.add(object);
249 for (Object item : getReferencedObjects(object,
250 AllEqualComparator.OBJECT_INSTANCE, packagePrefixes)) {
251 if (item != null && !set.contains(item)) {
252 buildReferenceSet(item, set, packagePrefixes);
253 }
254 }
255 }
256
257 /**
258 * Get all objects referenced by an object through methods that do return
259 * arrays.
260 */
261 private static ArrayList<Object> getArrayObjects(Object object,
262 Comparator<Object> comparator, String... packagePrefixes) {
263
264 ArrayList<Object> result = new ArrayList<Object>();
265
266 for (Method method : object.getClass().getMethods()) {
267 if (method.getParameterTypes().length == 0
268 && hasArrayReturnType(method, packagePrefixes)) {
269 Object returnValue = invoke(object, method);
270 if (returnValue != null) {
271 Object[] array = (Object[]) returnValue;
272 List<Object> list = Arrays.asList(array);
273
274 Collections.sort(list, comparator);
275 result.addAll(list);
276 }
277 }
278 }
279
280 return result;
281 }
282
283 /**
284 * Get all objects referenced by an object through methods that do return
285 * collections.
286 */
287 private static ArrayList<Object> getCollectionObjects(Object object,
288 Comparator<Object> comparator, String... packagePrefixes) {
289
290 ArrayList<Object> result = new ArrayList<Object>();
291
292 for (Method method : object.getClass().getMethods()) {
293 if (method.getParameterTypes().length == 0
294 && hasCollectionReturnType(method, packagePrefixes)) {
295 Object returnValue = invoke(object, method);
296
297 if (returnValue != null) {
298 Collection<?> collection = (Collection<?>) returnValue;
299 List<?> list = CollectionUtils.sort(collection, comparator);
300 result.addAll(list);
301 }
302 }
303
304 }
305
306 return result;
307 }
308
309 /**
310 * Get list of methods that (1) do not have parameters, (2) whose return
311 * type starts with one of the given prefixes and whose names is not
312 * <code>deepClone()</code>. The methods are ordered by name.
313 */
314 private static ArrayList<Method> getMethods(Object object,
315 String[] packagePrefixes) {
316 ArrayList<Method> methods = new ArrayList<Method>();
317 for (Method method : object.getClass().getMethods()) {
318 if (method.getName().equals(CLONE_METHOD_NAME)) {
319 continue;
320 }
321 if (method.getName().equals(DEEP_CLONE_METHOD_NAME)) {
322 continue;
323 }
324 if (method.getParameterTypes().length > 0) {
325 continue;
326 }
327 Class<?> returnType = method.getReturnType();
328 if (StringUtils.startsWithOneOf(returnType.getName(),
329 packagePrefixes)) {
330 methods.add(method);
331 }
332 }
333
334 Collections.sort(methods, MethodNameComparator.INSTANCE);
335
336 return methods;
337 }
338
339 /**
340 * Get all objects referenced by an object through methods that do
341 * <em>not</em> return collections.
342 */
343 private static ArrayList<Object> getNonCollectionObjects(Object object,
344 String... packagePrefixes) {
345
346 ArrayList<Object> objects = new ArrayList<Object>();
347
348 for (Method method : getMethods(object, packagePrefixes)) {
349 objects.add(invoke(object, method));
350 }
351 return objects;
352 }
353
354 /**
355 * Get all objects an object references in an order defined by the
356 * comparator.
357 */
358 private static ArrayList<Object> getReferencedObjects(Object object,
359 Comparator<Object> comparator, String... packagePrefixes) {
360
361 ArrayList<Object> result = new ArrayList<Object>();
362
363 ArrayList<Object> nonCollectionObjects = getNonCollectionObjects(
364 object, packagePrefixes);
365 result.addAll(nonCollectionObjects);
366
367 ArrayList<Object> collectionObjects = getCollectionObjects(object,
368 comparator, packagePrefixes);
369 result.addAll(collectionObjects);
370
371 ArrayList<Object> arrayObjects = getArrayObjects(object, comparator,
372 packagePrefixes);
373 result.addAll(arrayObjects);
374
375 return result;
376 }
377
378 /**
379 * Checks if a method returns an array with type that starts with one of the
380 * provided prefixes.
381 */
382 private static boolean hasArrayReturnType(Method method,
383 String... packagePrefixes) {
384 Class<?> returnType = method.getReturnType();
385 if (!returnType.isArray()) {
386 return false;
387 }
388 Class<?> actualType = returnType.getComponentType();
389 return StringUtils.startsWithOneOf(actualType.getName(),
390 packagePrefixes);
391 }
392
393 /**
394 * Checks if a method returns an collection whose generic type that starts
395 * with one of the provided prefixes.
396 */
397 private static boolean hasCollectionReturnType(Method method,
398 String... packagePrefixes) {
399 Class<?> returnType = method.getReturnType();
400
401 if (!Collection.class.isAssignableFrom(returnType)) {
402 return false;
403 }
404
405 Type genericReturnType = method.getGenericReturnType();
406 // Raw type
407 if (returnType == genericReturnType) {
408 return false;
409 }
410
411 ParameterizedType type = (ParameterizedType) method
412 .getGenericReturnType();
413
414 // Collections have only one type parameter
415 Type typeArg = type.getActualTypeArguments()[0];
416
417 // potentially this can be another parameterized type, e.g. for
418 // Set<Set<K>> or a wildcard type. Handling these is very tricky and is
419 // currently not supported. Hence, we silently ignore these.
420 if (!(typeArg instanceof Class<?>)) {
421 return false;
422 }
423
424 Class<?> actualType = (Class<?>) typeArg;
425
426 return StringUtils.startsWithOneOf(actualType.getName(),
427 packagePrefixes);
428 }
429
430 /**
431 * This simpy calls {@link Method#invoke(Object, Object...)}. If the called
432 * method throws an exception, this returns <code>null</code>. A possible
433 * {@link IllegalAccessException} is converted to a {@link RuntimeException}
434 * .
435 */
436 private static Object invoke(Object object, Method method) {
437 try {
438 return method.invoke(object);
439 } catch (RuntimeException e) {
440 return null;
441 } catch (IllegalAccessException e) {
442 throw new RuntimeException(e);
443 } catch (InvocationTargetException e) {
444 return null;
445 }
446 }
447 }