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    *      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.ref.Reference;
20  import java.lang.ref.SoftReference;
21  import java.lang.reflect.Field;
22  import java.lang.reflect.Method;
23  import java.util.Collections;
24  import java.util.EnumSet;
25  import java.util.Enumeration;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  import java.util.concurrent.ConcurrentHashMap;
31  import java.util.concurrent.atomic.AtomicInteger;
32  
33  import org.apache.commons.jexl3.JexlArithmetic;
34  import org.apache.commons.jexl3.JexlEngine;
35  import org.apache.commons.jexl3.JexlOperator;
36  import org.apache.commons.jexl3.internal.IntegerRange;
37  import org.apache.commons.jexl3.internal.LongRange;
38  import org.apache.commons.jexl3.internal.Operator;
39  import org.apache.commons.jexl3.introspection.JexlMethod;
40  import org.apache.commons.jexl3.introspection.JexlPermissions;
41  import org.apache.commons.jexl3.introspection.JexlPropertyGet;
42  import org.apache.commons.jexl3.introspection.JexlPropertySet;
43  import org.apache.commons.jexl3.introspection.JexlUberspect;
44  import org.apache.commons.logging.Log;
45  import org.apache.commons.logging.LogFactory;
46  
47  /**
48   * Implements Uberspect to provide the default introspective
49   * functionality of JEXL.
50   * <p>
51   * This is the class to derive to customize introspection.</p>
52   *
53   * @since 1.0
54   */
55  public class Uberspect implements JexlUberspect {
56  
57      /** Publicly exposed special failure object returned by tryInvoke. */
58      public static final Object TRY_FAILED = JexlEngine.TRY_FAILED;
59  
60      /** The logger to use for all warnings and errors. */
61      protected final Log logger;
62  
63      /** The resolver strategy. */
64      private final JexlUberspect.ResolverStrategy strategy;
65  
66      /** The permissions. */
67      private final JexlPermissions permissions;
68  
69      /** The introspector version. */
70      private final AtomicInteger version;
71  
72      /** The soft reference to the introspector currently in use. */
73      private volatile Reference<Introspector> ref;
74  
75      /** The class loader reference; used to recreate the introspector when necessary. */
76      private volatile Reference<ClassLoader> loader;
77  
78      /**
79       * The map from arithmetic classes to overloaded operator sets.
80       * <p>
81       * This map keeps track of which operator methods are overloaded per JexlArithmetic classes
82       * allowing a fail fast test during interpretation by avoiding seeking a method when there is none.
83       */
84      private final Map<Class<? extends JexlArithmetic>, Set<JexlOperator>> operatorMap;
85  
86      /**
87       * Creates a new Uberspect.
88       *
89       * @param runtimeLogger the logger used for all logging needs
90       * @param sty the resolver strategy
91       */
92      public Uberspect(final Log runtimeLogger, final JexlUberspect.ResolverStrategy sty) {
93          this(runtimeLogger, sty, null);
94      }
95  
96      /**
97       * Creates a new Uberspect.
98       *
99       * @param runtimeLogger the logger used for all logging needs
100      * @param sty the resolver strategy
101      * @param perms the introspector permissions
102      */
103     public Uberspect(final Log runtimeLogger, final JexlUberspect.ResolverStrategy sty, final JexlPermissions perms) {
104         final ClassLoader cl = getClass().getClassLoader();
105         logger = runtimeLogger == null ? LogFactory.getLog(JexlEngine.class) : runtimeLogger;
106         strategy = sty == null ? JexlUberspect.JEXL_STRATEGY : sty;
107         permissions = perms == null ? JexlPermissions.RESTRICTED : perms;
108         ref = new SoftReference<>(null);
109         loader = new SoftReference<>(cl);
110         operatorMap = new ConcurrentHashMap<>();
111         version = new AtomicInteger();
112     }
113 
114     /**
115      * Gets the current introspector base.
116      * <p>
117      * If the reference has been collected, this method will recreate the underlying introspector.</p>
118      *
119      * @return the introspector
120      */
121     protected final Introspector base() {
122         Introspector intro = ref.get();
123         if (intro == null) {
124             // double-checked locking is ok (fixed by Java 5 memory model).
125             synchronized (this) {
126                 intro = ref.get();
127                 if (intro == null) {
128                     intro = new Introspector(logger, loader.get(), permissions);
129                     ref = new SoftReference<>(intro);
130                     loader = new SoftReference<>(intro.getLoader());
131                     version.incrementAndGet();
132                 }
133             }
134         }
135         return intro;
136     }
137 
138     /**
139      * Computes which operators have an overload implemented in the arithmetic.
140      * <p>This is used to speed up resolution and avoid introspection when possible.</p>
141      *
142      * @param arithmetic the arithmetic instance
143      * @return the set of overloaded operators
144      */
145     Set<JexlOperator> getOverloads(final JexlArithmetic arithmetic) {
146         final Class<? extends JexlArithmetic> aclass = arithmetic.getClass();
147         return operatorMap.computeIfAbsent(aclass, k -> {
148             final Set<JexlOperator> newOps = EnumSet.noneOf(JexlOperator.class);
149             // deal only with derived classes
150             if (!JexlArithmetic.class.equals(aclass)) {
151                 for (final JexlOperator op : JexlOperator.values()) {
152                     final Method[] methods = getMethods(arithmetic.getClass(), op.getMethodName());
153                     if (methods != null) {
154                         for (final Method method : methods) {
155                             final Class<?>[] parms = method.getParameterTypes();
156                             if (parms.length != op.getArity()) {
157                                 continue;
158                             }
159                             // filter method that is an actual overload:
160                             // - not inherited (not declared by base class)
161                             // - nor overridden (not present in base class)
162                             if (!JexlArithmetic.class.equals(method.getDeclaringClass())) {
163                                 try {
164                                     JexlArithmetic.class.getMethod(method.getName(), method.getParameterTypes());
165                                 } catch (final NoSuchMethodException xmethod) {
166                                     // method was not found in JexlArithmetic; this is an operator definition
167                                     newOps.add(op);
168                                 }
169                             }
170                         }
171                     }
172                 }
173             }
174             return newOps;
175         });
176     }
177 
178     @Override
179     public JexlArithmetic.Uberspect getArithmetic(final JexlArithmetic arithmetic) {
180         final Set<JexlOperator> operators = arithmetic == null ? Collections.emptySet() : getOverloads(arithmetic);
181         return operators.isEmpty()? null : new Operator(this, arithmetic, operators);
182     }
183 
184     @Override
185     public Operator getOperator(final JexlArithmetic arithmetic) {
186         final Set<JexlOperator> operators = arithmetic == null ? Collections.emptySet() : getOverloads(arithmetic);
187         return new Operator(this, arithmetic, operators);
188     }
189 
190     /**
191      * Gets a class by name through this introspector class loader.
192      *
193      * @param className the class name
194      * @return the class instance or null if it could not be found
195      */
196     @Override
197     public final Class<?> getClassByName(final String className) {
198         return base().getClassByName(className);
199     }
200 
201     @Override
202     public ClassLoader getClassLoader() {
203         synchronized (this) {
204             return loader.get();
205         }
206     }
207 
208     @Override
209     public JexlMethod getConstructor(final Object ctorHandle, final Object... args) {
210         return ConstructorMethod.discover(base(), ctorHandle, args);
211     }
212 
213     /**
214      * Gets the field named by
215      * {@code key} for the class
216      * {@code c}.
217      *
218      * @param c   Class in which the field search is taking place
219      * @param key Name of the field being searched for
220      * @return a {@link java.lang.reflect.Field} or null if it does not exist or is not accessible
221      */
222     public final Field getField(final Class<?> c, final String key) {
223         return base().getField(c, key);
224     }
225 
226     /**
227      * Gets the accessible field names known for a given class.
228      *
229      * @param c the class
230      * @return the class field names
231      */
232     public final String[] getFieldNames(final Class<?> c) {
233         return base().getFieldNames(c);
234     }
235 
236     @Override
237     @SuppressWarnings("unchecked")
238     public Iterator<?> getIterator(final Object obj) {
239         // JEXL's own range types and plain arrays are safe language primitives that expose only primitive
240         // values; like arithmetic operators they iterate regardless of permissions.
241         if (!(obj instanceof IntegerRange) && !(obj instanceof LongRange)
242                 && !obj.getClass().isArray() && !permissions.allow(obj.getClass())) {
243             return null;
244         }
245         if (obj instanceof Iterator<?>) {
246             return (Iterator<?>) obj;
247         }
248         if (obj.getClass().isArray()) {
249             return new ArrayIterator(obj);
250         }
251         if (obj instanceof Map<?, ?>) {
252             return ((Map<?, ?>) obj).values().iterator();
253         }
254         if (obj instanceof Enumeration<?>) {
255             return new EnumerationIterator<>((Enumeration<Object>) obj);
256         }
257         if (obj instanceof Iterable<?>) {
258             return ((Iterable<?>) obj).iterator();
259         }
260         try {
261             // look for an iterator() method to support the JDK5 Iterable
262             // interface or any user tools/DTOs that want to work in
263             // foreach without implementing the Collection interface
264             final JexlMethod it = getMethod(obj, "iterator", (Object[]) null);
265             if (it != null && Iterator.class.isAssignableFrom(it.getReturnType())) {
266                 return (Iterator<Object>) it.invoke(obj, (Object[]) null);
267             }
268         } catch (final Exception xany) {
269             if (logger != null && logger.isDebugEnabled()) {
270                 logger.info("unable to solve iterator()", xany);
271             }
272         }
273         return null;
274     }
275 
276     /**
277      * Gets the method defined by
278      * {@code key} and for the Class
279      * {@code c}.
280      *
281      * @param c   Class in which the method search is taking place
282      * @param key MethodKey of the method being searched for
283      * @return a {@link java.lang.reflect.Method}
284      *         or null if no unambiguous method could be found through introspection.
285      */
286     public final Method getMethod(final Class<?> c, final MethodKey key) {
287         return base().getMethod(c, key);
288     }
289 
290     /**
291      * Gets the method defined by
292      * {@code name} and
293      * {@code params} for the Class
294      * {@code c}.
295      *
296      * @param c      Class in which the method search is taking place
297      * @param name   Name of the method being searched for
298      * @param params An array of Objects (not Classes) that describe the parameters
299      * @return a {@link java.lang.reflect.Method}
300      *         or null if no unambiguous method could be found through introspection.
301      */
302     public final Method getMethod(final Class<?> c, final String name, final Object[] params) {
303         return base().getMethod(c, new MethodKey(name, params));
304     }
305 
306     @Override
307     public JexlMethod getMethod(final Object obj, final String method, final Object... args) {
308         return MethodExecutor.discover(base(), obj, method, args);
309     }
310 
311     /**
312      * Gets the accessible methods names known for a given class.
313      *
314      * @param c the class
315      * @return the class method names
316      */
317     public final String[] getMethodNames(final Class<?> c) {
318         return base().getMethodNames(c);
319     }
320 
321     /**
322      * Gets all the methods with a given name from this map.
323      *
324      * @param c          the class
325      * @param methodName the seeked methods name
326      * @return the array of methods
327      */
328     public final Method[] getMethods(final Class<?> c, final String methodName) {
329         return base().getMethods(c, methodName);
330     }
331 
332     @Override
333     public JexlPropertyGet getPropertyGet(
334             final List<PropertyResolver> resolvers, final Object obj, final Object identifier
335     ) {
336         final Class<?> clazz = obj.getClass();
337         final String property = AbstractExecutor.castString(identifier);
338         final Introspector is = base();
339         final List<PropertyResolver> r = resolvers == null ? strategy.apply(null, obj) : resolvers;
340         JexlPropertyGet executor = null;
341         for (final PropertyResolver resolver : r) {
342             if (resolver instanceof JexlResolver) {
343                 switch ((JexlResolver) resolver) {
344                     case PROPERTY:
345                         // first try for a getFoo() type of property (also getfoo() )
346                         executor = PropertyGetExecutor.discover(is, clazz, property);
347                         if (executor == null) {
348                             executor = BooleanGetExecutor.discover(is, clazz, property);
349                         }
350                         break;
351                     case MAP:
352                         // let's see if we are a map...
353                         executor = MapGetExecutor.discover(is, clazz, identifier);
354                         break;
355                     case LIST:
356                         // let's see if this is a list or array
357                         final Integer index = AbstractExecutor.castInteger(identifier);
358                         if (index != null) {
359                             executor = ListGetExecutor.discover(is, clazz, index);
360                         }
361                         break;
362                     case DUCK:
363                         // if that didn't work, look for get(foo)
364                         executor = DuckGetExecutor.discover(is, clazz, identifier);
365                         if (executor == null && property != null && property != identifier) {
366                             // look for get("foo") if we did not try yet (just above)
367                             executor = DuckGetExecutor.discover(is, clazz, property);
368                         }
369                         break;
370                     case FIELD:
371                         // a field may be? (cannot be a number)
372                         executor = FieldGetExecutor.discover(is, clazz, property);
373                         // static class fields (enums included)
374                         if (obj instanceof Class<?>) {
375                             executor = FieldGetExecutor.discover(is, (Class<?>) obj, property);
376                         }
377                         break;
378                     case CONTAINER:
379                         // or an indexed property?
380                         executor = IndexedType.discover(is, obj, property);
381                         break;
382                     default:
383                         continue; // in case we add new ones in enum
384                 }
385             } else {
386                 executor = resolver.getPropertyGet(this, obj, identifier);
387             }
388             if (executor != null) {
389                 return executor;
390             }
391         }
392         return null;
393     }
394 
395     @Override
396     public JexlPropertyGet getPropertyGet(final Object obj, final Object identifier) {
397         return getPropertyGet(null, obj, identifier);
398     }
399 
400     @Override
401     public JexlPropertySet getPropertySet(
402             final List<PropertyResolver> resolvers, final Object obj, final Object identifier, final Object arg
403     ) {
404         final Class<?> clazz = obj.getClass();
405         final String property = AbstractExecutor.castString(identifier);
406         final Introspector is = base();
407         final List<PropertyResolver> actual = resolvers == null ? strategy.apply(null, obj) : resolvers;
408         JexlPropertySet executor = null;
409         for (final PropertyResolver resolver : actual) {
410             if (resolver instanceof JexlResolver) {
411                 switch ((JexlResolver) resolver) {
412                     case PROPERTY:
413                         // first try for a setFoo() type of property (also setfoo() )
414                         executor = PropertySetExecutor.discover(is, clazz, property, arg);
415                         break;
416                     case MAP:
417                         // let's see if we are a map...
418                         executor = MapSetExecutor.discover(is, clazz, identifier, arg);
419                         break;
420                     case LIST:
421                         // let's see if we can convert the identifier to an int,
422                         // if obj is an array or a list, we can still do something
423                         final Integer index = AbstractExecutor.castInteger(identifier);
424                         if (index != null) {
425                             executor = ListSetExecutor.discover(is, clazz, identifier, arg);
426                         }
427                         break;
428                     case DUCK:
429                         // if that didn't work, look for set(foo)
430                         executor = DuckSetExecutor.discover(is, clazz, identifier, arg);
431                         if (executor == null && property != null && property != identifier) {
432                             executor = DuckSetExecutor.discover(is, clazz, property, arg);
433                         }
434                         break;
435                     case FIELD:
436                         // a field may be?
437                         executor = FieldSetExecutor.discover(is, clazz, property, arg);
438                         break;
439                     case CONTAINER:
440                     default:
441                         continue; // in case we add new ones in enum
442                 }
443             } else {
444                 executor = resolver.getPropertySet(this, obj, identifier, arg);
445             }
446             if (executor != null) {
447                 return executor;
448             }
449         }
450         return null;
451     }
452 
453     @Override
454     public JexlPropertySet getPropertySet(final Object obj, final Object identifier, final Object arg) {
455         return getPropertySet(null, obj, identifier, arg);
456     }
457 
458     @Override
459     public List<PropertyResolver> getResolvers(final JexlOperator op, final Object obj) {
460         return strategy.apply(op, obj);
461     }
462 
463     @Override
464     public int getVersion() {
465         return version.intValue();
466     }
467 
468     @Override
469     public void setClassLoader(final ClassLoader loader) {
470         final ClassLoader classLoader = loader == null ? JexlUberspect.class.getClassLoader() : loader;
471         synchronized (this) {
472             Introspector intro = ref.get();
473             if (intro != null) {
474                 intro.setLoader(classLoader);
475             } else {
476                 intro = new Introspector(logger, classLoader, permissions);
477                 ref = new SoftReference<>(intro);
478             }
479             this.loader = new SoftReference<>(intro.getLoader());
480             operatorMap.clear();
481             version.incrementAndGet();
482         }
483     }
484 }