Package org.apache.tapestry.internal.services

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

// Copyright 2006, 2007 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.IOCConstants.PERTHREAD_SCOPE;
import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newSet;

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

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.DTDToken;
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.annotations.Scope;
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.apache.tapestry.ioc.util.Stack;
import org.slf4j.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.EntityResolver;
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.
*/
@Scope(PERTHREAD_SCOPE)
public class TemplateParserImpl implements TemplateParser, LexicalHandler, ContentHandler,
        EntityResolver
{
    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 elements

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

    // Used to accumulate text provided by the characters() method. 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 Logger _logger;

    private final Map<String, URL> _configuration;

    private final Stack<Runnable> _endTagHandlerStack = new Stack<Runnable>();

    private final Runnable _addEndElementToken = new Runnable()
    {
        public void run()
        {
            _tokens.add(new EndElementToken(getCurrentLocation()));
        }
    };

    private final Runnable _ignoreEndElement = new Runnable()
    {
        public void run()
        {
        }
    };

    // 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(Logger logger, Map<String, URL> configuration)
    {
        _logger = logger;
        _configuration = configuration;

        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;

        // Stack needs a clear();

        while (!_endTagHandlerStack.isEmpty())
            _endTagHandlerStack.pop();
    }

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

                _reader.setContentHandler(this);

                _reader.setEntityResolver(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?

        startPossibleComponent(attributes, localName, null);
    }

    /**
     * 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)
                _logger.error(ServicesMessages.contentInsideBodyNotAllowed(getCurrentLocation()));

            _insideBodyErrorLogged = true;
        }

        return _insideBody;
    }

    private void startTapestryElement(String qname, String localName, Attributes attributes)
    {
        if (localName.equalsIgnoreCase("body"))
        {
            startBody();
            return;
        }

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

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

        if (localName.equalsIgnoreCase("container"))
        {
            startContainer();
            return;
        }

        // The component type is derived from the element name. Since element names may not contain
        // slashes, we convert periods to slashes. Later down the pipeline, they'll probably be
        // converted back into periods, as part of a fully qualified class name.

        String componentType = localName.replace('.', '/');

        // With a component type specified, it's not just possibly a component ...
        startPossibleComponent(attributes, null, componentType);
    }

    private void startContainer()
    {
        // Neither the container nor its end tag are considered tokens, just the contents inside.
        _endTagHandlerStack.push(_ignoreEndElement);
    }

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

        // null is ok for blockId

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

    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()));
        _endTagHandlerStack.push(_addEndElementToken);
    }

    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 String nullForBlank(String input)
    {
        return InternalUtils.isBlank(input) ? null : input;
    }

    /**
     * @param attributes
     *            the attributes for the element
     * @param elementName
     *            the name of the element (to be assigned to the new token), may be null for a
     *            component in the Tapestry namespace
     * @param identifiedType
     *            the type of the element, usually null, but may be the component type derived from
     *            the element name (for an element in the Tapestry namespace)
     */
    private void startPossibleComponent(Attributes attributes, String elementName,
            String identifiedType)
    {
        String id = null;
        String type = identifiedType;
        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);

            // 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 (name.equalsIgnoreCase(ID_ATTRIBUTE_NAME))
                {
                    id = nullForBlank(value);
                    continue;
                }

                if (type == null && name.equalsIgnoreCase(TYPE_ATTRIBUTE_NAME))
                {
                    type = nullForBlank(value);
                    continue;
                }

                if (name.equalsIgnoreCase(MIXINS_ATTRIBUTE_NAME))
                {
                    mixins = nullForBlank(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(elementName),
                    location, null);

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

        _tokens.addAll(attributeTokens);

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

        // TODO: Is there value in having different end elements for components vs. ordinary
        // elements?

        _endTagHandlerStack.push(_addEndElementToken);
    }

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

        _insideBody = true;
        _insideBodyErrorLogged = false;

        _endTagHandlerStack.push(new Runnable()
        {
            public void run()
            {
                _insideBody = false;

                // And don't add an end element token.
            }
        });
    }

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

        _endTagHandlerStack.pop().run();
    }

    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
    {
        // notes:
        // 1) a DTD has to occur at the very start of a document. Since we don't start
        // recording characters until we hit the first element of a document (see
        // characters and startElement), there should be no text to process.
        // It's worth noting that the sax parser will puke if any of the following
        // occur:
        // 1) a doctype is encountered multiple times in the same document
        // 2) a doctype is encountered anywhere other than the very first item
        // in a document.
        // Hence, the assumption made in 1 should hold.
        // Since an exception is thrown for case #1 above, we can just add the DTDToken.
        // When we go to process the token (in PageLoaderProcessor), we can make sure
        // that the final page has only a single DTDToken (the first one).
        _tokens.add(new DTDToken(name, publicId, systemId, getCurrentLocation()));
    }

    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
    {
    }

    public InputSource resolveEntity(String publicId, String systemId) throws SAXException,
            IOException
    {
        URL url = _configuration.get(publicId);

        if (url != null) return new InputSource(url.openStream());

        return null;
    }

}
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.