    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}