View Javadoc

1   /*
2    * Copyright 2010-2013 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.callbackparams.combine.reflect;
18  
19  import java.lang.reflect.Array;
20  import java.lang.reflect.Field;
21  import java.lang.reflect.Method;
22  import java.lang.reflect.Modifier;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.HashSet;
26  import java.util.IdentityHashMap;
27  import java.util.Iterator;
28  import java.util.LinkedHashSet;
29  import java.util.Map;
30  import java.util.Set;
31  import org.callbackparams.combine.CombineAllPossible2Combinations;
32  import org.callbackparams.combine.CombineStrategy;
33  import org.callbackparams.combine.ValuesCollection;
34  import org.callbackparams.support.ExceptionUtil;
35  
36  /**
37   * Factory-class whose main purpose is to create callback-records from
38   * reflectively looked-up callback-values factory classes. The default
39   * recipe for a callback-values factory is a nested class that has a
40   * static array-returning method with the signature 'values()', e.g. a nested
41   * enum.
42   * 
43   * @author Henrik Kaipe
44   */
45  public abstract class CallbackRecordsFactory {
46  
47      private static CallbackRecordsFactory instance;
48  
49      /**
50       * Factory method that returns the implementation suitable for the
51       * java-version in use. I.e. a pretty simple implementation for JDK-1.4,
52       * which basicly only supports a default behaviour, and a more feature-rich
53       * implementation for JDK-1.5+, which makes it possible tweak the default
54       * behaviour through annotations.
55       */
56      public static CallbackRecordsFactory getInstance() {
57          if (null == instance) {
58              String annotationAwareFactoryClassName =
59                      "org.callbackparams.combine.annotation.AnnotationAwareCallbackRecordsFactory";
60              try {
61                  instance = (CallbackRecordsFactory)
62                          Class.forName(annotationAwareFactoryClassName)
63                          .newInstance();
64  //                System.out.println("CallbackRecordsFactory with TIGER support");
65  
66              } catch (UnsupportedClassVersionError detectionOf_JDK1_4) {
67                  instance = new CallbackRecordsFactory() {};
68  //                System.out.println("CallbackRecordsFactory for JDK-1.4");
69  
70              } catch (Exception shouldNeverHappen) {
71                  throw ExceptionUtil.unchecked(shouldNeverHappen);
72              }
73          }
74          return instance;
75      }
76      
77      /**
78       * Searches the specified class, its super-classes and all implemented
79       * interfaces for nested classes that provide callback-values, as
80       * defined by {@link #isCallbackValuesFactoryClass(java.lang.Class)}.
81       */
82      protected Set/*Class*/ findCallbackValuesFactoryClasses(Class testClass) {
83          if (null == testClass) {
84              return new LinkedHashSet();
85          }
86          final Set factoryClasses =
87                  findCallbackValuesFactoryClasses(testClass.getSuperclass());
88          
89          final Class[] interfaces = testClass.getInterfaces();
90          for (int i = 0 ; i < interfaces.length ; ++i) {
91              factoryClasses.addAll(
92                      findCallbackValuesFactoryClasses(interfaces[i]));
93          }
94          
95          final Class[] nestedClasses = testClass.getDeclaredClasses();
96          for (int i = 0 ; i < nestedClasses.length ; ++i) {
97              if (isCallbackValuesFactoryClass(nestedClasses[i])) {
98                  factoryClasses.add(nestedClasses[i]);
99              }
100         }
101         return factoryClasses;
102     }
103     
104     /**
105      * The default implementation returns true if the specified class has a
106      * static method "values()" that returns an array.
107      */
108     protected boolean isCallbackValuesFactoryClass(Class factoryProspect) {
109         try {
110             Method factoryMethod = factoryProspect
111                     .getDeclaredMethod("values", (Class[]) null);
112             return 0 < (factoryMethod.getModifiers() & Modifier.STATIC)
113                     && factoryMethod.getReturnType().isArray();
114             
115         } catch (NoSuchMethodException x) {
116             return false;
117         }
118     }
119 
120     /**
121      * @see #valuesMethodFor(Class)
122      */
123     private static Boolean[] booleanValues() {
124         return new Boolean[] {Boolean.FALSE, Boolean.TRUE};
125     }
126 
127     public static Method valuesMethodFor(Class valuesClass)
128     throws NoSuchMethodException {
129         boolean useBooleanValues =
130                 boolean.class == valuesClass
131                 || Boolean.class == valuesClass;
132         return (useBooleanValues ? CallbackRecordsFactory.class : valuesClass)
133                 .getDeclaredMethod(
134                         useBooleanValues ? "booleanValues" : "values",
135                         (Class[])null);
136     }
137 
138     /**
139      * The returned array (supposedly containing callback-values) is
140      * determined from the specified class. The default procedure to determine
141      * this array is simply to invoke the static method values() and
142      * assume it returns an array of callback-values.
143      * 
144      * The reason is that the static values()-method is the method that returns
145      * an array of all enum-constants for a java.lang.Enum implementation.
146      * <br/>
147      * However, it is not necessary that the argument class is an Enum, as long
148      * as it has its static values()-method returning an array of
149      * callback-values.
150      */
151     public Combined[] retrieveCombinedArray(Class valuesClass) {
152         try {
153             Method m = valuesMethodFor(valuesClass);
154             if (false == m.isAccessible()) {
155                 m.setAccessible(true);
156             }
157             int nbrOfValues = Array.getLength(m.invoke(null, (Object[])null));
158             Combined[] combinedValues = new Combined[nbrOfValues];
159             for (int i = 0; i < combinedValues.length; ++i) {
160                 combinedValues[i] = new Combined(m, i);
161             }
162             return combinedValues;
163             
164         } catch (Exception x) {
165             throw ExceptionUtil.unchecked(x);
166         }
167     }
168 
169     /**
170      * The default combine strategy is {@link CombineAllPossible2Combinations}
171      * but any other strategy could be used by overriding
172      * this class or by using the
173      * {@link org.callbackparams.combine.annotation.CombineConfig @CombineConfig}
174      * annotation (if using JDK-1.5+).
175      */
176     public CombineStrategy retrieveCombineStrategy(Class testClass) {
177         return new CombineAllPossible2Combinations();
178     }
179 
180     /**
181      * This is the method that integration harnesses (such as the
182      * {@link org.callbackparams.junit4.CallbackParamsRunner CallbackParamsRunner})
183      * should use for retrieving their callback-records in a test-developer
184      * friendly way.
185      *
186      * @param testClass the test class
187      * @return the collection of combined and ready-to-use callback-records
188      *         retrieved from testClass
189      */
190     public Collection collectCallbackRecordsReflectively(Class testClass) {
191         ValuesCollection vc = new ValuesCollection();
192 
193         Map/*Field,Class*/ fieldValues = collectValueInjections(testClass);
194 
195         addAsCombinedValues(vc, fieldValues);
196         Set/*Class*/ consumedValuesFactoryClasses =
197                 new HashSet(fieldValues.values());
198 
199         for (Iterator i = findCallbackValuesFactoryClasses(testClass)
200                 .iterator() ; i.hasNext() ;) {
201             final Class factoryClass = (Class) i.next();
202             if (false == consumedValuesFactoryClasses.contains(factoryClass)) {
203                 vc.add(retrieveCombinedArray(factoryClass));
204             }
205         }
206         return retrieveCombineStrategy(testClass).combine(vc);
207     }
208 
209     /**
210      * Collects values for the parameterized fields that are to be
211      * value-injected.
212      * This default implementation returns an empty map but the JDK-1.5+
213      * implementation overrides this and makes sure that
214      * {@link org.callbackparams.ParameterizedValue @ParameterizedValue}
215      * annotated fields are value-injected.
216      * @return a map that maps instances of {@link java.lang.reflect.Field}
217      * to the class, in which to find the <code>values()</code> method that will
218      * provide the values for this field
219      */
220     protected Map/*Field,Class*/ collectValueInjections(Class testClass) {
221         return new IdentityHashMap();
222     }
223 
224     /**
225      * Decides on whether the parameter value will also be available as callback
226      * when it is injected to the specified field. This defualt implementation
227      * will return true unless the field type is either boolean or Boolean.
228      * The default behaviour can be overridden by overriding this class or by
229      * using the annotation-attribute {@link
230      * org.callbackparams.annotation.ValueField#alsoAvailableAsCallback()}
231      * (when using JDK -1.5+).
232      */
233     protected boolean isValueAlsoAvailableAsCallback(Field f) {
234         return boolean.class != f.getType() && Boolean.class != f.getType();
235     }
236 
237     private void addAsCombinedValues(
238             ValuesCollection vc, Map/*Field,Class*/ fieldValues) {
239         for (Iterator/*Map.Entry*/ iter = fieldValues.entrySet().iterator();
240                 iter.hasNext();) {
241             Map.Entry/*Field,Class*/ entry = (Map.Entry) iter.next();
242             final Field f = (Field) entry.getKey();
243             final Combined[] combinedArray =
244                     retrieveCombinedArray((Class)entry.getValue());
245 
246             for (int i = 0; i < combinedArray.length; ++i) {
247                 combinedArray[i].toBeInjected(
248                         f, isValueAlsoAvailableAsCallback(f));
249             }
250             vc.add(combinedArray);
251         }
252     }
253 
254     /**
255      * Utility method that is used by subclasses and the JUnit-3.x utilities.
256      */
257     public static Combined[] asCombinedArray(Object record) {
258         Combined[] combinedArray = new Combined[sizeOfRecord(record)];
259         Iterator iRecord = iterateRecordElements(record);
260         for (int i = 0; i < combinedArray.length; ++i) {
261             final Object nextRecordElement = iRecord.next();
262             combinedArray[i] = nextRecordElement instanceof Combined
263                     ? (Combined)nextRecordElement
264                     : new Combined(nextRecordElement);
265         }
266         return combinedArray;
267     }
268 
269     private static Iterator iterateRecordElements(Object record) {
270         if (record instanceof Object[]) {
271             record = Arrays.asList((Object[])record);
272         }
273         return ((Collection)record).iterator();
274     }
275 
276     private static int sizeOfRecord(Object record) {
277         if (record instanceof Object[]) {
278             return ((Object[])record).length;
279         } else {
280             return ((Collection)record).size();
281         }
282     }
283 }