    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}