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