Package org.apache.tapestry.internal.services

Source Code of org.apache.tapestry.internal.services.TemplateParserImpl

// Copyright 2006 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.apache.tapestry.internal.services;

import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newSet;

import java.net.URL;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.tapestry.internal.parser.AttributeToken;
import org.apache.tapestry.internal.parser.BlockToken;
import org.apache.tapestry.internal.parser.BodyToken;
import org.apache.tapestry.internal.parser.CDATAToken;
import org.apache.tapestry.internal.parser.CommentToken;
import org.apache.tapestry.internal.parser.ComponentTemplate;
import org.apache.tapestry.internal.parser.ComponentTemplateImpl;
import org.apache.tapestry.internal.parser.EndElementToken;
import org.apache.tapestry.internal.parser.ExpansionToken;
import org.apache.tapestry.internal.parser.ParameterToken;
import org.apache.tapestry.internal.parser.StartComponentToken;
import org.apache.tapestry.internal.parser.StartElementToken;
import org.apache.tapestry.internal.parser.TemplateToken;
import org.apache.tapestry.internal.parser.TextToken;
import org.apache.tapestry.ioc.Location;
import org.apache.tapestry.ioc.Resource;
import org.apache.tapestry.ioc.internal.util.InternalUtils;
import org.apache.tapestry.ioc.internal.util.LocationImpl;
import org.apache.tapestry.ioc.internal.util.TapestryException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.XMLReaderFactory;

/**
* Non-threadsafe implementation; the IOC service uses the perthread lifecycle.
*/
public class TemplateParserImpl implements TemplateParser, LexicalHandler, ContentHandler
{
    private static final String MIXINS_ATTRIBUTE_NAME = "mixins";

    private static final String TYPE_ATTRIBUTE_NAME = "type";

    private static final String ID_ATTRIBUTE_NAME = "id";

    public static final String TAPESTRY_SCHEMA_5_0_0 = "http://tapestry.apache.org/schema/tapestry_5_0_0.xsd";

    private XMLReader _reader;

    // Resource being parsed
    private Resource _templateResource;

    private Locator _locator;

    private final List<TemplateToken> _tokens = newList();

    // Non-blank ids from start component (<comp>) elements

    private final Set<String> _componentIds = newSet();

    // Used to accumulate text provided by the characters(). Even contiguous characters may be
    // broken up across multiple invocations due to parser internals. We accumulate those together
    // before forming a text token.

    private final StringBuilder _textBuffer = new StringBuilder();

    private Location _textStartLocation;

    private boolean _textIsCData;

    private boolean _insideBody;

    private boolean _insideBodyErrorLogged;

    private boolean _ignoreEvents;

    private final Log _log;

    // Note the use of the non-greedy modifier; this prevents the pattern from merging multiple
    // expansions on the same text line into a single large
    // but invalid expansion.

    private final Pattern EXPANSION_PATTERN = Pattern.compile(
            "\\$\\{\\s*(.*?)\\s*}",
            Pattern.MULTILINE);

    public TemplateParserImpl(Log log)
    {
        _log = log;

        reset();
    }

    private void reset()
    {
        _tokens.clear();
        _componentIds.clear();
        _templateResource = null;
        _locator = null;
        _textBuffer.setLength(0);
        _textStartLocation = null;
        _textIsCData = false;
        _insideBody = false;
        _insideBodyErrorLogged = false;
        _ignoreEvents = true;

    }

    public ComponentTemplate parseTemplate(Resource templateResource)
    {
        if (_reader == null)
        {
            try
            {
                _reader = XMLReaderFactory.createXMLReader();

                _reader.setContentHandler(this);

                _reader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);

                _reader.setProperty("http://xml.org/sax/properties/lexical-handler", this);
            }
            catch (Exception ex)
            {
                throw new RuntimeException(ServicesMessages.newParserError(templateResource, ex),
                        ex);
            }
        }

        URL resourceURL = templateResource.toURL();

        if (resourceURL == null)
            throw new RuntimeException(ServicesMessages.missingTemplateResource(templateResource));

        _templateResource = templateResource;

        try
        {
            InputSource source = new InputSource(resourceURL.openStream());

            _reader.parse(source);

            return new ComponentTemplateImpl(_templateResource, _tokens, _componentIds);
        }
        catch (Exception ex)
        {
            // Some parsers get in an unknown state when an error occurs, and are are not
            // subsequently useable.

            _reader = null;

            throw new TapestryException(ServicesMessages.templateParseError(templateResource, ex),
                    getCurrentLocation(), ex);
        }
        finally
        {
            reset();
        }
    }

    public void setDocumentLocator(Locator locator)
    {
        _locator = locator;
    }

    /** Accumulates the characters into a text buffer. */
    public void characters(char[] ch, int start, int length) throws SAXException
    {
        if (_ignoreEvents)
            return;

        if (insideBody())
            return;

        if (_textBuffer.length() == 0)
            _textStartLocation = getCurrentLocation();

        _textBuffer.append(ch, start, length);
    }

    /**
     * Adds tokens corresponding to the content in the text buffer. For a non-CDATA section, we also
     * search for expansions (thus we may add more than one token). Clears the text buffer.
     */
    private void processTextBuffer()
    {
        if (_textBuffer.length() == 0)
            return;

        String text = _textBuffer.toString();

        if (_textIsCData)
        {
            _tokens.add(new CDATAToken(text, _textStartLocation));
        }
        else
        {
            addTokensForText(text);
        }

        _textBuffer.setLength(0);
    }

    /**
     * Scans the text, using a regular expression pattern, for expansion patterns, and adds
     * appropriate tokens for what it finds.
     *
     * @param text
     */
    private void addTokensForText(String text)
    {
        Matcher matcher = EXPANSION_PATTERN.matcher(text);

        int startx = 0;

        // The big problem with all this code is that everything gets assigned to the
        // start of the text block, even if there are line breaks leading up to it.
        // That's going to take a lot more work and there are bigger fish to fry.

        while (matcher.find())
        {
            int matchStart = matcher.start();

            if (matchStart != startx)
            {
                String prefix = text.substring(startx, matchStart);

                _tokens.add(new TextToken(prefix, _textStartLocation));
            }

            // Group 1 includes the real text of the expansion, which whitespace around the
            // expression (but inside the curly
            // braces) excluded.

            String expression = matcher.group(1);

            _tokens.add(new ExpansionToken(expression, _textStartLocation));

            startx = matcher.end();
        }

        // Catch anything after the final regexp match.

        if (startx < text.length())
            _tokens.add(new TextToken(text.substring(startx, text.length()), _textStartLocation));
    }

    public void startElement(String uri, String localName, String qName, Attributes attributes)
            throws SAXException
    {
        _ignoreEvents = false;

        if (_insideBody)
            throw new IllegalStateException(ServicesMessages
                    .mayNotNestElementsInsideBody(localName));

        // Add any accumulated text into a text token
        processTextBuffer();

        if (TAPESTRY_SCHEMA_5_0_0.equals(uri))
        {
            startTapestryElement(qName, localName, attributes);
            return;
        }

        // TODO: Handle interpolations inside attributes?

        startStaticElement(localName, attributes);
    }

    private void startStaticElement(String localName, Attributes attributes)
    {
        Location location = getCurrentLocation();

        List<TemplateToken> attributeTokens = newList();

        int count = attributes.getLength();

        String id = null;
        String type = null;
        String mixins = null;

        for (int i = 0; i < count; i++)
        {
            String name = attributes.getLocalName(i);

            // The name will be blank for an xmlns: attribute

            if (InternalUtils.isBlank(name))
                continue;

            String uri = attributes.getURI(i);

            String value = attributes.getValue(i);

            if (TAPESTRY_SCHEMA_5_0_0.equals(uri))
            {
                if (InternalUtils.isNonBlank(value))
                {
                    if (name.equals(ID_ATTRIBUTE_NAME))
                    {
                        id = value;
                        continue;
                    }

                    if (name.equals(TYPE_ATTRIBUTE_NAME))
                    {
                        type = value;
                        continue;
                    }

                    if (name.equals(MIXINS_ATTRIBUTE_NAME))
                    {
                        mixins = value;
                        continue;
                    }

                    // Anything else is the name of a Tapestry component parameter that is simply
                    // not part of the template's doctype for the element being instrumented.
                }
            }

            attributeTokens.add(new AttributeToken(name, value, location));
        }

        boolean isComponent = (id != null || type != null);

        // If provided t:mixins but not t:id or t:type, then its not quite a component

        if (mixins != null && !isComponent)
            throw new TapestryException(ServicesMessages.mixinsInvalidWithoutIdOrType(localName),
                    location, null);

        if (isComponent)
        {
            _tokens.add(new StartComponentToken(localName, id, type, mixins, location));
        }
        else
        {
            _tokens.add(new StartElementToken(localName, location));
        }

        _tokens.addAll(attributeTokens);

        if (id != null)
            _componentIds.add(id);
    }

    /**
     * Checks to see if currently inside a t:body element (which should always be empty). Content is
     * ignored inside a body. If inside a body, then a warning is logged (but only one warning per
     * body element).
     *
     * @return true if inside t:body, false otherwise
     */
    private boolean insideBody()
    {
        if (_insideBody)
        {
            // Limit to one logged error per infraction.

            if (!_insideBodyErrorLogged)
                _log.error(ServicesMessages.contentInsideBodyNotAllowed(getCurrentLocation()));

            _insideBodyErrorLogged = true;
        }

        return _insideBody;
    }

    private void startTapestryElement(String qname, String localName, Attributes attributes)
    {
        if (localName.equals("comp"))
        {
            startComponent(attributes);
            return;
        }

        if (localName.equals("body"))
        {
            startBody();
            return;
        }

        if (localName.equals("parameter"))
        {
            startParameter(attributes);
            return;
        }

        if (localName.equals("block"))
        {
            startBlock(attributes);
            return;
        }

        throw new TapestryException(ServicesMessages.undefinedTapestryElement(qname),
                getCurrentLocation(), null);
    }

    private void startBlock(Attributes attributes)
    {
        String blockId = findSingleParameter("block", "id", attributes);

        // null is ok for blockId

        _tokens.add(new BlockToken(blockId, getCurrentLocation()));
    }

    private void startParameter(Attributes attributes)
    {
        String parameterName = findSingleParameter("parameter", "name", attributes);

        if (InternalUtils.isBlank(parameterName))
            throw new TapestryException(ServicesMessages.parameterElementNameRequired(),
                    getCurrentLocation(), null);

        _tokens.add(new ParameterToken(parameterName, getCurrentLocation()));
    }

    private String findSingleParameter(String elementName, String attributeName,
            Attributes attributes)
    {
        String result = null;

        for (int i = 0; i < attributes.getLength(); i++)
        {
            String name = attributes.getLocalName(i);

            if (name.equals(attributeName))
            {
                result = attributes.getValue(i);
                continue;
            }

            // Only the name attribute is allowed.

            throw new TapestryException(ServicesMessages.undefinedTapestryAttribute(
                    elementName,
                    name,
                    attributeName), getCurrentLocation(), null);
        }

        return result;
    }

    private void startComponent(Attributes attributes)
    {
        String id = null;
        String type = null;
        String mixins = null;
        int count = attributes.getLength();
        Location location = getCurrentLocation();
        List<TemplateToken> attributeTokens = newList();

        for (int i = 0; i < count; i++)
        {
            String name = attributes.getLocalName(i);
            String value = attributes.getValue(i);

            // TODO: Validate that the id is a reasonable string.

            if (name.equals(ID_ATTRIBUTE_NAME))
            {
                if (InternalUtils.isNonBlank(value))
                    id = value;
                continue;
            }

            if (name.equals(TYPE_ATTRIBUTE_NAME))
            {

                if (InternalUtils.isNonBlank(value))
                    type = value;
                continue;
            }

            if (name.equals(MIXINS_ATTRIBUTE_NAME))
            {
                if (InternalUtils.isNonBlank(value))
                    mixins = value;
                continue;
            }

            // The name will be blank for an xmlns: attribute; we sometimes see this
            // in the root element if the root element is a component.

            if (name.equals(""))
                continue;

            attributeTokens.add(new AttributeToken(name, value, location));
        }

        if (id == null && type == null)
            throw new TapestryException(ServicesMessages.compRequiresIdOrType(), location, null);

        if (id != null)
            _componentIds.add(id);

        // Add the component
        _tokens.add(new StartComponentToken(null, id, type, mixins, location));
        _tokens.addAll(attributeTokens);
    }

    private void startBody()
    {
        _tokens.add(new BodyToken(getCurrentLocation()));

        _insideBody = true;
        _insideBodyErrorLogged = false;
    }

    public void endElement(String uri, String localName, String qName) throws SAXException
    {
        processTextBuffer();

        // TODO: Handle tapestry namespace elements?

        // Because XML tags are always balanced, we don't even need to know what element just closed
        // when we assemble things later.

        if (!_insideBody)
            _tokens.add(new EndElementToken(getCurrentLocation()));

        _insideBody = false;
    }

    private Location getCurrentLocation()
    {
        if (_locator == null)
            return null;

        return new LocationImpl(_templateResource, _locator.getLineNumber(), _locator
                .getColumnNumber());
    }

    public void comment(char[] ch, int start, int length) throws SAXException
    {
        if (_ignoreEvents || insideBody())
            return;

        processTextBuffer();

        // Remove excess whitespace. The Comment DOM node will add a leadig and trailing space.

        String comment = new String(ch, start, length).trim();

        // TODO: Perhaps comments need to be "aggregated" the same way we aggregate text and CDATA.
        // Hm. Probably not. Any whitespace between one comment and the next will become a
        // TextToken.
        // Unless we trim whitespace between consecutive comments ... and on down the rabbit hole.
        // Oops -- unless a single comment may be passed into this method as multiple calls
        // (have to check how multiline comments are handled).
        // Tests against Sun's built in parser does show that multiline comments are still
        // provided as a single call to comment(), so we're good for the meantime (until we find
        // out some parsers aren't so compliant).

        _tokens.add(new CommentToken(comment, getCurrentLocation()));
    }

    public void endCDATA() throws SAXException
    {
        // Add a token for any accumulated CDATA.

        processTextBuffer();

        // Again, CDATA doesn't nest, so we know we're back to ordinary markup.

        _textIsCData = false;
    }

    public void startCDATA() throws SAXException
    {
        if (_ignoreEvents || insideBody())
            return;

        processTextBuffer();

        // Because CDATA doesn't mix with any other SAX/lexical events, we can simply turn on a flag
        // here and turn it off when we see the end.

        _textIsCData = true;
    }

    // Empty methods defined by the various interfaces.

    public void endDTD() throws SAXException
    {
    }

    public void endEntity(String name) throws SAXException
    {
    }

    public void startDTD(String name, String publicId, String systemId) throws SAXException
    {
    }

    public void startEntity(String name) throws SAXException
    {
    }

    public void endDocument() throws SAXException
    {
    }

    public void endPrefixMapping(String prefix) throws SAXException
    {
    }

    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException
    {
    }

    public void processingInstruction(String target, String data) throws SAXException
    {
    }

    public void skippedEntity(String name) throws SAXException
    {
    }

    public void startDocument() throws SAXException
    {
    }

    public void startPrefixMapping(String prefix, String uri) throws SAXException
    {
    }

}
TOP

Related Classes of org.apache.tapestry.internal.services.TemplateParserImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.