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}