001 /*
002 * Copyright 1999-2004 The Apache Software Foundation.
003 * Modifications, Copyright 2005 Stephen Colebourne
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.joda.time.contrib.jsptag;
018
019 import java.io.IOException;
020 import java.util.Vector;
021
022 import javax.servlet.jsp.tagext.PageData;
023 import javax.servlet.jsp.tagext.TagLibraryValidator;
024 import javax.servlet.jsp.tagext.ValidationMessage;
025 import javax.xml.parsers.ParserConfigurationException;
026 import javax.xml.parsers.SAXParser;
027 import javax.xml.parsers.SAXParserFactory;
028
029 import org.xml.sax.Attributes;
030 import org.xml.sax.SAXException;
031 import org.xml.sax.helpers.DefaultHandler;
032
033 /**
034 * <p>
035 * A SAX-based TagLibraryValidator for the Joda tags. Currently implements the
036 * following checks:
037 * </p>
038 *
039 * <ul>
040 * <li>Tag bodies that must either be empty or non-empty given particular
041 * attributes.</li>
042 * <li>Expression syntax validation (NOTE: this has been disabled; per my
043 * understanding, it shouldn't be needed in JSP 2.0+ containers; see notes in
044 * source code for more information).
045 * </ul>
046 *
047 * @author Shawn Bayern
048 * @author Jan Luehe
049 * @author Jim Newsham
050 */
051 public class JodaTagLibraryValidator extends TagLibraryValidator {
052
053 /*
054 * Expression syntax validation has been disabled since when I ported this
055 * code over from Jakarta Taglib, I wanted to reduce dependencies. As I
056 * understand it, JSP 2.0 containers take over the responsibility of
057 * handling EL code (both in attribute tags, and externally), so this
058 * shouldn't be a problem unless you're using something old. If you want to
059 * restore this validation, you must uncomment the various lines in this
060 * source, include the Jakarta Taglib's standard.jar library at build and
061 * runtime, and (I believe, but don't know specifically) make a legacy-style
062 * tld which describes which attributes should be validated. Have a look at
063 * fmt.tld, fmt-1.0.tld, fmt-1.0-rt.tld in standard.jar for an example of
064 * this.
065 */
066
067 // *********************************************************************
068 // Implementation Overview
069 /*
070 * We essentially just run the page through a SAX parser, handling the
071 * callbacks that interest us. We collapse <jsp:text> elements into the text
072 * they contain, since this simplifies processing somewhat. Even a quick
073 * glance at the implementation shows its necessary, tree-oriented nature:
074 * multiple Stacks, an understanding of 'depth', and so on all are important
075 * as we recover necessary state upon each callback. This TLV demonstrates
076 * various techniques, from the general "how do I use a SAX parser for a
077 * TLV?" to "how do I read my init parameters and then validate?" But also,
078 * the specific SAX methodology was kept as general as possible to allow for
079 * experimentation and flexibility.
080 */
081
082 // *********************************************************************
083 // Constants
084 // tag names
085 private static final String SET_DATETIMEZONE = "setDateTimeZone";
086
087 private static final String PARSE_DATETIME = "parseDateTime";
088
089 private static final String JSP_TEXT = "jsp:text";
090
091 // attribute names
092 private static final String VALUE = "value";
093
094 // parameter names
095 // private final String EXP_ATT_PARAM = "expressionAttributes";
096
097 // attributes
098 private static final String VAR = "var";
099
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 }