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