View Javadoc

1   /*
2    * Copyright 1999-2004 The Apache Software Foundation.
3    * Modifications, Copyright 2005 Stephen Colebourne
4    * 
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    * 
9    *      http://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  package org.joda.time.contrib.jsptag;
18  
19  import java.io.IOException;
20  import java.util.Vector;
21  
22  import javax.servlet.jsp.tagext.PageData;
23  import javax.servlet.jsp.tagext.TagLibraryValidator;
24  import javax.servlet.jsp.tagext.ValidationMessage;
25  import javax.xml.parsers.ParserConfigurationException;
26  import javax.xml.parsers.SAXParser;
27  import javax.xml.parsers.SAXParserFactory;
28  
29  import org.xml.sax.Attributes;
30  import org.xml.sax.SAXException;
31  import org.xml.sax.helpers.DefaultHandler;
32  
33  /**
34   * <p>
35   * A SAX-based TagLibraryValidator for the Joda tags. Currently implements the
36   * following checks:
37   * </p>
38   * 
39   * <ul>
40   * <li>Tag bodies that must either be empty or non-empty given particular
41   * attributes.</li>
42   * <li>Expression syntax validation (NOTE: this has been disabled; per my
43   * understanding, it shouldn't be needed in JSP 2.0+ containers; see notes in
44   * source code for more information).
45   * </ul>
46   * 
47   * @author Shawn Bayern
48   * @author Jan Luehe
49   * @author Jim Newsham
50   */
51  public class JodaTagLibraryValidator extends TagLibraryValidator {
52  
53      /*
54       * Expression syntax validation has been disabled since when I ported this
55       * code over from Jakarta Taglib, I wanted to reduce dependencies. As I
56       * understand it, JSP 2.0 containers take over the responsibility of
57       * handling EL code (both in attribute tags, and externally), so this
58       * shouldn't be a problem unless you're using something old. If you want to
59       * restore this validation, you must uncomment the various lines in this
60       * source, include the Jakarta Taglib's standard.jar library at build and
61       * runtime, and (I believe, but don't know specifically) make a legacy-style
62       * tld which describes which attributes should be validated. Have a look at
63       * fmt.tld, fmt-1.0.tld, fmt-1.0-rt.tld in standard.jar for an example of
64       * this.
65       */
66  
67      // *********************************************************************
68      // Implementation Overview
69      /*
70       * We essentially just run the page through a SAX parser, handling the
71       * callbacks that interest us. We collapse <jsp:text> elements into the text
72       * they contain, since this simplifies processing somewhat. Even a quick
73       * glance at the implementation shows its necessary, tree-oriented nature:
74       * multiple Stacks, an understanding of 'depth', and so on all are important
75       * as we recover necessary state upon each callback. This TLV demonstrates
76       * various techniques, from the general "how do I use a SAX parser for a
77       * TLV?" to "how do I read my init parameters and then validate?" But also,
78       * the specific SAX methodology was kept as general as possible to allow for
79       * experimentation and flexibility.
80       */
81  
82      // *********************************************************************
83      // Constants
84      // tag names
85      private static final String SET_DATETIMEZONE = "setDateTimeZone";
86  
87      private static final String PARSE_DATETIME = "parseDateTime";
88  
89      private static final String JSP_TEXT = "jsp:text";
90  
91      // attribute names
92      private static final String VALUE = "value";
93  
94      // parameter names
95      // private final String EXP_ATT_PARAM = "expressionAttributes";
96  
97      // attributes
98      private static final String VAR = "var";
99  
100     private static final String SCOPE = "scope";
101 
102     // scopes
103     private static final String PAGE_SCOPE = "page";
104 
105     private static final String REQUEST_SCOPE = "request";
106 
107     private static final String SESSION_SCOPE = "session";
108 
109     private static final String APPLICATION_SCOPE = "application";
110 
111     // Relevant URIs
112     private final String JSP = "http://java.sun.com/JSP/Page";
113 
114     // *********************************************************************
115     // Validation and configuration state (protected)
116 
117     private String uri; // our taglib's uri (as passed by JSP container on XML
118                         // View)
119 
120     private String prefix; // our taglib's prefix
121 
122     private Vector messageVector; // temporary error messages
123 
124 //    private Map config; // configuration (Map of Sets)
125 //
126 //    private boolean failed; // have we failed >0 times?
127 
128     private String lastElementId; // the last element we've seen
129 
130     // *********************************************************************
131     // Constructor and lifecycle management
132 
133     public JodaTagLibraryValidator() {
134         init();
135     }
136 
137     private void init() {
138         messageVector = null;
139         prefix = null;
140 //        config = null;
141     }
142 
143     public void release() {
144         super.release();
145         init();
146     }
147 
148     public synchronized ValidationMessage[] validate(String prefix, String uri,
149             PageData page) {
150         try {
151             this.uri = uri;
152             // initialize
153             messageVector = new Vector();
154 
155             // save the prefix
156             this.prefix = prefix;
157 
158             // parse parameters if necessary
159             /*
160              * try { if (config == null) { configure((String)
161              * getInitParameters().get(EXP_ATT_PARAM)); } } catch
162              * (NoSuchElementException ex) { // parsing error return
163              * vmFromString(Resources.getMessage("TLV_PARAMETER_ERROR",
164              * EXP_ATT_PARAM)); }
165              */
166 
167             DefaultHandler h = new Handler();
168 
169             // parse the page
170             SAXParserFactory f = SAXParserFactory.newInstance();
171             f.setValidating(false);
172             f.setNamespaceAware(true);
173             SAXParser p = f.newSAXParser();
174             p.parse(page.getInputStream(), h);
175 
176             if (messageVector.size() == 0) {
177                 return null;
178             } else {
179                 return vmFromVector(messageVector);
180             }
181         } catch (SAXException ex) {
182             return vmFromString(ex.toString());
183         } catch (ParserConfigurationException ex) {
184             return vmFromString(ex.toString());
185         } catch (IOException ex) {
186             return vmFromString(ex.toString());
187         }
188     }
189 
190     /*
191      * // delegate validation to the appropriate expression language private
192      * String validateExpression(String elem, String att, String expr) { //
193      * let's just use the cache kept by the ExpressionEvaluatorManager
194      * ExpressionEvaluator current; try { current =
195      * ExpressionEvaluatorManager.getEvaluatorByName(
196      * ExpressionEvaluatorManager.EVALUATOR_CLASS); } catch (JspException ex) { //
197      * (using JspException here feels ugly, but it's what EEM uses) return
198      * ex.getMessage(); } String response = current.validate(att, expr); return
199      * (response == null) ? null : "tag = '" + elem + "' / attribute = '" + att +
200      * "': " + response; }
201      */
202 
203     // utility methods to help us match elements in our tagset
204     private boolean isTag(String tagUri, String tagLn, String matchUri,
205             String matchLn) {
206         if (tagUri == null || tagLn == null || matchUri == null
207                 || matchLn == null) {
208             return false;
209         }
210         // match beginning of URI since some suffix *_rt tags can
211         // be nested in EL enabled tags as defined by the spec
212         if (tagUri.length() > matchUri.length()) {
213             return (tagUri.startsWith(matchUri) && tagLn.equals(matchLn));
214         } else {
215             return (matchUri.startsWith(tagUri) && tagLn.equals(matchLn));
216         }
217     }
218 
219 //    private boolean isJspTag(String tagUri, String tagLn, String target) {
220 //        return isTag(tagUri, tagLn, JSP, target);
221 //    }
222 
223     private boolean isJodaTag(String tagUri, String tagLn, String target) {
224         return isTag(tagUri, tagLn, this.uri, target);
225     }
226 
227     // utility method to determine if an attribute exists
228     private boolean hasAttribute(Attributes a, String att) {
229         return (a.getValue(att) != null);
230     }
231 
232     /*
233      * method to assist with failure [ as if it's not easy enough already :-) ]
234      */
235     private void fail(String message) {
236 //        failed = true;
237         messageVector.add(new ValidationMessage(lastElementId, message));
238     }
239 
240 //    // returns true if the given attribute name is specified, false otherwise
241 //    private boolean isSpecified(TagData data, String attributeName) {
242 //        return (data.getAttribute(attributeName) != null);
243 //    }
244 
245     // returns true if the 'scope' attribute is valid
246     protected boolean hasNoInvalidScope(Attributes a) {
247         String scope = a.getValue(SCOPE);
248         if ((scope != null) && !scope.equals(PAGE_SCOPE)
249                 && !scope.equals(REQUEST_SCOPE) && !scope.equals(SESSION_SCOPE)
250                 && !scope.equals(APPLICATION_SCOPE)) {
251             return false;
252         }
253         return true;
254     }
255 
256     // returns true if the 'var' attribute is empty
257     protected boolean hasEmptyVar(Attributes a) {
258         return "".equals(a.getValue(VAR));
259     }
260 
261     // returns true if the 'scope' attribute is present without 'var'
262     protected boolean hasDanglingScope(Attributes a) {
263         return (a.getValue(SCOPE) != null && a.getValue(VAR) == null);
264     }
265 
266     // retrieves the local part of a QName
267     protected String getLocalPart(String qname) {
268         int colon = qname.indexOf(":");
269         return (colon == -1) ? qname : qname.substring(colon + 1);
270     }
271 
272     // parses our configuration parameter for element:attribute pairs
273     /*
274      * private void configure(String info) { // construct our configuration map
275      * config = new HashMap();
276      *  // leave the map empty if we have nothing to configure if (info == null) {
277      * return; }
278      *  // separate parameter into space-separated tokens and store them
279      * StringTokenizer st = new StringTokenizer(info); while
280      * (st.hasMoreTokens()) { String pair = st.nextToken(); StringTokenizer
281      * pairTokens = new StringTokenizer(pair, ":"); String element =
282      * pairTokens.nextToken(); String attribute = pairTokens.nextToken(); Object
283      * atts = config.get(element); if (atts == null) { atts = new HashSet();
284      * config.put(element, atts); } ((Set) atts).add(attribute); } }
285      */
286 
287     // constructs a ValidationMessage[] from a single String and no ID
288     private static ValidationMessage[] vmFromString(String message) {
289         return new ValidationMessage[] { new ValidationMessage(null, message) };
290     }
291 
292     // constructs a ValidationMessage[] from a ValidationMessage Vector
293     private static ValidationMessage[] vmFromVector(Vector v) {
294         ValidationMessage[] vm = new ValidationMessage[v.size()];
295         for (int i = 0; i < vm.length; i++) {
296             vm[i] = (ValidationMessage) v.get(i);
297         }
298         return vm;
299     }
300 
301     /**
302      * SAX event handler.
303      */
304     private class Handler extends DefaultHandler {
305         // parser state
306         private int depth = 0;
307 
308         private String lastElementName = null;
309 
310         private boolean bodyNecessary = false;
311 
312         private boolean bodyIllegal = false;
313 
314         // process under the existing context (state), then modify it
315         public void startElement(String ns, String ln, String qn, Attributes a) {
316             // substitute our own parsed 'ln' if it's not provided
317             if (ln == null) {
318                 ln = getLocalPart(qn);
319             }
320 
321             // for simplicity, we can ignore <jsp:text> for our purposes
322             // (don't bother distinguishing between it and its characters)
323             if (qn.equals(JSP_TEXT)) {
324                 return;
325             }
326 
327             // check body-related constraint
328             if (bodyIllegal) {
329                 fail(Resources.getMessage("TLV_ILLEGAL_BODY", lastElementName));
330             }
331 
332             // validate expression syntax if we need to
333             /*
334              * Set expAtts; if (qn.startsWith(prefix + ":") && (expAtts = (Set)
335              * config.get(ln)) != null) { for (int i = 0; i < a.getLength();
336              * i++) { String attName = a.getLocalName(i); if
337              * (expAtts.contains(attName)) { String vMsg =
338              * validateExpression(ln, attName, a.getValue(i)); if (vMsg != null) {
339              * fail(vMsg); } } } }
340              */
341 
342             // validate attributes
343             if (qn.startsWith(prefix + ":") && !hasNoInvalidScope(a)) {
344                 fail(Resources.getMessage("TLV_INVALID_ATTRIBUTE", SCOPE, qn, a
345                         .getValue(SCOPE)));
346             }
347             if (qn.startsWith(prefix + ":") && hasEmptyVar(a)) {
348                 fail(Resources.getMessage("TLV_EMPTY_VAR", qn));
349             }
350             if (qn.startsWith(prefix + ":")
351                     && !isJodaTag(ns, ln, SET_DATETIMEZONE)
352                     && hasDanglingScope(a)) {
353                 fail(Resources.getMessage("TLV_DANGLING_SCOPE", qn));
354             }
355 
356             // now, modify state
357 
358             // set up a check against illegal attribute/body combinations
359             bodyIllegal = false;
360             bodyNecessary = false;
361             if (isJodaTag(ns, ln, PARSE_DATETIME)) {
362                 if (hasAttribute(a, VALUE)) {
363                     bodyIllegal = true;
364                 } else {
365                     bodyNecessary = true;
366                 }
367             }
368 
369             // record the most recent tag (for error reporting)
370             lastElementName = qn;
371             lastElementId = a.getValue(JSP, "id");
372 
373             // we're a new element, so increase depth
374             depth++;
375         }
376 
377         public void characters(char[] ch, int start, int length) {
378             bodyNecessary = false; // body is no longer necessary!
379 
380             // ignore strings that are just whitespace
381             String s = new String(ch, start, length).trim();
382             if (s.equals("")) {
383                 return;
384             }
385 
386             // check and update body-related constraints
387             if (bodyIllegal) {
388                 fail(Resources.getMessage("TLV_ILLEGAL_BODY", lastElementName));
389             }
390         }
391 
392         public void endElement(String ns, String ln, String qn) {
393             // consistently, we ignore JSP_TEXT
394             if (qn.equals(JSP_TEXT)) {
395                 return;
396             }
397 
398             // handle body-related invariant
399             if (bodyNecessary) {
400                 fail(Resources.getMessage("TLV_MISSING_BODY", lastElementName));
401             }
402             bodyIllegal = false; // reset: we've left the tag
403 
404             // update our depth
405             depth--;
406         }
407     }
408 
409 }