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