View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3.internal.introspection;
18  
19  import java.lang.reflect.Constructor;
20  import java.lang.reflect.Field;
21  import java.lang.reflect.Method;
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.concurrent.locks.ReadWriteLock;
28  import java.util.concurrent.locks.ReentrantReadWriteLock;
29  
30  import org.apache.commons.jexl3.introspection.JexlPermissions;
31  import org.apache.commons.jexl3.introspection.JexlUberspect;
32  import org.apache.commons.logging.Log;
33  
34  /**
35   * This basic function of this class is to return a Method object for a
36   * particular class given the name of a method and the parameters to the method
37   * in the form of an Object[].
38   *
39   * <p>The first time the Introspector sees a class it creates a class method map
40   * for the class in question.
41   * Basically the class method map is a Hashtable where Method objects are keyed by the aggregation of
42   * the method name and the array of parameters classes.
43   * This mapping is performed for all the public methods of a class and stored.</p>
44   *
45   * @since 1.0
46   */
47  public final class Introspector {
48  
49      /**
50       * A Constructor get cache-miss.
51       */
52      private static final class CacheMiss {
53  
54          /** The constructor used as cache-miss. */
55          @SuppressWarnings("unused")
56          public CacheMiss() {
57              // empty
58          }
59      }
60  
61      /**
62       * The cache-miss marker for the constructors map.
63       */
64      private static final Constructor<?> CTOR_MISS = CacheMiss.class.getConstructors()[0];
65  
66      /**
67       * Checks whether a class is loaded through a given class loader or one of its ascendants.
68       *
69       * @param loader the class loader
70       * @param clazz  the class to check
71       * @return true if clazz was loaded through the loader, false otherwise
72       */
73      private static boolean isLoadedBy(final ClassLoader loader, final Class<?> clazz) {
74          if (loader != null) {
75              ClassLoader cloader = clazz.getClassLoader();
76              while (cloader != null) {
77                  if (cloader.equals(loader)) {
78                      return true;
79                  }
80                  cloader = cloader.getParent();
81              }
82          }
83          return false;
84      }
85  
86      /**
87       * the logger.
88       */
89      private final Log logger;
90  
91      /**
92       * The permissions.
93       */
94      private final JexlPermissions permissions;
95  
96      /**
97       * The read/write lock.
98       */
99      private final ReadWriteLock lock = new ReentrantReadWriteLock();
100 
101     /**
102      * Holds the method maps for the classes we know about, keyed by Class.
103      */
104     private final Map<Class<?>, ClassMap> classMethodMaps = new HashMap<>();
105 
106     /**
107      * Holds the map of classes ctors we know about as well as unknown ones.
108      */
109     private final Map<MethodKey, Constructor<?>> constructorsMap = new HashMap<>();
110 
111     /**
112      * Holds the set of classes we have introspected.
113      */
114     private final Map<String, Class<?>> constructibleClasses = new HashMap<>();
115 
116     /**
117      * The class loader used to solve constructors if needed.
118      * <p>Cheap read-write lock pattern: exclusive lock for write, read visibility through volatile.</p>
119      */
120     @SuppressWarnings("java:S3077")
121     private volatile ClassLoader loader;
122 
123     /**
124      * Create the introspector.
125      *
126      * @param log     the logger to use
127      * @param cloader the class loader
128      */
129     public Introspector(final Log log, final ClassLoader cloader) {
130         this(log, cloader, null);
131     }
132 
133     /**
134      * Create the introspector.
135      *
136      * @param log     the logger to use
137      * @param loader the class loader
138      * @param perms the permissions
139      */
140     public Introspector(final Log log, final ClassLoader loader, final JexlPermissions perms) {
141         this.logger = log;
142         this.loader = loader != null ? loader : JexlUberspect.class.getClassLoader();
143         this.permissions = perms == null ? JexlPermissions.RESTRICTED : perms;
144     }
145 
146     /**
147      * Gets a class by name through this introspector class loader.
148      *
149      * @param className the class name
150      * @return the class instance or null if it could not be found
151      */
152     public Class<?> getClassByName(final String className) {
153         try {
154             final ClassLoader classLoader = loader;
155             final Class<?> clazz = Class.forName(className, false, classLoader);
156             return permissions.allow(clazz)? clazz : null;
157         } catch (final ClassNotFoundException xignore) {
158             return null;
159         }
160     }
161 
162     /**
163      * Gets the constructor defined by the {@code MethodKey}.
164      *
165      * @param c   the class we want to instantiate
166      * @param key Key of the constructor being searched for
167      * @return The desired constructor object
168      * or null if no unambiguous constructor could be found through introspection.
169      */
170     public Constructor<?> getConstructor(final Class<?> c, final MethodKey key) {
171         Constructor<?> ctor;
172         lock.readLock().lock();
173         try {
174             ctor = constructorsMap.get(key);
175             if (ctor != null) {
176                 // miss or not?
177                 return CTOR_MISS.equals(ctor) ? null : ctor;
178             }
179         } finally {
180             lock.readLock().unlock();
181         }
182         // let's introspect...
183         lock.writeLock().lock();
184         try {
185             // again for kicks
186             ctor = constructorsMap.get(key);
187             if (ctor != null) {
188                 // miss or not?
189                 return CTOR_MISS.equals(ctor) ? null : ctor;
190             }
191             final String constructorName = key.getMethod();
192             // do we know about this class?
193             Class<?> clazz = constructibleClasses.get(constructorName);
194             try {
195                 // do find the most specific ctor
196                 if (clazz == null) {
197                     if (c != null && c.getName().equals(key.getMethod())) {
198                         clazz = c;
199                     } else {
200                         // read under lock
201                         clazz = loader.loadClass(constructorName);
202                     }
203                     // add it to list of known loaded classes
204                     constructibleClasses.put(constructorName, clazz);
205                 }
206                 final List<Constructor<?>> constructors = new ArrayList<>();
207                 for (final Constructor<?> ictor : clazz.getConstructors()) {
208                     if (permissions.allow(ictor)) {
209                         constructors.add(ictor);
210                     }
211                 }
212                 // try to find one
213                 ctor = key.getMostSpecificConstructor(constructors.toArray(new Constructor<?>[0]));
214                 if (ctor != null) {
215                     constructorsMap.put(key, ctor);
216                 } else {
217                     constructorsMap.put(key, CTOR_MISS);
218                 }
219             } catch (final ClassNotFoundException xnotfound) {
220                 if (logger != null && logger.isDebugEnabled()) {
221                     logger.debug("unable to find class: "
222                             + constructorName + "."
223                             + key.debugString(), xnotfound);
224                 }
225             } catch (final MethodKey.AmbiguousException xambiguous) {
226                 if (logger != null  && xambiguous.isSevere() &&  logger.isInfoEnabled()) {
227                     logger.info("ambiguous constructor invocation: "
228                             + constructorName + "."
229                             + key.debugString(), xambiguous);
230                 }
231                 ctor = null;
232             }
233             return ctor;
234         } finally {
235             lock.writeLock().unlock();
236         }
237     }
238 
239     /**
240      * Gets the constructor defined by the {@code MethodKey}.
241      *
242      * @param key Key of the constructor being searched for
243      * @return The desired constructor object
244      * or null if no unambiguous constructor could be found through introspection.
245      */
246     public Constructor<?> getConstructor(final MethodKey key) {
247         return getConstructor(null, key);
248     }
249 
250     /**
251      * Gets the field named by {@code key} for the class {@code c}.
252      *
253      * @param c   Class in which the field search is taking place
254      * @param key Name of the field being searched for
255      * @return the desired field or null if it does not exist or is not accessible
256      */
257     public Field getField(final Class<?> c, final String key) {
258         return getMap(c).getField(key);
259     }
260 
261     /**
262      * Gets the array of accessible field names known for a given class.
263      *
264      * @param c the class
265      * @return the class field names
266      */
267     public String[] getFieldNames(final Class<?> c) {
268         if (c == null) {
269             return new String[0];
270         }
271         final ClassMap classMap = getMap(c);
272         return classMap.getFieldNames();
273     }
274 
275     /**
276      * Gets the class loader used by this introspector.
277      *
278      * @return the class loader
279      */
280     public ClassLoader getLoader() {
281         return loader;
282     }
283 
284     /**
285      * Gets the ClassMap for a given class.
286      *
287      * @param c the class
288      * @return the class map
289      */
290     private ClassMap getMap(final Class<?> c) {
291         ClassMap classMap;
292         lock.readLock().lock();
293         try {
294             classMap = classMethodMaps.get(c);
295         } finally {
296             lock.readLock().unlock();
297         }
298         if (classMap == null) {
299             lock.writeLock().lock();
300             try {
301                 // try again
302                 classMap = classMethodMaps.get(c);
303                 if (classMap == null) {
304                     classMap = permissions.allow(c)
305                             ? new ClassMap(c, permissions, logger)
306                             : ClassMap.empty();
307                     classMethodMaps.put(c, classMap);
308                 }
309             } finally {
310                 lock.writeLock().unlock();
311             }
312 
313         }
314         return classMap;
315     }
316 
317     /**
318      * Gets the method defined by the {@code MethodKey} for the class {@code c}.
319      *
320      * @param c   Class in which the method search is taking place
321      * @param key Key of the method being searched for
322      * @return The desired method object
323      * @throws MethodKey.AmbiguousException if no unambiguous method could be found through introspection
324      */
325     public Method getMethod(final Class<?> c, final MethodKey key) {
326         try {
327             return getMap(c).getMethod(key);
328         } catch (final MethodKey.AmbiguousException xambiguous) {
329             // whoops. Ambiguous and not benign. Make a nice log message and return null...
330             if (logger != null && xambiguous.isSevere() && logger.isInfoEnabled()) {
331                 logger.info("ambiguous method invocation: "
332                         + c.getName() + "."
333                         + key.debugString(), xambiguous);
334             }
335             return null;
336         }
337     }
338 
339     /**
340      * Gets a method defined by a class, a name and a set of parameters.
341      *
342      * @param c      the class
343      * @param name   the method name
344      * @param params the method parameters
345      * @return the desired method object
346      * @throws MethodKey.AmbiguousException if no unambiguous method could be found through introspection
347      */
348     public Method getMethod(final Class<?> c, final String name, final Object... params) {
349         return getMethod(c, new MethodKey(name, params));
350     }
351 
352     /**
353      * Gets the array of accessible methods names known for a given class.
354      *
355      * @param c the class
356      * @return the class method names
357      */
358     public String[] getMethodNames(final Class<?> c) {
359         if (c == null) {
360             return new String[0];
361         }
362         final ClassMap classMap = getMap(c);
363         return classMap.getMethodNames();
364     }
365 
366     /**
367      * Gets the array of accessible method known for a given class.
368      *
369      * @param c          the class
370      * @param methodName the method name
371      * @return the array of methods (null or not empty)
372      */
373     public Method[] getMethods(final Class<?> c, final String methodName) {
374         if (c == null) {
375             return null;
376         }
377         final ClassMap classMap = getMap(c);
378         return classMap.getMethods(methodName);
379     }
380 
381     /**
382      * Sets the class loader used to solve constructors.
383      * <p>Also cleans the constructors and methods caches.</p>
384      *
385      * @param classLoader the class loader; if null, use this instance class loader
386      */
387     public void setLoader(final ClassLoader classLoader) {
388         final ClassLoader current = classLoader == null ? JexlUberspect.class.getClassLoader() : classLoader;
389         lock.writeLock().lock();
390         try {
391             final ClassLoader previous = loader;
392             if (!current.equals(previous)) {
393                 // clean up constructor and class maps
394                 final Iterator<Map.Entry<MethodKey, Constructor<?>>> constructors = constructorsMap.entrySet().iterator();
395                 while (constructors.hasNext()) {
396                     final Map.Entry<MethodKey, Constructor<?>> entry = constructors.next();
397                     final Class<?> clazz = entry.getValue().getDeclaringClass();
398                     if (isLoadedBy(previous, clazz)) {
399                         constructors.remove();
400                         if (!CTOR_MISS.equals(entry.getValue())) {
401                             // the method name is the name of the class
402                             constructibleClasses.remove(entry.getKey().getMethod());
403                         }
404                     }
405                 }
406                 // clean up method maps
407                 final Iterator<Map.Entry<Class<?>, ClassMap>> methods = classMethodMaps.entrySet().iterator();
408                 while (methods.hasNext()) {
409                     final Map.Entry<Class<?>, ClassMap> entry = methods.next();
410                     final Class<?> clazz = entry.getKey();
411                     if (isLoadedBy(previous, clazz)) {
412                         methods.remove();
413                     }
414                 }
415                 loader = current;
416             }
417         } finally {
418             lock.writeLock().unlock();
419         }
420     }
421 }