View Javadoc

1   /*
2    * Copyright 2004-2009 the original author or authors.
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 org.springmodules.validation.valang.parser;
18  
19  import java.lang.reflect.Method;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  
24  import javassist.ClassPool;
25  import javassist.CtClass;
26  import javassist.CtMethod;
27  import javassist.CtNewMethod;
28  
29  import org.apache.commons.collections.Predicate;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  import org.springframework.beans.BeansException;
33  import org.springframework.context.ApplicationContext;
34  import org.springframework.context.ApplicationContextAware;
35  import org.springframework.util.ClassUtils;
36  import org.springframework.util.ReflectionUtils;
37  import org.springframework.util.StringUtils;
38  import org.springmodules.validation.util.PrimitiveClassUtils;
39  import org.springmodules.validation.util.date.DefaultDateParser;
40  import org.springmodules.validation.valang.ValangException;
41  import org.springmodules.validation.valang.functions.AbstractInitializableFunction;
42  import org.springmodules.validation.valang.functions.BeanPropertyFunction;
43  import org.springmodules.validation.valang.functions.EmailFunction;
44  import org.springmodules.validation.valang.functions.Function;
45  import org.springmodules.validation.valang.functions.InRoleFunction;
46  import org.springmodules.validation.valang.functions.LengthOfFunction;
47  import org.springmodules.validation.valang.functions.LowerCaseFunction;
48  import org.springmodules.validation.valang.functions.NotFunction;
49  import org.springmodules.validation.valang.functions.RegExFunction;
50  import org.springmodules.validation.valang.functions.ResolveFunction;
51  import org.springmodules.validation.valang.functions.UpperCaseFunction;
52  import org.springmodules.validation.valang.predicates.BetweenTestPredicate;
53  import org.springmodules.validation.valang.predicates.EqualsTestPredicate;
54  import org.springmodules.validation.valang.predicates.GenericTestPredicate;
55  import org.springmodules.validation.valang.predicates.GreaterThanOrEqualTestPredicate;
56  import org.springmodules.validation.valang.predicates.GreaterThanTestPredicate;
57  import org.springmodules.validation.valang.predicates.InTestPredicate;
58  import org.springmodules.validation.valang.predicates.LessThanOrEqualTestPredicate;
59  import org.springmodules.validation.valang.predicates.LessThanTestPredicate;
60  import org.springmodules.validation.valang.predicates.NotBetweenTestPredicate;
61  import org.springmodules.validation.valang.predicates.NotEqualsTestPredicate;
62  import org.springmodules.validation.valang.predicates.NotInTestPredicate;
63  import org.springmodules.validation.valang.predicates.Operator;
64  
65  /**
66   * <p>Allows registration of custom functions. Custom functions can overwrite default functions.
67   * <p/>
68   * <p>Default functions are:
69   * <p/>
70   * <ul>
71   * <li>len, length, size
72   * <li>upper
73   * <li>lower
74   * <li>!
75   * </ul>
76   *
77   * @author Steven Devijver
78   * @since Apr 23, 2005
79   */
80  public class DefaultVisitor implements ApplicationContextAware, ValangVisitor {
81  
82      final Logger logger = LoggerFactory.getLogger(DefaultVisitor.class);
83  
84      // FIX ME: use DI
85      final static Map<String, Function> hGeneratedBeanFunctions = new HashMap<String, Function>();
86  
87      /**
88       * The delimiter that preceeds the zero-relative subscript for an
89       * indexed reference.
90       */
91      public static final char INDEXED_DELIM_BEGIN = '[';
92      public static final char INDEXED_DELIM_END = ']';
93      
94      private ValangVisitor visitor = null;
95      private DefaultDateParser dateParser = null;
96      private ApplicationContext applicationContext = null;
97  
98      /**
99       * Constructor
100      */
101     public DefaultVisitor() {
102         this.dateParser = new DefaultDateParser();
103     }
104 
105     /**
106      * Get a function based on the function name and arguments.
107      */
108     public Function getFunction(String name, Function[] arguments, int line, int column) {
109         return doGetFunction(name, arguments, line, column);
110     }
111     
112     /**
113      * Get bytecode generation function.
114      * 
115      * @param       className       Name of the <code>Class</code> to generated 
116      *                              a <code>Function</code> to retrieve a property.
117      * @param       propertyName    The name of the property to retrieve a value from in the function.
118      *                              For example, expects 'message' and will call <code>getMessage()</code>.
119      *                              
120      * @return      Function        Bytecode generated <code>Function</code> based on the class and property name. 
121      */
122     public Function getFunction(String className, String propName) {
123         Function result = null;
124         String propertyName = propName;
125         
126         // FIX ME: consolidate logic checking between this and BeanPropertyFunction
127         
128         // FIXME: make excluded list: 'jsonMessageString'
129         if (!StringUtils.hasText(className) || propertyName.startsWith("this") || 
130             (propertyName.indexOf(INDEXED_DELIM_END) != -1 && (propName.length() > (propertyName.indexOf(INDEXED_DELIM_END) + 1))) ||
131             propertyName.toLowerCase().indexOf("jsonMessageString".toLowerCase()) != -1) {
132             result = new BeanPropertyFunction(propertyName);
133         } else {
134             String classFunctionName = null;
135             String property = null;
136             String reflectProperty = null;
137             String indexedKey = null;
138             
139             boolean listOrMapProperty = propertyName.indexOf(INDEXED_DELIM_BEGIN) != -1;
140             
141             if (listOrMapProperty) {
142                 int indexedDelimEnd = propertyName.indexOf(INDEXED_DELIM_END);
143                 
144                 indexedKey = propertyName.substring((propertyName.indexOf(INDEXED_DELIM_BEGIN) + 1), indexedDelimEnd);
145                 propertyName = propertyName.substring(0, propertyName.indexOf(INDEXED_DELIM_BEGIN));
146             }
147             
148             classFunctionName = className + org.apache.commons.lang.StringUtils.capitalize(propertyName) + 
149                                 (indexedKey != null ? org.apache.commons.lang.StringUtils.capitalize(indexedKey) : "") + 
150                                 "BeanPropertyFunction$$Valang";
151             
152             
153             String key = classFunctionName;
154 
155             if (hGeneratedBeanFunctions.containsKey(key)) {
156                 result = hGeneratedBeanFunctions.get(key);
157             } else {
158                 try {
159                     Class<?> clazz = ClassUtils.forName(className, ClassUtils.getDefaultClassLoader());
160                     Class<?> methodReturnType = null;
161                     boolean indexedProperty = propertyName.indexOf(".") != -1;
162                     
163                     if (indexedProperty) {
164                         String[] properties = org.apache.commons.lang.StringUtils.split(propertyName, ".");
165                         
166                         int count = 1;
167                         StringBuilder sb = new StringBuilder();
168                         Class<?> nestedMethodReturnType = null;
169     
170                         for (String prop : properties) {
171                             reflectProperty = "get" + org.apache.commons.lang.StringUtils.capitalize(prop);
172                             
173                             sb.append(reflectProperty);
174                             sb.append("()");
175                               
176                             if (count < properties.length) {
177                                 sb.append(".");
178                             }
179                             
180                             if (nestedMethodReturnType == null) {
181                                 Method nestedMethod = ReflectionUtils.findMethod(clazz, reflectProperty);
182                                 nestedMethodReturnType = nestedMethod.getReturnType();
183                             } else {
184                                 Method nestedMethod = ReflectionUtils.findMethod(nestedMethodReturnType, reflectProperty);
185                                 nestedMethodReturnType = nestedMethod.getReturnType();                                    
186                             }
187                             
188                             count++;
189                         }
190                         
191                         property = sb.toString();
192 //                        property = processProperty(propertyName, wrapper); //sb.toString();
193                         methodReturnType = nestedMethodReturnType;
194                     } else {
195                         StringBuilder sb = new StringBuilder();
196          
197                         sb.append("get");
198                         sb.append(org.apache.commons.lang.StringUtils.capitalize(propertyName));
199                         
200                         reflectProperty = sb.toString();
201                         
202                         sb.append("()");
203                         
204                         property = sb.toString();
205    
206                         methodReturnType = ReflectionUtils.findMethod(clazz, reflectProperty).getReturnType();
207                     }
208                     
209                     Class<?> primitiveClass = PrimitiveClassUtils.resolvePrimitiveClassName(methodReturnType);
210 
211                     ClassPool pool = ClassPool.getDefault();
212                     CtClass cc = pool.makeClass(classFunctionName);
213 
214                     cc.addInterface(pool.get("org.springmodules.validation.valang.functions.Function"));
215 
216                     StringBuilder generatedMethod = new StringBuilder();
217                     generatedMethod.append("public Object getResult(Object target) {");
218                     generatedMethod.append("    return ");
219 
220                     if (primitiveClass != null) {
221                         generatedMethod.append(" new " + primitiveClass.getName() + "(");
222                     }
223                     
224                     generatedMethod.append(" ((" + className + ")target).");
225                     
226                     generatedMethod.append(property);
227                     
228                     if (primitiveClass != null) {
229                         generatedMethod.append(")");
230                     } else if (listOrMapProperty && 
231                                org.apache.commons.lang.ClassUtils.isAssignable(methodReturnType, Map.class)) {
232                         generatedMethod.append(".get(\"" + indexedKey + "\")");
233                     } else if (listOrMapProperty && 
234                                org.apache.commons.lang.ClassUtils.isAssignable(methodReturnType, List.class)) {
235                         generatedMethod.append(".get(" + indexedKey + ")");
236                     } else if (methodReturnType.isArray()) {
237                         int arrayIndex = Integer.parseInt(indexedKey);
238                         
239                         generatedMethod.append("[" + arrayIndex + "]");
240                     }
241                     
242                     generatedMethod.append(";");
243                     generatedMethod.append("}");
244 
245                     CtMethod m = CtNewMethod.make(generatedMethod.toString(), cc);
246                     cc.addMethod(m);
247 
248                     result = (Function)cc.toClass().newInstance();
249 
250                     hGeneratedBeanFunctions.put(key, result);
251                     
252                     logger.info("Generated bytecode for {}.{} as '{}'.", 
253                                 new Object[] { className, 
254                                                property + "()" + (indexedKey != null ? "[" + indexedKey + "]" : ""), 
255                                                classFunctionName }); 
256                 } catch (Exception e) {
257                     logger.error("Problem generating bytecode for {}.{}.  {}", 
258                                  new Object[] { classFunctionName, 
259                                                 org.apache.commons.lang.StringUtils.capitalize(propertyName),
260                                                 e.getMessage() });
261                     
262                     // fallback to standard bean property function
263                     result = new BeanPropertyFunction(propName);
264                 }
265             }
266         }
267         
268         return result;
269     }
270     
271     private Function doGetFunction(String name, Function[] arguments, int line, int column) {
272 
273         Function function = resolveCustomFunction(name, arguments, line, column);
274         if (function != null) {
275             return function;
276         }
277 
278         function = resolveFunctionFromApplicationContext(name, arguments, line, column);
279         if (function != null) {
280             return function;
281         }
282 
283         function = resolveDefaultFunction(name, arguments, line, column);
284         if (function != null) {
285             return function;
286         }
287 
288         throw new ValangException("Could not find function [" + name + "]", line, column);
289     }
290 
291     private Function resolveCustomFunction(String name, Function[] arguments, int line, int column) {
292         if (getVisitor() == null) {
293             return null;
294         }
295         return getVisitor().getFunction(name, arguments, line, column);
296     }
297 
298     private Function resolveFunctionFromApplicationContext(String name, Function[] arguments, int line, int column) {
299         if (applicationContext == null || !applicationContext.containsBean(name)) {
300             return null;
301         }
302         
303         Object bean = applicationContext.getBean(name);
304         
305         if (!AbstractInitializableFunction.class.isInstance(bean)) {
306             logger.warn("Bean '" + name + "' is not of a '" + AbstractInitializableFunction.class.getName() + "' type");
307             return null;
308         }
309         
310         AbstractInitializableFunction function = (AbstractInitializableFunction) bean;
311         function.init(arguments, line, column);
312         
313         return function;
314     }
315 
316     private Function resolveDefaultFunction(String name, Function[] arguments, int line, int column) {
317         if ("len".equals(name) || "length".equals(name) || "size".equals(name) || "count".equals(name)) {
318             return new LengthOfFunction(arguments, line, column);
319         } else if ("upper".equals(name)) {
320             return new UpperCaseFunction(arguments, line, column);
321         } else if ("lower".equals(name)) {
322             return new LowerCaseFunction(arguments, line, column);
323         } else if ("!".equals(name)) {
324             return new NotFunction(arguments, line, column);
325         } else if ("resolve".equals(name)) {
326             return new ResolveFunction(arguments, line, column);
327         } else if ("match".equals(name) || "matches".equals(name)) {
328             return new RegExFunction(arguments, line, column);
329         } else if ("inRole".equals(name)) {
330             return new InRoleFunction(arguments, line, column);
331         } else if ("email".equals(name)) {
332             return new EmailFunction(arguments, line, column);
333         }
334 
335         return null;
336     }
337 
338     /**
339      * Gets visitor.
340      */
341     public ValangVisitor getVisitor() {
342         return this.visitor;
343     }
344 
345     /**
346      * <p>Register a custom visitor to look up custom functions. Lookup of functions
347      * will first be delegated to this visitor. If no function has been returned (null)
348      * lookup will be handled by DefaultVisitor.
349      *
350      * @param   visitor     The custom visitor.
351      */
352     public void setVisitor(ValangVisitor visitor) {
353         this.visitor = visitor;
354     }
355 
356     /**
357      * Gets predicate.
358      * 
359      * @param       leftFunction        Left comparison <code>Function</code>.
360      * @param       operator            <code>Operation</code> for comparison.
361      * @param       rightFunction       Right comparison <code>Function</code>.
362      * @param       line                Line number of parsed expression.
363      * @param       column              Column number of parsed expression.
364      * 
365      * @return      Predicate           Comparison predicate based on input.
366      */
367     public Predicate getPredicate(Function leftFunction, Operator operator, Function rightFunction, int line, int column) {
368         Predicate result = null;
369 
370         switch (operator) {
371             case EQUAL:
372                 result =  new EqualsTestPredicate(leftFunction, operator, rightFunction, line, column);
373                 break;
374             case NOT_EQUAL:
375                 result =  new NotEqualsTestPredicate(leftFunction, operator, rightFunction, line, column);
376                 break;
377             case LESS_THAN:
378                 result =  new LessThanTestPredicate(leftFunction, operator, rightFunction, line, column);
379                 break;
380             case LESS_THAN_OR_EQUAL:
381                 result =  new LessThanOrEqualTestPredicate(leftFunction, operator, rightFunction, line, column);
382                 break;
383             case GREATER_THAN:
384                 result =  new GreaterThanTestPredicate(leftFunction, operator, rightFunction, line, column);
385                 break;
386             case GREATER_THAN_OR_EQUAL:
387                 result =  new GreaterThanOrEqualTestPredicate(leftFunction, operator, rightFunction, line, column);
388                 break;
389             case BETWEEN:
390                 result =  new BetweenTestPredicate(leftFunction, operator, rightFunction, line, column);
391                 break;
392             case NOT_BETWEEN:
393                 result =  new NotBetweenTestPredicate(leftFunction, operator, rightFunction, line, column);
394                 break;
395             case IN:
396                 result =  new InTestPredicate(leftFunction, operator, rightFunction, line, column);
397                 break;
398             case NOT_IN:
399                 result =  new NotInTestPredicate(leftFunction, operator, rightFunction, line, column);
400                 break;
401             default:
402                 result = new GenericTestPredicate(leftFunction, operator, rightFunction, line, column);
403                 break;
404         }
405             
406         return result;
407     }
408 
409     /**
410      * Gets date parser.
411      */
412     public DefaultDateParser getDateParser() {
413         return dateParser;
414     }
415 
416     /**
417      * Implementation of <code>ApplicationContextAware</code>.
418      */
419     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
420         this.applicationContext = applicationContext;
421     }
422     
423 }