001    // Copyright 2009, 2011 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry5.internal.services;
016    
017    import org.apache.tapestry5.internal.parser.*;
018    import org.apache.tapestry5.ioc.Location;
019    import org.apache.tapestry5.ioc.Resource;
020    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
021    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
022    import org.apache.tapestry5.ioc.internal.util.TapestryException;
023    
024    import javax.xml.namespace.QName;
025    import java.net.URL;
026    import java.util.List;
027    import java.util.Map;
028    import java.util.Set;
029    import java.util.regex.Matcher;
030    import java.util.regex.Pattern;
031    
032    import static org.apache.tapestry5.internal.services.SaxTemplateParser.Version.*;
033    
034    /**
035     * SAX-based template parser logic, taking a {@link Resource} to a Tapestry
036     * template file and returning
037     * a {@link ComponentTemplate}.
038     * <p/>
039     * Earlier versions of this code used the StAX (streaming XML parser), but that
040     * was really, really bad for Google App Engine. This version uses SAX under the
041     * covers, but kind of replicates the important bits of the StAX API as
042     * {@link XMLTokenStream}.
043     *
044     * @since 5.2.0
045     */
046    @SuppressWarnings(
047            {"JavaDoc"})
048    public class SaxTemplateParser
049    {
050        private static final String MIXINS_ATTRIBUTE_NAME = "mixins";
051    
052        private static final String TYPE_ATTRIBUTE_NAME = "type";
053    
054        private static final String ID_ATTRIBUTE_NAME = "id";
055    
056        public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
057    
058        private static final Map<String, Version> NAMESPACE_URI_TO_VERSION = CollectionFactory.newMap();
059    
060        {
061            NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_0_0.xsd", T_5_0);
062            NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_1_0.xsd", T_5_1);
063            NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_3.xsd", T_5_3);
064        }
065    
066        /**
067         * Special namespace used to denote Block parameters to components, as a
068         * (preferred) alternative to the t:parameter
069         * element. The simple element name is the name of the parameter.
070         */
071        private static final String TAPESTRY_PARAMETERS_URI = "tapestry:parameter";
072    
073        /**
074         * URI prefix used to identify a Tapestry library, the remainder of the URI
075         * becomes a prefix on the element name.
076         */
077        private static final String LIB_NAMESPACE_URI_PREFIX = "tapestry-library:";
078    
079        /**
080         * Pattern used to parse the path portion of the library namespace URI. A
081         * series of simple identifiers with slashes
082         * allowed as seperators.
083         */
084    
085        private static final Pattern LIBRARY_PATH_PATTERN = Pattern.compile("^[a-z]\\w*(/[a-z]\\w*)*$",
086                Pattern.CASE_INSENSITIVE);
087    
088        private static final Pattern ID_PATTERN = Pattern.compile("^[a-z]\\w*$",
089                Pattern.CASE_INSENSITIVE);
090    
091        /**
092         * Any amount of mixed simple whitespace (space, tab, form feed) mixed with
093         * at least one carriage return or line
094         * feed, followed by any amount of whitespace. Will be reduced to a single
095         * linefeed.
096         */
097        private static final Pattern REDUCE_LINEBREAKS_PATTERN = Pattern.compile(
098                "[ \\t\\f]*[\\r\\n]\\s*", Pattern.MULTILINE);
099    
100        /**
101         * Used when compressing whitespace, matches any sequence of simple
102         * whitespace (space, tab, formfeed). Applied after
103         * REDUCE_LINEBREAKS_PATTERN.
104         */
105        private static final Pattern REDUCE_WHITESPACE_PATTERN = Pattern.compile("[ \\t\\f]+",
106                Pattern.MULTILINE);
107    
108        // Note the use of the non-greedy modifier; this prevents the pattern from
109        // merging multiple
110        // expansions on the same text line into a single large
111        // but invalid expansion.
112    
113        private static final Pattern EXPANSION_PATTERN = Pattern.compile("\\$\\{\\s*(((?!\\$\\{).)*)\\s*}");
114        private static final char EXPANSION_STRING_DELIMITTER = '\'';
115        private static final char OPEN_BRACE = '{';
116        private static final char CLOSE_BRACE = '}';
117    
118        private static final Set<String> MUST_BE_ROOT = CollectionFactory.newSet("extend", "container");
119    
120        private final Resource resource;
121    
122        private final XMLTokenStream tokenStream;
123    
124        private final StringBuilder textBuffer = new StringBuilder();
125    
126        private final List<TemplateToken> tokens = CollectionFactory.newList();
127    
128        // This starts pointing at tokens but occasionally shifts to a list inside
129        // the overrides Map.
130        private List<TemplateToken> tokenAccumulator = tokens;
131    
132        /**
133         * Primarily used as a set of componentIds (to check for duplicates and
134         * conflicts).
135         */
136        private final Map<String, Location> componentIds = CollectionFactory.newCaseInsensitiveMap();
137    
138        /**
139         * Map from override id to a list of tokens; this actually works both for
140         * overrides defined by this template and
141         * overrides provided by this template.
142         */
143        private Map<String, List<TemplateToken>> overrides;
144    
145        private boolean extension;
146    
147        private Location textStartLocation;
148    
149        private boolean active = true;
150    
151        private final Map<String, Boolean> extensionPointIdSet = CollectionFactory.newCaseInsensitiveMap();
152    
153        public SaxTemplateParser(Resource resource, Map<String, URL> publicIdToURL)
154        {
155            this.resource = resource;
156            this.tokenStream = new XMLTokenStream(resource, publicIdToURL);
157        }
158    
159        public ComponentTemplate parse(boolean compressWhitespace)
160        {
161            try
162            {
163                tokenStream.parse();
164    
165                TemplateParserState initialParserState = new TemplateParserState()
166                        .compressWhitespace(compressWhitespace);
167    
168                root(initialParserState);
169    
170                return new ComponentTemplateImpl(resource, tokens, componentIds, extension, overrides);
171            } catch (Exception ex)
172            {
173                throw new TapestryException(String.format("Failure parsing template %s: %s", resource,
174                        InternalUtils.toMessage(ex)), tokenStream.getLocation(), ex);
175            }
176    
177        }
178    
179        void root(TemplateParserState state)
180        {
181            while (active && tokenStream.hasNext())
182            {
183                switch (tokenStream.next())
184                {
185                    case DTD:
186    
187                        dtd();
188    
189                        break;
190    
191                    case START_ELEMENT:
192    
193                        rootElement(state);
194    
195                        break;
196    
197                    case END_DOCUMENT:
198                        // Ignore it.
199                        break;
200    
201                    default:
202                        textContent(state);
203                }
204            }
205        }
206    
207        private void rootElement(TemplateParserState initialState)
208        {
209            TemplateParserState state = setupForElement(initialState);
210    
211            String uri = tokenStream.getNamespaceURI();
212            String name = tokenStream.getLocalName();
213            Version version = NAMESPACE_URI_TO_VERSION.get(uri);
214    
215            if (T_5_1.before(version))
216            {
217                if (name.equalsIgnoreCase("extend"))
218                {
219                    extend(state);
220                    return;
221                }
222            }
223    
224            if (version != null)
225            {
226                if (name.equalsIgnoreCase("container"))
227                {
228                    container(state);
229                    return;
230                }
231            }
232    
233            element(state);
234        }
235    
236        private void extend(TemplateParserState state)
237        {
238            extension = true;
239    
240            while (active)
241            {
242                switch (tokenStream.next())
243                {
244                    case START_ELEMENT:
245    
246                        if (T_5_1.before(NAMESPACE_URI_TO_VERSION.get(tokenStream.getNamespaceURI()))
247                                && tokenStream.getLocalName().equalsIgnoreCase("replace"))
248                        {
249                            replace(state);
250                            break;
251                        }
252    
253                        throw new RuntimeException("Child element of <extend> must be <replace>.");
254    
255                    case END_ELEMENT:
256    
257                        return;
258    
259                    // Ignore spaces and characters inside <extend>.
260    
261                    case COMMENT:
262                    case SPACE:
263                        break;
264    
265                    // Other content (characters, etc.) are forbidden.
266    
267                    case CHARACTERS:
268                        if (InternalUtils.isBlank(tokenStream.getText()))
269                            break;
270    
271                    default:
272                        unexpectedEventType();
273                }
274            }
275        }
276    
277        private void replace(TemplateParserState state)
278        {
279            String id = getRequiredIdAttribute();
280    
281            addContentToOverride(setupForElement(state), id);
282        }
283    
284        private void unexpectedEventType()
285        {
286            XMLTokenType eventType = tokenStream.getEventType();
287    
288            throw new IllegalStateException(String.format("Unexpected XML parse event %s.", eventType
289                    .name()));
290        }
291    
292        private void dtd()
293        {
294            DTDData dtdInfo = tokenStream.getDTDInfo();
295    
296            tokenAccumulator.add(new DTDToken(dtdInfo.rootName, dtdInfo.publicId, dtdInfo
297                    .systemId, getLocation()));
298        }
299    
300        private Location getLocation()
301        {
302            return tokenStream.getLocation();
303        }
304    
305        /**
306         * Processes an element through to its matching end tag.
307         * <p/>
308         * An element can be:
309         * <p/>
310         * a Tapestry component via &lt;t:type&gt;
311         * <p/>
312         * a Tapestry component via t:type="type" and/or t:id="id"
313         * <p/>
314         * a Tapestry component via a library namespace
315         * <p/>
316         * A parameter element via &lt;t:parameter&gt;
317         * <p/>
318         * A parameter element via &lt;p:name&gt;
319         * <p/>
320         * A &lt;t:remove&gt; element (in the 5.1 schema)
321         * <p/>
322         * A &lt;t:content&gt; element (in the 5.1 schema)
323         * <p/>
324         * A &lt;t:block&gt; element
325         * <p/>
326         * The body &lt;t:body&gt;
327         * <p/>
328         * An ordinary element
329         */
330        void element(TemplateParserState initialState)
331        {
332            TemplateParserState state = setupForElement(initialState);
333    
334            String uri = tokenStream.getNamespaceURI();
335            String name = tokenStream.getLocalName();
336            Version version = NAMESPACE_URI_TO_VERSION.get(uri);
337    
338            if (T_5_1.before(version))
339            {
340    
341                if (name.equalsIgnoreCase("remove"))
342                {
343                    removeContent();
344    
345                    return;
346                }
347    
348                if (name.equalsIgnoreCase("content"))
349                {
350                    limitContent(state);
351    
352                    return;
353                }
354    
355                if (name.equalsIgnoreCase("extension-point"))
356                {
357                    extensionPoint(state);
358    
359                    return;
360                }
361    
362                if (name.equalsIgnoreCase("replace"))
363                {
364                    throw new RuntimeException(
365                            "The <replace> element may only appear directly within an extend element.");
366                }
367    
368                if (MUST_BE_ROOT.contains(name))
369                    mustBeRoot(name);
370            }
371    
372            if (version != null)
373            {
374    
375                if (name.equalsIgnoreCase("body"))
376                {
377                    body();
378                    return;
379                }
380    
381                if (name.equalsIgnoreCase("container"))
382                {
383                    mustBeRoot(name);
384                }
385    
386                if (name.equalsIgnoreCase("block"))
387                {
388                    block(state);
389                    return;
390                }
391    
392                if (name.equalsIgnoreCase("parameter"))
393                {
394                    if (T_5_3.before(version))
395                    {
396                        throw new RuntimeException(
397                                String.format("The <parameter> element has been deprecated in Tapestry 5.3 in favour of '%s' namespace.", TAPESTRY_PARAMETERS_URI));
398                    }
399    
400                    classicParameter(state);
401    
402                    return;
403                }
404    
405                possibleTapestryComponent(state, null, tokenStream.getLocalName().replace('.', '/'));
406    
407                return;
408            }
409    
410            if (uri != null && uri.startsWith(LIB_NAMESPACE_URI_PREFIX))
411            {
412                libraryNamespaceComponent(state);
413    
414                return;
415            }
416    
417            if (TAPESTRY_PARAMETERS_URI.equals(uri))
418            {
419                parameterElement(state);
420    
421                return;
422            }
423    
424            // Just an ordinary element ... unless it has t:id or t:type
425    
426            possibleTapestryComponent(state, tokenStream.getLocalName(), null);
427        }
428    
429        /**
430         * Processes a body of an element including text and (recursively) nested
431         * elements. Adds an
432         * {@link org.apache.tapestry5.internal.parser.TokenType#END_ELEMENT} token
433         * before returning.
434         *
435         * @param state
436         */
437        private void processBody(TemplateParserState state)
438        {
439            while (active)
440            {
441                switch (tokenStream.next())
442                {
443                    case START_ELEMENT:
444    
445                        // The recursive part: when we see a new element start.
446    
447                        element(state);
448                        break;
449    
450                    case END_ELEMENT:
451    
452                        // At the end of an element, we're done and can return.
453                        // This is the matching end element for the start element
454                        // that invoked this method.
455    
456                        endElement(state);
457    
458                        return;
459    
460                    default:
461                        textContent(state);
462                }
463            }
464        }
465    
466        private TemplateParserState setupForElement(TemplateParserState initialState)
467        {
468            processTextBuffer(initialState);
469    
470            return checkForXMLSpaceAttribute(initialState);
471        }
472    
473        /**
474         * Handles an extension point, putting a RenderExtension token in position
475         * in the template.
476         *
477         * @param state
478         */
479        private void extensionPoint(TemplateParserState state)
480        {
481            // An extension point adds a token that represents where the override
482            // (either the default
483            // provided in the parent template, or the true override from a child
484            // template) is positioned.
485    
486            String id = getRequiredIdAttribute();
487    
488            if (extensionPointIdSet.containsKey(id))
489            {
490                throw new TapestryException(String.format("Extension point '%s' is already defined for this template. Extension point ids must be unique.", id), getLocation(), null);
491            } else
492            {
493                extensionPointIdSet.put(id, true);
494            }
495    
496            tokenAccumulator.add(new ExtensionPointToken(id, getLocation()));
497    
498            addContentToOverride(state.insideComponent(false), id);
499        }
500    
501        private String getRequiredIdAttribute()
502        {
503            String id = getSingleParameter("id");
504    
505            if (InternalUtils.isBlank(id))
506                throw new RuntimeException(String.format("The <%s> element must have an id attribute.",
507                        tokenStream.getLocalName()));
508    
509            return id;
510        }
511    
512        private void addContentToOverride(TemplateParserState state, String id)
513    
514        {
515            List<TemplateToken> savedTokenAccumulator = tokenAccumulator;
516    
517            tokenAccumulator = CollectionFactory.newList();
518    
519            // TODO: id should probably be unique; i.e., you either define an
520            // override or you
521            // provide an override, but you don't do both in the same template.
522    
523            if (overrides == null)
524                overrides = CollectionFactory.newCaseInsensitiveMap();
525    
526            overrides.put(id, tokenAccumulator);
527    
528            while (active)
529            {
530                switch (tokenStream.next())
531                {
532                    case START_ELEMENT:
533                        element(state);
534                        break;
535    
536                    case END_ELEMENT:
537    
538                        processTextBuffer(state);
539    
540                        // Restore everthing to how it was before the
541                        // extention-point was reached.
542    
543                        tokenAccumulator = savedTokenAccumulator;
544                        return;
545    
546                    default:
547                        textContent(state);
548                }
549            }
550        }
551    
552        private void mustBeRoot(String name)
553        {
554            throw new RuntimeException(String.format(
555                    "Element <%s> is only valid as the root element of a template.", name));
556        }
557    
558        /**
559         * Triggered by &lt;t:content&gt; element; limits template content to just
560         * what's inside.
561         */
562    
563        private void limitContent(TemplateParserState state)
564        {
565            if (state.isCollectingContent())
566                throw new IllegalStateException(
567                        "The <content> element may not be nested within another <content> element.");
568    
569            TemplateParserState newState = state.collectingContent().insideComponent(false);
570    
571            // Clear out any tokens that precede the <t:content> element
572    
573            tokens.clear();
574    
575            // I'm not happy about this; you really shouldn't define overrides just
576            // to clear them out,
577            // but it is consistent. Perhaps this should be an error if overrides is
578            // non-empty.
579    
580            overrides = null;
581    
582            // Make sure that if the <t:content> appears inside a <t:replace> or
583            // <t:extension-point>, that
584            // it is still handled correctly.
585    
586            tokenAccumulator = tokens;
587    
588            while (active)
589            {
590                switch (tokenStream.next())
591                {
592                    case START_ELEMENT:
593                        element(newState);
594                        break;
595    
596                    case END_ELEMENT:
597    
598                        // The active flag is global, once we hit it, the entire
599                        // parse is aborted, leaving
600                        // tokens with just tokens defined inside <t:content>.
601    
602                        active = false;
603    
604                        break;
605    
606                    default:
607                        textContent(state);
608                }
609            }
610    
611        }
612    
613        private void removeContent()
614        {
615            int depth = 1;
616    
617            while (active)
618            {
619                switch (tokenStream.next())
620                {
621                    case START_ELEMENT:
622                        depth++;
623                        break;
624    
625                    // The matching end element.
626    
627                    case END_ELEMENT:
628                        depth--;
629    
630                        if (depth == 0)
631                            return;
632    
633                        break;
634    
635                    default:
636                        // Ignore anything else (text, comments, etc.)
637                }
638            }
639        }
640    
641        private String nullForBlank(String input)
642        {
643            return InternalUtils.isBlank(input) ? null : input;
644        }
645    
646        /**
647         * Added in release 5.1.
648         */
649        private void libraryNamespaceComponent(TemplateParserState state)
650        {
651            String uri = tokenStream.getNamespaceURI();
652    
653            // The library path is encoded into the namespace URI.
654    
655            String path = uri.substring(LIB_NAMESPACE_URI_PREFIX.length());
656    
657            if (!LIBRARY_PATH_PATTERN.matcher(path).matches())
658                throw new RuntimeException(ServicesMessages.invalidPathForLibraryNamespace(uri));
659    
660            possibleTapestryComponent(state, null, path + "/" + tokenStream.getLocalName());
661        }
662    
663        /**
664         * @param elementName
665         * @param identifiedType the type of the element, usually null, but may be the
666         *                       component type derived from element
667         */
668        private void possibleTapestryComponent(TemplateParserState state, String elementName,
669                                               String identifiedType)
670        {
671            String id = null;
672            String type = identifiedType;
673            String mixins = null;
674    
675            int count = tokenStream.getAttributeCount();
676    
677            Location location = getLocation();
678    
679            List<TemplateToken> attributeTokens = CollectionFactory.newList();
680    
681            for (int i = 0; i < count; i++)
682            {
683                QName qname = tokenStream.getAttributeName(i);
684    
685                if (isXMLSpaceAttribute(qname))
686                    continue;
687    
688                // The name will be blank for an xmlns: attribute
689    
690                String localName = qname.getLocalPart();
691    
692                if (InternalUtils.isBlank(localName))
693                    continue;
694    
695                String uri = qname.getNamespaceURI();
696    
697                String value = tokenStream.getAttributeValue(i);
698    
699                if (NAMESPACE_URI_TO_VERSION.containsKey(uri))
700                {
701                    if (localName.equalsIgnoreCase(ID_ATTRIBUTE_NAME))
702                    {
703                        id = nullForBlank(value);
704    
705                        validateId(id, "invalid-component-id");
706    
707                        continue;
708                    }
709    
710                    if (type == null && localName.equalsIgnoreCase(TYPE_ATTRIBUTE_NAME))
711                    {
712                        type = nullForBlank(value);
713                        continue;
714                    }
715    
716                    if (localName.equalsIgnoreCase(MIXINS_ATTRIBUTE_NAME))
717                    {
718                        mixins = nullForBlank(value);
719                        continue;
720                    }
721    
722                    // Anything else is the name of a Tapestry component parameter
723                    // that is simply
724                    // not part of the template's doctype for the element being
725                    // instrumented.
726                }
727    
728                attributeTokens.add(new AttributeToken(uri, localName, value, location));
729            }
730    
731            boolean isComponent = (id != null || type != null);
732    
733            // If provided t:mixins but not t:id or t:type, then its not quite a
734            // component
735    
736            if (mixins != null && !isComponent)
737                throw new TapestryException(ServicesMessages.mixinsInvalidWithoutIdOrType(elementName),
738                        location, null);
739    
740            if (isComponent)
741            {
742                tokenAccumulator.add(new StartComponentToken(elementName, id, type, mixins, location));
743            } else
744            {
745                tokenAccumulator.add(new StartElementToken(tokenStream.getNamespaceURI(), elementName,
746                        location));
747            }
748    
749            addDefineNamespaceTokens();
750    
751            tokenAccumulator.addAll(attributeTokens);
752    
753            if (id != null)
754                componentIds.put(id, location);
755    
756            processBody(state.insideComponent(isComponent));
757        }
758    
759        private void addDefineNamespaceTokens()
760        {
761            for (int i = 0; i < tokenStream.getNamespaceCount(); i++)
762            {
763                String uri = tokenStream.getNamespaceURI(i);
764    
765                // These URIs are strictly part of the server-side Tapestry template
766                // and are not ever sent to the client.
767    
768                if (NAMESPACE_URI_TO_VERSION.containsKey(uri))
769                    continue;
770    
771                if (uri.equals(TAPESTRY_PARAMETERS_URI))
772                    continue;
773    
774                if (uri.startsWith(LIB_NAMESPACE_URI_PREFIX))
775                    continue;
776    
777                tokenAccumulator.add(new DefineNamespacePrefixToken(uri, tokenStream
778                        .getNamespacePrefix(i), getLocation()));
779            }
780        }
781    
782        private TemplateParserState checkForXMLSpaceAttribute(TemplateParserState state)
783        {
784            for (int i = 0; i < tokenStream.getAttributeCount(); i++)
785            {
786                QName qName = tokenStream.getAttributeName(i);
787    
788                if (isXMLSpaceAttribute(qName))
789                {
790                    boolean compress = !"preserve".equals(tokenStream.getAttributeValue(i));
791    
792                    return state.compressWhitespace(compress);
793                }
794            }
795    
796            return state;
797        }
798    
799        /**
800         * Processes the text buffer and then adds an end element token.
801         */
802        private void endElement(TemplateParserState state)
803        {
804            processTextBuffer(state);
805    
806            tokenAccumulator.add(new EndElementToken(getLocation()));
807        }
808    
809        /**
810         * Handler for Tapestry 5.0's "classic" &lt;t:parameter&gt; element. This
811         * turns into a {@link org.apache.tapestry5.internal.parser.ParameterToken}
812         * and the body and end element are provided normally.
813         */
814        private void classicParameter(TemplateParserState state)
815        {
816            String parameterName = getSingleParameter("name");
817    
818            if (InternalUtils.isBlank(parameterName))
819                throw new TapestryException(ServicesMessages.parameterElementNameRequired(),
820                        getLocation(), null);
821    
822            ensureParameterWithinComponent(state);
823    
824            tokenAccumulator.add(new ParameterToken(parameterName, getLocation()));
825    
826            processBody(state.insideComponent(false));
827        }
828    
829        private void ensureParameterWithinComponent(TemplateParserState state)
830        {
831            if (!state.isInsideComponent())
832                throw new RuntimeException(
833                        "Block parameters are only allowed directly within component elements.");
834        }
835    
836        /**
837         * Tapestry 5.1 uses a special namespace (usually mapped to "p:") and the
838         * name becomes the parameter element.
839         */
840        private void parameterElement(TemplateParserState state)
841        {
842            ensureParameterWithinComponent(state);
843    
844            if (tokenStream.getAttributeCount() > 0)
845                throw new TapestryException(ServicesMessages.parameterElementDoesNotAllowAttributes(),
846                        getLocation(), null);
847    
848            tokenAccumulator.add(new ParameterToken(tokenStream.getLocalName(), getLocation()));
849    
850            processBody(state.insideComponent(false));
851        }
852    
853        /**
854         * Checks that a body element is empty. Returns after the body's close
855         * element. Adds a single body token (but not an
856         * end token).
857         */
858        private void body()
859        {
860            tokenAccumulator.add(new BodyToken(getLocation()));
861    
862            while (active)
863            {
864                switch (tokenStream.next())
865                {
866                    case END_ELEMENT:
867                        return;
868    
869                    default:
870                        throw new IllegalStateException(ServicesMessages
871                                .contentInsideBodyNotAllowed(getLocation()));
872                }
873            }
874        }
875    
876        /**
877         * Driven by the &lt;t:container&gt; element, this state adds elements for
878         * its body but not its start or end tags.
879         *
880         * @param state
881         */
882        private void container(TemplateParserState state)
883        {
884            while (active)
885            {
886                switch (tokenStream.next())
887                {
888                    case START_ELEMENT:
889                        element(state);
890                        break;
891    
892                    // The matching end-element for the container. Don't add a
893                    // token.
894    
895                    case END_ELEMENT:
896    
897                        processTextBuffer(state);
898    
899                        return;
900    
901                    default:
902                        textContent(state);
903                }
904            }
905        }
906    
907        /**
908         * A block adds a token for its start tag and end tag and allows any content
909         * within.
910         */
911        private void block(TemplateParserState state)
912        {
913            String blockId = getSingleParameter("id");
914    
915            validateId(blockId, "invalid-block-id");
916    
917            tokenAccumulator.add(new BlockToken(blockId, getLocation()));
918    
919            processBody(state.insideComponent(false));
920        }
921    
922        private String getSingleParameter(String attributeName)
923        {
924            String result = null;
925    
926            for (int i = 0; i < tokenStream.getAttributeCount(); i++)
927            {
928                QName qName = tokenStream.getAttributeName(i);
929    
930                if (isXMLSpaceAttribute(qName))
931                    continue;
932    
933                if (qName.getLocalPart().equalsIgnoreCase(attributeName))
934                {
935                    result = tokenStream.getAttributeValue(i);
936                    continue;
937                }
938    
939                // Only the named attribute is allowed.
940    
941                throw new TapestryException(ServicesMessages.undefinedTapestryAttribute(tokenStream
942                        .getLocalName(), qName.toString(), attributeName), getLocation(), null);
943            }
944    
945            return result;
946        }
947    
948        private void validateId(String id, String messageKey)
949        {
950            if (id == null)
951                return;
952    
953            if (ID_PATTERN.matcher(id).matches())
954                return;
955    
956            // Not a match.
957    
958            throw new TapestryException(ServicesMessages.invalidId(messageKey, id), getLocation(), null);
959        }
960    
961        private boolean isXMLSpaceAttribute(QName qName)
962        {
963            return XML_NAMESPACE_URI.equals(qName.getNamespaceURI())
964                    && "space".equals(qName.getLocalPart());
965        }
966    
967        /**
968         * Processes text content if in the correct state, or throws an exception.
969         * This is used as a default for matching
970         * case statements.
971         *
972         * @param state
973         */
974        private void textContent(TemplateParserState state)
975        {
976            switch (tokenStream.getEventType())
977            {
978                case COMMENT:
979                    comment(state);
980                    break;
981    
982                case CDATA:
983                    cdata(state);
984                    break;
985    
986                case CHARACTERS:
987                case SPACE:
988                    characters();
989                    break;
990    
991                default:
992                    unexpectedEventType();
993            }
994        }
995    
996        private void characters()
997        {
998            if (textStartLocation == null)
999                textStartLocation = getLocation();
1000    
1001            textBuffer.append(tokenStream.getText());
1002        }
1003    
1004        private void cdata(TemplateParserState state)
1005        {
1006            processTextBuffer(state);
1007    
1008            tokenAccumulator.add(new CDATAToken(tokenStream.getText(), getLocation()));
1009        }
1010    
1011        private void comment(TemplateParserState state)
1012        {
1013            processTextBuffer(state);
1014    
1015            String comment = tokenStream.getText();
1016    
1017            tokenAccumulator.add(new CommentToken(comment, getLocation()));
1018        }
1019    
1020        /**
1021         * Processes the accumulated text in the text buffer as a text token.
1022         */
1023        private void processTextBuffer(TemplateParserState state)
1024        {
1025            if (textBuffer.length() != 0)
1026                convertTextBufferToTokens(state);
1027    
1028            textStartLocation = null;
1029        }
1030    
1031        private void convertTextBufferToTokens(TemplateParserState state)
1032        {
1033            String text = textBuffer.toString();
1034    
1035            textBuffer.setLength(0);
1036    
1037            if (state.isCompressWhitespace())
1038            {
1039                text = compressWhitespaceInText(text);
1040    
1041                if (InternalUtils.isBlank(text))
1042                    return;
1043            }
1044    
1045            addTokensForText(text);
1046        }
1047    
1048        /**
1049         * Reduces vertical whitespace to a single newline, then reduces horizontal
1050         * whitespace to a single space.
1051         *
1052         * @param text
1053         * @return compressed version of text
1054         */
1055        private String compressWhitespaceInText(String text)
1056        {
1057            String linebreaksReduced = REDUCE_LINEBREAKS_PATTERN.matcher(text).replaceAll("\n");
1058    
1059            return REDUCE_WHITESPACE_PATTERN.matcher(linebreaksReduced).replaceAll(" ");
1060        }
1061    
1062        /**
1063         * Scans the text, using a regular expression pattern, for expansion
1064         * patterns, and adds appropriate tokens for what
1065         * it finds.
1066         *
1067         * @param text to add as
1068         *             {@link org.apache.tapestry5.internal.parser.TextToken}s and
1069         *             {@link org.apache.tapestry5.internal.parser.ExpansionToken}s
1070         */
1071        private void addTokensForText(String text)
1072        {
1073            Matcher matcher = EXPANSION_PATTERN.matcher(text);
1074    
1075            int startx = 0;
1076    
1077            // The big problem with all this code is that everything gets assigned
1078            // to the
1079            // start of the text block, even if there are line breaks leading up to
1080            // it.
1081            // That's going to take a lot more work and there are bigger fish to
1082            // fry. In addition,
1083            // TAPESTRY-2028 means that the whitespace has likely been stripped out
1084            // of the text
1085            // already anyway.
1086            while (matcher.find())
1087            {
1088                int matchStart = matcher.start();
1089    
1090                if (matchStart != startx)
1091                {
1092                    String prefix = text.substring(startx, matchStart);
1093                    tokenAccumulator.add(new TextToken(prefix, textStartLocation));
1094                }
1095    
1096                // Group 1 includes the real text of the expansion, with whitespace
1097                // around the
1098                // expression (but inside the curly braces) excluded.
1099                // But note that we run into a problem.  The original 
1100                // EXPANSION_PATTERN used a reluctant quantifier to match the 
1101                // smallest instance of ${} possible.  But if you have ${'}'} or 
1102                // ${{'key': 'value'}} (maps, cf TAP5-1605) then you run into issues
1103                // b/c the expansion becomes {'key': 'value' which is wrong.
1104                // A fix to use greedy matching with negative lookahead to prevent 
1105                // ${...}...${...} all matching a single expansion is close, but 
1106                // has issues when an expansion is used inside a javascript function
1107                // (see TAP5-1620). The solution is to use the greedy 
1108                // EXPANSION_PATTERN as before to bound the search for a single 
1109                // expansion, then check for {} consistency, ignoring opening and 
1110                // closing braces that occur within '' (the property expression 
1111                // language doesn't support "" for strings). That should include: 
1112                // 'This string has a } in it' and 'This string has a { in it.'
1113                // Note also that the property expression language doesn't support
1114                // escaping the string character ('), so we don't have to worry 
1115                // about that. 
1116                String expression = matcher.group(1);
1117                //count of 'open' braces. Expression ends when it hits 0. In most cases,
1118                // it should end up as 1 b/c "expression" is everything inside ${}, so 
1119                // the following will typically not find the end of the expression.
1120                int openBraceCount = 1;
1121                int expressionEnd = expression.length();
1122                boolean inQuote = false;
1123                for (int i = 0; i < expression.length(); i++)
1124                {
1125                    char c = expression.charAt(i);
1126                    //basically, if we're inQuote, we ignore everything until we hit the quote end, so we only care if the character matches the quote start (meaning we're at the end of the quote).
1127                    //note that I don't believe expression support escaped quotes...
1128                    if (c == EXPANSION_STRING_DELIMITTER)
1129                    {
1130                        inQuote = !inQuote;
1131                        continue;
1132                    } else if (inQuote)
1133                    {
1134                        continue;
1135                    } else if (c == CLOSE_BRACE)
1136                    {
1137                        openBraceCount--;
1138                        if (openBraceCount == 0)
1139                        {
1140                            expressionEnd = i;
1141                            break;
1142                        }
1143                    } else if (c == OPEN_BRACE)
1144                    {
1145                        openBraceCount++;
1146                    }
1147                }
1148                if (expressionEnd < expression.length())
1149                {
1150                    //then we gobbled up some } that we shouldn't have... like the closing } of a javascript
1151                    //function.
1152                    tokenAccumulator.add(new ExpansionToken(expression.substring(0, expressionEnd), textStartLocation));
1153                    //can't just assign to 
1154                    startx = matcher.start(1) + expressionEnd + 1;
1155                } else
1156                {
1157                    tokenAccumulator.add(new ExpansionToken(expression.trim(), textStartLocation));
1158    
1159                    startx = matcher.end();
1160                }
1161            }
1162    
1163            // Catch anything after the final regexp match.
1164    
1165            if (startx < text.length())
1166                tokenAccumulator.add(new TextToken(text.substring(startx, text.length()),
1167                        textStartLocation));
1168        }
1169    
1170        static enum Version
1171        {
1172            T_5_0(5, 0), T_5_1(5, 1), T_5_3(5, 3);
1173    
1174            private int major;
1175            private int minor;
1176    
1177    
1178            private Version(int major, int minor)
1179            {
1180                this.major = major;
1181                this.minor = minor;
1182            }
1183    
1184            public boolean before(Version other)
1185            {
1186                if (other == null)
1187                    return false;
1188    
1189                if (this == other)
1190                    return true;
1191    
1192                return major <= other.major && minor <= other.minor;
1193            }
1194        }
1195    
1196    }