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 }