View Javadoc

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          // NOTE: We can play all kinds of tricks of generating java classes and
93          // using Unsafe if needed
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 }