001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.jexl3.internal.introspection;
018
019import org.apache.commons.jexl3.JexlArithmetic;
020import org.apache.commons.jexl3.JexlEngine;
021import org.apache.commons.jexl3.JexlOperator;
022import org.apache.commons.jexl3.introspection.JexlMethod;
023import org.apache.commons.jexl3.introspection.JexlPropertyGet;
024import org.apache.commons.jexl3.introspection.JexlPropertySet;
025import org.apache.commons.jexl3.introspection.JexlUberspect;
026
027import org.apache.commons.logging.Log;
028
029import java.lang.reflect.Field;
030import java.lang.reflect.Method;
031
032import java.util.EnumSet;
033import java.util.Enumeration;
034import java.util.Iterator;
035import java.util.Map;
036import java.util.Set;
037import java.util.concurrent.atomic.AtomicInteger;
038import java.util.concurrent.ConcurrentHashMap;
039
040import java.lang.ref.Reference;
041import java.lang.ref.SoftReference;
042import java.util.List;
043
044/**
045 * Implementation of Uberspect to provide the default introspective
046 * functionality of JEXL.
047 * <p>
048 * This is the class to derive to customize introspection.</p>
049 *
050 * @since 1.0
051 */
052public class Uberspect implements JexlUberspect {
053    /** Publicly exposed special failure object returned by tryInvoke. */
054    public static final Object TRY_FAILED = JexlEngine.TRY_FAILED;
055    /** The logger to use for all warnings and errors. */
056    protected final Log logger;
057    /** The resolver strategy. */
058    private final JexlUberspect.ResolverStrategy strategy;
059    /** The permissions. */
060    private final Permissions permissions;
061    /** The introspector version. */
062    private final AtomicInteger version;
063    /** The soft reference to the introspector currently in use. */
064    private volatile Reference<Introspector> ref;
065    /** The class loader reference; used to recreate the introspector when necessary. */
066    private volatile Reference<ClassLoader> loader;
067    /**
068     * The map from arithmetic classes to overloaded operator sets.
069     * <p>
070     * This keeps track of which operator methods are overloaded per JexlArithemtic classes
071     * allowing a fail fast test during interpretation by avoiding seeking a method when there is none.
072     */
073    private final Map<Class<? extends JexlArithmetic>, Set<JexlOperator>> operatorMap;
074
075    /**
076     * Creates a new Uberspect.
077     * @param runtimeLogger the logger used for all logging needs
078     * @param sty the resolver strategy
079     */
080    public Uberspect(final Log runtimeLogger, final JexlUberspect.ResolverStrategy sty) {
081        this(runtimeLogger, sty, null);
082    }
083
084    /**
085     * Creates a new Uberspect.
086     * @param runtimeLogger the logger used for all logging needs
087     * @param sty the resolver strategy
088     * @param perms the introspector permissions
089     */
090    public Uberspect(final Log runtimeLogger, final JexlUberspect.ResolverStrategy sty, final Permissions perms) {
091        logger = runtimeLogger;
092        strategy = sty == null? JexlUberspect.JEXL_STRATEGY : sty;
093        permissions  = perms;
094        ref = new SoftReference<Introspector>(null);
095        loader = new SoftReference<ClassLoader>(getClass().getClassLoader());
096        operatorMap = new ConcurrentHashMap<Class<? extends JexlArithmetic>, Set<JexlOperator>>();
097        version = new AtomicInteger(0);
098    }
099
100    /**
101     * Gets the current introspector base.
102     * <p>
103     * If the reference has been collected, this method will recreate the underlying introspector.</p>
104     * @return the introspector
105     */
106    // CSOFF: DoubleCheckedLocking
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<Introspector>(intro);
116                    loader = new SoftReference<ClassLoader>(intro.getLoader());
117                    version.incrementAndGet();
118                }
119            }
120        }
121        return intro;
122    }
123    // CSON: DoubleCheckedLocking
124
125    @Override
126    public void setClassLoader(final ClassLoader nloader) {
127        synchronized (this) {
128            Introspector intro = ref.get();
129            if (intro != null) {
130                intro.setLoader(nloader);
131            } else {
132                intro = new Introspector(logger, nloader, permissions);
133                ref = new SoftReference<Introspector>(intro);
134            }
135            loader = new SoftReference<ClassLoader>(intro.getLoader());
136            operatorMap.clear();
137            version.incrementAndGet();
138        }
139    }
140
141    @Override
142    public ClassLoader getClassLoader() {
143        return loader.get();
144    }
145
146    @Override
147    public int getVersion() {
148        return version.intValue();
149    }
150
151    /**
152     * Gets a class by name through this introspector class loader.
153     * @param className the class name
154     * @return the class instance or null if it could not be found
155     */
156    public final Class<?> getClassByName(final String className) {
157        return base().getClassByName(className);
158    }
159
160    /**
161     * Gets the field named by
162     * <code>key</code> for the class
163     * <code>c</code>.
164     *
165     * @param c   Class in which the field search is taking place
166     * @param key Name of the field being searched for
167     * @return a {@link java.lang.reflect.Field} or null if it does not exist or is not accessible
168     */
169    public final Field getField(final Class<?> c, final String key) {
170        return base().getField(c, key);
171    }
172
173    /**
174     * Gets the accessible field names known for a given class.
175     * @param c the class
176     * @return the class field names
177     */
178    public final String[] getFieldNames(final Class<?> c) {
179        return base().getFieldNames(c);
180    }
181
182    /**
183     * Gets the method defined by
184     * <code>name</code> and
185     * <code>params</code> for the Class
186     * <code>c</code>.
187     *
188     * @param c      Class in which the method search is taking place
189     * @param name   Name of the method being searched for
190     * @param params An array of Objects (not Classes) that describe the
191     *               the parameters
192     *
193     * @return a {@link java.lang.reflect.Method}
194     *         or null if no unambiguous method could be found through introspection.
195     */
196    public final Method getMethod(final Class<?> c, final String name, final Object[] params) {
197        return base().getMethod(c, new MethodKey(name, params));
198    }
199
200    /**
201     * Gets the method defined by
202     * <code>key</code> and for the Class
203     * <code>c</code>.
204     *
205     * @param c   Class in which the method search is taking place
206     * @param key MethodKey of the method being searched for
207     *
208     * @return a {@link java.lang.reflect.Method}
209     *         or null if no unambiguous method could be found through introspection.
210     */
211    public final Method getMethod(final Class<?> c, final MethodKey key) {
212        return base().getMethod(c, key);
213    }
214
215    /**
216     * Gets the accessible methods names known for a given class.
217     * @param c the class
218     * @return the class method names
219     */
220    public final String[] getMethodNames(final Class<?> c) {
221        return base().getMethodNames(c);
222    }
223
224    /**
225     * Gets all the methods with a given name from this map.
226     * @param c          the class
227     * @param methodName the seeked methods name
228     * @return the array of methods
229     */
230    public final Method[] getMethods(final Class<?> c, final String methodName) {
231        return base().getMethods(c, methodName);
232    }
233
234    @Override
235    public JexlMethod getMethod(final Object obj, final String method, final Object... args) {
236        return MethodExecutor.discover(base(), obj, method, args);
237    }
238
239    @Override
240    public List<PropertyResolver> getResolvers(final JexlOperator op, final Object obj) {
241        return strategy.apply(op, obj);
242    }
243
244    @Override
245    public JexlPropertyGet getPropertyGet(final Object obj, final Object identifier) {
246        return getPropertyGet(null, obj, identifier);
247    }
248
249    @Override
250    public JexlPropertyGet getPropertyGet(
251            final List<PropertyResolver> resolvers, final Object obj, final Object identifier
252    ) {
253        final Class<?> claz = obj.getClass();
254        final String property = AbstractExecutor.castString(identifier);
255        final Introspector is = base();
256        final List<PropertyResolver> r = resolvers == null? strategy.apply(null, obj) : resolvers;
257        JexlPropertyGet executor = null;
258        for (final PropertyResolver resolver : r) {
259            if (resolver instanceof JexlResolver) {
260                switch ((JexlResolver) resolver) {
261                    case PROPERTY:
262                        // first try for a getFoo() type of property (also getfoo() )
263                        executor = PropertyGetExecutor.discover(is, claz, property);
264                        if (executor == null) {
265                            executor = BooleanGetExecutor.discover(is, claz, property);
266                        }
267                        break;
268                    case MAP:
269                        // let's see if we are a map...
270                        executor = MapGetExecutor.discover(is, claz, identifier);
271                        break;
272                    case LIST:
273                        // let's see if this is a list or array
274                        final Integer index = AbstractExecutor.castInteger(identifier);
275                        if (index != null) {
276                            executor = ListGetExecutor.discover(is, claz, index);
277                        }
278                        break;
279                    case DUCK:
280                        // if that didn't work, look for get(foo)
281                        executor = DuckGetExecutor.discover(is, claz, identifier);
282                        if (executor == null && property != null && property != identifier) {
283                            // look for get("foo") if we did not try yet (just above)
284                            executor = DuckGetExecutor.discover(is, claz, property);
285                        }
286                        break;
287                    case FIELD:
288                        // a field may be? (can not be a number)
289                        executor = FieldGetExecutor.discover(is, claz, property);
290                        // static class fields (enums included)
291                        if (obj instanceof Class<?>) {
292                            executor = FieldGetExecutor.discover(is, (Class<?>) obj, property);
293                        }
294                        break;
295                    case CONTAINER:
296                        // or an indexed property?
297                        executor = IndexedType.discover(is, obj, property);
298                        break;
299                    default:
300                        continue; // in case we add new ones in enum
301                }
302            } else {
303                executor = resolver.getPropertyGet(this, obj, identifier);
304            }
305            if (executor != null) {
306                return executor;
307            }
308        }
309        return null;
310    }
311
312    @Override
313    public JexlPropertySet getPropertySet(final Object obj, final Object identifier, final Object arg) {
314        return getPropertySet(null, obj, identifier, arg);
315    }
316
317    @Override
318    public JexlPropertySet getPropertySet(
319            final List<PropertyResolver> resolvers, final Object obj, final Object identifier, final Object arg
320    ) {
321        final Class<?> claz = obj.getClass();
322        final String property = AbstractExecutor.castString(identifier);
323        final Introspector is = base();
324        final List<PropertyResolver> actual = resolvers == null? strategy.apply(null, obj) : resolvers;
325        JexlPropertySet executor = null;
326        for (final PropertyResolver resolver : actual) {
327            if (resolver instanceof JexlResolver) {
328                switch ((JexlResolver) resolver) {
329                    case PROPERTY:
330                        // first try for a setFoo() type of property (also setfoo() )
331                        executor = PropertySetExecutor.discover(is, claz, property, arg);
332                        break;
333                    case MAP:
334                        // let's see if we are a map...
335                        executor = MapSetExecutor.discover(is, claz, identifier, arg);
336                        break;
337                    case LIST:
338                    // let's see if we can convert the identifier to an int,
339                        // if obj is an array or a list, we can still do something
340                        final Integer index = AbstractExecutor.castInteger(identifier);
341                        if (index != null) {
342                            executor = ListSetExecutor.discover(is, claz, identifier, arg);
343                        }
344                        break;
345                    case DUCK:
346                        // if that didn't work, look for set(foo)
347                        executor = DuckSetExecutor.discover(is, claz, identifier, arg);
348                        if (executor == null && property != null && property != identifier) {
349                            executor = DuckSetExecutor.discover(is, claz, property, arg);
350                        }
351                        break;
352                    case FIELD:
353                        // a field may be?
354                        executor = FieldSetExecutor.discover(is, claz, property, arg);
355                        break;
356                    case CONTAINER:
357                    default:
358                        continue; // in case we add new ones in enum
359                }
360            } else {
361                executor = resolver.getPropertySet(this, obj, identifier, arg);
362            }
363            if (executor != null) {
364                return executor;
365            }
366        }
367        return null;
368    }
369
370    @Override
371    @SuppressWarnings("unchecked")
372    public Iterator<?> getIterator(final Object obj) {
373        if (obj instanceof Iterator<?>) {
374            return ((Iterator<?>) obj);
375        }
376        if (obj.getClass().isArray()) {
377            return new ArrayIterator(obj);
378        }
379        if (obj instanceof Map<?, ?>) {
380            return ((Map<?, ?>) obj).values().iterator();
381        }
382        if (obj instanceof Enumeration<?>) {
383            return new EnumerationIterator<Object>((Enumeration<Object>) obj);
384        }
385        if (obj instanceof Iterable<?>) {
386            return ((Iterable<?>) obj).iterator();
387        }
388        try {
389            // look for an iterator() method to support the JDK5 Iterable
390            // interface or any user tools/DTOs that want to work in
391            // foreach without implementing the Collection interface
392            final JexlMethod it = getMethod(obj, "iterator", (Object[]) null);
393            if (it != null && Iterator.class.isAssignableFrom(it.getReturnType())) {
394                return (Iterator<Object>) it.invoke(obj, (Object[]) null);
395            }
396        } catch (final Exception xany) {
397            if (logger != null && logger.isDebugEnabled()) {
398                logger.info("unable to solve iterator()", xany);
399            }
400        }
401        return null;
402    }
403
404    @Override
405    public JexlMethod getConstructor(final Object ctorHandle, final Object... args) {
406        return ConstructorMethod.discover(base(), ctorHandle, args);
407    }
408
409    /**
410     * The concrete uberspect Arithmetic class.
411     */
412    protected class ArithmeticUberspect implements JexlArithmetic.Uberspect {
413        /** The arithmetic instance being analyzed. */
414        private final JexlArithmetic arithmetic;
415        /** The set of overloaded operators. */
416        private final Set<JexlOperator> overloads;
417
418        /**
419         * Creates an instance.
420         * @param theArithmetic the arithmetic instance
421         * @param theOverloads  the overloaded operators
422         */
423        private ArithmeticUberspect(final JexlArithmetic theArithmetic, final Set<JexlOperator> theOverloads) {
424            this.arithmetic = theArithmetic;
425            this.overloads = theOverloads;
426        }
427
428        @Override
429        public JexlMethod getOperator(final JexlOperator operator, final Object... args) {
430            return overloads.contains(operator) && args != null
431                   ? getMethod(arithmetic, operator.getMethodName(), args)
432                   : null;
433        }
434
435        @Override
436        public boolean overloads(final JexlOperator operator) {
437            return overloads.contains(operator);
438        }
439    }
440
441    @Override
442    public JexlArithmetic.Uberspect getArithmetic(final JexlArithmetic arithmetic) {
443        JexlArithmetic.Uberspect jau = null;
444        if (arithmetic != null) {
445            final Class<? extends JexlArithmetic> aclass = arithmetic.getClass();
446            Set<JexlOperator> ops = operatorMap.get(aclass);
447            if (ops == null) {
448                ops = EnumSet.noneOf(JexlOperator.class);
449                // deal only with derived classes
450                if (!JexlArithmetic.class.equals(aclass)) {
451                    for (final JexlOperator op : JexlOperator.values()) {
452                        final Method[] methods = getMethods(arithmetic.getClass(), op.getMethodName());
453                        if (methods != null) {
454                            mloop:
455                            for (final Method method : methods) {
456                                final Class<?>[] parms = method.getParameterTypes();
457                                if (parms.length != op.getArity()) {
458                                    continue;
459                                }
460                                // filter method that is an actual overload:
461                                // - not inherited (not declared by base class)
462                                // - nor overridden (not present in base class)
463                                if (!JexlArithmetic.class.equals(method.getDeclaringClass())) {
464                                    try {
465                                        JexlArithmetic.class.getMethod(method.getName(), method.getParameterTypes());
466                                    } catch (final NoSuchMethodException xmethod) {
467                                        // method was not found in JexlArithmetic; this is an operator definition
468                                        ops.add(op);
469                                    }
470                                }
471                            }
472                        }
473                    }
474                }
475                // register this arithmetic class in the operator map
476                operatorMap.put(aclass, ops);
477            }
478            jau = new ArithmeticUberspect(arithmetic, ops);
479        }
480        return jau;
481    }
482}