/*
 * Copyright 2004-2012 the Seasar Foundation and the Others.
 *
 * 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.seasar.mayaa.impl.builder;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Stack;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.seasar.mayaa.builder.SequenceIDGenerator;
import org.seasar.mayaa.builder.TemplateBuilder;
import org.seasar.mayaa.builder.injection.InjectionChain;
import org.seasar.mayaa.builder.injection.InjectionResolver;
import org.seasar.mayaa.builder.library.LibraryManager;
import org.seasar.mayaa.builder.library.ProcessorDefinition;
import org.seasar.mayaa.cycle.ServiceCycle;
import org.seasar.mayaa.cycle.scope.ApplicationScope;
import org.seasar.mayaa.cycle.script.CompiledScript;
import org.seasar.mayaa.engine.Template;
import org.seasar.mayaa.engine.processor.OptimizableProcessor;
import org.seasar.mayaa.engine.processor.ProcessorTreeWalker;
import org.seasar.mayaa.engine.processor.TemplateProcessor;
import org.seasar.mayaa.engine.specification.NodeTreeWalker;
import org.seasar.mayaa.engine.specification.PrefixMapping;
import org.seasar.mayaa.engine.specification.QName;
import org.seasar.mayaa.engine.specification.Specification;
import org.seasar.mayaa.engine.specification.SpecificationNode;
import org.seasar.mayaa.engine.specification.URI;
import org.seasar.mayaa.impl.builder.injection.DefaultInjectionChain;
import org.seasar.mayaa.impl.builder.parser.AdditionalHandler;
import org.seasar.mayaa.impl.builder.parser.TemplateParser;
import org.seasar.mayaa.impl.builder.parser.TemplateScanner;
import org.seasar.mayaa.impl.cycle.CycleUtil;
import org.seasar.mayaa.impl.engine.processor.AttributeProcessor;
import org.seasar.mayaa.impl.engine.processor.CharactersProcessor;
import org.seasar.mayaa.impl.engine.processor.CommentProcessor;
import org.seasar.mayaa.impl.engine.processor.DoBodyProcessor;
import org.seasar.mayaa.impl.engine.processor.ElementProcessor;
import org.seasar.mayaa.impl.engine.processor.LiteralCharactersProcessor;
import org.seasar.mayaa.impl.engine.specification.QNameImpl;
import org.seasar.mayaa.impl.engine.specification.SpecificationUtil;
import org.seasar.mayaa.impl.provider.ProviderUtil;
import org.seasar.mayaa.impl.util.ObjectUtil;
import org.seasar.mayaa.impl.util.StringUtil;
import org.seasar.mayaa.impl.util.xml.XMLReaderPool;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

/**
 * @author Masataka Kurihara (Gluegent, Inc.)
 */
public class TemplateBuilderImpl extends SpecificationBuilderImpl
        implements TemplateBuilder {
    public static final String DEFAULT_CHARSET = "defaultCharset";
    public static final String BALANCE_TAG = "balanceTag";
    public static final String REPLACE_SSI_INCLUDE = "replaceSSIInclude";
    public static final String OPTIMIZE = "optimize";
    public static final String OUTPUT_TEMPLATE_WHITESPACE = "outputTemplateWhitespace";

    private static final Log LOG = LogFactory.getLog(TemplateBuilderImpl.class);
    private static final long serialVersionUID = -1031702086020145692L;

    private List _resolvers = new ArrayList();
    private List _unmodifiableResolvers =
        Collections.unmodifiableList(_resolvers);
    private HtmlReaderPool _htmlReaderPool = new HtmlReaderPool();
    private transient InjectionChain _chain = new DefaultInjectionChain();
    private boolean _outputTemplateWhitespace = true;
    private boolean _optimize = true;
    private boolean _isSSIIncludeReplacementEnabled = false;

    public void addInjectionResolver(InjectionResolver resolver) {
        if (resolver == null) {
            throw new IllegalArgumentException();
        }
        synchronized (_resolvers) {
            _resolvers.add(resolver);
        }
    }

    protected List getInjectionResolvers() {
        return _unmodifiableResolvers;
    }

    protected boolean isHTML(String mimeType) {
        return mimeType != null && (mimeType.indexOf("html") != -1);
    }

    protected XMLReaderPool getXMLReaderPool(String systemID) {
        ApplicationScope application = CycleUtil.getServiceCycle().getApplicationScope();
        String mimeType = application.getMimeType(systemID);
        if (isHTML(mimeType)) {
            return _htmlReaderPool;
        }
        return super.getXMLReaderPool(systemID);
    }

    protected SpecificationNodeHandler createContentHandler(
            Specification specification) {
        if (specification instanceof Template == false) {
            throw new IllegalArgumentException();
        }
        TemplateNodeHandler handler =
            new TemplateNodeHandler((Template) specification);
        handler.setOutputTemplateWhitespace(isOutputTemplateWhitespace());
        handler.setSSIIncludeReplacementEnabled(isSSIIncludeReplacementEnabled());
        return handler;
    }

    protected String getPublicID() {
        return URI_MAYAA + "/template";
    }

    /**
     * SSI includeが有効なときに自動的にm:doRenderで囲むファイルの拡張子。
     * デフォルト実装では".inc"。(ASPの慣習)
     * @return 自動的にm:doRenderで全体を囲む処理をするファイルの拡張子。
     */
    protected String getIncludeExtension() {
        return ".inc";
    }

    protected void afterBuild(Specification specification) {
        if ((specification instanceof Template) == false) {
            throw new IllegalArgumentException();
        }

        Template template = (Template) specification;
        String systemID = specification.getSystemID();
        /*
         * SSI includeが有効なとき、{@link #getIncludeExtension()}の拡張子であれば
         * divで囲んで自動的にm:doRenderになるようにする。ただしNekoHTMLParserの都合上、
         * 最低限何かのタグで囲む必要がある。(ルートに文字列があっても無視される)
         */
        if (isSSIIncludeReplacementEnabled() && systemID.endsWith(getIncludeExtension())) {
            int childCount = template.getChildNodeSize();
            if (childCount > 0) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("enclose " + systemID + " with m:doRender.(name=\"\")");
                }

                // 強制的にデフォルト名前空間をmime-typeからセットする。
                ApplicationScope application = CycleUtil.getServiceCycle().getApplicationScope();
                String mimeType = application.getMimeType(systemID);
                if (StringUtil.hasValue(mimeType)) {
                    if (mimeType.indexOf("xhtml") >= 0) {
                        setDefaultNamespaceURI(template, URI_XHTML);
                    } else if (mimeType.indexOf("html") >= 0) {
                        setDefaultNamespaceURI(template, URI_HTML);
                    }
                }

                URI namespace = ((SpecificationNode)template.getChildNode(0)).getDefaultNamespaceURI();
                QName qName = QNameImpl.getInstance(namespace, "div");
                SpecificationNode root = SpecificationUtil.createSpecificationNode(
                        qName, systemID, 0, true, specification.nextSequenceID());
                root.addPrefixMapping("m", URI_MAYAA);
                root.addAttribute(QM_INJECT, "m:doRender");
                for (int i = 0; i < childCount; i++) {
                    root.addChildNode(template.getChildNode(i));
                }
                template.clearChildNodes();
                template.addChildNode(root);
            }
        }

        LOG.debug("built node tree from template. " + systemID);
        doInjection((Template) specification);
        if (isOptimizeEnabled()) {
            nodeOptimize((Template) specification);
        }
        LOG.debug("built processor tree from node tree. " + specification.getSystemID());
    }

    /**
     * {@code template}の先頭子ノードのデフォルト名前空間が{@code null}かまたはXMLの名前空間の場合、
     * このテンプレート上でデフォルト名前空間がXML, HTML, XHTMLのいずれかであるものすべての
     * デフォルト名前空間を{@code namespace}に変更する。
     * @param template
     * @param namespace
     */
    protected void setDefaultNamespaceURI(Template template, URI namespace) {
        int size = template.getChildNodeSize();
        if (size > 0) {
            URI firstDefaultNS = ((SpecificationNode)template.getChildNode(0)).getDefaultNamespaceURI();
            if (firstDefaultNS == null || firstDefaultNS.equals(URI_XML)) {
                for (int i = 0; i < size; i++) {
                    setDefaultNamespaceURIToNode((SpecificationNode)template.getChildNode(i), namespace);
                }
            }
        }
    }

    /**
     * parentNodeのdefault namespaceがXML, HTML, XHTMLのいずれかなら、強制的にパラメータのnamespaceを
     * default namespaceにする。子ノードも同様に処理する。
     * @param parentNode
     * @param namespace
     */
    protected void setDefaultNamespaceURIToNode(SpecificationNode parentNode, URI namespace) {
        if (parentNode.getDefaultNamespaceURI() == null ||
                parentNode.getDefaultNamespaceURI().equals(URI_XML) ||
                parentNode.getDefaultNamespaceURI().equals(URI_HTML) ||
                parentNode.getDefaultNamespaceURI().equals(URI_XHTML)) {
            parentNode.setDefaultNamespaceURI(namespace);
        }
        int size = parentNode.getChildNodeSize();
        for (int i = 0; i < size; i++) {
            SpecificationNode node = (SpecificationNode)parentNode.getChildNode(i);
            if (node.getChildNodeSize() > 0) {
                setDefaultNamespaceURIToNode(node, namespace);
            }
        }
    }

    private static class AbsoluteCompareList extends ArrayList {
        private static final long serialVersionUID = 1L;

        protected AbsoluteCompareList() {
            // do nothing.
        }

        public int indexOf(Object elem) {
            for (int i = 0; i < size(); i++) {
                if (get(i) == elem) {
                    return i;
                }
            }
            return -1;
        }
    }

    private static class NodeAndChildren {
        public SpecificationNode _originalNode;
        public List _list = new ArrayList();
        public NodeAndChildren(SpecificationNode originalNode) {
            _originalNode = originalNode;
        }
    }

    protected void nodeOptimize(Template template) {
        SequenceIDGenerator idGenerator = template;
        List children = new ArrayList();
        List usingNodes = new AbsoluteCompareList();
        walkTreeOptimizeNode(idGenerator, template, children, usingNodes);

        SpecificationNode mayaaNode = SpecificationUtil.getMayaaNode(template);
        breakRelationship(template);
        idGenerator.resetSequenceID(1);
        walkTreeNodeChainModify(idGenerator, template, children);
        template.addChildNode(mayaaNode);
    }

    protected void breakRelationship(NodeTreeWalker parent) {
        for (int i = 0; i < parent.getChildNodeSize(); i++) {
            breakRelationship(parent.getChildNode(i));
        }
        parent.clearChildNodes();
    }

    protected void walkTreeNodeChainModify(SequenceIDGenerator idGenerator,
            NodeTreeWalker parent, List children) {
        parent.clearChildNodes();
        for (int i = 0; i < children.size(); i++) {
            NodeAndChildren nodeAndChildren = (NodeAndChildren) children.get(i);
            SpecificationNode node = nodeAndChildren._originalNode;
            node.setSequenceID(idGenerator.nextSequenceID());
            node.setParentNode(parent);
            parent.addChildNode(node);
            walkTreeNodeChainModify(idGenerator, node, nodeAndChildren._list);
        }
    }

    protected void walkTreeOptimizeNode(
            SequenceIDGenerator idGenerator, ProcessorTreeWalker parent,
            List children, List usingNodes) {
        for (int i = 0; i < parent.getChildProcessorSize(); i++) {
            ProcessorTreeWalker walker = parent.getChildProcessor(i);
            if (walker instanceof TemplateProcessor) {
                TemplateProcessor processor = (TemplateProcessor) walker;
                SpecificationNode node = processor.getOriginalNode();
                if (processor instanceof LiteralCharactersProcessor) {
                    node = SpecificationUtil.createSpecificationNode(
                            QM_CHARACTERS, node.getSystemID(),
                            node.getLineNumber(), true,
                            idGenerator.nextSequenceID());
                    //node.addAttribute(QM_TEXT, "");   /*don't set text. because memory is not used.*/
                    processor.setOriginalNode(node);

                    SpecificationNode injectedNode = processor.getInjectedNode();
                    injectedNode = SpecificationUtil.createSpecificationNode(
                            QM_LITERALS, injectedNode.getSystemID(),
                            injectedNode.getLineNumber(), false, 1);
                    processor.setInjectedNode(injectedNode);
                } else {
                    if (processor instanceof ElementProcessor == false
                            && processor instanceof AttributeProcessor == false) {
                        //Namespace root = NamespaceImpl.getInstance("/null\n");
                        processor.getOriginalNode().setParentSpace(null);
                        processor.getInjectedNode().setParentSpace(null);
                        //System.out.println(processor.getClass().getName());
                    }
                }
                // to repair for originalNode infinite loop
                if (parent instanceof ElementProcessor
                		&& ((ElementProcessor)parent).getOriginalNode() == node) {
                	continue;
                }
                NodeAndChildren nodeAndChildren = new NodeAndChildren(node);
                children.add(nodeAndChildren);
                usingNodes.add(node);
                if (processor.getChildProcessorSize() > 0) {
                    walkTreeOptimizeNode(idGenerator, processor,
                            nodeAndChildren._list, usingNodes);
                }
            }
        }
    }

    protected void saveToCycle(NodeTreeWalker originalNode,
            NodeTreeWalker injectedNode) {
        ServiceCycle cycle = CycleUtil.getServiceCycle();
        cycle.setOriginalNode(originalNode);
        cycle.setInjectedNode(injectedNode);
    }

    protected TemplateProcessor findConnectPoint(
            TemplateProcessor processor) {
        if (processor instanceof ElementProcessor
                && ((ElementProcessor) processor).isDuplicated()) {
            // "processor"'s m:replace is true, ripping duplicated element.
            return findConnectPoint(
                    (TemplateProcessor) processor.getChildProcessor(0));
        }
        for (int i = 0; i < processor.getChildProcessorSize(); i++) {
            ProcessorTreeWalker child = processor.getChildProcessor(i);
            if (child instanceof CommentProcessor) {
            	return null;
            } else if (child instanceof CharactersProcessor) {
                CharactersProcessor charsProc = (CharactersProcessor) child;
                CompiledScript script = charsProc.getText().getValue();
                if (script.isLiteral()) {
                    String value = script.getScriptText();
                    if (StringUtil.hasValue(value.trim())) {
                        // "processor" has child which is not empty.
                        return null;
                    }
                } else {
                    // "processor" has child which is scriptlet.
                    return null;
                }
            } else if (child instanceof AttributeProcessor == false) {
                // "processor" has child which is implicit m:characters or
                // nested child node, but is NOT m:attribute
                return null;
            }
        }
        return processor;
    }

    protected TemplateProcessor createProcessor(
            SpecificationNode original, SpecificationNode injected) {
        QName name = injected.getQName();
        LibraryManager libraryManager = ProviderUtil.getLibraryManager();
        ProcessorDefinition def = libraryManager.getProcessorDefinition(name);
        if (def != null) {
            TemplateProcessor proc =
                def.createTemplateProcessor(original, injected);
            proc.setOriginalNode(original);
            proc.setInjectedNode(injected);
            return proc;
        }
        return null;
    }

    protected InjectionChain getDefaultInjectionChain() {
        return _chain;
    }

    protected TemplateProcessor resolveInjectedNode(Template template,
            Stack stack, SpecificationNode original, SpecificationNode injected,
            Set divided) {
        if (injected == null) {
            throw new IllegalArgumentException();
        }
        saveToCycle(original, injected);
        TemplateProcessor processor = createProcessor(original, injected);
        if (processor == null) {
            PrefixMapping mapping = original.getMappingFromPrefix("", true);
            if (mapping == null) {
                throw new IllegalStateException();
            }
            URI defaultURI = mapping.getNamespaceURI();
            if (defaultURI.equals(injected.getQName().getNamespaceURI())) {
                InjectionChain chain = getDefaultInjectionChain();
                SpecificationNode retry = chain.getNode(injected);
                processor = createProcessor(original, retry);
            }
            if (processor == null) {
                throw new ProcessorNotInjectedException(injected.toString());
            }
        }
        ProcessorTreeWalker parent = (ProcessorTreeWalker) stack.peek();
        parent.addChildProcessor(processor);
        processor.initialize();
        Iterator it = injected.iterateChildNode();
        if (it.hasNext() == false) {
            return processor;
        }
        // "injected" node has children, nested node definition on .mayaa
        stack.push(processor);
        while (it.hasNext()) {
            SpecificationNode childNode = (SpecificationNode) it.next();
            saveToCycle(original, childNode);
            TemplateProcessor childProcessor =
                resolveInjectedNode(
                    template, stack, original, childNode, divided);
            if (childProcessor instanceof DoBodyProcessor) {
                stack.push(childProcessor);
                walkParsedTree(template, stack, original, divided);
                stack.pop();
            }
        }
        stack.pop();
        saveToCycle(original, injected);
        return findConnectPoint(processor);
    }

    /**
     * ノードを最適化する。
     * 親要素{@link parent}に子要素のリスト{@link collector}をすべて追加する。
     * その際、静的部分と動的部分を分割可能なものがあれば分割し、一度分割したという
     * マーキング的な意味合いで{@link divided}に追加する。
     * @param idGenerator
     * @param parent 親要素
     * @param collector 子要素
     * @param divided 静的部分と動的部分を分割し
     */
    protected void optimizeProcessors(SequenceIDGenerator idGenerator,
            ProcessorTreeWalker parent, List collector, Set divided) {
        List expands = new ArrayList();
        Iterator it = collector.iterator();
        while (it.hasNext()) {
            ProcessorTreeWalker processor = (ProcessorTreeWalker) it.next();
            if (processor == null) {
                throw new IllegalStateException("processor is null");
            }
            if (processor instanceof OptimizableProcessor
                    && divided.contains(processor) == false) {
                ProcessorTreeWalker[] processors =
                    ((OptimizableProcessor) processor).divide(idGenerator);
                for (int i = 0; i < processors.length; i++) {
                    expands.add(processors[i]);
                }
                divided.add(processor);
            } else {
                expands.add(processor);
            }
        }

        List packs = new ArrayList();
        for (int i = 0; i < expands.size(); i++) {
            ProcessorTreeWalker node = (ProcessorTreeWalker) expands.get(i);
            node = convertCharactersProcessor(node, idGenerator);

            if (packs.size() > 0 && node instanceof LiteralCharactersProcessor) {
                Object last = packs.get(packs.size()-1);
                if (last instanceof LiteralCharactersProcessor) {
                    LiteralCharactersProcessor rawLast =
                        (LiteralCharactersProcessor) last;
                    rawLast.setText(rawLast.getText() +
                            ((LiteralCharactersProcessor) node).getText());
                } else {
                    packs.add(node);
                }
            } else {
                packs.add(node);
            }
        }
        for (int i = 0; i < packs.size(); i++) {
            ProcessorTreeWalker child = (ProcessorTreeWalker)packs.get(i);
            child.setParentProcessor(parent);
            parent.addChildProcessor(child);
        }
    }

    protected ProcessorTreeWalker convertCharactersProcessor(
            ProcessorTreeWalker processor, SequenceIDGenerator idGenerator) {
        if (processor instanceof CharactersProcessor) {
            CharactersProcessor cnode = (CharactersProcessor)processor;
            if (cnode.getText() != null
                    && cnode.getText().getValue() != null
                    && cnode.getText().getValue().isLiteral()) {
                LiteralCharactersProcessor literalProcessor =
                    new LiteralCharactersProcessor(
                            cnode.getText().getValue().getScriptText());
                BuilderUtil.characterProcessorCopy(
                        cnode, literalProcessor, idGenerator);
                processor = literalProcessor;
            }
        }
        return processor;
    }

    protected SpecificationNode resolveOriginalNode(
            SpecificationNode original, InjectionChain chain) {
        if (original == null || chain == null) {
            throw new IllegalArgumentException();
        }
        if (_resolvers.size() > 0) {
            InjectionChainImpl first = new InjectionChainImpl(chain);
            return first.getNode(original);
        }
        return chain.getNode(original);
    }

    protected void walkParsedTree(
            Template template, Stack stack, NodeTreeWalker original,
            Set divided) {
        if (original == null) {
            throw new IllegalArgumentException();
        }
        Iterator it = original.iterateChildNode();
        while (it.hasNext()) {
            SpecificationNode child;
            try {
                child = (SpecificationNode) it.next();
            } catch(ConcurrentModificationException e) {
                LOG.error("original.childNodes をイテレート中に更新されてしまった。", e);
                throw e;
            }
            saveToCycle(child, child);
            if (QM_MAYAA.equals(child.getQName())) {
                continue;
            }
            InjectionChain chain = getDefaultInjectionChain();
            SpecificationNode injected = resolveOriginalNode(child, chain);
            if (injected == null) {
                throw new TemplateNodeNotResolvedException(original.toString());
            }
            saveToCycle(child, injected);
            ProcessorTreeWalker processor = resolveInjectedNode(
                    template, stack, child, injected, divided);
            if (processor != null) {
                stack.push(processor);
                walkParsedTree(template, stack, child, divided);
                stack.pop();
            }
        }
        if (isOptimizeEnabled()) {
            ProcessorTreeWalker parent = (ProcessorTreeWalker) stack.peek();
            int count = parent.getChildProcessorSize();
            if (count > 0) {
                List childProcessors = new ArrayList();
                for (int i = 0; i < count; i++) {
                    childProcessors.add(parent.getChildProcessor(i));
                }
                parent.clearChildProcessors();
                optimizeProcessors(template, parent, childProcessors, divided);
            }
        }
    }

    protected boolean isOutputTemplateWhitespace() {
        return _outputTemplateWhitespace;
    }
    protected void setOutputTemplateWhitespace(boolean outputTemplateWhitespace) {
        _outputTemplateWhitespace = outputTemplateWhitespace;
    }
    protected boolean isSSIIncludeReplacementEnabled() {
        return _isSSIIncludeReplacementEnabled;
    }
    protected void setSSIIncludeReplacementEnabled(boolean isSSIIncludeReplacementEnabled) {
        _isSSIIncludeReplacementEnabled = isSSIIncludeReplacementEnabled;
    }
    protected boolean isOptimizeEnabled() {
        return _optimize;
    }
    protected void setOptimizeEnabled(boolean optimize) {
        _optimize = optimize;
    }

    // Parameterizable implements ------------------------------------

    public void setParameter(String name, String value) {
        if (OUTPUT_TEMPLATE_WHITESPACE.equals(name)) {
            setOutputTemplateWhitespace(ObjectUtil.booleanValue(value, true));
        } else if (OPTIMIZE.equals(name)) {
            setOptimizeEnabled(ObjectUtil.booleanValue(value, true));
        } else if (REPLACE_SSI_INCLUDE.equals(name)) {
            setSSIIncludeReplacementEnabled(ObjectUtil.booleanValue(value, false));
        } else if (DEFAULT_CHARSET.equals(name)) {
            try {
                "".getBytes(value);
                _htmlReaderPool.setDefaultCharset(value);
            } catch (UnsupportedEncodingException e) {
                String message =
                    StringUtil.getMessage(TemplateBuilderImpl.class, 0, value);
                LOG.warn(message, e);
            }
        } else if (BALANCE_TAG.equals(name)) {
        	_htmlReaderPool.setBalanceTag(ObjectUtil.booleanValue(value, true));
        }
        super.setParameter(name, value);
    }

    protected void doInjection(Template template) {
        if (template == null) {
            throw new IllegalArgumentException();
        }
        saveToCycle(template, template);
        Stack stack = new Stack();
        stack.push(template);
        SpecificationNode mayaa = SpecificationUtil.createSpecificationNode(
                QM_MAYAA, template.getSystemID(), 0, true, 0);
        template.addChildNode(mayaa);

        Set divided = null;
        if (isOptimizeEnabled()) {
            divided = new HashSet();
        }
        walkParsedTree(template, stack, template, divided);
        if (template.equals(stack.peek()) == false) {
            throw new IllegalStateException();
        }
        saveToCycle(template, template);
    }


    // deserialization
    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject();

        _chain = new DefaultInjectionChain();
    }

    // support class --------------------------------------------------

    protected static class HtmlReaderPool extends XMLReaderPool {

        private static final long serialVersionUID = -5203349759797583368L;
        private String _defaultCharset = TEMPLATE_DEFAULT_CHARSET;
        private boolean _balanceTag = true;

        protected void setDefaultCharset(String charset) {
            _defaultCharset = charset;
        }

        protected void setBalanceTag(boolean balanceTag) {
        	_balanceTag = balanceTag;
        }

        protected Object createObject() {
            return new TemplateParser(new TemplateScanner(), _defaultCharset, _balanceTag);
        }

        protected boolean validateObject(Object object) {
            return object instanceof TemplateParser;
        }

        public XMLReader borrowXMLReader(ContentHandler handler,
                boolean namespaces, boolean validation, boolean xmlSchema) {
            XMLReader htmlReader = super.borrowXMLReader(
                    handler, namespaces, validation, xmlSchema);
            if (handler instanceof AdditionalHandler) {
                try {
                    htmlReader.setProperty(
                            AdditionalHandler.ADDITIONAL_HANDLER, handler);
                } catch (SAXException e) {
                    throw new RuntimeException(e);
                }
            }
            return htmlReader;
        }

    }

    protected class InjectionChainImpl implements InjectionChain {

        private int _index;
        private InjectionChain _external;

        public InjectionChainImpl(InjectionChain external) {
            _external = external;
        }

        public SpecificationNode getNode(SpecificationNode original) {
            if (original == null) {
                throw new IllegalArgumentException();
            }
            if (_index < getInjectionResolvers().size()) {
                InjectionResolver resolver =
                    (InjectionResolver) getInjectionResolvers().get(_index);
                _index++;
                InjectionChain chain;
                if (_index == getInjectionResolvers().size()) {
                    chain = _external;
                } else {
                    chain = this;
                }
                return resolver.getNode(original, chain);
            }
            throw new IndexOutOfBoundsException();
        }

    }

}
