    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}