    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.logging.Log;
    020
    021import java.lang.reflect.Constructor;
    022import java.lang.reflect.Field;
    023import java.lang.reflect.Method;
    024
    025import java.util.ArrayList;
    026import java.util.HashMap;
    027import java.util.Iterator;
    028import java.util.List;
    029import java.util.Map;
    030
    031import java.util.concurrent.locks.ReadWriteLock;
    032import java.util.concurrent.locks.ReentrantReadWriteLock;
    033
    034/**
    035 * This basic function of this class is to return a Method object for a
    036 * particular class given the name of a method and the parameters to the method
    037 * in the form of an Object[].
    038 *
    039 * <p>The first time the Introspector sees a class it creates a class method map
    040 * for the class in question.
    041 * Basically the class method map is a Hashtable where Method objects are keyed by the aggregation of
    042 * the method name and the array of parameters classes.
    043 * This mapping is performed for all the public methods of a class and stored.</p>
    044 *
    045 * @since 1.0
    046 */
    047public final class Introspector {
    048    /**
    049     * A Constructor get cache-miss.
    050     */
    051    private static class CacheMiss {
    052        /** The constructor used as cache-miss. */
    053        @SuppressWarnings("unused")
    054        public CacheMiss() {
    055        }
    056    }
    057    /**
    058     * The cache-miss marker for the constructors map.
    059     */
    060    private static final Constructor<?> CTOR_MISS = CacheMiss.class.getConstructors()[0];
    061    /**
    062     * the logger.
    063     */
    064    protected final Log logger;
    065    /**
    066     * The class loader used to solve constructors if needed.
    067     */
    068    private ClassLoader loader;
    069    /**
    070     * The permissions.
    071     */
    072    private final Permissions permissions;
    073    /**
    074     * The read/write lock.
    075     */
    076    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    077    /**
    078     * Holds the method maps for the classes we know about, keyed by Class.
    079     */
    080    private final Map<Class<?>, ClassMap> classMethodMaps = new HashMap<Class<?>, ClassMap>();
    081    /**
    082     * Holds the map of classes ctors we know about as well as unknown ones.
    083     */
    084    private final Map<MethodKey, Constructor<?>> constructorsMap = new HashMap<MethodKey, Constructor<?>>();
    085    /**
    086     * Holds the set of classes we have introspected.
    087     */
    088    private final Map<String, Class<?>> constructibleClasses = new HashMap<String, Class<?>>();
    089
    090    /**
    091     * Create the introspector.
    092     * @param log     the logger to use
    093     * @param cloader the class loader
    094     */
    095    public Introspector(final Log log, final ClassLoader cloader) {
    096        this(log, cloader, null);
    097    }
    098
    099    /**
    100     * Create the introspector.
    101     * @param log     the logger to use
    102     * @param cloader the class loader
    103     * @param perms the permissions
    104     */
    105    public Introspector(final Log log, final ClassLoader cloader, final Permissions perms) {
    106        this.logger = log;
    107        this.loader = cloader;
    108        this.permissions = perms != null? perms : Permissions.DEFAULT;
    109    }
    110
    111    /**
    112     * Gets a class by name through this introspector class loader.
    113     * @param className the class name
    114     * @return the class instance or null if it could not be found
    115     */
    116    public Class<?> getClassByName(final String className) {
    117        try {
    118            return Class.forName(className, false, loader);
    119        } catch (final ClassNotFoundException xignore) {
    120            return null;
    121        }
    122    }
    123
    124    /**
    125     * Gets a method defined by a class, a name and a set of parameters.
    126     * @param c      the class
    127     * @param name   the method name
    128     * @param params the method parameters
    129     * @return the desired method object
    130     * @throws MethodKey.AmbiguousException if no unambiguous method could be found through introspection
    131     */
    132    public Method getMethod(final Class<?> c, final String name, final Object[] params) {
    133        return getMethod(c, new MethodKey(name, params));
    134    }
    135
    136    /**
    137     * Gets the method defined by the <code>MethodKey</code> for the class <code>c</code>.
    138     *
    139     * @param c   Class in which the method search is taking place
    140     * @param key Key of the method being searched for
    141     * @return The desired method object
    142     * @throws MethodKey.AmbiguousException if no unambiguous method could be found through introspection
    143     */
    144    public Method getMethod(final Class<?> c, final MethodKey key) {
    145        try {
    146            return getMap(c).getMethod(key);
    147        } catch (final MethodKey.AmbiguousException xambiguous) {
    148            // whoops. Ambiguous and not benign. Make a nice log message and return null...
    149            if (logger != null && xambiguous.isSevere() && logger.isInfoEnabled()) {
    150                logger.info("ambiguous method invocation: "
    151                        + c.getName() + "."
    152                        + key.debugString(), xambiguous);
    153            }
    154            return null;
    155        }
    156    }
    157
    158    /**
    159     * Gets the field named by <code>key</code> for the class <code>c</code>.
    160     *
    161     * @param c   Class in which the field search is taking place
    162     * @param key Name of the field being searched for
    163     * @return the desired field or null if it does not exist or is not accessible
    164     */
    165    public Field getField(final Class<?> c, final String key) {
    166        return getMap(c).getField(key);
    167    }
    168
    169    /**
    170     * Gets the array of accessible field names known for a given class.
    171     * @param c the class
    172     * @return the class field names
    173     */
    174    public String[] getFieldNames(final Class<?> c) {
    175        if (c == null) {
    176            return new String[0];
    177        }
    178        final ClassMap classMap = getMap(c);
    179        return classMap.getFieldNames();
    180    }
    181
    182    /**
    183     * Gets the array of accessible methods names known for a given class.
    184     * @param c the class
    185     * @return the class method names
    186     */
    187    public String[] getMethodNames(final Class<?> c) {
    188        if (c == null) {
    189            return new String[0];
    190        }
    191        final ClassMap classMap = getMap(c);
    192        return classMap.getMethodNames();
    193    }
    194
    195    /**
    196     * Gets the array of accessible method known for a given class.
    197     * @param c          the class
    198     * @param methodName the method name
    199     * @return the array of methods (null or not empty)
    200     */
    201    public Method[] getMethods(final Class<?> c, final String methodName) {
    202        if (c == null) {
    203            return null;
    204        }
    205        final ClassMap classMap = getMap(c);
    206        return classMap.getMethods(methodName);
    207    }
    208
    209    /**
    210     * Gets the constructor defined by the <code>MethodKey</code>.
    211     *
    212     * @param key Key of the constructor being searched for
    213     * @return The desired constructor object
    214     * or null if no unambiguous constructor could be found through introspection.
    215     */
    216    public Constructor<?> getConstructor(final MethodKey key) {
    217        return getConstructor(null, key);
    218    }
    219
    220    /**
    221     * Gets the constructor defined by the <code>MethodKey</code>.
    222     * @param c   the class we want to instantiate
    223     * @param key Key of the constructor being searched for
    224     * @return The desired constructor object
    225     * or null if no unambiguous constructor could be found through introspection.
    226     */
    227    public Constructor<?> getConstructor(final Class<?> c, final MethodKey key) {
    228        Constructor<?> ctor;
    229        lock.readLock().lock();
    230        try {
    231            ctor = constructorsMap.get(key);
    232            if (ctor != null) {
    233                // miss or not?
    234                return CTOR_MISS.equals(ctor) ? null : ctor;
    235            }
    236        } finally {
    237            lock.readLock().unlock();
    238        }
    239        // let's introspect...
    240        lock.writeLock().lock();
    241        try {
    242            // again for kicks
    243            ctor = constructorsMap.get(key);
    244            if (ctor != null) {
    245                // miss or not?
    246                return CTOR_MISS.equals(ctor) ? null : ctor;
    247            }
    248            final String cname = key.getMethod();
    249            // do we know about this class?
    250            Class<?> clazz = constructibleClasses.get(cname);
    251            try {
    252                // do find the most specific ctor
    253                if (clazz == null) {
    254                    if (c != null && c.getName().equals(key.getMethod())) {
    255                        clazz = c;
    256                    } else {
    257                        clazz = loader.loadClass(cname);
    258                    }
    259                    // add it to list of known loaded classes
    260                    constructibleClasses.put(cname, clazz);
    261                }
    262                final List<Constructor<?>> l = new ArrayList<>();
    263                for (final Constructor<?> ictor : clazz.getConstructors()) {
    264                    if (permissions.allow(ictor)) {
    265                        l.add(ictor);
    266                    }
    267                }
    268                // try to find one
    269                ctor = key.getMostSpecificConstructor(l.toArray(new Constructor<?>[l.size()]));
    270                if (ctor != null) {
    271                    constructorsMap.put(key, ctor);
    272                } else {
    273                    constructorsMap.put(key, CTOR_MISS);
    274                }
    275            } catch (final ClassNotFoundException xnotfound) {
    276                if (logger != null && logger.isDebugEnabled()) {
    277                    logger.debug("unable to find class: "
    278                            + cname + "."
    279                            + key.debugString(), xnotfound);
    280                }
    281                ctor = null;
    282            } catch (final MethodKey.AmbiguousException xambiguous) {
    283                if (logger != null  && xambiguous.isSevere() &&  logger.isInfoEnabled()) {
    284                    logger.info("ambiguous constructor invocation: "
    285                            + cname + "."
    286                            + key.debugString(), xambiguous);
    287                }
    288                ctor = null;
    289            }
    290            return ctor;
    291        } finally {
    292            lock.writeLock().unlock();
    293        }
    294    }
    295
    296    /**
    297     * Gets the ClassMap for a given class.
    298     * @param c the class
    299     * @return the class map
    300     */
    301    private ClassMap getMap(final Class<?> c) {
    302        ClassMap classMap;
    303        lock.readLock().lock();
    304        try {
    305            classMap = classMethodMaps.get(c);
    306        } finally {
    307            lock.readLock().unlock();
    308        }
    309        if (classMap == null) {
    310            lock.writeLock().lock();
    311            try {
    312                // try again
    313                classMap = classMethodMaps.get(c);
    314                if (classMap == null) {
    315                    classMap = new ClassMap(c, permissions, logger);
    316                    classMethodMaps.put(c, classMap);
    317                }
    318            } finally {
    319                lock.writeLock().unlock();
    320            }
    321
    322        }
    323        return classMap;
    324    }
    325
    326    /**
    327     * Sets the class loader used to solve constructors.
    328     * <p>Also cleans the constructors and methods caches.</p>
    329     * @param cloader the class loader; if null, use this instance class loader
    330     */
    331    public void setLoader(ClassLoader cloader) {
    332        final ClassLoader previous = loader;
    333        if (cloader == null) {
    334            cloader = getClass().getClassLoader();
    335        }
    336        if (!cloader.equals(loader)) {
    337            lock.writeLock().lock();
    338            try {
    339                // clean up constructor and class maps
    340                final Iterator<Map.Entry<MethodKey, Constructor<?>>> mentries = constructorsMap.entrySet().iterator();
    341                while (mentries.hasNext()) {
    342                    final Map.Entry<MethodKey, Constructor<?>> entry = mentries.next();
    343                    final Class<?> clazz = entry.getValue().getDeclaringClass();
    344                    if (isLoadedBy(previous, clazz)) {
    345                        mentries.remove();
    346                        // the method name is the name of the class
    347                        constructibleClasses.remove(entry.getKey().getMethod());
    348                    }
    349                }
    350                // clean up method maps
    351                final Iterator<Map.Entry<Class<?>, ClassMap>> centries = classMethodMaps.entrySet().iterator();
    352                while (centries.hasNext()) {
    353                    final Map.Entry<Class<?>, ClassMap> entry = centries.next();
    354                    final Class<?> clazz = entry.getKey();
    355                    if (isLoadedBy(previous, clazz)) {
    356                        centries.remove();
    357                    }
    358                }
    359                loader = cloader;
    360            } finally {
    361                lock.writeLock().unlock();
    362            }
    363        }
    364    }
    365
    366    /**
    367     * Gets the class loader used by this introspector.
    368     * @return the class loader
    369     */
    370    public ClassLoader getLoader() {
    371        return loader;
    372    }
    373
    374    /**
    375     * Checks whether a class is loaded through a given class loader or one of its ascendants.
    376     * @param loader the class loader
    377     * @param clazz  the class to check
    378     * @return true if clazz was loaded through the loader, false otherwise
    379     */
    380    private static boolean isLoadedBy(final ClassLoader loader, final Class<?> clazz) {
    381        if (loader != null) {
    382            ClassLoader cloader = clazz.getClassLoader();
    383            while (cloader != null) {
    384                if (cloader.equals(loader)) {
    385                    return true;
    386                }
    387                cloader = cloader.getParent();
    388            }
    389        }
    390        return false;
    391    }
    392}