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.Field;
20 import java.lang.reflect.Method;
21 import java.lang.reflect.Modifier;
22 import java.util.AbstractMap;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.Comparator;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.concurrent.ConcurrentHashMap;
31
32 import org.apache.commons.jexl3.introspection.JexlPermissions;
33 import org.apache.commons.logging.Log;
34
35 /**
36 * A cache of introspection information for a specific class instance.
37 * Keys objects by an aggregation of the method name and the classes
38 * that make up the parameters.
39 * <p>
40 * Originally taken from the Velocity tree so we can be self-sufficient.
41 * </p>
42 *
43 * @see MethodKey
44 * @since 1.0
45 */
46 final class ClassMap {
47 /**
48 * The cache miss marker method.
49 */
50 static final Method CACHE_MISS = cacheMiss();
51
52 /**
53 * Singleton for permissions non-allowed classes.
54 */
55 private static final ClassMap EMPTY = new ClassMap();
56 /**
57 * A method that returns itself used as a marker for cache miss,
58 * allows the underlying cache map to be strongly typed.
59 *
60 * @return itself as a method
61 */
62 public static Method cacheMiss() {
63 try {
64 return ClassMap.class.getMethod("cacheMiss");
65 } catch (final Exception xio) {
66 // this really can't make an error...
67 return null;
68 }
69 }
70 /**
71 * Populate the Map of direct hits. These are taken from all the public methods
72 * that our class, its parents and their implemented interfaces provide.
73 *
74 * @param cache the ClassMap instance we create
75 * @param permissions the permissions to apply during introspection
76 * @param clazz the class to cache
77 * @param log the Log
78 */
79 private static void create(final ClassMap cache, final JexlPermissions permissions, final Class<?> clazz, final Log log) {
80 //
81 // Build a list of all elements in the class hierarchy. This one is bottom-first; we start
82 // with the actual declaring class and its interfaces and then move up (superclass etc.) until we
83 // hit java.lang.Object. That is important because it will give us the methods of the declaring class
84 // which might in turn be abstract further up the tree.
85 //
86 // We also ignore all SecurityExceptions that might happen due to SecurityManager restrictions.
87 //
88 for (Class<?> classToReflect = clazz; classToReflect != null; classToReflect = classToReflect.getSuperclass()) {
89 if (Modifier.isPublic(classToReflect.getModifiers()) && ClassTool.isExported(classToReflect)) {
90 populateWithClass(cache, permissions, classToReflect, log);
91 }
92 final Class<?>[] interfaces = classToReflect.getInterfaces();
93 for (final Class<?> anInterface : interfaces) {
94 populateWithInterface(cache, permissions, anInterface, log);
95 }
96 }
97 // now that we've got all methods keyed in, lets organize them by name
98 if (!cache.byKey.isEmpty()) {
99 final List<Method> lm = new ArrayList<>(cache.byKey.values());
100 // sort all methods by name
101 lm.sort(Comparator.comparing(Method::getName));
102 // put all lists of methods with same name in byName cache
103 int start = 0;
104 while (start < lm.size()) {
105 final String name = lm.get(start).getName();
106 int end = start + 1;
107 while (end < lm.size()) {
108 final String walk = lm.get(end).getName();
109 if (!walk.equals(name)) {
110 break;
111 }
112 end += 1;
113 }
114 final Method[] lmn = lm.subList(start, end).toArray(new Method[0]);
115 cache.byName.put(name, lmn);
116 start = end;
117 }
118 }
119 }
120 /**
121 * @return the empty classmap instance
122 */
123 static ClassMap empty() {
124 return EMPTY;
125 }
126
127 /**
128 * Recurses up class hierarchy to get all super classes.
129 *
130 * @param cache the cache to fill
131 * @param permissions the permissions to apply during introspection
132 * @param clazz the class to populate the cache from
133 * @param log the Log
134 */
135 private static void populateWithClass(final ClassMap cache,
136 final JexlPermissions permissions,
137 final Class<?> clazz,
138 final Log log) {
139 try {
140 final Method[] methods = clazz.getDeclaredMethods();
141 for (final Method mi : methods) {
142 // method must be public
143 if (!Modifier.isPublic(mi.getModifiers())) {
144 continue;
145 }
146 // add method to byKey cache; do not override
147 final MethodKey key = new MethodKey(mi);
148 final Method pmi = cache.byKey.putIfAbsent(key, permissions.allow(mi) ? mi : CACHE_MISS);
149 if (pmi != null && pmi != CACHE_MISS && log.isDebugEnabled() && !key.equals(new MethodKey(pmi))) {
150 // foo(int) and foo(Integer) have the same signature for JEXL
151 log.debug("Method " + pmi + " is already registered, key: " + key.debugString());
152 }
153 }
154 } catch (final SecurityException se) {
155 // Everybody feels better with...
156 if (log.isDebugEnabled()) {
157 log.debug("While accessing methods of " + clazz + ": ", se);
158 }
159 }
160 }
161
162 /**
163 * Recurses up interface hierarchy to get all super interfaces.
164 *
165 * @param cache the cache to fill
166 * @param permissions the permissions to apply during introspection
167 * @param iface the interface to populate the cache from
168 * @param log the Log
169 */
170 private static void populateWithInterface(final ClassMap cache,
171 final JexlPermissions permissions,
172 final Class<?> iface,
173 final Log log) {
174 if (Modifier.isPublic(iface.getModifiers())) {
175 populateWithClass(cache, permissions, iface, log);
176 final Class<?>[] supers = iface.getInterfaces();
177 for (final Class<?> aSuper : supers) {
178 populateWithInterface(cache, permissions, aSuper, log);
179 }
180 }
181 }
182
183 /**
184 * This is the cache to store and look up the method information.
185 * <p>
186 * It stores the association between:
187 * - a key made of a method name and an array of argument types.
188 * - a method.
189 * </p>
190 * <p>
191 * Since the invocation of the associated method is dynamic, there is no need (nor way) to differentiate between
192 * foo(int,int) and foo(Integer,Integer) since in practice only the latter form will be used through a call.
193 * This of course, applies to all 8 primitive types.
194 * </p>
195 * Uses ConcurrentMap since 3.0, marginally faster than 2.1 under contention.
196 */
197 private final Map<MethodKey, Method> byKey ;
198
199 /**
200 * Keep track of all methods with the same name; this is not modified after creation.
201 */
202 private final Map<String, Method[]> byName;
203
204 /**
205 * Cache of fields.
206 */
207 private final Map<String, Field> fieldCache;
208
209 /**
210 * Empty map.
211 */
212 private ClassMap() {
213 this.byKey = Collections.unmodifiableMap(new AbstractMap<MethodKey, Method>() {
214 @Override
215 public Set<Entry<MethodKey, Method>> entrySet() {
216 return Collections.emptySet();
217 }
218 @Override public Method get(final Object name) {
219 return CACHE_MISS;
220 }
221 @Override
222 public String toString() {
223 return "emptyClassMap{}";
224 }
225 });
226 this.byName = Collections.emptyMap();
227 this.fieldCache = Collections.emptyMap();
228 }
229
230 /**
231 * Standard constructor.
232 *
233 * @param aClass the class to deconstruct.
234 * @param permissions the permissions to apply during introspection
235 * @param log the logger.
236 */
237 @SuppressWarnings("LeakingThisInConstructor")
238 ClassMap(final Class<?> aClass, final JexlPermissions permissions, final Log log) {
239 this.byKey = new ConcurrentHashMap<>();
240 this.byName = new HashMap<>();
241 // eagerly cache methods
242 create(this, permissions, aClass, log);
243 // eagerly cache public fields
244 final Field[] fields = aClass.getFields();
245 if (fields.length > 0) {
246 final Map<String, Field> cache = new HashMap<>();
247 for (final Field field : fields) {
248 if (permissions.allow(field)) {
249 cache.put(field.getName(), field);
250 }
251 }
252 fieldCache = cache;
253 } else {
254 fieldCache = Collections.emptyMap();
255 }
256 }
257
258 /**
259 * Find a Field using its name.
260 *
261 * @param fieldName the field name
262 * @return A Field object representing the field to invoke or null.
263 */
264 Field getField(final String fieldName) {
265 return fieldCache.get(fieldName);
266 }
267
268 /**
269 * Gets the field names cached by this map.
270 *
271 * @return the array of field names
272 */
273 String[] getFieldNames() {
274 return fieldCache.keySet().toArray(new String[0]);
275 }
276
277 /**
278 * Find a Method using the method name and parameter objects.
279 * <p>
280 * Look in the methodMap for an entry. If found,
281 * it'll either be a CACHE_MISS, in which case we
282 * simply give up, or it'll be a Method, in which
283 * case, we return it.
284 * </p>
285 * <p>
286 * If nothing is found, then we must actually go
287 * and introspect the method from the MethodMap.
288 * </p>
289 *
290 * @param methodKey the method key
291 * @return A Method object representing the method to invoke or null.
292 * @throws MethodKey.AmbiguousException When more than one method is a match for the parameters.
293 */
294 Method getMethod(final MethodKey methodKey) throws MethodKey.AmbiguousException {
295 // Look up by key
296 Method cacheEntry = byKey.get(methodKey);
297 // We looked this up before and failed.
298 if (cacheEntry == CACHE_MISS) {
299 return null;
300 }
301 if (cacheEntry == null) {
302 try {
303 // That one is expensive...
304 final Method[] methodList = byName.get(methodKey.getMethod());
305 if (methodList != null) {
306 cacheEntry = methodKey.getMostSpecificMethod(methodList);
307 }
308 byKey.put(methodKey, cacheEntry == null ? CACHE_MISS : cacheEntry);
309 } catch (final MethodKey.AmbiguousException ae) {
310 // that's a miss :-)
311 byKey.put(methodKey, CACHE_MISS);
312 throw ae;
313 }
314 }
315
316 // Yes, this might just be null.
317 return cacheEntry;
318 }
319
320 /**
321 * Gets the methods names cached by this map.
322 *
323 * @return the array of method names
324 */
325 String[] getMethodNames() {
326 return byName.keySet().toArray(new String[0]);
327 }
328
329 /**
330 * Gets all the methods with a given name from this map.
331 *
332 * @param methodName the seeked methods name
333 * @return the array of methods (null or non-empty)
334 */
335 Method[] getMethods(final String methodName) {
336 final Method[] lm = byName.get(methodName);
337 if (lm != null && lm.length > 0) {
338 return lm.clone();
339 }
340 return null;
341 }
342 }