001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 The ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016+-------------------------------------------------------------------------*/
017package org.conqat.lib.commons.test;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.lang.reflect.ParameterizedType;
022import java.lang.reflect.Type;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.Comparator;
028import java.util.List;
029
030import org.conqat.lib.commons.collections.AllEqualComparator;
031import org.conqat.lib.commons.collections.CollectionUtils;
032import org.conqat.lib.commons.collections.IdentityHashSet;
033import org.conqat.lib.commons.reflect.MethodNameComparator;
034import org.conqat.lib.commons.string.StringUtils;
035
036/**
037 * This class provides various utility methods used to test deep cloning
038 * implementations.
039 */
040public class SimulinkTestUtils {
041
042        /** Name of the clone method. */
043        private static final String CLONE_METHOD_NAME = "clone";
044
045        /**
046         * This works analogous to {@link #getAllReferencedObjects(Object, String...)}
047         * but allows to limit the results to a certain type.
048         */
049        public static <T> IdentityHashSet<T> getAllReferencedObjects(Object root, Class<T> type,
050                        String... packagePrefixes) {
051                IdentityHashSet<T> result = new IdentityHashSet<>();
052                for (Object object : getAllReferencedObjects(root, packagePrefixes)) {
053                        if (type.isAssignableFrom(object.getClass())) {
054                                @SuppressWarnings("unchecked")
055                                T object2 = (T) object;
056                                result.add(object2);
057                        }
058                }
059                return result;
060        }
061
062        /**
063         * Get all objects an object references. To to find the objects, this method
064         * uses reflection to determine the methods that define the object networks,
065         * typically examples are <code>getChildren()</code> or
066         * <code>getParent()</code>. To limit the selection of methods, a list of
067         * package prefixes must be specified. Only methods that do return a type
068         * matched by one of the prefixes are taken into account. Additionally all
069         * methods that return an implementation of {@link Collection} are considered.
070         *
071         * @param root
072         *            root of the object network
073         * @param packagePrefixes
074         *            list of package prefixes to take into account
075         */
076        public static IdentityHashSet<Object> getAllReferencedObjects(Object root, String... packagePrefixes) {
077                IdentityHashSet<Object> result = new IdentityHashSet<>();
078                buildReferenceSet(root, result, packagePrefixes);
079                return result;
080        }
081
082        /** Recursively build set of all referenced objects. */
083        private static void buildReferenceSet(Object object, IdentityHashSet<Object> set, String[] packagePrefixes) {
084
085                set.add(object);
086                for (Object item : getReferencedObjects(object, AllEqualComparator.OBJECT_INSTANCE, packagePrefixes)) {
087                        if (item != null && !set.contains(item)) {
088                                buildReferenceSet(item, set, packagePrefixes);
089                        }
090                }
091        }
092
093        /**
094         * Get all objects referenced by an object through methods that do return
095         * arrays.
096         */
097        private static ArrayList<Object> getArrayObjects(Object object, Comparator<Object> comparator,
098                        String... packagePrefixes) {
099
100                ArrayList<Object> result = new ArrayList<>();
101
102                for (Method method : object.getClass().getMethods()) {
103                        if (method.getParameterTypes().length == 0 && !isExcluded(method)
104                                        && hasArrayReturnType(method, packagePrefixes)) {
105                                Object returnValue = invoke(object, method);
106                                if (returnValue != null) {
107                                        Object[] array = (Object[]) returnValue;
108                                        List<Object> list = Arrays.asList(array);
109
110                                        Collections.sort(list, comparator);
111                                        result.addAll(list);
112                                }
113                        }
114                }
115
116                return result;
117        }
118
119        /**
120         * Get all objects referenced by an object through methods that do return
121         * collections.
122         */
123        private static ArrayList<Object> getCollectionObjects(Object object, Comparator<Object> comparator,
124                        String... packagePrefixes) {
125
126                ArrayList<Object> result = new ArrayList<>();
127
128                for (Method method : object.getClass().getMethods()) {
129                        if (method.getParameterTypes().length == 0 && !isExcluded(method)
130                                        && hasCollectionReturnType(method, packagePrefixes)) {
131                                Object returnValue = invoke(object, method);
132
133                                if (returnValue != null) {
134                                        Collection<?> collection = (Collection<?>) returnValue;
135                                        List<?> list = CollectionUtils.sort(collection, comparator);
136                                        result.addAll(list);
137                                }
138                        }
139
140                }
141
142                return result;
143        }
144
145        /**
146         * Get list of methods that (1) do not have parameters, (2) whose return type
147         * starts with one of the given prefixes and whose names is not
148         * <code>deepClone()</code>. The methods are ordered by name.
149         */
150        private static ArrayList<Method> getMethods(Object object, String[] packagePrefixes) {
151                ArrayList<Method> methods = new ArrayList<>();
152                for (Method method : object.getClass().getMethods()) {
153                        if (method.getName().equals(CLONE_METHOD_NAME)) {
154                                continue;
155                        }
156                        if (method.getParameterTypes().length > 0) {
157                                continue;
158                        }
159                        if (method.isSynthetic()) {
160                                continue;
161                        }
162                        Class<?> returnType = method.getReturnType();
163                        if (StringUtils.startsWithOneOf(returnType.getName(), packagePrefixes)) {
164                                methods.add(method);
165                        }
166                }
167
168                Collections.sort(methods, MethodNameComparator.INSTANCE);
169
170                return methods;
171        }
172
173        /**
174         * Get all objects referenced by an object through methods that do <em>not</em>
175         * return collections.
176         */
177        private static ArrayList<Object> getNonCollectionObjects(Object object, String... packagePrefixes) {
178
179                ArrayList<Object> objects = new ArrayList<>();
180
181                for (Method method : getMethods(object, packagePrefixes)) {
182                        if (!isExcluded(method)) {
183                                objects.add(invoke(object, method));
184                        }
185                }
186                return objects;
187        }
188
189        /**
190         * Returns true if the given method should be excluded from testing. This checks
191         * for the presence of the {@link SimulinkTestExclude} annotation.
192         */
193        private static boolean isExcluded(Method method) {
194                return method.isAnnotationPresent(SimulinkTestExclude.class);
195        }
196
197        /**
198         * Get all objects an object references in an order defined by the comparator.
199         */
200        private static ArrayList<Object> getReferencedObjects(Object object, Comparator<Object> comparator,
201                        String... packagePrefixes) {
202
203                ArrayList<Object> result = new ArrayList<>();
204
205                ArrayList<Object> nonCollectionObjects = getNonCollectionObjects(object, packagePrefixes);
206                result.addAll(nonCollectionObjects);
207
208                ArrayList<Object> collectionObjects = getCollectionObjects(object, comparator, packagePrefixes);
209                result.addAll(collectionObjects);
210
211                ArrayList<Object> arrayObjects = getArrayObjects(object, comparator, packagePrefixes);
212                result.addAll(arrayObjects);
213
214                return result;
215        }
216
217        /**
218         * Checks if a method returns an array with type that starts with one of the
219         * provided prefixes.
220         */
221        private static boolean hasArrayReturnType(Method method, String... packagePrefixes) {
222                Class<?> returnType = method.getReturnType();
223                if (!returnType.isArray()) {
224                        return false;
225                }
226                Class<?> actualType = returnType.getComponentType();
227                return StringUtils.startsWithOneOf(actualType.getName(), packagePrefixes);
228        }
229
230        /**
231         * Checks if a method returns an collection whose generic type that starts with
232         * one of the provided prefixes.
233         */
234        private static boolean hasCollectionReturnType(Method method, String... packagePrefixes) {
235                Class<?> returnType = method.getReturnType();
236
237                if (!Collection.class.isAssignableFrom(returnType)) {
238                        return false;
239                }
240
241                Type genericReturnType = method.getGenericReturnType();
242                // Raw type
243                if (returnType == genericReturnType) {
244                        return false;
245                }
246
247                ParameterizedType type = (ParameterizedType) method.getGenericReturnType();
248
249                // Collections have only one type parameter
250                Type typeArg = type.getActualTypeArguments()[0];
251
252                // potentially this can be another parameterized type, e.g. for
253                // Set<Set<K>> or a wildcard type. Handling these is very tricky and is
254                // currently not supported. Hence, we silently ignore these.
255                if (!(typeArg instanceof Class<?>)) {
256                        return false;
257                }
258
259                Class<?> actualType = (Class<?>) typeArg;
260
261                return StringUtils.startsWithOneOf(actualType.getName(), packagePrefixes);
262        }
263
264        /**
265         * This simpy calls {@link Method#invoke(Object, Object...)}. If the called
266         * method throws an exception, this returns <code>null</code>. A possible
267         * {@link IllegalAccessException} is converted to a {@link RuntimeException} .
268         */
269        private static Object invoke(Object object, Method method) {
270                try {
271                        return method.invoke(object);
272                } catch (RuntimeException e) {
273                        return null;
274                } catch (IllegalAccessException e) {
275                        throw new RuntimeException(e);
276                } catch (InvocationTargetException e) {
277                        return null;
278                }
279        }
280}