View Javadoc
1   package org.codehaus.plexus.interpolation.object;
2   
3   /*
4    * Copyright 2001-2008 Codehaus Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  import java.lang.reflect.Array;
20  import java.lang.reflect.Field;
21  import java.security.AccessController;
22  import java.security.PrivilegedAction;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.HashSet;
27  import java.util.LinkedList;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.WeakHashMap;
32  
33  import org.codehaus.plexus.interpolation.BasicInterpolator;
34  import org.codehaus.plexus.interpolation.InterpolationException;
35  import org.codehaus.plexus.interpolation.Interpolator;
36  import org.codehaus.plexus.interpolation.RecursionInterceptor;
37  import org.codehaus.plexus.interpolation.SimpleRecursionInterceptor;
38  
39  /**
40   * Reflectively traverses an object graph and uses an {@link Interpolator} instance to resolve any String fields in the
41   * graph.
42   * <p>
43   * NOTE: This code is based on a reimplementation of ModelInterpolator in
44   * maven-project 2.1.0-M1, which became a performance bottleneck when the
45   * interpolation process became a hotspot.</p>
46   *
47   * @author jdcasey
48   */
49  public class FieldBasedObjectInterpolator implements ObjectInterpolator {
50      public static final Set<String> DEFAULT_BLACKLISTED_FIELD_NAMES;
51  
52      public static final Set<String> DEFAULT_BLACKLISTED_PACKAGE_PREFIXES;
53  
54      private static final Map<Class, Field[]> fieldsByClass = new WeakHashMap<Class, Field[]>();
55  
56      private static final Map<Class, Boolean> fieldIsPrimitiveByClass = new WeakHashMap<Class, Boolean>();
57  
58      static {
59          Set<String> blacklistedFields = new HashSet<String>();
60          blacklistedFields.add("parent");
61  
62          DEFAULT_BLACKLISTED_FIELD_NAMES = Collections.unmodifiableSet(blacklistedFields);
63  
64          Set<String> blacklistedPackages = new HashSet<String>();
65          blacklistedPackages.add("java");
66  
67          DEFAULT_BLACKLISTED_PACKAGE_PREFIXES = Collections.unmodifiableSet(blacklistedPackages);
68      }
69  
70      /**
71       * Clear out the Reflection caches kept for the most expensive operations encountered: field lookup and primitive
72       * queries for fields. These caches are static since they apply at the class level, not the instance level.
73       */
74      public static void clearCaches() {
75          fieldsByClass.clear();
76          fieldIsPrimitiveByClass.clear();
77      }
78  
79      private Set<String> blacklistedFieldNames;
80  
81      private Set<String> blacklistedPackagePrefixes;
82  
83      private List<ObjectInterpolationWarning> warnings = new ArrayList<ObjectInterpolationWarning>();
84  
85      /**
86       * Use the default settings for blacklisted fields and packages, where fields named 'parent' and classes in packages
87       * starting with 'java' will not be interpolated.
88       */
89      public FieldBasedObjectInterpolator() {
90          this.blacklistedFieldNames = DEFAULT_BLACKLISTED_FIELD_NAMES;
91          this.blacklistedPackagePrefixes = DEFAULT_BLACKLISTED_PACKAGE_PREFIXES;
92      }
93  
94      /**
95       * Use the given black-lists to limit the interpolation of fields and classes (by package).
96       *
97       * @param blacklistedFieldNames      The list of field names to ignore
98       * @param blacklistedPackagePrefixes The list of package prefixes whose classes should be ignored
99       */
100     public FieldBasedObjectInterpolator(Set<String> blacklistedFieldNames, Set<String> blacklistedPackagePrefixes) {
101         this.blacklistedFieldNames = blacklistedFieldNames;
102         this.blacklistedPackagePrefixes = blacklistedPackagePrefixes;
103     }
104 
105     /**
106      * Returns true if the last interpolation execution generated warnings.
107      */
108     public boolean hasWarnings() {
109         return warnings != null && !warnings.isEmpty();
110     }
111 
112     /**
113      * Retrieve the {@link List} of warnings ({@link ObjectInterpolationWarning}
114      * instances) generated during the last interpolation execution.
115      */
116     public List<ObjectInterpolationWarning> getWarnings() {
117         return new ArrayList<ObjectInterpolationWarning>(warnings);
118     }
119 
120     /**
121      * Using reflective field access and mutation, traverse the object graph from the given starting point and
122      * interpolate any Strings found in that graph using the given {@link Interpolator}. Limits to this process can be
123      * managed using the black lists configured in the constructor.
124      *
125      * @param target       The starting point of the object graph to traverse
126      * @param interpolator The {@link Interpolator} used to resolve any Strings encountered during traversal.
127      *                     NOTE: Uses {@link SimpleRecursionInterceptor}.
128      */
129     public void interpolate(Object target, BasicInterpolator interpolator) throws InterpolationException {
130         interpolate(target, interpolator, new SimpleRecursionInterceptor());
131     }
132 
133     /**
134      * Using reflective field access and mutation, traverse the object graph from the given starting point and
135      * interpolate any Strings found in that graph using the given {@link Interpolator}. Limits to this process can be
136      * managed using the black lists configured in the constructor.
137      *
138      * @param target               The starting point of the object graph to traverse
139      * @param interpolator         The {@link Interpolator} used to resolve any Strings encountered during traversal.
140      * @param recursionInterceptor The {@link RecursionInterceptor} used to detect cyclical expressions in the graph
141      */
142     public void interpolate(Object target, BasicInterpolator interpolator, RecursionInterceptor recursionInterceptor)
143             throws InterpolationException {
144         warnings.clear();
145 
146         InterpolateObjectAction action = new InterpolateObjectAction(
147                 target,
148                 interpolator,
149                 recursionInterceptor,
150                 blacklistedFieldNames,
151                 blacklistedPackagePrefixes,
152                 warnings);
153 
154         InterpolationException error = (InterpolationException) AccessController.doPrivileged(action);
155 
156         if (error != null) {
157             throw error;
158         }
159     }
160 
161     private static final class InterpolateObjectAction implements PrivilegedAction {
162 
163         private final LinkedList<InterpolationTarget> interpolationTargets;
164 
165         private final BasicInterpolator interpolator;
166 
167         private final Set blacklistedFieldNames;
168 
169         private final String[] blacklistedPackagePrefixes;
170 
171         private final List<ObjectInterpolationWarning> warningCollector;
172 
173         private final RecursionInterceptor recursionInterceptor;
174 
175         /**
176          * Setup an object graph traversal for the given target starting point. This will initialize a queue of objects
177          * to traverse and interpolate by adding the target object.
178          */
179         public InterpolateObjectAction(
180                 Object target,
181                 BasicInterpolator interpolator,
182                 RecursionInterceptor recursionInterceptor,
183                 Set blacklistedFieldNames,
184                 Set blacklistedPackagePrefixes,
185                 List<ObjectInterpolationWarning> warningCollector) {
186             this.recursionInterceptor = recursionInterceptor;
187             this.blacklistedFieldNames = blacklistedFieldNames;
188             this.warningCollector = warningCollector;
189             this.blacklistedPackagePrefixes =
190                     (String[]) blacklistedPackagePrefixes.toArray(new String[blacklistedPackagePrefixes.size()]);
191 
192             this.interpolationTargets = new LinkedList<InterpolationTarget>();
193             interpolationTargets.add(new InterpolationTarget(target, ""));
194 
195             this.interpolator = interpolator;
196         }
197 
198         /**
199          * As long as the traversal queue is non-empty, traverse the next object in the queue. If an interpolation error
200          * occurs, return it immediately.
201          */
202         public Object run() {
203             while (!interpolationTargets.isEmpty()) {
204                 InterpolationTarget target = interpolationTargets.removeFirst();
205 
206                 try {
207                     traverseObjectWithParents(target.value.getClass(), target);
208                 } catch (InterpolationException e) {
209                     return e;
210                 }
211             }
212 
213             return null;
214         }
215 
216         /**
217          * Traverse the given object, interpolating any String fields and adding non-primitive field values to the
218          * interpolation queue for later processing.
219          */
220         private void traverseObjectWithParents(Class cls, InterpolationTarget target) throws InterpolationException {
221             Object obj = target.value;
222             String basePath = target.path;
223 
224             if (cls == null) {
225                 return;
226             }
227 
228             if (cls.isArray()) {
229                 evaluateArray(obj, basePath);
230             } else if (isQualifiedForInterpolation(cls)) {
231                 Field[] fields = fieldsByClass.get(cls);
232                 if (fields == null) {
233                     fields = cls.getDeclaredFields();
234                     fieldsByClass.put(cls, fields);
235                 }
236 
237                 for (Field field : fields) {
238                     Class type = field.getType();
239                     if (isQualifiedForInterpolation(field, type)) {
240                         boolean isAccessible = field.isAccessible();
241                         synchronized (cls) {
242                             field.setAccessible(true);
243                             try {
244                                 try {
245                                     if (String.class == type) {
246                                         interpolateString(obj, field);
247                                     } else if (Collection.class.isAssignableFrom(type)) {
248                                         if (interpolateCollection(obj, basePath, field)) {
249                                             continue;
250                                         }
251                                     } else if (Map.class.isAssignableFrom(type)) {
252                                         interpolateMap(obj, basePath, field);
253                                     } else {
254                                         interpolateObject(obj, basePath, field);
255                                     }
256                                 } catch (IllegalArgumentException e) {
257                                     warningCollector.add(new ObjectInterpolationWarning(
258                                             "Failed to interpolate field. Skipping.",
259                                             basePath + "." + field.getName(),
260                                             e));
261                                 } catch (IllegalAccessException e) {
262                                     warningCollector.add(new ObjectInterpolationWarning(
263                                             "Failed to interpolate field. Skipping.",
264                                             basePath + "." + field.getName(),
265                                             e));
266                                 }
267                             } finally {
268                                 field.setAccessible(isAccessible);
269                             }
270                         }
271                     }
272                 }
273 
274                 traverseObjectWithParents(cls.getSuperclass(), target);
275             }
276         }
277 
278         private void interpolateObject(Object obj, String basePath, Field field)
279                 throws IllegalAccessException, InterpolationException {
280             Object value = field.get(obj);
281             if (value != null) {
282                 if (field.getType().isArray()) {
283                     evaluateArray(value, basePath + "." + field.getName());
284                 } else {
285                     interpolationTargets.add(new InterpolationTarget(value, basePath + "." + field.getName()));
286                 }
287             }
288         }
289 
290         private void interpolateMap(Object obj, String basePath, Field field)
291                 throws IllegalAccessException, InterpolationException {
292             Map m = (Map) field.get(obj);
293             if (m != null && !m.isEmpty()) {
294                 for (Object o : m.entrySet()) {
295                     Map.Entry entry = (Map.Entry) o;
296 
297                     Object value = entry.getValue();
298 
299                     if (value != null) {
300                         if (String.class == value.getClass()) {
301                             String interpolated = interpolator.interpolate((String) value, recursionInterceptor);
302 
303                             if (!interpolated.equals(value)) {
304                                 try {
305                                     entry.setValue(interpolated);
306                                 } catch (UnsupportedOperationException e) {
307                                     warningCollector.add(new ObjectInterpolationWarning(
308                                             "Field is an unmodifiable collection. Skipping interpolation.",
309                                             basePath + "." + field.getName(),
310                                             e));
311                                     continue;
312                                 }
313                             }
314                         } else {
315                             if (value.getClass().isArray()) {
316                                 evaluateArray(value, basePath + "." + field.getName());
317                             } else {
318                                 interpolationTargets.add(
319                                         new InterpolationTarget(value, basePath + "." + field.getName()));
320                             }
321                         }
322                     }
323                 }
324             }
325         }
326 
327         private boolean interpolateCollection(Object obj, String basePath, Field field)
328                 throws IllegalAccessException, InterpolationException {
329             Collection c = (Collection) field.get(obj);
330             if (c != null && !c.isEmpty()) {
331                 List originalValues = new ArrayList(c);
332                 try {
333                     c.clear();
334                 } catch (UnsupportedOperationException e) {
335                     warningCollector.add(new ObjectInterpolationWarning(
336                             "Field is an unmodifiable collection. Skipping interpolation.",
337                             basePath + "." + field.getName(),
338                             e));
339                     return true;
340                 }
341 
342                 for (Object value : originalValues) {
343                     if (value != null) {
344                         if (String.class == value.getClass()) {
345                             String interpolated = interpolator.interpolate((String) value, recursionInterceptor);
346 
347                             if (!interpolated.equals(value)) {
348                                 c.add(interpolated);
349                             } else {
350                                 c.add(value);
351                             }
352                         } else {
353                             c.add(value);
354                             if (value.getClass().isArray()) {
355                                 evaluateArray(value, basePath + "." + field.getName());
356                             } else {
357                                 interpolationTargets.add(
358                                         new InterpolationTarget(value, basePath + "." + field.getName()));
359                             }
360                         }
361                     } else {
362                         // add the null back in...not sure what else to do...
363                         c.add(value);
364                     }
365                 }
366             }
367             return false;
368         }
369 
370         private void interpolateString(Object obj, Field field) throws IllegalAccessException, InterpolationException {
371             String value = (String) field.get(obj);
372             if (value != null) {
373                 String interpolated = interpolator.interpolate(value, recursionInterceptor);
374 
375                 if (!interpolated.equals(value)) {
376                     field.set(obj, interpolated);
377                 }
378             }
379         }
380 
381         /**
382          * Using the package-prefix blacklist, determine whether the given class is qualified for interpolation, or
383          * whether it should be ignored.
384          */
385         private boolean isQualifiedForInterpolation(Class cls) {
386             String pkgName = cls.getPackage().getName();
387             for (String prefix : blacklistedPackagePrefixes) {
388                 if (pkgName.startsWith(prefix)) {
389                     return false;
390                 }
391             }
392 
393             return true;
394         }
395 
396         /**
397          * Using the field-name blacklist and the primitive-field cache, determine whether the given field in the given
398          * class is qualified for interpolation. Primitive fields and fields listed in the blacklist will be ignored.
399          * The primitive-field cache is used to improve the performance of the reflective operations in this method,
400          * since this method is a hotspot.
401          */
402         private boolean isQualifiedForInterpolation(Field field, Class fieldType) {
403             if (!fieldIsPrimitiveByClass.containsKey(fieldType)) {
404                 fieldIsPrimitiveByClass.put(fieldType, fieldType.isPrimitive());
405             }
406 
407             //noinspection UnnecessaryUnboxing
408             if (fieldIsPrimitiveByClass.get(fieldType)) {
409                 return false;
410             }
411 
412             return !blacklistedFieldNames.contains(field.getName());
413         }
414 
415         /**
416          * Traverse the elements of an array, and interpolate any qualified objects or add them to the traversal queue.
417          */
418         private void evaluateArray(Object target, String basePath) throws InterpolationException {
419             int len = Array.getLength(target);
420             for (int i = 0; i < len; i++) {
421                 Object value = Array.get(target, i);
422                 if (value != null) {
423                     if (String.class == value.getClass()) {
424                         String interpolated = interpolator.interpolate((String) value, recursionInterceptor);
425 
426                         if (!interpolated.equals(value)) {
427                             Array.set(target, i, interpolated);
428                         }
429                     } else {
430                         interpolationTargets.add(new InterpolationTarget(value, basePath + "[" + i + "]"));
431                     }
432                 }
433             }
434         }
435     }
436 
437     private static final class InterpolationTarget {
438         private Object value;
439 
440         private String path;
441 
442         private InterpolationTarget(Object value, String path) {
443             this.value = value;
444             this.path = path;
445         }
446     }
447 }