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.javascript;
18  
19  import java.io.IOException;
20  import java.io.InputStreamReader;
21  import java.io.Reader;
22  import java.io.Writer;
23  import java.util.Calendar;
24  import java.util.Collection;
25  import java.util.Date;
26  import java.util.Iterator;
27  
28  import org.apache.commons.collections.Predicate;
29  import org.apache.commons.collections.functors.AndPredicate;
30  import org.apache.commons.collections.functors.NotPredicate;
31  import org.apache.commons.collections.functors.OrPredicate;
32  import org.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  import org.springframework.context.support.MessageSourceAccessor;
35  import org.springframework.util.Assert;
36  import org.springframework.util.ClassUtils;
37  import org.springframework.util.StringUtils;
38  import org.springframework.web.util.JavaScriptUtils;
39  import org.springmodules.validation.valang.functions.AbstractFunction;
40  import org.springmodules.validation.valang.functions.AbstractMathFunction;
41  import org.springmodules.validation.valang.functions.AddFunction;
42  import org.springmodules.validation.valang.functions.BeanPropertyFunction;
43  import org.springmodules.validation.valang.functions.DateLiteralFunction;
44  import org.springmodules.validation.valang.functions.DivideFunction;
45  import org.springmodules.validation.valang.functions.Function;
46  import org.springmodules.validation.valang.functions.LengthOfFunction;
47  import org.springmodules.validation.valang.functions.LiteralFunction;
48  import org.springmodules.validation.valang.functions.LowerCaseFunction;
49  import org.springmodules.validation.valang.functions.MapEntryFunction;
50  import org.springmodules.validation.valang.functions.ModuloFunction;
51  import org.springmodules.validation.valang.functions.MultiplyFunction;
52  import org.springmodules.validation.valang.functions.NotFunction;
53  import org.springmodules.validation.valang.functions.SubtractFunction;
54  import org.springmodules.validation.valang.functions.TargetBeanFunction;
55  import org.springmodules.validation.valang.functions.UpperCaseFunction;
56  import org.springmodules.validation.valang.predicates.AbstractPropertyPredicate;
57  import org.springmodules.validation.valang.predicates.BasicValidationRule;
58  import org.springmodules.validation.valang.predicates.Operator;
59  import org.springmodules.validation.valang.predicates.ValidationRule;
60  
61  /**
62   * Translates a collection of valang validation rules into a JavaScript statement
63   * that is capable of validating a HTML form and writes this to a provided
64   * <code>Writer</code>. This class is <b>not</b> thread safe so it is recommended
65   * that a new instance be created each time a translation is required.
66   * <p/>
67   * <p>The generated JavaScript code is dependent on the code base found in the
68   * file "valang_codebase.js" having already been loaded into the page where
69   * the validation will occur.
70   *
71   * @author Oliver Hutchison
72   */
73  public class ValangJavaScriptTranslator {
74  
75      /**
76       * Returns a <code>Reader</code> for accessing the JavaScript codebase used by the
77       * translated validation rules.
78       */
79      public static Reader getCodebaseReader() {
80          return new InputStreamReader(ValangJavaScriptTranslator.class.getResourceAsStream("valang_codebase.js"));
81      }
82  
83      private static final Logger logger = LoggerFactory.getLogger(ValangJavaScriptTranslator.class);
84  
85      private static final ReflectiveVisitorHelper reflectiveVisitorHelper = new ReflectiveVisitorHelper();
86  
87      private Writer writer;
88  
89      public ValangJavaScriptTranslator() {
90      }
91  
92      /**
93       * Translates the provided set of Valang <code>BasicValidationRule</code>s into JavaScript
94       * code capable of validating a HTML form and outputs the translated code into the provided
95       * writer.
96       *
97       * @param writer the writer to output the JavaScript code into
98       * @param name the name of the command that is being validated
99       * @param installSelfWithForm should the generated JavaScript attempt to install
100      * its self with the form on creation
101      * @param rules the collection of <code>BasicValidationRule</code>s to translate
102      * @param messageSource the message source accessor that will be used to resolve validation
103      * messages.
104      */
105     public void writeJavaScriptValangValidator(Writer writer, String name, boolean installSelfWithForm,
106                                                Collection<ValidationRule> rules, MessageSourceAccessor messageSource) 
107             throws IOException {
108         try {
109             setWriter(writer);
110             append("new ValangValidator(");
111             appendJsString(name);
112             append(',');
113             append(Boolean.toString(installSelfWithForm));
114             append(',');
115             appendArrayValidators(rules, messageSource);
116             append(')');
117         }
118         finally {
119             clearWriter();
120         }
121     }
122 
123     protected void setWriter(Writer writer) {
124         Assert.state(this.writer == null,
125             "Attempted to set writer when one already set - is this class being used is multiple threads?");
126         this.writer = writer;
127     }
128 
129     protected void clearWriter() {
130         writer = null;
131     }
132 
133     protected void append(String string) throws IOException {
134         writer.write(string);
135     }
136 
137     protected void appendJsString(String string) throws IOException {
138         writer.write('\'');
139         if (string == null) {
140             writer.write("null");
141         } else {
142             writer.write(JavaScriptUtils.javaScriptEscape(string));
143         }
144         writer.write('\'');
145     }
146 
147     protected void append(char c) throws IOException {
148         writer.write(c);
149     }
150 
151     private void append(int i) throws IOException {
152         writer.write(Integer.toString(i));
153     }
154 
155     protected void appendArrayValidators(Collection<ValidationRule> rules, MessageSourceAccessor messageSource) throws IOException {
156         append("new Array(");
157         
158         for (Iterator i = rules.iterator(); i.hasNext();) {
159             appendValidatorRule((BasicValidationRule) i.next(), messageSource);
160             if (i.hasNext()) {
161                 append(',');
162             }
163         }
164         append(')');
165     }
166 
167     protected void appendValidatorRule(BasicValidationRule rule, MessageSourceAccessor messageSource)
168         throws IOException {
169         append("new ValangValidator.Rule('");
170         append(rule.getField());
171         append("','not implemented',");
172         appendJsString(getErrorMessage(rule, messageSource));
173         append(',');
174         appendValidationFunction(rule.getPredicate());
175         append(')');
176     }
177 
178     protected String getErrorMessage(BasicValidationRule rule, MessageSourceAccessor messageSource) {
179         if (StringUtils.hasLength(rule.getErrorKey())) {
180             if (rule.getErrorArgs() != null && !rule.getErrorArgs().isEmpty()) {
181                 // TODO: implement message arguments in JavaScript
182                 logger.warn("Translating error message with arguments is not implemented; using default message");
183                 return rule.getErrorMessage();
184             } else {
185                 return messageSource.getMessage(rule.getErrorKey(), rule.getErrorMessage());
186             }
187         } else {
188             return rule.getErrorMessage();
189         }
190     }
191 
192     protected void appendValidationFunction(Predicate p) throws IOException {
193         append("function() {return ");
194         doVisit(p);
195         append('}');
196     }
197 
198     protected void doVisit(Object value) throws IOException {
199         reflectiveVisitorHelper.invokeVisit(this, value);
200     }
201 
202     void visitNull() throws IOException {
203         append("null");
204     }
205 
206     void visit(Function f) throws IOException {
207         if (logger.isWarnEnabled()) {
208             logger.warn("Encountered unsupported custom function '" + f.getClass().getName() + "'");
209         }
210         append("this._throwError('don\\'t know how to handle custom function \\'");
211         append(getNameForCustomFunction(f));
212         append("\\'')");
213     }
214 
215     void visit(AbstractFunction f) throws IOException {
216         Function[] arguments = f.getArguments();
217         append(getNameForCustomFunction(f));
218         append('(');
219         for (int i = 0; i < arguments.length; i++) {
220             doVisit(arguments[i]);
221             if (i < arguments.length - 1) {
222                 append(',');
223             }
224         }
225         append(')');
226     }
227 
228     protected String getNameForCustomFunction(Function f) {
229         return "this." + ClassUtils.getShortName(f.getClass());
230     }
231 
232     void visit(NotPredicate p) throws IOException {
233         Assert.isTrue(p.getPredicates().length == 1);
234         append("! (");
235         doVisit(p.getPredicates()[0]);
236         append(')');
237     }
238 
239     void visit(AndPredicate p) throws IOException {
240         String op = ") && ";
241         for (int i = 0; i < p.getPredicates().length; i++) {
242             Predicate innerP = p.getPredicates()[i];
243             append('(');
244             doVisit(innerP);
245             if (i < p.getPredicates().length - 1) {
246                 append(op);
247             } else {
248                 append(')');
249             }
250         }
251     }
252 
253     void visit(OrPredicate p) throws IOException {
254         String op = ") || ";
255         for (int i = 0; i < p.getPredicates().length; i++) {
256             Predicate innerP = p.getPredicates()[i];
257             append('(');
258             doVisit(innerP);
259             if (i < p.getPredicates().length - 1) {
260                 append(op);
261             } else {
262                 append(')');
263             }
264         }
265     }
266 
267     void visit(AbstractPropertyPredicate p) throws IOException {
268         append(operatorToFunctionName(p.getOperator()));
269         append("((");
270         doVisit(p.getLeftFunction());
271         append("), (");
272         doVisit(p.getRightFunction());
273         append("))");
274     }
275 
276     protected String operatorToFunctionName(Operator operator) {
277         switch(operator) {
278             case EQUAL:
279                 return "this.equals";
280             case NOT_EQUAL:
281                 return "! this.equals";
282             case LESS_THAN:
283                 return "this.lessThan";
284             case LESS_THAN_OR_EQUAL:
285                 return "this.lessThanOrEquals";
286             case GREATER_THAN:
287                 return "this.moreThan";
288             case GREATER_THAN_OR_EQUAL:
289                 return "this.moreThanOrEquals";
290             case IN:
291                 return "this.inFunc";
292             case NOT_IN:
293                 return "! this.inFunc";
294             case BETWEEN:
295                 return "this.between";
296             case NOT_BETWEEN:
297                 return "! this.between";
298             case NULL:
299                 return "this.nullFunc";
300             case NOT_NULL:
301                 return "! this.nullFunc";
302             case HAS_TEXT:
303                 return "this.hasText";
304             case HAS_NO_TEXT:
305                 return "! this.hasText";
306             case HAS_LENGTH:
307                 return "this.hasLength";
308             case HAS_NO_LENGTH:
309                 return "! this.hasLength";
310             case IS_BLANK:
311                 return "this.isBlank";
312             case IS_NOT_BLANK:
313                 return "! this.isBlank";
314             case IS_WORD:
315                 return "this.isWord";
316             case IS_NOT_WORD:
317                 return "! this.isWord";
318             case IS_UPPERCASE:
319                 return "this.isUpper";
320             case IS_NOT_UPPERCASE:
321                 return "! this.isUpper";
322             case IS_LOWERCASE:
323                 return "this.isLower";
324             case IS_NOT_LOWERCASE:
325                 return "! this.isLower";
326             default:
327                 throw new UnsupportedOperationException("Unexpected operator type '" + operator.getClass().getName() + "'");
328         }
329     }
330 
331     void visit(TargetBeanFunction f) throws IOException {
332         append("this.getTargetBean()");
333     }
334 
335     void visit(BeanPropertyFunction f) throws IOException {
336         append("this.getPropertyValue(");
337         appendJsString(f.getField());
338         append(')');
339     }
340 
341     void visit(MapEntryFunction f) throws IOException {
342         append("(");
343         doVisit(f.getMapFunction());
344         append("[");
345         doVisit(f.getKeyFunction());
346         append("])");
347     }
348 
349     void visit(LiteralFunction f) throws IOException {
350         Object literal = f.getResult(null);
351         if (literal instanceof String) {
352             appendJsString((String) literal);
353         } else if (literal instanceof Number) {
354             append(literal.toString());
355         } else if (literal instanceof Boolean) {
356             append(literal.toString());
357         } else if (literal instanceof Function[]) {
358             Function[] functions = (Function[]) literal;
359             appeandLiteralArray(functions);
360         } else if (literal instanceof Collection) {
361             appeandLiteralArray((((Collection) literal).toArray()));
362         } else {
363             throw new UnsupportedOperationException("Unexpected literal type '" + literal.getClass() + "'");
364         }
365     }
366 
367     void appeandLiteralArray(Object[] functions) throws IOException {
368         append("new Array(");
369         for (int i = 0; i < functions.length; i++) {
370             doVisit(functions[i]);
371             if (i < functions.length - 1) {
372                 append(",");
373             }
374         }
375         append(')');
376     }
377 
378     void visit(DateLiteralFunction f) throws IOException {
379         Calendar cal = Calendar.getInstance();
380         cal.setTime((Date) f.getResult(null));
381         append("new Date(");
382         append(cal.get(Calendar.YEAR));
383         append(", ");
384         append(cal.get(Calendar.MONTH));
385         append(", ");
386         append(cal.get(Calendar.DATE));
387         append(", ");
388         append(cal.get(Calendar.HOUR_OF_DAY));
389         append(", ");
390         append(cal.get(Calendar.MINUTE));
391         append(", ");
392         append(cal.get(Calendar.SECOND));
393         append(", ");
394         append(cal.get(Calendar.MILLISECOND));
395         append(')');
396     }
397 
398     void visit(LengthOfFunction f) throws IOException {
399         append("this.lengthOf(");
400         doVisit(f.getArguments()[0]);
401         append(')');
402     }
403 
404     void visit(NotFunction f) throws IOException {
405         append("! ");
406         doVisit(f.getArguments()[0]);
407     }
408 
409     void visit(UpperCaseFunction f) throws IOException {
410         append("this.upperCase(");
411         doVisit(f.getArguments()[0]);
412         append(')');
413     }
414 
415     void visit(LowerCaseFunction f) throws IOException {
416         append("this.lowerCase(");
417         doVisit(f.getArguments()[0]);
418         append(')');
419     }
420 
421     void visit(AbstractMathFunction f) throws IOException {
422         append(mathToFunctionName(f));
423         append("((");
424         doVisit(f.getLeftFunction());
425         append("),(");
426         doVisit(f.getRightFunction());
427         append("))");
428     }
429 
430     protected String mathToFunctionName(AbstractMathFunction f) {
431         if (f instanceof AddFunction) {
432             return "this.add";
433         } else if (f instanceof DivideFunction) {
434             return "this.divide";
435         } else if (f instanceof ModuloFunction) {
436             return "this.modulo";
437         } else if (f instanceof MultiplyFunction) {
438             return "this.multiply";
439         } else if (f instanceof SubtractFunction) {
440             return "this.subtract";
441         } else {
442             throw new UnsupportedOperationException("Unexpected math function type '" + f.getClass().getName() + "'");
443         }
444     }
445 }