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