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 }