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}.
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}.
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} for the class {@code c}.
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} for the class {@code c}.
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 }