1 /***
2 * Copyright 2003-2010 Terracotta, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package net.sf.ehcache.search.attribute;
18
19 import java.io.Serializable;
20 import java.lang.reflect.Field;
21 import java.lang.reflect.InvocationTargetException;
22 import java.lang.reflect.Method;
23
24 import net.sf.ehcache.Element;
25 import net.sf.ehcache.config.InvalidConfigurationException;
26
27 /***
28 * Built-in search attribute extractor driven by method/value dotted expression
29 * chains.<br>
30 * <br>
31 * The expression chain must start with one of either "key", "value", or
32 * "element". From the starting object a chain of either method calls or field
33 * names follows. Method calls and field names can be freely mixed in the chain.
34 * Some examples:
35 * <ol>
36 * <li>"key.getName()" -- call getName() on the key object
37 * <li>"value.person.getAge()" -- get the "person" field of the value object and call getAge() on it
38 * <li>"element.toString()" -- call toString() on the element
39 * </ol>
40 * The method and field name portions of the expression are case sensitive
41 *
42 * @author teck
43 */
44 public class ReflectionAttributeExtractor implements AttributeExtractor {
45
46 private static final String ELEMENT = "element";
47 private static final String KEY = "key";
48 private static final String VALUE = "value";
49
50 private final String expression;
51 private final Part[] parts;
52 private final StartType start;
53
54 /***
55 * Create a new ReflectionAttributeExtractor
56 *
57 * @param expression
58 */
59 public ReflectionAttributeExtractor(String expression) throws InvalidConfigurationException {
60 String trimmed = expression.trim();
61
62 String[] tokens = trimmed.split("//.");
63
64 if (tokens.length == 0) {
65 throw new InvalidConfigurationException("Invalid attribute expression: " + trimmed);
66 }
67
68 String startToken = tokens[0];
69
70 if (startToken.equalsIgnoreCase(ELEMENT)) {
71 start = StartType.ELEMENT;
72 } else if (startToken.equalsIgnoreCase(KEY)) {
73 start = StartType.KEY;
74 } else if (startToken.equalsIgnoreCase(VALUE)) {
75 start = StartType.VALUE;
76 } else {
77 throw new InvalidConfigurationException("Expression must start with either \"" + ELEMENT + "\", \"" + KEY + "\" or \"" + VALUE
78 + "\": " + trimmed);
79 }
80
81 this.parts = parseExpression(tokens, trimmed);
82 this.expression = trimmed;
83 }
84
85 /***
86 * Evaluate the expression for the given element
87 *
88 * @return the attribute value
89 * @throws AttributeExtractorException if there is an error in evaluating the expression
90 */
91 public Object attributeFor(Element e, String attributeName) throws AttributeExtractorException {
92
93
94
95 Object startObject;
96 switch (start) {
97 case ELEMENT: {
98 startObject = e;
99 break;
100 }
101 case KEY: {
102 startObject = e.getObjectKey();
103 break;
104 }
105 case VALUE: {
106 startObject = e.getObjectValue();
107 break;
108 }
109 default: {
110 throw new AssertionError(start.name());
111 }
112 }
113
114 Object rv = startObject;
115 for (Part part : parts) {
116 rv = part.eval(rv);
117 }
118
119 return rv;
120 }
121
122 private static Part[] parseExpression(String[] tokens, String expression) {
123 Part[] parts = new Part[tokens.length - 1];
124
125 for (int i = 1; i < tokens.length; i++) {
126 String token = tokens[i];
127
128 boolean method = false;
129 if (token.endsWith("()")) {
130 method = true;
131 token = token.substring(0, token.length() - 2);
132 }
133 verifyToken(token, expression);
134
135 if (method) {
136 parts[i - 1] = new MethodPart(token);
137 } else {
138 parts[i - 1] = new FieldPart(token);
139 }
140 }
141
142 return parts;
143 }
144
145 private static void verifyToken(String token, String expression) {
146 if (token.length() == 0) {
147 throw new InvalidConfigurationException("Empty element in expression: " + expression);
148 }
149
150 for (int i = 0; i < token.length(); i++) {
151 char c = token.charAt(i);
152 if (i == 0) {
153 if (!Character.isJavaIdentifierStart(c)) {
154 throw new InvalidConfigurationException("Invalid element (" + token + ") in expression: " + expression);
155 }
156 } else {
157 if (!Character.isJavaIdentifierPart(c)) {
158 throw new InvalidConfigurationException("Invalid element (" + token + ") in expression: " + expression);
159 }
160 }
161 }
162 }
163
164 /***
165 * The various types of the start of the expression
166 */
167 private enum StartType {
168 ELEMENT, VALUE, KEY;
169 }
170
171 /***
172 * A part (method or field) of the expression
173 */
174 private interface Part extends Serializable {
175 Object eval(Object target);
176 }
177
178 /***
179 * A field expression part
180 */
181 private static class FieldPart implements Part {
182
183 private final String fieldName;
184
185 private transient volatile FieldRef cache;
186
187 public FieldPart(String field) {
188 this.fieldName = field;
189 }
190
191 public Object eval(Object target) {
192 if (target == null) {
193 throw new AttributeExtractorException("null reference encountered trying to read field " + fieldName);
194 }
195
196 Class c = target.getClass();
197 FieldRef ref = cache;
198
199 if (ref == null || ref.target != c) {
200 while (true) {
201 try {
202 Field field = c.getDeclaredField(fieldName);
203 field.setAccessible(true);
204 ref = new FieldRef(target.getClass(), field);
205 cache = ref;
206 break;
207 } catch (NoSuchFieldException e) {
208 c = c.getSuperclass();
209 if (c == null) {
210 throw new AttributeExtractorException("No such field named \"" + fieldName + "\" present in instance of "
211 + target.getClass());
212 }
213 } catch (Exception e) {
214 throw new AttributeExtractorException(e);
215 }
216 }
217 }
218
219 try {
220 return ref.field.get(target);
221 } catch (Exception e) {
222 throw new AttributeExtractorException(e);
223 }
224 }
225
226 }
227
228 /***
229 * A reference to a resolved Field instance
230 */
231 private static class FieldRef {
232 private final Class target;
233 private final Field field;
234
235 FieldRef(Class target, Field field) {
236 this.target = target;
237 this.field = field;
238 }
239 }
240
241 /***
242 * A reference to a resolved Method instance
243 */
244 private static class MethodRef {
245 private final Method method;
246 private final Class target;
247
248 MethodRef(Class target, Method method) {
249 this.target = target;
250 this.method = method;
251 }
252 }
253
254 /***
255 * A method expression part
256 */
257 private static class MethodPart implements Part {
258
259 private final String methodName;
260 private transient volatile MethodRef cache;
261
262 public MethodPart(String method) {
263 this.methodName = method;
264 }
265
266 public Object eval(Object target) {
267 if (target == null) {
268 throw new AttributeExtractorException("null reference encountered trying to call " + methodName + "()");
269 }
270
271 Class c = target.getClass();
272
273 MethodRef ref = cache;
274
275 if (ref == null || ref.target != c) {
276 while (true) {
277 try {
278 Method method = c.getDeclaredMethod(methodName);
279 method.setAccessible(true);
280 ref = new MethodRef(target.getClass(), method);
281 cache = ref;
282 break;
283 } catch (NoSuchMethodException e) {
284 c = c.getSuperclass();
285 if (c == null) {
286 throw new AttributeExtractorException("No such method named \"" + methodName + "\" present on instance of "
287 + target.getClass());
288 }
289 } catch (Exception e) {
290 throw new AttributeExtractorException(e);
291 }
292 }
293 }
294
295 try {
296 return ref.method.invoke(target);
297 } catch (InvocationTargetException e) {
298 throw new AttributeExtractorException(e.getTargetException());
299 } catch (Exception e) {
300 throw new AttributeExtractorException(e);
301 }
302 }
303 }
304 }