1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.jexl3.internal.introspection;
18
19 import java.lang.reflect.Array;
20 import java.lang.reflect.InvocationTargetException;
21
22 import org.apache.commons.jexl3.JexlException;
23
24 /**
25 * Specialized executor to set a property in an object.
26 * @since 2.0
27 */
28 public class PropertySetExecutor extends AbstractExecutor.Set {
29 /** Index of the first character of the set{p,P}roperty. */
30 private static final int SET_START_INDEX = 3;
31 /**
32 * Discovers a PropertySetExecutor.
33 * <p>The method to be found should be named "set{P,p}property.</p>
34 *
35 * @param is the introspector
36 * @param clazz the class to find the get method from
37 * @param property the property name to find
38 * @param value the value to assign to the property
39 * @return the executor if found, null otherwise
40 */
41 public static PropertySetExecutor discover(final Introspector is,
42 final Class<?> clazz,
43 final String property,
44 final Object value) {
45 if (property == null || property.isEmpty()) {
46 return null;
47 }
48 final java.lang.reflect.Method method = discoverSet(is, clazz, property, value);
49 return method != null ? new PropertySetExecutor(clazz, method, property, value) : null;
50 }
51 /**
52 * Discovers the method for a {@link org.apache.commons.jexl3.introspection.JexlPropertySet}.
53 * <p>The method to be found should be named "set{P,p}property.
54 * As a special case, any empty array will try to find a valid array-setting non-ambiguous method.
55 *
56 * @param is the introspector
57 * @param clazz the class to find the get method from
58 * @param property the name of the property to set
59 * @param arg the value to assign to the property
60 * @return the method if found, null otherwise
61 */
62 private static java.lang.reflect.Method discoverSet(final Introspector is,
63 final Class<?> clazz,
64 final String property,
65 final Object arg) {
66 // first, we introspect for the set<identifier> setter method
67 final Object[] params = {arg};
68 final StringBuilder sb = new StringBuilder("set");
69 sb.append(property);
70 // uppercase nth char
71 final char c = sb.charAt(SET_START_INDEX);
72 sb.setCharAt(SET_START_INDEX, Character.toUpperCase(c));
73 java.lang.reflect.Method method = is.getMethod(clazz, sb.toString(), params);
74 // lowercase nth char
75 if (method == null) {
76 sb.setCharAt(SET_START_INDEX, Character.toLowerCase(c));
77 method = is.getMethod(clazz, sb.toString(), params);
78 // uppercase nth char, try array
79 if (method == null && isEmptyArray(arg)) {
80 sb.setCharAt(SET_START_INDEX, Character.toUpperCase(c));
81 method = lookupSetEmptyArray(is, clazz, sb.toString());
82 // lowercase nth char
83 if (method == null) {
84 sb.setCharAt(SET_START_INDEX, Character.toLowerCase(c));
85 method = lookupSetEmptyArray(is, clazz, sb.toString());
86 }
87 }
88 }
89 return method;
90 }
91
92 /**
93 * Checks whether an argument is an empty array.
94 * @param arg the argument
95 * @return true if {@code arg} is an empty array
96 */
97 private static boolean isEmptyArray(final Object arg) {
98 return arg != null && arg.getClass().isArray() && Array.getLength(arg) == 0;
99 }
100
101 /**
102 * Finds an empty array property setter method by {@code methodName}.
103 * <p>This checks only one method with that name accepts an array as sole parameter.
104 * @param is the introspector
105 * @param clazz the class to find the get method from
106 * @param methodName the method name to find
107 * @return the sole method that accepts an array as parameter
108 */
109 private static java.lang.reflect.Method lookupSetEmptyArray(final Introspector is, final Class<?> clazz, final String methodName) {
110 java.lang.reflect.Method candidate = null;
111 final java.lang.reflect.Method[] methods = is.getMethods(clazz, methodName);
112 if (methods != null) {
113 for (final java.lang.reflect.Method method : methods) {
114 final Class<?>[] paramTypes = method.getParameterTypes();
115 if (paramTypes.length == 1 && paramTypes[0].isArray()) {
116 if (candidate != null) {
117 // because the setter method is overloaded for different parameter type,
118 // return null here to report the ambiguity.
119 return null;
120 }
121 candidate = method;
122 }
123 }
124 }
125 return candidate;
126 }
127
128 /** The property. */
129 protected final String property;
130
131 /** The property value class. */
132 protected final Class<?> valueClass;
133
134 /**
135 * Creates an instance.
136 * @param clazz the class the set method applies to
137 * @param method the method called through this executor
138 * @param key the key to use as 1st argument to the set method
139 * @param value the value
140 */
141 protected PropertySetExecutor(final Class<?> clazz,
142 final java.lang.reflect.Method method,
143 final String key,
144 final Object value) {
145 super(clazz, method);
146 property = key;
147 valueClass = classOf(value);
148 }
149
150 @Override
151 public Object getTargetProperty() {
152 return property;
153 }
154
155 @Override
156 public Object invoke(final Object o, final Object argument) throws IllegalAccessException, InvocationTargetException {
157 Object arg = argument;
158 if (method != null) {
159 // handle the empty array case
160 if (isEmptyArray(arg)) {
161 // if array is empty but its component type is different from the method first parameter component type,
162 // replace argument with a new empty array instance (of the method first parameter component type)
163 final Class<?> componentType = method.getParameterTypes()[0].getComponentType();
164 if (componentType != null && !componentType.equals(arg.getClass().getComponentType())) {
165 arg = Array.newInstance(componentType, 0);
166 }
167 }
168 method.invoke(o, arg);
169 }
170 return arg;
171 }
172
173 @Override
174 public Object tryInvoke(final Object o, final Object identifier, final Object value) {
175 if (o != null && method != null
176 // ensure method name matches the property name
177 && property.equals(castString(identifier))
178 // object class should be same as executor's method declaring class
179 && objectClass.equals(o.getClass())
180 // argument class should be eq
181 && valueClass.equals(classOf(value))) {
182 try {
183 return invoke(o, value);
184 } catch (IllegalAccessException | IllegalArgumentException xill) {
185 return TRY_FAILED; // fail
186 } catch (final InvocationTargetException xinvoke) {
187 throw JexlException.tryFailed(xinvoke); // throw
188 }
189 }
190 return TRY_FAILED;
191 }
192 }