org.zenonpagetemplates.onePhaseImpl.PageTemplateImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.zenonpagetemplates.onePhaseImpl.PageTemplateImpl.java

Source

package org.zenonpagetemplates.onePhaseImpl;

import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.TreeMap;
import java.util.Map.Entry;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;

import org.apache.xerces.xni.parser.XMLDocumentFilter;

import org.cyberneko.html.parsers.SAXParser;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;

import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;

import org.xnap.commons.i18n.I18n;
import org.zenonpagetemplates.common.ExpressionTokenizer;
import org.zenonpagetemplates.common.Filter;
import org.zenonpagetemplates.common.TemplateError;
import org.zenonpagetemplates.common.exceptions.EvaluationException;
import org.zenonpagetemplates.common.exceptions.ExpressionSyntaxException;
import org.zenonpagetemplates.common.exceptions.NoSuchPathException;
import org.zenonpagetemplates.common.exceptions.PageTemplateException;
import org.zenonpagetemplates.common.helpers.DateHelper;
import org.zenonpagetemplates.common.scripting.EvaluationHelper;
import org.zenonpagetemplates.twoPhasesImpl.ScriptFactory;

/**
 * <p>
 *   Main class to implement ZPT in one phase.
 * </p>
 * 
 * 
 *  Zenon Page Templates
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 3 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 *
 * @author <a href="mailto:chris@christophermrossi.com">Chris Rossi</a>
 * @author <a href="mailto:david.javapagetemplates@gmail.com">David Cana</a>
 * @version $Revision: 1.16 $
 */
public class PageTemplateImpl implements OnePhasePageTemplate {

    private static final String CDATA = "CDATA";
    public static final String VOID_STRING = "";
    private static final String ENCODING = "UTF-8";
    private static final int MAXIMUM_NUMBER_OF_ATTRIBUTES = 20;
    private static final String I18N_DOMAIN_VAR_NAME = "i18nDomain";
    private static final String ON_ERROR_VAR_NAME = "on-error";
    public static final String TEMPLATE_ERROR_VAR_NAME = "error";
    private static final String I18N_EXPRESSION_PREFIX = "i18nExp: ";
    private URI uri;
    private Document template;
    private Resolver userResolver = null;
    private String id;
    private boolean recoveredFromCache = false;

    // Map of macros contained in this template
    Map<String, Macro> macros = null;

    private static SAXReader htmlReader = null;

    static final SAXReader getHTMLReader() throws Exception {
        if (htmlReader == null && ZPTContext.getInstance().isUseHTMLReader()) {
            htmlReader = new SAXReader();
            SAXParser parser = new SAXParser();
            parser.setProperty("http://cyberneko.org/html/properties/names/elems", "match");
            parser.setProperty("http://cyberneko.org/html/properties/names/attrs", "no-change");
            //parser.setProperty( "http://cyberneko.org/html/properties/default-encoding", "ISO-8859-1" );

            parser.setProperty("http://cyberneko.org/html/properties/default-encoding", ENCODING);
            //parser.setProperty( "http://cyberneko.org/html/features/balance-tags/document-fragment ", "true");
            //parser.setProperty( "http://cyberneko.org/html/features/balance-tags", "false" );
            //parser.setProperty( "http://apache.org/xml/features/scanner/notify-builtin-refs", "true");
            //parser.setProperty( "http://cyberneko.org/html/features/scanner/script/strip-comment-delims", "true"); 

            // Attach ZenonWriter to the parser as a filter
            XMLDocumentFilter[] filters = { new Filter() };
            parser.setProperty("http://cyberneko.org/html/properties/filters", filters);

            htmlReader.setXMLReader(parser);
        }
        return htmlReader;
    }

    private static SAXReader xmlReader = null;

    static final SAXReader getXMLReader() throws Exception {
        if (xmlReader == null) {
            xmlReader = new SAXReader();
            xmlReader.setEncoding(ENCODING);
            xmlReader.setIgnoreComments(true);
            //xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        }
        return xmlReader;
    }

    public PageTemplateImpl(InputStream input) throws PageTemplateException {
        this(input, null);
    }

    public PageTemplateImpl(InputStream input, Resolver resolver) throws PageTemplateException {
        try {
            this.uri = null;
            this.userResolver = resolver == null ? new DefaultResolver() : resolver;

            SAXReader reader = getXMLReader();
            try {
                this.template = reader.read(input);
            } catch (DocumentException e) {
                try {
                    reader = getHTMLReader();
                    if (reader == null) {
                        throw (e);
                    }
                    this.template = reader.read(input);
                } catch (NoClassDefFoundError ee) {
                    // Allow user to omit nekohtml package
                    // to disable html parsing
                    //System.err.println( "Warning: no nekohtml" );
                    //ee.printStackTrace();
                    throw e;
                }
            }
        } catch (Exception e) {
            throw new PageTemplateException(e);
        }
    }

    public PageTemplateImpl(URL url) throws PageTemplateException {
        try {
            this.id = url.toString();
            this.uri = new URI(url.toString());

            this.recoveredFromCache = this.recoverFromCache();
            if (this.recoveredFromCache) {
                return;
            }

            SAXReader reader = getXMLReader();
            try {
                this.template = reader.read(url);
            } catch (DocumentException e) {
                try {
                    reader = getHTMLReader();
                    if (reader == null) {
                        throw (e);
                    }
                    this.template = reader.read(url);
                } catch (NoClassDefFoundError ee) {
                    // Allow user to omit nekohtml package
                    // to disable html parsing
                    //System.err.println( "Warning: no nekohtml" );
                    //ee.printStackTrace();
                    throw e;
                }
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new PageTemplateException(e);
        }
    }

    public String getId() {
        return this.id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public Resolver getResolver() {
        return this.userResolver;
    }

    @Override
    public void setResolver(Resolver resolver) {
        this.userResolver = resolver;
    }

    @Override
    public void process(OutputStream output, Object context) throws PageTemplateException {
        process(output, context, null);
    }

    static final SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance();

    @Override
    public void process(OutputStream output, Object context, Map<String, Object> dictionary)
            throws PageTemplateException {

        try {
            TransformerHandler handler = factory.newTransformerHandler();
            Transformer transformer = handler.getTransformer();

            transformer.setOutputProperty(OutputKeys.ENCODING, ENCODING);
            //transformer.setOutputProperty(OutputKeys.METHOD, "html");
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");

            //OutputStreamWriter writer = new OutputStreamWriter(output, ENCODING);
            //handler.setResult(new StreamResult(writer));
            handler.setResult(new StreamResult(output));
            //handler.processingInstruction(Result.PI_DISABLE_OUTPUT_ESCAPING, "");

            process(handler, handler, context, dictionary);

        } catch (PageTemplateException e) {
            throw e;

        } catch (Exception e) {
            throw new PageTemplateException(e);
        }
    }

    @Override
    public void process(ContentHandler contentHandler, LexicalHandler lexicalHandler, Object context,
            Map<String, Object> dictionary) throws PageTemplateException {

        try {
            // Initialize the bean shell and register in the context
            EvaluationHelper evaluationHelper = this.getEvaluationHelper(context, dictionary);

            // Process
            Element root = this.template.getRootElement();
            contentHandler.startDocument();
            processElement(root, contentHandler, lexicalHandler, evaluationHelper, new Stack<Map<String, Slot>>());
            contentHandler.endDocument();

            this.saveToCache();

        } catch (PageTemplateException e) {
            throw e;
        } catch (Exception e) {
            throw new PageTemplateException(e);
        }
    }

    private EvaluationHelper getEvaluationHelper(Object context, Map<String, Object> dictionary)
            throws EvaluationException {

        EvaluationHelper result = ScriptFactory.getInstance().createEvaluationHelper();

        this.addVarsToEvaluationHelper(context, dictionary, result);

        return result;
    }

    private void saveToCache() throws IOException {

        try {
            if (!ZPTContext.getInstance().isCacheOn() || this.id == null || this.recoveredFromCache
                    || ZPTContext.getInstance().getTemplateCache() == null) {
                return;
            }

            ZPTContext.getInstance().getTemplateCache().put(this.id, this.template);

        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    private boolean recoverFromCache() throws IOException {

        try {
            if (!ZPTContext.getInstance().isCacheOn() || this.id == null
                    || ZPTContext.getInstance().getTemplateCache() == null) {
                return false;
            }

            this.template = ZPTContext.getInstance().getTemplateCache().get(this.id);

            return !(this.template == null);

        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    private void addVarsToEvaluationHelper(Object context, Map<String, Object> dictionary,
            EvaluationHelper evaluationHelper) throws EvaluationException {

        // Add vars from dictionary
        if (dictionary != null) {
            for (Entry<String, Object> entry : dictionary.entrySet()) {
                evaluationHelper.set(entry.getKey(), entry.getValue());
            }
        }

        // Add more vars
        evaluationHelper.set(HERE_VAR_NAME, context);
        evaluationHelper.set(TEMPLATE_VAR_NAME, this);
        evaluationHelper.set(RESOLVER_VAR_NAME, new DefaultResolver());
        evaluationHelper.set(DATE_HELPER_VAR_NAME, new DateHelper());
        evaluationHelper.set(SHELL_VAR_NAME, evaluationHelper);
    }

    private Map<String, String> namespaces = new TreeMap<String, String>();

    private String getNamespaceURIFromPrefix(String prefix) {
        String uri = (String) this.namespaces.get(prefix);
        return uri == null ? VOID_STRING : uri;
    }

    public static void setVar(EvaluationHelper evaluationHelper, List<String> varsToUnset,
            Map<String, Object> varsToSet, String name, Object value) throws EvaluationException {

        Object currentValue = evaluationHelper.get(name);

        if (currentValue != null) {
            varsToSet.put(name, currentValue);

        } else {
            varsToUnset.add(name);
        }

        evaluationHelper.set(name, value);
    }

    protected void processElement(Element element, ContentHandler contentHandler, LexicalHandler lexicalHandler,
            EvaluationHelper evaluationHelper, Stack<Map<String, Slot>> slotStack)
            throws SAXException, PageTemplateException, IOException, EvaluationException {

        Expressions expressions = new Expressions();
        AttributesImpl attributes = getAttributes(element, expressions);

        List<String> varsToUnset = new ArrayList<String>();
        Map<String, Object> varsToSet = new HashMap<String, Object>();

        try {
            // Process instructions
            if (expressions.repeat != null) {

                // Process repeat
                Loop loop = new Loop(expressions.repeat, evaluationHelper, varsToUnset, varsToSet);
                while (loop.repeat(evaluationHelper)) {
                    if (!processElement(element, contentHandler, lexicalHandler, evaluationHelper, slotStack,
                            expressions, attributes, varsToUnset, varsToSet)) {
                        return;
                    }
                }

            } else {

                // Process non repeat
                if (!processElement(element, contentHandler, lexicalHandler, evaluationHelper, slotStack,
                        expressions, attributes, varsToUnset, varsToSet)) {
                    return;
                }
            }

            processVars(evaluationHelper, varsToUnset, varsToSet);

        } catch (PageTemplateException e) {

            if (!treatError(element, contentHandler, lexicalHandler, evaluationHelper, varsToUnset, varsToSet, e)) {
                throw (e);
            }
        }
    }

    private boolean treatError(Element element, ContentHandler contentHandler, LexicalHandler lexicalHandler,
            EvaluationHelper evaluationHelper, List<String> varsToUnset, Map<String, Object> varsToSet,
            Exception exception) throws EvaluationException, PageTemplateException, SAXException {

        // Exit if there is no on-error expression defined
        String onErrorExpression = (String) evaluationHelper.get(ON_ERROR_VAR_NAME);
        if (onErrorExpression == null) {
            return false;
        }

        String content = null;
        String i18nContent = null;
        String i18nParams = null;

        if (onErrorExpression.startsWith(I18N_EXPRESSION_PREFIX)) {
            i18nContent = onErrorExpression.substring(I18N_EXPRESSION_PREFIX.length());
        } else {
            content = onErrorExpression;
        }

        // Set the error variable
        TemplateError templateError = new TemplateError(exception);
        setVar(evaluationHelper, varsToUnset, varsToSet, TEMPLATE_ERROR_VAR_NAME, templateError);

        try {
            // Process content
            zptContent(contentHandler, lexicalHandler,
                    processContent(content, evaluationHelper, i18nContent, i18nParams));

        } catch (Exception e) {
            processVars(evaluationHelper, varsToUnset, varsToSet);
            throw new PageTemplateException(e);
        }

        processVars(evaluationHelper, varsToUnset, varsToSet);

        return true;
    }

    private boolean processElement(Element element, ContentHandler contentHandler, LexicalHandler lexicalHandler,
            EvaluationHelper evaluationHelper, Stack<Map<String, Slot>> slotStack, Expressions expressions,
            AttributesImpl attributes, List<String> varsToUnset, Map<String, Object> varsToSet)
            throws PageTemplateException, SAXException, IOException, EvaluationException {

        String configurableTag = null;

        /* Normal elements */

        // on-error
        if (expressions.onError != null || expressions.i18nOnError != null) {
            processOnErrors(expressions.onError, expressions.i18nOnError, evaluationHelper, varsToUnset, varsToSet);
        }

        // define
        if (expressions.define != null) {
            processDefine(expressions.define, evaluationHelper, varsToUnset, varsToSet);
        }

        // i18nDomain
        if (expressions.i18nDomain != null) {
            processI18nDomain(expressions.i18nDomain, evaluationHelper, varsToUnset, varsToSet);
        }

        // i18n:define
        if (expressions.i18nDefine != null) {
            processI18nDefine(expressions.i18nDefine, expressions.i18nParams, evaluationHelper, varsToUnset,
                    varsToSet);
        }

        // condition
        if (expressions.condition != null && !Expression.evaluateBoolean(expressions.condition, evaluationHelper)) {
            // Skip this element (and children)
            return false;
        }

        // tag
        if (expressions.tag != null) {
            configurableTag = (String) Expression.evaluate(expressions.tag, evaluationHelper);
        }

        /* Macro related elements */

        // use macro
        if (expressions.useMacro != null) {
            processMacro(expressions.useMacro, element, contentHandler, lexicalHandler, evaluationHelper,
                    slotStack);
            return false;
        }

        // fill slot
        if (expressions.defineSlot != null
                && !processDefineSlot(contentHandler, lexicalHandler, evaluationHelper, slotStack, expressions)) {
            return false;
        }

        /* Content elements */

        // content or replace
        Object zptContent = null;
        if (expressions.content != null || expressions.i18nContent != null) {
            zptContent = processContent(expressions.content, evaluationHelper, expressions.i18nContent,
                    expressions.i18nParams);
        }

        // attributes
        if (expressions.attributes != null || expressions.i18nAttributes != null) {
            processAttributes(attributes, expressions.attributes, expressions.i18nParams, evaluationHelper,
                    expressions.i18nAttributes);
        }

        // omit-tag
        boolean zptOmitTag = getZPTOmitTag(evaluationHelper, expressions, element);

        // Declare element
        if (!zptOmitTag) {
            startElement(element, contentHandler, attributes, configurableTag);
        }

        // Content
        if (zptContent != null) {
            zptContent(contentHandler, lexicalHandler, zptContent);
        } else {
            defaultContent(element, contentHandler, lexicalHandler, evaluationHelper, slotStack);
        }

        // End element
        if (!zptOmitTag) {
            endElement(element, contentHandler, configurableTag);
        }

        return true;
    }

    protected void startElement(Element element, ContentHandler contentHandler, AttributesImpl attributes,
            String configurableTag) throws SAXException {

        if (configurableTag != null) {
            contentHandler.startElement(VOID_STRING, configurableTag, configurableTag, attributes);
            return;
        }

        contentHandler.startElement(VOID_STRING, element.getName(), element.getQualifiedName(), attributes);
    }

    protected void endElement(Element element, ContentHandler contentHandler, String configurableTag)
            throws SAXException {

        if (configurableTag != null) {
            contentHandler.endElement(VOID_STRING, configurableTag, configurableTag);
            return;
        }

        contentHandler.endElement(VOID_STRING, element.getName(), element.getQualifiedName());
    }

    private boolean processDefineSlot(ContentHandler contentHandler, LexicalHandler lexicalHandler,
            EvaluationHelper evaluationHelper, Stack<Map<String, Slot>> slotStack, Expressions expressions)
            throws SAXException, PageTemplateException, IOException, EvaluationException {

        if (!slotStack.isEmpty()) {
            Map<String, Slot> slots = slotStack.pop();
            Slot slot = slots.get(expressions.defineSlot);
            if (slot != null) {
                slot.process(contentHandler, lexicalHandler, evaluationHelper, slotStack);
                slotStack.push(slots);
                return false;
            }
            // else { use content in macro }
            slotStack.push(slots);

            return true;
        } else {
            throw new PageTemplateException("Slot definition not allowed outside of macro");
        }
    }

    private void zptContent(ContentHandler contentHandler, LexicalHandler lexicalHandler, Object zptContent)
            throws PageTemplateException, SAXException {

        // Content for this element has been generated dynamically
        if (zptContent instanceof HTMLFragment) {

            HTMLFragment html = (HTMLFragment) zptContent;

            if (ZPTContext.getInstance().isParseHTMLFragments()) {
                html.toXhtml(contentHandler, lexicalHandler);
            } else {
                char[] text = html.getHTML().toCharArray();
                contentHandler.characters(text, 0, text.length);
            }
        }

        // plain text
        else {
            char[] text = ((String) zptContent).toCharArray();
            contentHandler.characters(text, 0, text.length);
        }
    }

    private void processOnErrors(String onError, String i18nOnError, EvaluationHelper evaluationHelper,
            List<String> varsToUnset, Map<String, Object> varsToSet)
            throws EvaluationException, PageTemplateException {

        if (onError != null && i18nOnError != null) {
            throw new PageTemplateException(
                    "tal:on-error and i18n:on-error can not be at the same tag, " + "please remove one of them.");
        }

        if (onError != null) {
            setVar(evaluationHelper, varsToUnset, varsToSet, ON_ERROR_VAR_NAME, onError);
            return;
        }

        setVar(evaluationHelper, varsToUnset, varsToSet, ON_ERROR_VAR_NAME, I18N_EXPRESSION_PREFIX + i18nOnError);
    }

    static private boolean getZPTOmitTag(EvaluationHelper evaluationHelper, Expressions expressions,
            Element element) throws PageTemplateException {

        boolean zptOmitTag = false;

        // Omit tag when it is from TAL name space
        if (TAL_NAMESPACE_URI.equals(element.getNamespace().getURI())) {
            return true;
        }

        // Omit tag depending on the value of omitTag attribute
        if (expressions.omitTag != null) {
            if (expressions.omitTag.equals(VOID_STRING)) {
                zptOmitTag = true;
            } else {
                zptOmitTag = Expression.evaluateBoolean(expressions.omitTag, evaluationHelper);
            }
        }
        return zptOmitTag;
    }

    static private void processVars(EvaluationHelper evaluationHelper, List<String> varsToUnset,
            Map<String, Object> varsToSet) throws EvaluationException {

        Iterator<String> i = varsToUnset.iterator();

        while (i.hasNext()) {
            String varName = i.next();
            evaluationHelper.unset(varName);
        }

        i = varsToSet.keySet().iterator();

        while (i.hasNext()) {
            String name = i.next();
            Object value = varsToSet.get(name);
            evaluationHelper.set(name, value);
        }
    }

    @SuppressWarnings({ "unchecked" })
    private void defaultContent(Element element, ContentHandler contentHandler, LexicalHandler lexicalHandler,
            EvaluationHelper evaluationHelper, Stack<Map<String, Slot>> slotStack)
            throws SAXException, PageTemplateException, IOException, EvaluationException {
        // Use default template content
        for (Iterator<Node> i = element.nodeIterator(); i.hasNext();) {
            Node node = i.next();
            switch (node.getNodeType()) {
            case Node.ELEMENT_NODE:
                processElement((Element) node, contentHandler, lexicalHandler, evaluationHelper, slotStack);
                break;

            case Node.TEXT_NODE:
                char[] text = node.getText().toCharArray();
                contentHandler.characters(text, 0, text.length);
                break;

            case Node.COMMENT_NODE:
                char[] comment = node.getText().toCharArray();
                lexicalHandler.comment(comment, 0, comment.length);
                break;

            case Node.CDATA_SECTION_NODE:
                lexicalHandler.startCDATA();
                char[] cdata = node.getText().toCharArray();
                contentHandler.characters(cdata, 0, cdata.length);
                lexicalHandler.endCDATA();
                break;

            case Node.NAMESPACE_NODE:
                Namespace declared = (Namespace) node;
                //System.err.println( "Declared namespace: " + declared.getPrefix() + ":" + declared.getURI() );
                this.namespaces.put(declared.getPrefix(), declared.getURI());
                //if ( declared.getURI().equals( TAL_NAMESPACE_URI ) ) {
                //    this.talNamespacePrefix = declared.getPrefix();
                //} 
                //else if (declared.getURI().equals( METAL_NAMESPACE_URI ) ) {
                //    this.metalNamespacePrefix = declared.getPrefix();
                //}
                break;

            case Node.ATTRIBUTE_NODE:
                // Already handled
                break;

            case Node.DOCUMENT_TYPE_NODE:
            case Node.ENTITY_REFERENCE_NODE:
            case Node.PROCESSING_INSTRUCTION_NODE:
            default:
                //System.err.println( "WARNING: Node type not supported: " + node.getNodeTypeName() );       
            }
        }
    }

    @SuppressWarnings("unchecked")
    AttributesImpl getAttributes(Element element, Expressions expressions) throws PageTemplateException {

        AttributesImpl attributes = new AttributesImpl();
        for (Iterator<Attribute> i = element.attributeIterator(); i.hasNext();) {
            Attribute attribute = i.next();
            Namespace namespace = attribute.getNamespace();
            String name = attribute.getName();

            // Handle ZPT attributes
            if (TAL_NAMESPACE_URI.equals(namespace.getURI())) {

                // tal:define
                if (name.equals(TAL_DEFINE)) {
                    expressions.define = attribute.getValue();
                }

                // tal:condition
                else if (name.equals(TAL_CONDITION)) {
                    expressions.condition = attribute.getValue();
                }

                // tal:repeat
                else if (name.equals(TAL_REPEAT)) {
                    expressions.repeat = attribute.getValue();
                }

                // tal:content
                else if (name.equals(TAL_CONTENT)) {
                    expressions.content = attribute.getValue();
                }

                // tal:replace
                else if (name.equals(TAL_REPLACE)) {
                    if (expressions.omitTag == null) {
                        expressions.omitTag = VOID_STRING;
                    }
                    expressions.content = attribute.getValue();
                }

                // tal:attributes
                else if (name.equals(TAL_ATTRIBUTES)) {
                    expressions.attributes = attribute.getValue();
                }

                // tal:omit-tag
                else if (name.equals(TAL_OMIT_TAG)) {
                    expressions.omitTag = attribute.getValue();
                }

                // tal:on-error
                else if (name.equals(TAL_ON_ERROR)) {
                    expressions.onError = attribute.getValue();
                }

                // tal:tag
                else if (name.equals(TAL_TAG)) {
                    expressions.tag = attribute.getValue();
                }

                // error
                else {
                    throw new PageTemplateException(
                            "Unknown tal attribute: " + name + " in '" + this.template.getName() + "' template");
                }
            } else if (METAL_NAMESPACE_URI.equals(namespace.getURI())) {

                // metal:use-macro
                if (name.equals(METAL_USE_MACRO)) {
                    expressions.useMacro = attribute.getValue();
                }

                // metal:define-slot
                else if (name.equals(METAL_DEFINE_SLOT)) {
                    expressions.defineSlot = attribute.getValue();
                }

                // metal:define-macro
                // metal:fill-slot
                else if (name.equals(METAL_DEFINE_MACRO) || name.equals(METAL_FILL_SLOT)) {
                    // these are ignored here, as they don't affect processing of current
                    // template, but are called from other templates
                }

                // error
                else {
                    throw new PageTemplateException(
                            "Unknown metal attribute: " + name + " in '" + this.template.getName() + "' template");
                }
            }

            else if (I18N_NAMESPACE_URI.equals(namespace.getURI())) {

                // i18n:domain
                if (name.equals(I18N_DOMAIN)) {
                    expressions.i18nDomain = attribute.getValue();
                }

                // i18n:define
                else if (name.equals(I18N_DEFINE)) {
                    expressions.i18nDefine = attribute.getValue();
                }

                // i18n:content
                else if (name.equals(I18N_CONTENT)) {
                    expressions.i18nContent = attribute.getValue();
                }

                // i18n:replace
                else if (name.equals(I18N_REPLACE)) {
                    if (expressions.omitTag == null) {
                        expressions.omitTag = VOID_STRING;
                    }
                    expressions.i18nContent = attribute.getValue();
                }

                // i18n:attributes
                else if (name.equals(I18N_ATTRIBUTES)) {
                    expressions.i18nAttributes = attribute.getValue();
                }

                // i18n:params
                else if (name.equals(I18N_PARAMS)) {
                    expressions.i18nParams = attribute.getValue();
                }

                // i18n:on-error
                else if (name.equals(I18N_ON_ERROR)) {
                    expressions.i18nOnError = attribute.getValue();
                }

                // error
                else {
                    throw new PageTemplateException(
                            "Unknown i18n attribute: " + name + " in '" + this.template.getName() + "' template");
                }

            }

            // Pass on all other attributes
            else {
                attributes.addAttribute(namespace.getURI(), name, attribute.getQualifiedName(), CDATA,
                        attribute.getValue());
            }
        }
        return attributes;
    }

    @SuppressWarnings("unchecked")
    private List<I18n> getI18n(EvaluationHelper evaluationHelper) throws PageTemplateException {

        try {
            Object result = evaluationHelper.get(I18N_DOMAIN_VAR_NAME);

            if (result instanceof List<?>) {
                return (List<I18n>) result;
            }
        } catch (EvaluationException e) {
            throw new PageTemplateException("The " + I18N_DOMAIN_VAR_NAME + " var is not an instance ofI18n class "
                    + " in '" + this.template.getName() + "' template");
        }

        throw new PageTemplateException(I18N_DOMAIN_VAR_NAME + " instance not found.");
    }

    private Object processContent(String expression, EvaluationHelper evaluationHelper, String i18nTranslate,
            String i18nParams) throws PageTemplateException {

        // Nothing to translate, process content as usual
        if (i18nTranslate == null) {
            return processContent(expression, evaluationHelper);
        }

        return processI18nContent(evaluationHelper, i18nTranslate, i18nParams);
    }

    private Object processI18nContent(EvaluationHelper evaluationHelper, String i18nContent, String i18nParams)
            throws PageTemplateException {

        // Get the i18n instance
        List<I18n> i18nList = getI18n(evaluationHelper);

        try {
            // Translate with no params
            if (i18nParams == null) {
                return ZPTContext.getInstance().getTranslator().tr(i18nList, i18nContent);
            }

            // Translate with params
            return ZPTContext.getInstance().getTranslator().tr(i18nList, i18nContent,
                    this.getArrayFromI18nParams(i18nParams, evaluationHelper));

        } catch (NullPointerException e) {
            throw new PageTemplateException("I18n subsystem of ZPT was not initialized.");
        }
    }

    private Object[] getArrayFromI18nParams(String i18nParams, EvaluationHelper evaluationHelper)
            throws PageTemplateException {

        Object[] result = new Object[MAXIMUM_NUMBER_OF_ATTRIBUTES];

        int i = 0;
        ExpressionTokenizer tokens = new ExpressionTokenizer(i18nParams, ' ');
        while (tokens.hasMoreTokens()) {
            String valueExpression = tokens.nextToken();
            Object value = Expression.evaluate(valueExpression, evaluationHelper);
            try {
                result[i++] = value;
            } catch (ArrayIndexOutOfBoundsException e) {
                throw new PageTemplateException("Too many number of attributes, the maximum is "
                        + MAXIMUM_NUMBER_OF_ATTRIBUTES + " in '" + this.template.getName() + "' template");
            }
        }

        return result;
    }

    private Object processContent(String exp, EvaluationHelper evaluationHelper) throws PageTemplateException {

        String expression = exp;

        // Structured text, preserve xml structure
        if (expression.startsWith(STRING_EXPR_STRUCTURE)) {
            expression = expression.substring(STRING_EXPR_STRUCTURE.length());
            Object content = Expression.evaluate(expression, evaluationHelper);
            if (!(content instanceof HTMLFragment)) {
                content = new HTMLFragment(String.valueOf(content));
            }
            return content;
        } else if (expression.startsWith(STRING_EXPR_TEXT)) {
            expression = expression.substring(STRING_EXPR_TEXT.length());
        }
        return String.valueOf(Expression.evaluate(expression, evaluationHelper));
    }

    private void processI18nDomain(String expression, EvaluationHelper evaluationHelper, List<String> varsToUnset,
            Map<String, Object> varsToSet) throws PageTemplateException {

        List<I18n> i18nList = new ArrayList<I18n>();
        ExpressionTokenizer tokens = new ExpressionTokenizer(expression, ' ', true);
        while (tokens.hasMoreTokens()) {
            String valueExpression = tokens.nextToken().trim();
            I18n i18n = (I18n) Expression.evaluate(valueExpression, evaluationHelper);
            i18nList.add(i18n);
        }

        setVar(evaluationHelper, varsToUnset, varsToSet, I18N_DOMAIN_VAR_NAME, i18nList);
    }

    private void processI18nDefine(String expression, String i18nParams, EvaluationHelper evaluationHelper,
            List<String> varsToUnset, Map<String, Object> varsToSet) throws PageTemplateException {
        this.processDefineOrI18nDefine(expression, i18nParams, evaluationHelper, varsToUnset, varsToSet, true);
    }

    private void processDefine(String expression, EvaluationHelper evaluationHelper, List<String> varsToUnset,
            Map<String, Object> varsToSet) throws PageTemplateException {
        this.processDefineOrI18nDefine(expression, null, evaluationHelper, varsToUnset, varsToSet, false);
    }

    private void processDefineOrI18nDefine(String expression, String i18nParams, EvaluationHelper evaluationHelper,
            List<String> varsToUnset, Map<String, Object> varsToSet, boolean translate)
            throws PageTemplateException {

        ExpressionTokenizer tokens = new ExpressionTokenizer(expression, DEFINE_DELIMITER, true);
        while (tokens.hasMoreTokens()) {
            String variable = tokens.nextToken().trim();
            int space = variable.indexOf(IN_DEFINE_DELIMITER);
            if (space == -1) {
                throw new ExpressionSyntaxException(
                        "Bad variable definition: " + variable + " in '" + this.template.getName() + "' template");
            }
            String name = variable.substring(0, space);
            String valueExpression = variable.substring(space + 1).trim();
            Object value = Expression.evaluate(valueExpression, evaluationHelper);

            if (translate) {
                setVar(evaluationHelper, varsToUnset, varsToSet, name,
                        processI18nContent(evaluationHelper, valueExpression, i18nParams));
            } else {
                setVar(evaluationHelper, varsToUnset, varsToSet, name, value);
            }
        }
    }

    private void processAttributes(AttributesImpl attributes, String expression, String i18nParams,
            EvaluationHelper evaluationHelper, String i18nAttributes) throws PageTemplateException {

        if (i18nAttributes != null) {
            processAttributes(attributes, i18nAttributes, i18nParams, evaluationHelper, true);
        }
        if (expression != null) {
            processAttributes(attributes, expression, null, evaluationHelper, false);
        }
    }

    private void processAttributes(AttributesImpl attributes, String expression, String i18nParams,
            EvaluationHelper evaluationHelper, boolean translate) throws PageTemplateException {

        ExpressionTokenizer tokens = new ExpressionTokenizer(expression, ATTRIBUTE_DELIMITER, true);
        while (tokens.hasMoreTokens()) {
            String attribute = tokens.nextToken().trim();
            int space = attribute.indexOf(IN_ATTRIBUTE_DELIMITER);
            if (space == -1) {
                throw new ExpressionSyntaxException("Bad attributes expression: " + attribute + " in '"
                        + this.template.getName() + "' template");
            }
            String qualifiedName = attribute.substring(0, space);
            String valueExpression = attribute.substring(space + 1).trim();
            Object value = translate ? processI18nContent(evaluationHelper, valueExpression, i18nParams)
                    : Expression.evaluate(valueExpression, evaluationHelper);
            //System.err.println( "attribute:\n" + qualifiedName + "\n" + valueExpression + "\n" + value );
            removeAttribute(attributes, qualifiedName);
            if (value != null) {
                String name = VOID_STRING;
                String uri = VOID_STRING;
                int colon = qualifiedName.indexOf(":");
                if (colon != -1) {
                    String prefix = qualifiedName.substring(0, colon);
                    name = qualifiedName.substring(colon + 1);
                    uri = getNamespaceURIFromPrefix(prefix);
                }
                attributes.addAttribute(uri, name, qualifiedName, CDATA, String.valueOf(value));
            }
        }
    }

    private void removeAttribute(AttributesImpl attributes, String qualifiedName) {
        int index = attributes.getIndex(qualifiedName);
        if (index != -1) {
            attributes.removeAttribute(index);
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private void processMacro(String expression, Element element, ContentHandler contentHandler,
            LexicalHandler lexicalHandler, EvaluationHelper evaluationHelper, Stack<Map<String, Slot>> slotStack)
            throws SAXException, PageTemplateException, IOException, EvaluationException {

        Object object = Expression.evaluate(expression, evaluationHelper);
        if (object == null) {
            throw new NoSuchPathException(
                    "Could not find macro: '" + expression + "' in '" + this.template.getName() + "' template");
        }

        if (object instanceof Macro) {
            // Find slots to fill inside this macro call
            Map<String, Slot> slots = new HashMap<String, Slot>();
            findSlots(element, slots);

            // Slots filled in later templates (processed earlier) 
            // Take precedence over slots filled in intermediate
            // templates.
            if (!slotStack.isEmpty()) {
                Map laterSlots = slotStack.peek();
                slots.putAll(laterSlots);
            }
            slotStack.push(slots);
            //System.err.println( "found slots: " + slots.keySet() );

            // Call macro
            Macro macro = (Macro) object;
            macro.process(contentHandler, lexicalHandler, evaluationHelper, slotStack);
        } else {
            throw new PageTemplateException("Expression '" + expression + "' does not evaluate to macro: "
                    + object.getClass().getName() + " in '" + this.template.getName() + "' template");
        }
    }

    @SuppressWarnings({ "rawtypes" })
    private void findSlots(Element element, Map<String, Slot> slots) {

        // Look for our attribute
        //String qualifiedAttributeName = this.metalNamespacePrefix + ":fill-slot";
        //String name = element.attributeValue( qualifiedAttributeName );
        String name = element.attributeValue(METAL_FILL_SLOT);
        if (name != null) {
            slots.put(name, new SlotImpl(element));
        }

        // Recurse into child elements
        for (Iterator i = element.elementIterator(); i.hasNext();) {
            findSlots((Element) i.next(), slots);
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private void findMacros(Element element, Map<String, Macro> macros) {
        // Process any declared namespaces
        for (Iterator<Namespace> i = element.declaredNamespaces().iterator(); i.hasNext();) {
            Namespace namespace = i.next();
            this.namespaces.put(namespace.getPrefix(), namespace.getURI());
        }

        // Look for our attribute
        String name = element.attributeValue(METAL_DEFINE_MACRO);
        if (name != null) {
            macros.put(name, new MacroImpl(element));
        }

        // Recurse into child elements
        for (Iterator i = element.elementIterator(); i.hasNext();) {
            findMacros((Element) i.next(), macros);
        }
    }

    @Override
    public String toLetter(int n) {
        return Loop.formatLetter(n);
    }

    @Override
    public String toCapitalLetter(int n) {
        return Loop.formatCapitalLetter(n);
    }

    @Override
    public String toRoman(int n) {
        return Loop.formatRoman(n);
    }

    @Override
    public String toCapitalRoman(int n) {
        return Loop.formatCapitalRoman(n);
    }

    @Override
    public Map<String, Macro> getMacros() {

        if (this.macros == null) {
            this.macros = new HashMap<String, Macro>();
            findMacros(this.template.getRootElement(), this.macros);
        }
        return this.macros;
    }

    class DefaultResolver extends Resolver {
        URIResolver uriResolver;

        DefaultResolver() {
            if (PageTemplateImpl.this.uri != null) {
                this.uriResolver = new URIResolver(PageTemplateImpl.this.uri);
            }
        }

        @Override
        public URL getResource(String path) throws MalformedURLException {

            URL resource = null;

            // If user has supplied resolver, use it
            if (PageTemplateImpl.this.userResolver != null) {
                resource = PageTemplateImpl.this.userResolver.getResource(path);
            }

            // If resource not found by user resolver
            // fall back to resolving by uri
            if (resource == null && this.uriResolver != null) {
                resource = this.uriResolver.getResource(path);
            }

            return resource;
        }

        @Override
        public OnePhasePageTemplate getPageTemplate(String path)
                throws PageTemplateException, MalformedURLException {

            OnePhasePageTemplate template = null;

            // If user has supplied resolver, use it
            if (PageTemplateImpl.this.userResolver != null) {
                template = PageTemplateImpl.this.userResolver.getPageTemplate(path);

                // template inherits user resolver
                template.setResolver(PageTemplateImpl.this.userResolver);
            }

            // If template not found by user resolver
            // fall back to resolving by uri
            if (template == null && this.uriResolver != null) {
                template = this.uriResolver.getPageTemplate(path);
            }

            return template;
        }
    }

    class MacroImpl implements Macro {
        Element element;

        MacroImpl(Element element) {
            this.element = element;
        }

        @Override
        public void process(ContentHandler contentHandler, LexicalHandler lexicalHandler,
                EvaluationHelper evaluationHelper, Stack<Map<String, Slot>> slotStack)
                throws SAXException, PageTemplateException, IOException, EvaluationException {

            processElement(this.element, contentHandler, lexicalHandler, evaluationHelper, slotStack);
            saveToCache();
        }
    }

    class SlotImpl implements Slot {
        Element element;

        SlotImpl(Element element) {
            this.element = element;
        }

        @Override
        public void process(ContentHandler contentHandler, LexicalHandler lexicalHandler,
                EvaluationHelper evaluationHelper, Stack<Map<String, Slot>> slotStack)
                throws SAXException, PageTemplateException, IOException, EvaluationException {

            processElement(this.element, contentHandler, lexicalHandler, evaluationHelper, slotStack);
            saveToCache();
        }
    }
}