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.introspection;
018
019import java.beans.IntrospectionException;
020import org.apache.commons.jexl2.internal.Introspector;
021import java.lang.reflect.Constructor;
022import java.lang.reflect.Field;
023import java.lang.reflect.Modifier;
024import java.lang.reflect.InvocationTargetException;
025
026import java.lang.reflect.Method;
027import java.util.Arrays;
028import java.util.Enumeration;
029import java.util.Iterator;
030import java.util.Map;
031
032import org.apache.commons.jexl2.JexlInfo;
033import org.apache.commons.jexl2.JexlException;
034import org.apache.commons.jexl2.internal.AbstractExecutor;
035import org.apache.commons.jexl2.internal.ArrayIterator;
036import org.apache.commons.jexl2.internal.EnumerationIterator;
037import org.apache.commons.jexl2.internal.introspection.MethodKey;
038import org.apache.commons.logging.Log;
039
040/**
041 * Implementation of Uberspect to provide the default introspective
042 * functionality of JEXL.
043 * <p>This is the class to derive to customize introspection.</p>
044 *
045 * @since 1.0
046 */
047public class UberspectImpl extends Introspector implements Uberspect {
048    /**
049     * Publicly exposed special failure object returned by tryInvoke.
050     */
051    public static final Object TRY_FAILED = AbstractExecutor.TRY_FAILED;
052
053    /**
054     * Creates a new UberspectImpl.
055     * @param runtimeLogger the logger used for all logging needs
056     */
057    public UberspectImpl(Log runtimeLogger) {
058        super(runtimeLogger);
059    }
060
061    /**
062     * Resets this Uberspect class loader.
063     * @param cloader the class loader to use
064     * @since 2.1
065     */
066    public void setLoader(ClassLoader cloader) {
067        base().setLoader(cloader);
068    }
069
070    /**
071     * {@inheritDoc}
072     */
073    @SuppressWarnings("unchecked")
074    public Iterator<?> getIterator(Object obj, JexlInfo info) {
075        if (obj instanceof Iterator<?>) {
076            return ((Iterator<?>) obj);
077        }
078        if (obj.getClass().isArray()) {
079            return new ArrayIterator(obj);
080        }
081        if (obj instanceof Map<?, ?>) {
082            return ((Map<?, ?>) obj).values().iterator();
083        }
084        if (obj instanceof Enumeration<?>) {
085            return new EnumerationIterator<Object>((Enumeration<Object>) obj);
086        }
087        if (obj instanceof Iterable<?>) {
088            return ((Iterable<?>) obj).iterator();
089        }
090        try {
091            // look for an iterator() method to support the JDK5 Iterable
092            // interface or any user tools/DTOs that want to work in
093            // foreach without implementing the Collection interface
094            AbstractExecutor.Method it = getMethodExecutor(obj, "iterator", null);
095            if (it != null && Iterator.class.isAssignableFrom(it.getReturnType())) {
096                return (Iterator<Object>) it.execute(obj, null);
097            }
098        } catch (Exception xany) {
099            throw new JexlException(info, "unable to generate iterator()", xany);
100        }
101        return null;
102    }
103
104    /**
105     * {@inheritDoc}
106     */
107    public JexlMethod getMethod(Object obj, String method, Object[] args, JexlInfo info) {
108        return getMethodExecutor(obj, method, args);
109    }
110
111    /**
112     * {@inheritDoc}
113     */
114    @Deprecated
115    public Constructor<?> getConstructor(Object ctorHandle, Object[] args, JexlInfo info) {
116        return getConstructor(ctorHandle, args);
117    }
118    
119    /**
120     * {@inheritDoc}
121     * @since 2.1
122     */
123    public JexlMethod getConstructorMethod(Object ctorHandle, Object[] args, JexlInfo info) {
124        final Constructor<?> ctor = getConstructor(ctorHandle, args);
125        if (ctor != null) {
126            return new ConstructorMethod(ctor);
127        } else {
128            return null;
129        }
130    }
131
132    /**
133     * {@inheritDoc}
134     */
135    public JexlPropertyGet getPropertyGet(Object obj, Object identifier, JexlInfo info) {
136        JexlPropertyGet get = getGetExecutor(obj, identifier);
137        if (get == null && obj != null && identifier != null) {
138            get = getIndexedGet(obj, identifier.toString());
139            if (get == null) {
140                Field field = getField(obj, identifier.toString(), info);
141                if (field != null) {
142                    return new FieldPropertyGet(field);
143                }
144            }
145        }
146        return get;
147    }
148
149    /**
150     * {@inheritDoc}
151     */
152    public JexlPropertySet getPropertySet(final Object obj, final Object identifier, Object arg, JexlInfo info) {
153        JexlPropertySet set = getSetExecutor(obj, identifier, arg);
154        if (set == null && obj != null && identifier != null) {
155            Field field = getField(obj, identifier.toString(), info);
156            if (field != null
157                    && !Modifier.isFinal(field.getModifiers())
158                    && (arg == null || MethodKey.isInvocationConvertible(field.getType(), arg.getClass(), false))) {
159                return new FieldPropertySet(field);
160            }
161        }
162        return set;
163    }
164
165    /**
166     * Returns a class field.
167     * Only for use by sub-classes, will be made protected in a later version
168     * @param obj the object
169     * @param name the field name
170     * @param info debug info
171     * @return a {@link Field}.
172     */
173    public Field getField(Object obj, String name, JexlInfo info) {
174        final Class<?> clazz = obj instanceof Class<?> ? (Class<?>) obj : obj.getClass();
175        return getField(clazz, name);
176    }
177
178    /**
179     * Attempts to find an indexed-property getter in an object.
180     * The code attempts to find the list of methods getXXX() and setXXX().
181     * Note that this is not equivalent to the strict bean definition of indexed properties; the type of the key
182     * is not necessarily an int and the set/get arrays are not resolved.
183     * @param object the object
184     * @param name the container name
185     * @return a JexlPropertyGet is successfull, null otherwise
186     * @since 2.1
187     */
188    protected JexlPropertyGet getIndexedGet(Object object, String name) {
189        if (object != null && name != null) {
190            String base = name.substring(0, 1).toUpperCase() + name.substring(1);
191            final String container = name;
192            final Class<?> clazz = object.getClass();
193            final Method[] getters = getMethods(object.getClass(), "get" + base);
194            final Method[] setters = getMethods(object.getClass(), "set" + base);
195            if (getters != null) {
196                return new IndexedType(container, clazz, getters, setters);
197            }
198        }
199        return null;
200    }
201
202    /**
203     * Abstract an indexed property container.
204     * This stores the container name and owning class as well as the list of available getter and setter methods.
205     * It implements JexlPropertyGet since such a container can only be accessed from its owning instance (not set).
206     * @since 2.1
207     */
208    private static final class IndexedType implements JexlPropertyGet {
209        /** The container name. */
210        private final String container;
211        /** The owning class. */
212        private final Class<?> clazz;
213        /** The array of getter methods. */
214        private final Method[] getters;
215        /** The array of setter methods. */
216        private final Method[] setters;
217
218        /**
219         * Creates a new indexed type.
220         * @param name the container name
221         * @param c the owning class
222         * @param gets the array of getter methods
223         * @param sets the array of setter methods
224         */
225        IndexedType(String name, Class<?> c, Method[] gets, Method[] sets) {
226            this.container = name;
227            this.clazz = c;
228            this.getters = gets;
229            this.setters = sets;
230        }
231
232        /**
233         * {@inheritDoc}
234         */
235        public Object invoke(Object obj) throws Exception {
236            if (obj != null && clazz.equals(obj.getClass())) {
237                return new IndexedContainer(this, obj);
238            } else {
239                throw new IntrospectionException("property resolution error");
240            }
241        }
242
243        /**
244         * {@inheritDoc}
245         */
246        public Object tryInvoke(Object obj, Object key) {
247            if (obj != null && key != null && clazz.equals(obj.getClass()) && container.equals(key.toString())) {
248                return new IndexedContainer(this, obj);
249            } else {
250                return TRY_FAILED;
251            }
252        }
253
254        /**
255         * {@inheritDoc}
256         */
257        public boolean tryFailed(Object rval) {
258            return rval == TRY_FAILED;
259        }
260
261        /**
262         * {@inheritDoc}
263         */
264        public boolean isCacheable() {
265            return true;
266        }
267
268        /**
269         * Gets the value of a property from a container.
270         * @param object the instance owning the container (not null)
271         * @param key the property key (not null)
272         * @return the property value
273         * @throws Exception if invocation failed; IntrospectionException if a property getter could not be found
274         */
275        private Object invokeGet(Object object, Object key) throws Exception {
276            if (getters != null) {
277                final Object[] args = {key};
278                final Method jm;
279                if (getters.length == 1) {
280                    jm = getters[0];
281                } else {
282                    jm = new MethodKey(getters[0].getName(), args).getMostSpecificMethod(Arrays.asList(getters));
283                }
284                if (jm != null) {
285                    return jm.invoke(object, args);
286                }
287            }
288            throw new IntrospectionException("property get error: "
289                    + object.getClass().toString() + "@" + key.toString());
290        }
291
292        /**
293         * Sets the value of a property in a container.
294         * @param object the instance owning the container (not null)
295         * @param key the property key (not null)
296         * @param value the property value (not null)
297         * @return the result of the method invocation (frequently null)
298         * @throws Exception if invocation failed; IntrospectionException if a property setter could not be found
299         */
300        private Object invokeSet(Object object, Object key, Object value) throws Exception {
301            if (setters != null) {
302                final Object[] args = {key, value};
303                final Method jm;
304                if (setters.length == 1) {
305                    jm = setters[0];
306                } else {
307                    jm = new MethodKey(setters[0].getName(), args).getMostSpecificMethod(Arrays.asList(setters));
308                }
309                if (jm != null) {
310                    return jm.invoke(object, args);
311                }
312            }
313            throw new IntrospectionException("property set error: "
314                    + object.getClass().toString() + "@" + key.toString());
315        }
316    }
317
318    /**
319     * A generic indexed property container, exposes get(key) and set(key, value) and solves method call dynamically
320     * based on arguments.
321     * @since 2.1
322     */
323    public static final class IndexedContainer {
324        /** The instance owning the container. */
325        private final Object object;
326        /** The container type instance. */
327        private final IndexedType type;
328
329        /**
330         * Creates a new duck container.
331         * @param theType the container type
332         * @param theObject the instance owning the container
333         */
334        private IndexedContainer(IndexedType theType, Object theObject) {
335            this.type = theType;
336            this.object = theObject;
337        }
338
339        /**
340         * Gets a property from a container.
341         * @param key the property key
342         * @return the property value
343         * @throws Exception if inner invocation fails
344         */
345        public Object get(Object key) throws Exception {
346            return type.invokeGet(object, key);
347        }
348
349        /**
350         * Sets a property in a container.
351         * @param key the property key
352         * @param value the property value
353         * @return the invocation result (frequently null)
354         * @throws Exception if inner invocation fails
355         */
356        public Object set(Object key, Object value) throws Exception {
357            return type.invokeSet(object, key, value);
358        }
359    }
360
361    /**
362     * A JexlMethod that wraps constructor.
363     * @since 2.1
364     */
365    private final class ConstructorMethod implements JexlMethod {
366        /** The wrapped constructor. */
367        private final Constructor<?> ctor;
368
369        /**
370         * Creates a constructor method.
371         * @param theCtor the constructor to wrap
372         */
373        private ConstructorMethod(Constructor<?> theCtor) {
374            this.ctor = theCtor;
375        }
376
377        /**
378         * {@inheritDoc}
379         */
380        public Object invoke(Object obj, Object[] params) throws Exception {
381            Class<?> clazz = null;
382            if (obj instanceof Class<?>) {
383                clazz = (Class<?>) obj;
384            } else if (obj != null) {
385                clazz = getClassByName(obj.toString());
386            } else {
387                clazz = ctor.getDeclaringClass();
388            }
389            if (clazz.equals(ctor.getDeclaringClass())) {
390                return ctor.newInstance(params);
391            } else {
392                throw new IntrospectionException("constructor resolution error");
393            }
394        }
395
396        /**
397         * {@inheritDoc}
398         */
399        public Object tryInvoke(String name, Object obj, Object[] params) {
400            Class<?> clazz = null;
401            if (obj instanceof Class<?>) {
402                clazz = (Class<?>) obj;
403            } else if (obj != null) {
404                clazz = getClassByName(obj.toString());
405            } else {
406                clazz = ctor.getDeclaringClass();
407            }
408            if (clazz.equals(ctor.getDeclaringClass())
409                    && (name == null || name.equals(clazz.getName()))) {
410                try {
411                    return ctor.newInstance(params);
412                } catch (InstantiationException xinstance) {
413                    return TRY_FAILED;
414                } catch (IllegalAccessException xaccess) {
415                    return TRY_FAILED;
416                } catch (IllegalArgumentException xargument) {
417                    return TRY_FAILED;
418                } catch (InvocationTargetException xinvoke) {
419                    return TRY_FAILED;
420                }
421            }
422            return TRY_FAILED;
423        }
424
425        /**
426         * {@inheritDoc}
427         */
428        public boolean tryFailed(Object rval) {
429            return rval == TRY_FAILED;
430        }
431
432        /**
433         * {@inheritDoc}
434         */
435        public boolean isCacheable() {
436            return true;
437        }
438
439        /**
440         * {@inheritDoc}
441         */
442        public Class<?> getReturnType() {
443            return ctor.getDeclaringClass();
444        }
445    }
446
447    /**
448     * A JexlPropertyGet for public fields.
449     * @deprecated Do not use externally - will be made private in a later version
450     */
451    @Deprecated
452    public static final class FieldPropertyGet implements JexlPropertyGet {
453        /**
454         * The public field.
455         */
456        private final Field field;
457
458        /**
459         * Creates a new instance of FieldPropertyGet.
460         * @param theField the class public field
461         */
462        public FieldPropertyGet(Field theField) {
463            field = theField;
464        }
465
466        /**
467         * {@inheritDoc}
468         */
469        public Object invoke(Object obj) throws Exception {
470            return field.get(obj);
471        }
472
473        /**
474         * {@inheritDoc}
475         */
476        public Object tryInvoke(Object obj, Object key) {
477            if (obj.getClass().equals(field.getDeclaringClass()) && key.equals(field.getName())) {
478                try {
479                    return field.get(obj);
480                } catch (IllegalAccessException xill) {
481                    return TRY_FAILED;
482                }
483            }
484            return TRY_FAILED;
485        }
486
487        /**
488         * {@inheritDoc}
489         */
490        public boolean tryFailed(Object rval) {
491            return rval == TRY_FAILED;
492        }
493
494        /**
495         * {@inheritDoc}
496         */
497        public boolean isCacheable() {
498            return true;
499        }
500    }
501
502    /**
503     * A JexlPropertySet for public fields.
504    * @deprecated Do not use externally - will be made private in a later version
505     */
506    @Deprecated
507    public static final class FieldPropertySet implements JexlPropertySet {
508        /**
509         * The public field.
510         */
511        private final Field field;
512
513        /**
514         * Creates a new instance of FieldPropertySet.
515         * @param theField the class public field
516         */
517        public FieldPropertySet(Field theField) {
518            field = theField;
519        }
520
521        /**
522         * {@inheritDoc}
523         */
524        public Object invoke(Object obj, Object arg) throws Exception {
525            field.set(obj, arg);
526            return arg;
527        }
528
529        /**
530         * {@inheritDoc}
531         */
532        public Object tryInvoke(Object obj, Object key, Object value) {
533            if (obj.getClass().equals(field.getDeclaringClass())
534                    && key.equals(field.getName())
535                    && (value == null || MethodKey.isInvocationConvertible(field.getType(), value.getClass(), false))) {
536                try {
537                    field.set(obj, value);
538                    return value;
539                } catch (IllegalAccessException xill) {
540                    return TRY_FAILED;
541                }
542            }
543            return TRY_FAILED;
544        }
545
546        /**
547         * {@inheritDoc}
548         */
549        public boolean tryFailed(Object rval) {
550            return rval == TRY_FAILED;
551        }
552
553        /**
554         * {@inheritDoc}
555         */
556        public boolean isCacheable() {
557            return true;
558        }
559    }
560}