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