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