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 java.lang.reflect.Array;
020import java.lang.reflect.InvocationTargetException;
021import org.apache.commons.jexl3.JexlException;
022
023/**
024 * Specialized executor to set a property in an object.
025 * @since 2.0
026 */
027public class PropertySetExecutor extends AbstractExecutor.Set {
028    /** Index of the first character of the set{p,P}roperty. */
029    private static final int SET_START_INDEX = 3;
030    /** The property. */
031    protected final String property;
032    /** The property value class. */
033    protected final Class<?> valueClass;
034
035    /**
036     * Discovers a PropertySetExecutor.
037     * <p>The method to be found should be named "set{P,p}property.</p>
038     *
039     * @param is       the introspector
040     * @param clazz    the class to find the get method from
041     * @param property the property name to find
042     * @param value      the value to assign to the property
043     * @return the executor if found, null otherwise
044     */
045    public static PropertySetExecutor discover(final Introspector is,
046                                               final Class<?> clazz,
047                                               final String property,
048                                               final Object value) {
049        if (property == null || property.isEmpty()) {
050            return null;
051        }
052        final java.lang.reflect.Method method = discoverSet(is, clazz, property, value);
053        return method != null? new PropertySetExecutor(clazz, method, property, value) : null;
054    }
055
056    /**
057     * Creates an instance.
058     * @param clazz  the class the set method applies to
059     * @param method the method called through this executor
060     * @param key    the key to use as 1st argument to the set method
061     * @param value    the value
062     */
063    protected PropertySetExecutor(final Class<?> clazz,
064                                  final java.lang.reflect.Method method,
065                                  final String key,
066                                  final Object value) {
067        super(clazz, method);
068        property = key;
069        valueClass = classOf(value);
070    }
071
072    @Override
073    public Object getTargetProperty() {
074        return property;
075    }
076
077    @Override
078    public Object invoke(final Object o, Object arg) throws IllegalAccessException, InvocationTargetException {
079        if (method != null) {
080            // handle the empty array case
081            if (isEmptyArray(arg)) {
082                // if array is empty but its component type is different from the method first parameter component type,
083                // replace argument with a new empty array instance (of the method first parameter component type)
084                final Class<?> componentType = method.getParameterTypes()[0].getComponentType();
085                if (componentType != null && !componentType.equals(arg.getClass().getComponentType())) {
086                    arg = Array.newInstance(componentType, 0);
087                }
088            }
089                method.invoke(o, arg);
090            }
091        return arg;
092    }
093
094    @Override
095    public Object tryInvoke(final Object o, final Object identifier, final Object value) {
096        if (o != null && method != null
097            // ensure method name matches the property name
098            && property.equals(castString(identifier))
099            // object class should be same as executor's method declaring class
100            && objectClass.equals(o.getClass())
101            // argument class should be eq
102            && valueClass.equals(classOf(value))) {
103            try {
104                return invoke(o, value);
105            } catch (IllegalAccessException | IllegalArgumentException xill) {
106                return TRY_FAILED;// fail
107            } catch (final InvocationTargetException xinvoke) {
108                throw JexlException.tryFailed(xinvoke); // throw
109            }
110        }
111        return TRY_FAILED;
112    }
113
114    /**
115     * Checks whether an argument is an empty array.
116     * @param arg the argument
117     * @return true if <code>arg</code> is an empty array
118     */
119    private static boolean isEmptyArray(final Object arg) {
120        return (arg != null && arg.getClass().isArray() && Array.getLength(arg) == 0);
121    }
122
123    /**
124     * Discovers the method for a {@link org.apache.commons.jexl3.introspection.JexlPropertySet}.
125     * <p>The method to be found should be named "set{P,p}property.
126     * As a special case, any empty array will try to find a valid array-setting non-ambiguous method.
127     *
128     * @param is       the introspector
129     * @param clazz    the class to find the get method from
130     * @param property the name of the property to set
131     * @param arg      the value to assign to the property
132     * @return the method if found, null otherwise
133     */
134    private static java.lang.reflect.Method discoverSet(final Introspector is,
135                                                        final Class<?> clazz,
136                                                        final String property,
137                                                        final Object arg) {
138        // first, we introspect for the set<identifier> setter method
139        final Object[] params = {arg};
140        final StringBuilder sb = new StringBuilder("set");
141        sb.append(property);
142        // uppercase nth char
143        final char c = sb.charAt(SET_START_INDEX);
144        sb.setCharAt(SET_START_INDEX, Character.toUpperCase(c));
145        java.lang.reflect.Method method = is.getMethod(clazz, sb.toString(), params);
146        // lowercase nth char
147        if (method == null) {
148            sb.setCharAt(SET_START_INDEX, Character.toLowerCase(c));
149            method = is.getMethod(clazz, sb.toString(), params);
150            // uppercase nth char, try array
151            if (method == null && isEmptyArray(arg)) {
152                sb.setCharAt(SET_START_INDEX, Character.toUpperCase(c));
153                method = lookupSetEmptyArray(is, clazz, sb.toString());
154                // lowercase nth char
155                if (method == null) {
156                    sb.setCharAt(SET_START_INDEX, Character.toLowerCase(c));
157                    method = lookupSetEmptyArray(is, clazz, sb.toString());
158                }
159            }
160        }
161        return method;
162    }
163
164    /**
165     * Finds an empty array property setter method by <code>methodName</code>.
166     * <p>This checks only one method with that name accepts an array as sole parameter.
167     * @param is       the introspector
168     * @param clazz    the class to find the get method from
169     * @param mname    the method name to find
170     * @return         the sole method that accepts an array as parameter
171     */
172    private static java.lang.reflect.Method lookupSetEmptyArray(final Introspector is, final Class<?> clazz, final String mname) {
173        java.lang.reflect.Method candidate = null;
174        final java.lang.reflect.Method[] methods = is.getMethods(clazz, mname);
175        if (methods != null) {
176            for (final java.lang.reflect.Method method : methods) {
177                final Class<?>[] paramTypes = method.getParameterTypes();
178                if (paramTypes.length == 1 && paramTypes[0].isArray()) {
179                    if (candidate != null) {
180                        // because the setter method is overloaded for different parameter type,
181                        // return null here to report the ambiguity.
182                        return null;
183                    }
184                    candidate = method;
185                }
186            }
187        }
188        return candidate;
189    }
190}