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    *      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 }