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