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    }