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 * https://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 *
27 * @since 2.0
28 */
29 public class PropertySetExecutor extends AbstractExecutor.Set {
30
31 /** Index of the first character of the "set{p,P}roperty". */
32 private static final int SET_START_INDEX = 3;
33
34 /**
35 * Discovers a PropertySetExecutor.
36 * <p>The method to be found should be named "set{P,p}property".</p>
37 *
38 * @param is the introspector
39 * @param clazz the class to find the get method from
40 * @param property the property name to find
41 * @param value the value to assign to the property
42 * @return the executor if found, null otherwise
43 */
44 public static PropertySetExecutor discover(final Introspector is,
45 final Class<?> clazz,
46 final String property,
47 final Object value) {
48 if (property == null || property.isEmpty()) {
49 return null;
50 }
51 final java.lang.reflect.Method method = discoverSet(is, clazz, property, value);
52 return method != null ? new PropertySetExecutor(clazz, method, property, value) : null;
53 }
54
55 /**
56 * Discovers the method for a {@link org.apache.commons.jexl3.introspection.JexlPropertySet}.
57 * <p>The method to be found should be named "set{P,p}property".
58 * As a special case, any empty array will try to find a valid array-setting non-ambiguous method.
59 *
60 * @param is the introspector
61 * @param clazz the class to find the get method from
62 * @param property the name of the property to set
63 * @param arg the value to assign to the property
64 * @return the method if found, null otherwise
65 */
66 private static java.lang.reflect.Method discoverSet(final Introspector is,
67 final Class<?> clazz,
68 final String property,
69 final Object arg) {
70 // first, we introspect for the set<identifier> setter method
71 final Object[] params = {arg};
72 final StringBuilder sb = new StringBuilder("set");
73 sb.append(property);
74 // uppercase nth char
75 final char c = sb.charAt(SET_START_INDEX);
76 sb.setCharAt(SET_START_INDEX, Character.toUpperCase(c));
77 java.lang.reflect.Method method = is.getMethod(clazz, sb.toString(), params);
78 // lowercase nth char
79 if (method == null) {
80 sb.setCharAt(SET_START_INDEX, Character.toLowerCase(c));
81 method = is.getMethod(clazz, sb.toString(), params);
82 // uppercase nth char, try array
83 if (method == null && isEmptyArray(arg)) {
84 sb.setCharAt(SET_START_INDEX, Character.toUpperCase(c));
85 method = lookupSetEmptyArray(is, clazz, sb.toString());
86 // lowercase nth char
87 if (method == null) {
88 sb.setCharAt(SET_START_INDEX, Character.toLowerCase(c));
89 method = lookupSetEmptyArray(is, clazz, sb.toString());
90 }
91 }
92 }
93 return method;
94 }
95
96 /**
97 * Checks whether an argument is an empty array.
98 *
99 * @param arg the argument
100 * @return true if {@code arg} is an empty array
101 */
102 private static boolean isEmptyArray(final Object arg) {
103 return arg != null && arg.getClass().isArray() && Array.getLength(arg) == 0;
104 }
105
106 /**
107 * Finds an empty array property setter method by {@code methodName}.
108 * <p>This checks only one method with that name accepts an array as sole parameter.
109 *
110 * @param is the introspector
111 * @param clazz the class to find the get method from
112 * @param methodName the method name to find
113 * @return the sole method that accepts an array as parameter
114 */
115 private static java.lang.reflect.Method lookupSetEmptyArray(final Introspector is, final Class<?> clazz, final String methodName) {
116 java.lang.reflect.Method candidate = null;
117 final java.lang.reflect.Method[] methods = is.getMethods(clazz, methodName);
118 if (methods != null) {
119 for (final java.lang.reflect.Method method : methods) {
120 final Class<?>[] paramTypes = method.getParameterTypes();
121 if (paramTypes.length == 1 && paramTypes[0].isArray()) {
122 if (candidate != null) {
123 // because the setter method is overloaded for different parameter type,
124 // return null here to report the ambiguity.
125 return null;
126 }
127 candidate = method;
128 }
129 }
130 }
131 return candidate;
132 }
133
134 /** The property. */
135 protected final String property;
136
137 /** The property value class. */
138 protected final Class<?> valueClass;
139
140 /**
141 * Creates an instance.
142 *
143 * @param clazz the class the set method applies to
144 * @param method the method called through this executor
145 * @param key the key to use as 1st argument to the set method
146 * @param value the value
147 */
148 protected PropertySetExecutor(final Class<?> clazz,
149 final java.lang.reflect.Method method,
150 final String key,
151 final Object value) {
152 super(clazz, method);
153 property = key;
154 valueClass = classOf(value);
155 }
156
157 @Override
158 public Object getTargetProperty() {
159 return property;
160 }
161
162 @Override
163 public Object invoke(final Object o, final Object argument) throws IllegalAccessException, InvocationTargetException {
164 Object arg = argument;
165 if (method != null) {
166 // handle the empty array case
167 if (isEmptyArray(arg)) {
168 // if array is empty but its component type is different from the method first parameter component type,
169 // replace argument with a new empty array instance (of the method first parameter component type)
170 final Class<?> componentType = method.getParameterTypes()[0].getComponentType();
171 if (componentType != null && !componentType.equals(arg.getClass().getComponentType())) {
172 arg = Array.newInstance(componentType, 0);
173 }
174 }
175 method.invoke(o, arg);
176 }
177 return arg;
178 }
179
180 @Override
181 public Object tryInvoke(final Object o, final Object identifier, final Object value) {
182 if (o != null && method != null
183 // ensure method name matches the property name
184 && property.equals(castString(identifier))
185 // object class should be same as executor's method declaring class
186 && objectClass.equals(o.getClass())
187 // argument class should be eq
188 && valueClass.equals(classOf(value))) {
189 try {
190 return invoke(o, value);
191 } catch (IllegalAccessException | IllegalArgumentException illegal) {
192 return TRY_FAILED; // fail
193 } catch (final InvocationTargetException invoke) {
194 throw JexlException.tryFailed(invoke); // throw
195 }
196 }
197 return TRY_FAILED;
198 }
199 }