View Javadoc

1   /*
2    * Copyright 2011-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.util.Collection;
23  import java.util.IdentityHashMap;
24  import java.util.Map;
25  import org.callbackparams.support.ExceptionUtil;
26  
27  /**
28   * @author Henrik Kaipe
29   */
30  public class Combined {
31  
32      private static Class[] METHOD_HAS_NO_PARAMETERS = {};
33  
34      private final Object coreValue;
35      private Method staticMethod;
36      private final int index;
37  
38      /**
39       * The field subIndex comes to play when callback-records are specified with
40       * the
41       * annotation {@link org.callbackparams.combine.annotation.CallbackRecords}.
42       * @see org.callbackparams.combine.annotation.CallbackRecords
43       */
44      private final int subIndex;
45  
46      /**
47       * Might be set to false by {@link #toBeInjected(Field, boolean)}
48       */
49      private boolean isCallback = true;
50      private Field valueField = null;
51  
52      public Combined(Object coreValue) {
53          this(null, -1, -1, coreValue);
54      }
55  
56      Combined(Method staticMethod, int index) {
57          this(staticMethod, index, -1);
58      }
59  
60      Combined(Method staticMethod, int index, int subIndex) {
61          this(staticMethod, index, subIndex, null);
62      }
63  
64      Combined(Method staticMethod, int index, int subIndex, Object originalValue) {
65          this.staticMethod = staticMethod;
66          this.index = index;
67          this.subIndex = subIndex;
68          this.coreValue = originalValue;
69  
70          try {
71              if (null != staticMethod) {
72                  staticMethod.setAccessible(true);
73              }
74          } catch (SecurityException ignoreAndContinueAnyway) {}
75      }
76  
77      public static Combined resurrectFromOtherClassLoader(Object combined2reload) {
78          if (combined2reload instanceof Combined) {
79              return (Combined) combined2reload;
80  
81          } else {
82              Combined resurrected = new Combined(
83                      (Method) valueOfField("staticMethod", combined2reload),
84                      ((Integer)valueOfField("index", combined2reload)).intValue(),
85                      ((Integer)valueOfField("subIndex", combined2reload)).intValue(),
86                      valueOfField("coreValue", combined2reload));
87              resurrected.toBeInjected(
88                      (Field) valueOfField("valueField", combined2reload),
89                      Boolean
90                      /* Must use "equals" here or integration with class-reloading
91                       * 3rd-party JUnit-runners might break! Operator '==' does
92                       * not work under PowerMockRunner because its class-loader
93                       * seems to internally handle the state of boolean field
94                       * "isCallback" with a Boolean-instance that has been
95                       * created separately. */
96                      .TRUE.equals(valueOfField("isCallback", combined2reload)));
97              return resurrected;
98          }
99      }
100 
101     private static Object valueOfField(String fieldName, Object obj) {
102         Class c = obj.getClass();
103         try {
104             Field f = c.getDeclaredField(fieldName);
105             f.setAccessible(true);
106             return f.get(obj);
107         } catch (Exception ex) {
108             throw ExceptionUtil.unchecked(ex);
109         }
110     }
111 
112     void toBeInjected(Field valueField, boolean alsoAvailableAsCallback) {
113         if (null != this.valueField) {
114             throw new IllegalStateException(
115                     "Field for value-injection has already been set: " + this.valueField);
116         }
117         this.valueField = valueField;
118         this.isCallback = alsoAvailableAsCallback;
119 
120         try {
121             if (null != valueField) {
122                 this.valueField.setAccessible(true);
123             }
124         } catch (SecurityException ignoreAndContinueAnyway) {}
125     }
126 
127     public Object getCoreValue() {
128         return null!=coreValue || null==staticMethod ? coreValue : getCoreValue(
129                 staticMethod.getDeclaringClass().getClassLoader(),
130                 new IdentityHashMap(1));
131     }
132 
133     private Object getCoreValue(
134             ClassLoader cl, Map/*Method,Object[]*/ valuesCache) {
135          if (null == this.staticMethod) {
136             /*
137              * Does not quite work for all situations, such as when
138              * wrapping another runner ...
139              */
140             return this.coreValue;
141         }
142         /*
143          * Get a local instance of staticMethod to avoid concurrency problems
144          * for multi-threaded contexts. (The field staticMethod is somewhat
145          * volatile.)
146          */
147         Method staticMethod = getStaticMethodOnClassLoader(cl);
148        if (false == valuesCache.containsValue(staticMethod)) {
149             try {
150                 valuesCache.put(staticMethod, staticMethod
151                         .invoke(null, (Object[])METHOD_HAS_NO_PARAMETERS));
152             } catch (Exception ex) {
153                 throw ExceptionUtil.unchecked(ex);
154             }
155         }
156         Object valuesElement = Array.get(valuesCache.get(staticMethod), index);
157         if (subIndex < 0) {
158             return valuesElement;
159         } else if (valuesElement.getClass().isArray()) {
160             return Array.get(valuesElement, subIndex);
161         } else {
162             return ((Collection)valuesElement).toArray()[subIndex];
163         }
164     }
165 
166     /**
167      * Retrieves the core value of this Combined instance and does at least one
168      * out of two things:
169      * <br/> 1) Has the core value injected as a parameterized value into the
170      * specified {@link #valueField}
171      * <br/> 2) Returns the core value in case it is to be available as callback,
172      * as specified by {@link #isCallback}
173      * @param testInstance the test instance, onto which a possible parameterized value
174      * is to be injected; or the test-class
175      * @param valuesCache maps static methods to their return-value arrays as an
176      * attempt to avoid multiple invocations of {@link #staticMethod} - in case
177      * it is an expensive method that needs to access the file-system on every
178      * invocation or something
179      * @return the callback-value that is represented by this Combined instance;
180      * or null in case {@link #isCallback} is false and therewith specifies that
181      * the core value of this Combined instance is not a callback
182      */
183     public Object retrieve(Object testInstance, Map/*Method,Object[]*/ valuesCache) {
184         final ClassLoader cl = testInstance.getClass().getClassLoader();
185         final Object coreValue = getCoreValue(
186                 testInstance.getClass().getClassLoader(), valuesCache);
187 
188         if (null != valueField) {
189             try {
190                 getFieldOnClassLoader(cl).set(testInstance, coreValue);
191             } catch (Exception ex) {
192                 throw new Error("Unable to inject " + valueField
193                         + " with core value: " + coreValue, ex);
194             }
195         }
196         return isCallback ? coreValue : null;
197     }
198 
199     private Method getStaticMethodOnClassLoader(ClassLoader cl) {
200         Method staticMethod = this.staticMethod;
201         if (null == cl) {
202             /* Seems to be possible for JDK-classes ... */
203             return staticMethod;
204         }
205         try {
206             final Class declaringClass = cl
207                     .loadClass(staticMethod.getDeclaringClass().getName());
208             if (declaringClass != staticMethod.getDeclaringClass()) {
209                 staticMethod = declaringClass.getDeclaredMethod(
210                         staticMethod.getName(), METHOD_HAS_NO_PARAMETERS);
211                 staticMethod.setAccessible(true);
212                 /*
213                  * Replace static method on this combine instance for easier
214                  * retrieval next time ...
215                  */
216                 this.staticMethod = staticMethod;
217             }
218         } catch (SecurityException ignoreAndContinueAnyway) {
219         } catch (Exception ex) {
220             throw new Error("Cannot reach static method on test class-loader: "
221                     + staticMethod, ex);
222         }
223         return staticMethod;
224     }
225 
226     private Field getFieldOnClassLoader(ClassLoader cl) {
227         Field valueField = this.valueField;
228         try {
229             final Class declaringClass = cl
230                     .loadClass(valueField.getDeclaringClass().getName());
231             if (declaringClass != valueField.getDeclaringClass()) {
232                 valueField = declaringClass.getDeclaredField(valueField.getName());
233                 valueField.setAccessible(true);
234                 /*
235                  * Replace static method on this combine instance for easier
236                  * retrieval next time ...
237                  */
238                 this.valueField = valueField;
239             }
240         } catch (SecurityException ignoreAndContinueAnyway) {
241         } catch (Exception ex) {
242             throw new Error(
243                     "Cannot reach @ParameterizedValue-field on test class-loader: "
244                     + valueField, ex);
245         }
246         return valueField;
247     }
248 
249     // @Override (if it had been Java-5)
250     public String toString() {
251         return (null == valueField ? "" : valueField.getName() + '=')
252                 + getCoreValue();
253     }
254 }