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     */
017    package org.apache.commons.jexl2.introspection;
018    
019    import java.beans.IntrospectionException;
020    import org.apache.commons.jexl2.internal.Introspector;
021    import java.lang.reflect.Constructor;
022    import java.lang.reflect.Field;
023    import java.lang.reflect.Modifier;
024    import java.lang.reflect.InvocationTargetException;
025    
026    import java.lang.reflect.Method;
027    import java.util.Arrays;
028    import java.util.Enumeration;
029    import java.util.Iterator;
030    import java.util.Map;
031    
032    import org.apache.commons.jexl2.JexlInfo;
033    import org.apache.commons.jexl2.JexlException;
034    import org.apache.commons.jexl2.internal.AbstractExecutor;
035    import org.apache.commons.jexl2.internal.ArrayIterator;
036    import org.apache.commons.jexl2.internal.EnumerationIterator;
037    import org.apache.commons.jexl2.internal.introspection.MethodKey;
038    import 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     */
047    public 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    }