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 }