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