PropertySetExecutor.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.jexl3.internal.introspection;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import org.apache.commons.jexl3.JexlException;
/**
* Specialized executor to set a property in an object.
* @since 2.0
*/
public class PropertySetExecutor extends AbstractExecutor.Set {
/** Index of the first character of the set{p,P}roperty. */
private static final int SET_START_INDEX = 3;
/**
* Discovers a PropertySetExecutor.
* <p>The method to be found should be named "set{P,p}property.</p>
*
* @param is the introspector
* @param clazz the class to find the get method from
* @param property the property name to find
* @param value the value to assign to the property
* @return the executor if found, null otherwise
*/
public static PropertySetExecutor discover(final Introspector is,
final Class<?> clazz,
final String property,
final Object value) {
if (property == null || property.isEmpty()) {
return null;
}
final java.lang.reflect.Method method = discoverSet(is, clazz, property, value);
return method != null ? new PropertySetExecutor(clazz, method, property, value) : null;
}
/**
* Discovers the method for a {@link org.apache.commons.jexl3.introspection.JexlPropertySet}.
* <p>The method to be found should be named "set{P,p}property.
* As a special case, any empty array will try to find a valid array-setting non-ambiguous method.
*
* @param is the introspector
* @param clazz the class to find the get method from
* @param property the name of the property to set
* @param arg the value to assign to the property
* @return the method if found, null otherwise
*/
private static java.lang.reflect.Method discoverSet(final Introspector is,
final Class<?> clazz,
final String property,
final Object arg) {
// first, we introspect for the set<identifier> setter method
final Object[] params = {arg};
final StringBuilder sb = new StringBuilder("set");
sb.append(property);
// uppercase nth char
final char c = sb.charAt(SET_START_INDEX);
sb.setCharAt(SET_START_INDEX, Character.toUpperCase(c));
java.lang.reflect.Method method = is.getMethod(clazz, sb.toString(), params);
// lowercase nth char
if (method == null) {
sb.setCharAt(SET_START_INDEX, Character.toLowerCase(c));
method = is.getMethod(clazz, sb.toString(), params);
// uppercase nth char, try array
if (method == null && isEmptyArray(arg)) {
sb.setCharAt(SET_START_INDEX, Character.toUpperCase(c));
method = lookupSetEmptyArray(is, clazz, sb.toString());
// lowercase nth char
if (method == null) {
sb.setCharAt(SET_START_INDEX, Character.toLowerCase(c));
method = lookupSetEmptyArray(is, clazz, sb.toString());
}
}
}
return method;
}
/**
* Checks whether an argument is an empty array.
* @param arg the argument
* @return true if <code>arg</code> is an empty array
*/
private static boolean isEmptyArray(final Object arg) {
return arg != null && arg.getClass().isArray() && Array.getLength(arg) == 0;
}
/**
* Finds an empty array property setter method by <code>methodName</code>.
* <p>This checks only one method with that name accepts an array as sole parameter.
* @param is the introspector
* @param clazz the class to find the get method from
* @param methodName the method name to find
* @return the sole method that accepts an array as parameter
*/
private static java.lang.reflect.Method lookupSetEmptyArray(final Introspector is, final Class<?> clazz, final String methodName) {
java.lang.reflect.Method candidate = null;
final java.lang.reflect.Method[] methods = is.getMethods(clazz, methodName);
if (methods != null) {
for (final java.lang.reflect.Method method : methods) {
final Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length == 1 && paramTypes[0].isArray()) {
if (candidate != null) {
// because the setter method is overloaded for different parameter type,
// return null here to report the ambiguity.
return null;
}
candidate = method;
}
}
}
return candidate;
}
/** The property. */
protected final String property;
/** The property value class. */
protected final Class<?> valueClass;
/**
* Creates an instance.
* @param clazz the class the set method applies to
* @param method the method called through this executor
* @param key the key to use as 1st argument to the set method
* @param value the value
*/
protected PropertySetExecutor(final Class<?> clazz,
final java.lang.reflect.Method method,
final String key,
final Object value) {
super(clazz, method);
property = key;
valueClass = classOf(value);
}
@Override
public Object getTargetProperty() {
return property;
}
@Override
public Object invoke(final Object o, final Object argument) throws IllegalAccessException, InvocationTargetException {
Object arg = argument;
if (method != null) {
// handle the empty array case
if (isEmptyArray(arg)) {
// if array is empty but its component type is different from the method first parameter component type,
// replace argument with a new empty array instance (of the method first parameter component type)
final Class<?> componentType = method.getParameterTypes()[0].getComponentType();
if (componentType != null && !componentType.equals(arg.getClass().getComponentType())) {
arg = Array.newInstance(componentType, 0);
}
}
method.invoke(o, arg);
}
return arg;
}
@Override
public Object tryInvoke(final Object o, final Object identifier, final Object value) {
if (o != null && method != null
// ensure method name matches the property name
&& property.equals(castString(identifier))
// object class should be same as executor's method declaring class
&& objectClass.equals(o.getClass())
// argument class should be eq
&& valueClass.equals(classOf(value))) {
try {
return invoke(o, value);
} catch (IllegalAccessException | IllegalArgumentException xill) {
return TRY_FAILED; // fail
} catch (final InvocationTargetException xinvoke) {
throw JexlException.tryFailed(xinvoke); // throw
}
}
return TRY_FAILED;
}
}