View Javadoc
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 }