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