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
18 package org.apache.commons.jexl3.internal.introspection;
19
20 import java.beans.IntrospectionException;
21 import java.lang.reflect.Method;
22
23 import org.apache.commons.jexl3.introspection.JexlPropertyGet;
24
25 /**
26 * Abstract an indexed property container.
27 * <p>This allows getting properties from expressions like {@code var.container.property}.
28 * This stores the container name and class as well as the list of available getter and setter methods.
29 * It implements JexlPropertyGet since such a container can only be accessed from its owning instance (not set).
30 */
31 public final class IndexedType implements JexlPropertyGet {
32
33 /**
34 * A generic indexed property container, exposes get(key) and set(key, value)
35 * and solves method call dynamically based on arguments.
36 * <p>Must remain public for introspection purpose.</p>
37 */
38 public static final class IndexedContainer {
39
40 /** The container instance. */
41 final Object container;
42
43 /** The container type instance. */
44 final IndexedType type;
45
46 /**
47 * Creates a new duck container.
48 *
49 * @param theType the container type
50 * @param theContainer the container instance
51 */
52 IndexedContainer(final IndexedType theType, final Object theContainer) {
53 this.type = theType;
54 this.container = theContainer;
55 }
56
57 /**
58 * Gets a property from this indexed container.
59 *
60 * @param key the property key
61 * @return the property value
62 * @throws Exception if inner invocation fails
63 */
64 public Object get(final Object key) throws Exception {
65 return type.invokeGet(container, key);
66 }
67
68 /**
69 * Gets the property container class.
70 *
71 * @return the container class
72 */
73 public Class<?> getContainerClass() {
74 return type.clazz;
75 }
76
77 /**
78 * Gets the property container name.
79 *
80 * @return the container name
81 */
82 public String getContainerName() {
83 return type.container;
84 }
85
86 /**
87 * Sets a property in this indexed container.
88 *
89 * @param key the property key
90 * @param value the property value
91 * @return the invocation result (frequently null)
92 * @throws Exception if inner invocation fails
93 */
94 public Object set(final Object key, final Object value) throws Exception {
95 return type.invokeSet(container, key, value);
96 }
97 }
98
99 /**
100 * Attempts to find an indexed-property getter in an object.
101 * The code attempts to find the list of methods getXXX() and setXXX().
102 * Note that this is not equivalent to the strict bean definition of indexed properties; the type of the key
103 * is not necessarily an int and the set/get arrays are not resolved.
104 *
105 * @param is the introspector
106 * @param object the object
107 * @param name the container name
108 * @return a JexlPropertyGet is successful, null otherwise
109 */
110 public static JexlPropertyGet discover(final Introspector is, final Object object, final String name) {
111 if (object != null && name != null && !name.isEmpty()) {
112 final String base = name.substring(0, 1).toUpperCase() + name.substring(1);
113 final Class<?> clazz = object.getClass();
114 final Method[] getters = is.getMethods(object.getClass(), "get" + base);
115 final Method[] setters = is.getMethods(object.getClass(), "set" + base);
116 if (getters != null) {
117 return new IndexedType(name, clazz, getters, setters);
118 }
119 }
120 return null;
121 }
122
123 /** The container name. */
124 final String container;
125
126 /** The container class. */
127 final Class<?> clazz;
128
129 /** The array of getter methods. */
130 private final Method[] getters;
131
132 /** Last get method used. */
133 private volatile Method get;
134
135 /** The array of setter methods. */
136 private final Method[] setters;
137
138 /** Last set method used. */
139 private volatile Method set;
140
141 /**
142 * Creates a new indexed property container type.
143 *
144 * @param name the container name
145 * @param c the owning class
146 * @param gets the array of getter methods
147 * @param sets the array of setter methods
148 */
149 private IndexedType(final String name, final Class<?> c, final Method[] gets, final Method[] sets) {
150 this.container = name;
151 this.clazz = c;
152 this.getters = gets;
153 this.setters = sets;
154 }
155
156 @Override
157 public Object invoke(final Object obj) throws Exception {
158 if (obj != null && clazz.equals(obj.getClass())) {
159 return new IndexedContainer(this, obj);
160 }
161 throw new IntrospectionException("property resolution error");
162 }
163
164 /**
165 * Gets the value of a property from a container.
166 *
167 * @param object the container instance (not null)
168 * @param key the property key (not null)
169 * @return the property value
170 * @throws Exception if invocation failed;
171 * IntrospectionException if a property getter could not be found
172 */
173 Object invokeGet(final Object object, final Object key) throws Exception {
174 if (getters != null && getters.length > 0) {
175 Method jm = get;
176 if (jm != null) {
177 final Class<?>[] types = jm.getParameterTypes();
178 if (types[0].isAssignableFrom(key.getClass())) {
179 return jm.invoke(object, key);
180 }
181 }
182 final Object[] args = {key};
183 final String methodName = getters[0].getName();
184 final MethodKey km = new MethodKey(methodName, args);
185 jm = km.getMostSpecificMethod(getters);
186 if (jm != null) {
187 final Object invoked = jm.invoke(object, args);
188 get = jm;
189 return invoked;
190 }
191 }
192 throw new IntrospectionException("property get error: "
193 + object.getClass() + "@" + key.toString());
194 }
195
196 /**
197 * Sets the value of a property in a container.
198 *
199 * @param object the container instance (not null)
200 * @param key the property key (not null)
201 * @param value the property value (not null)
202 * @return the result of the method invocation (frequently null)
203 * @throws Exception if invocation failed;
204 * IntrospectionException if a property setter could not be found
205 */
206 Object invokeSet(final Object object, final Object key, final Object value) throws Exception {
207 if (setters != null && setters.length > 0) {
208 Method jm = set;
209 if (jm != null) {
210 final Class<?>[] types = jm.getParameterTypes();
211 if (types[0].isAssignableFrom(key.getClass())
212 && (value == null
213 || types[1].isAssignableFrom(value.getClass()))) {
214 return jm.invoke(object, key, value);
215 }
216 }
217 final Object[] args = {key, value};
218 final String methodName = setters[0].getName();
219 final MethodKey km = new MethodKey(methodName, args);
220 jm = km.getMostSpecificMethod(setters);
221 if (jm != null) {
222 final Object invoked = jm.invoke(object, args);
223 set = jm;
224 return invoked;
225 }
226 }
227 throw new IntrospectionException("property set error: "
228 + object.getClass() + "@" + key.toString());
229 }
230
231 @Override
232 public boolean isCacheable() {
233 return true;
234 }
235
236 @Override
237 public boolean tryFailed(final Object val) {
238 return val == Uberspect.TRY_FAILED;
239 }
240
241 @Override
242 public Object tryInvoke(final Object obj, final Object key) {
243 if (obj != null && key != null
244 && clazz.equals(obj.getClass())
245 && container.equals(key.toString())) {
246 return new IndexedContainer(this, obj);
247 }
248 return Uberspect.TRY_FAILED;
249 }
250
251 }