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
018package org.apache.commons.jexl3.internal.introspection;
019
020
021import org.apache.commons.jexl3.introspection.JexlPropertyGet;
022import java.lang.reflect.Method;
023import java.beans.IntrospectionException;
024
025/**
026 * Abstract an indexed property container.
027 * <p>This allows getting properties from expressions like <code>var.container.property</code>.
028 * This stores the container name and class as well as the list of available getter and setter methods.
029 * It implements JexlPropertyGet since such a container can only be accessed from its owning instance (not set).
030 */
031public final class IndexedType implements JexlPropertyGet {
032     /** The container name. */
033    private final String container;
034    /** The container class. */
035    private final Class<?> clazz;
036    /** The array of getter methods. */
037    private final Method[] getters;
038    /** Last get method used. */
039    private volatile Method get = null;
040    /** The array of setter methods. */
041    private final Method[] setters;
042    /** Last set method used. */
043    private volatile Method set = null;
044
045    /**
046     * Attempts to find an indexed-property getter in an object.
047     * The code attempts to find the list of methods getXXX() and setXXX().
048     * Note that this is not equivalent to the strict bean definition of indexed properties; the type of the key
049     * is not necessarily an int and the set/get arrays are not resolved.
050     *
051     * @param is the introspector
052     * @param object the object
053     * @param name the container name
054     * @return a JexlPropertyGet is successful, null otherwise
055     */
056    public static JexlPropertyGet discover(final Introspector is, final Object object, final String name) {
057        if (object != null && name != null && !name.isEmpty()) {
058            final String base = name.substring(0, 1).toUpperCase() + name.substring(1);
059            final String container = name;
060            final Class<?> clazz = object.getClass();
061            final Method[] getters = is.getMethods(object.getClass(), "get" + base);
062            final Method[] setters = is.getMethods(object.getClass(), "set" + base);
063            if (getters != null) {
064                return new IndexedType(container, clazz, getters, setters);
065            }
066        }
067        return null;
068    }
069
070    /**
071     * A generic indexed property container, exposes get(key) and set(key, value)
072     * and solves method call dynamically based on arguments.
073     * <p>Must remain public for introspection purpose.</p>
074     */
075    public static final class IndexedContainer {
076        /** The container instance. */
077        private final Object container;
078        /** The container type instance. */
079        private final IndexedType type;
080
081        /**
082         * Creates a new duck container.
083         * @param theType the container type
084         * @param theContainer the container instance
085         */
086        private IndexedContainer(final IndexedType theType, final Object theContainer) {
087            this.type = theType;
088            this.container = theContainer;
089        }
090
091        /**
092         * Gets the property container name.
093         * @return the container name
094         */
095        public String getContainerName() {
096            return type.container;
097        }
098
099        /**
100         * Gets the property container class.
101         * @return the container class
102         */
103        public Class<?> getContainerClass() {
104            return type.clazz;
105        }
106
107        /**
108         * Gets a property from this indexed container.
109         * @param key the property key
110         * @return the property value
111         * @throws Exception if inner invocation fails
112         */
113        public Object get(final Object key) throws Exception {
114            return type.invokeGet(container, key);
115        }
116
117        /**
118         * Sets a property in this indexed container.
119         * @param key the property key
120         * @param value the property value
121         * @return the invocation result (frequently null)
122         * @throws Exception if inner invocation fails
123         */
124        public Object set(final Object key, final Object value) throws Exception {
125            return type.invokeSet(container, key, value);
126        }
127    }
128
129    /**
130     * Creates a new indexed property container type.
131     * @param name the container name
132     * @param c the owning class
133     * @param gets the array of getter methods
134     * @param sets the array of setter methods
135     */
136    private IndexedType(final String name, final Class<?> c, final Method[] gets, final Method[] sets) {
137        this.container = name;
138        this.clazz = c;
139        this.getters = gets;
140        this.setters = sets;
141    }
142
143    @Override
144    public Object invoke(final Object obj) throws Exception {
145        if (obj != null && clazz.equals(obj.getClass())) {
146            return new IndexedContainer(this, obj);
147        }
148        throw new IntrospectionException("property resolution error");
149    }
150
151    @Override
152    public Object tryInvoke(final Object obj, final Object key) {
153        if (obj != null && key != null
154            && clazz.equals(obj.getClass())
155            && container.equals(key.toString())) {
156            return new IndexedContainer(this, obj);
157        }
158        return Uberspect.TRY_FAILED;
159    }
160
161    @Override
162    public boolean tryFailed(final Object rval) {
163        return rval == Uberspect.TRY_FAILED;
164    }
165
166    @Override
167    public boolean isCacheable() {
168        return true;
169    }
170
171    /**
172     * Gets the value of a property from a container.
173     * @param object the container instance (not null)
174     * @param key the property key (not null)
175     * @return the property value
176     * @throws Exception if invocation failed;
177     *         IntrospectionException if a property getter could not be found
178     */
179    private Object invokeGet(final Object object, final Object key) throws Exception {
180        if (getters != null && getters.length > 0) {
181            Method jm = get;
182            if (jm != null) {
183                final Class<?>[] ptypes = jm.getParameterTypes();
184                if (ptypes[0].isAssignableFrom(key.getClass())) {
185                    return jm.invoke(object, key);
186                }
187            }
188            final Object[] args = {key};
189            final String mname = getters[0].getName();
190            final MethodKey km = new MethodKey(mname, args);
191            jm = km.getMostSpecificMethod(getters);
192            if (jm != null) {
193                final Object invoked = jm.invoke(object, args);
194                get = jm;
195                return invoked;
196            }
197        }
198        throw new IntrospectionException("property get error: "
199                + object.getClass().toString()
200                + "@" + key.toString());
201    }
202
203    /**
204     * Sets the value of a property in a container.
205     * @param object the container instance (not null)
206     * @param key the property key (not null)
207     * @param value the property value (not null)
208     * @return the result of the method invocation (frequently null)
209     * @throws Exception if invocation failed;
210     *         IntrospectionException if a property setter could not be found
211     */
212    private Object invokeSet(final Object object, final Object key, final Object value) throws Exception {
213        if (setters != null && setters.length > 0) {
214            Method jm = set;
215            if (jm != null) {
216                final Class<?>[] ptypes = jm.getParameterTypes();
217                if (ptypes[0].isAssignableFrom(key.getClass())
218                    && (value == null
219                        || ptypes[1].isAssignableFrom(value.getClass()))) {
220                    return jm.invoke(object, key, value);
221                }
222            }
223            final Object[] args = {key, value};
224            final String mname = setters[0].getName();
225            final MethodKey km = new MethodKey(mname, args);
226            jm = km.getMostSpecificMethod(setters);
227            if (jm != null) {
228                final Object invoked = jm.invoke(object, args);
229                set = jm;
230                return invoked;
231            }
232        }
233        throw new IntrospectionException("property set error: "
234                + object.getClass().toString()
235                + "@" + key.toString());
236    }
237
238}