    001// Copyright 2006, 2007, 2008, 2009, 2010, 2014 The Apache Software Foundation
    002//
    003// Licensed under the Apache License, Version 2.0 (the "License");
    004// you may not use this file except in compliance with the License.
    005// You may obtain a copy of the License at
    006//
    007// http://www.apache.org/licenses/LICENSE-2.0
    008//
    009// Unless required by applicable law or agreed to in writing, software
    010// distributed under the License is distributed on an "AS IS" BASIS,
    011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012// See the License for the specific language governing permissions and
    013// limitations under the License.
    014
    015package org.apache.tapestry5.dom;
    016
    017import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    018import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    019
    020import java.io.PrintWriter;
    021import java.util.Collections;
    022import java.util.List;
    023import java.util.Map;
    024
    025/**
    026 * The root node of a DOM.
    027 */
    028public final class Document extends Node
    029{
    030    /**
    031     * XML Namespace URI. May be bound to the "xml" but must not be bound to any other prefix.
    032     */
    033    public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
    034
    035    /**
    036     * Namespace used exclusively for defining namespaces.
    037     */
    038    public static final String XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";
    039
    040    private Element rootElement;
    041
    042    private DTD dtd;
    043
    044    private final MarkupModel model;
    045
    046    private final String encoding;
    047
    048    private final String mimeType;
    049
    050    /**
    051     * Non-element content that comes between the DOCTYPE and the root element.
    052     */
    053    private List<Node> preamble;
    054
    055    /**
    056     * Same as Document(model, null).
    057     * @param model a {@link MarkupModel}.
    058     */
    059    public Document(final MarkupModel model)
    060    {
    061        this(model, null);
    062    }
    063
    064    /**
    065     * Same as Document(model, encoding, null).
    066     * @param model a {@link MarkupModel}.
    067     */
    068    public Document(final MarkupModel model, final String encoding) {
    069        this(model, encoding, null);
    070    }
    071
    072    /**
    073     * Creates a document instance with a given markup model, encoding and MIME type.
    074     * @param model a {@link MarkupModel}.
    075     * @param encoding the encoding.
    076     * @param mimeType the MIME type.
    077     * @since 5.4
    078     */
    079    public Document(final MarkupModel model, final String encoding, final String mimeType)
    080    {
    081        super(null);
    082
    083        assert model != null;
    084
    085        this.model = model;
    086        this.encoding = encoding;
    087        this.mimeType = mimeType;
    088    }
    089
    090    @Override
    091    public Document getDocument()
    092    {
    093        return this;
    094    }
    095
    096    /**
    097     * Finds an element based on a path of element names.
    098     *
    099     * @param path slash separated series of element names
    100     * @return the matching element, or null if not found
    101     * @see Element#find(String)
    102     */
    103    public Element find(String path)
    104    {
    105        assert InternalUtils.isNonBlank(path);
    106
    107        if (rootElement == null)
    108            return null;
    109
    110        int slashx = path.indexOf("/");
    111
    112        String rootElementName = slashx < 0 ? path : path.substring(0, slashx);
    113
    114        if (!rootElement.getName().equals(rootElementName))
    115            return null;
    116
    117        return slashx < 0 ? rootElement : rootElement.find(path.substring(slashx + 1));
    118    }
    119
    120    /**
    121     * Builds with an instance of {@link DefaultMarkupModel}.
    122     */
    123    public Document()
    124    {
    125        this(new DefaultMarkupModel());
    126    }
    127
    128    public MarkupModel getMarkupModel()
    129    {
    130        return model;
    131    }
    132
    133    /**
    134     * Creates the root element for this document, replacing any previous root element.
    135     */
    136    public Element newRootElement(String name)
    137    {
    138        rootElement = new Element(this, null, name);
    139
    140        return rootElement;
    141    }
    142
    143    /**
    144     * Creates a new root element within a namespace.
    145     *
    146     * @param namespace URI of namespace containing the element
    147     * @param name      name of element with namespace
    148     * @return the root element
    149     */
    150    public Element newRootElement(String namespace, String name)
    151    {
    152        rootElement = new Element(this, namespace, name);
    153
    154        return rootElement;
    155    }
    156
    157    @Override
    158    public void toMarkup(Document document, PrintWriter writer, Map<String, String> namespaceURIToPrefix)
    159    {
    160        if (model.isXML())
    161        {
    162            writer.print("<?xml version=\"1.0\"");
    163
    164            if (encoding != null)
    165                writer.printf(" encoding=\"%s\"", encoding);
    166
    167            writer.print("?>\n");
    168        }
    169        if (dtd != null)
    170        {
    171            dtd.toMarkup(writer);
    172        }
    173
    174        if (preamble != null)
    175        {
    176            for (Node n : preamble)
    177                n.toMarkup(this, writer, namespaceURIToPrefix);
    178        }
    179
    180        if (rootElement == null)
    181            return;
    182
    183        Map<String, String> initialNamespaceMap = CollectionFactory.newMap();
    184
    185        initialNamespaceMap.put("xml", "http://www.w3.org/XML/1998/namespace");
    186        initialNamespaceMap.put("xmlns", "http://www.w3.org/2000/xmlns/");
    187
    188        rootElement.toMarkup(document, writer, initialNamespaceMap);
    189    }
    190
    191    public Element getRootElement()
    192    {
    193        return rootElement;
    194    }
    195
    196    /**
    197     * Tries to find an element in this document whose id is specified.
    198     *
    199     * @param id the value of the id attribute of the element being looked for
    200     * @return the element if found. null if not found.
    201     */
    202    public Element getElementById(String id)
    203    {
    204        return rootElement.getElementById(id);
    205    }
    206
    207    /**
    208     * Sets the DTD for the document, overriding any prior DTD.
    209     *
    210     * @param name     non-blank name of document type (i.e., "html")
    211     * @param publicId optional
    212     * @param systemId optional
    213     */
    214    public void dtd(String name, String publicId, String systemId)
    215    {
    216        dtd = new DTD(name, publicId, systemId);
    217    }
    218
    219    /**
    220     * Returns true if the document has an explicit DTD (set via {@link #dtd(String, String, String)}).
    221     *
    222     * @since 5.3
    223     */
    224    public boolean hasDTD()
    225    {
    226        return dtd != null;
    227    }
    228
    229    @Override
    230    protected Map<String, String> getNamespaceURIToPrefix()
    231    {
    232        if (rootElement == null)
    233        {
    234            return Collections.emptyMap();
    235        }
    236
    237        return rootElement.getNamespaceURIToPrefix();
    238    }
    239
    240    /**
    241     * Visits the root element of the document.
    242     *
    243     * @param visitor callback
    244     * @since 5.1.0.0
    245     */
    246    void visit(Visitor visitor)
    247    {
    248        rootElement.visit(visitor);
    249    }
    250
    251    private <T extends Node> T newChild(T child)
    252    {
    253        if (preamble == null)
    254            preamble = CollectionFactory.newList();
    255
    256        preamble.add(child);
    257
    258        return child;
    259    }
    260
    261    /**
    262     * Adds the comment and returns this document for further construction.
    263     *
    264     * @since 5.1.0.0
    265     */
    266    public Document comment(String text)
    267    {
    268        newChild(new Comment(null, text));
    269
    270        return this;
    271    }
    272
    273    /**
    274     * Adds the raw text and returns this document for further construction.
    275     *
    276     * @since 5.1.0.0
    277     */
    278    public Document raw(String text)
    279    {
    280        newChild(new Raw(null, text));
    281
    282        return this;
    283    }
    284
    285    /**
    286     * Adds and returns a new text node (the text node is returned so that {@link Text#write(String)} or [@link
    287     * {@link Text#writef(String, Object[])} may be invoked.
    288     *
    289     * @param text initial text for the node
    290     * @return the new Text node
    291     */
    292    public Text text(String text)
    293    {
    294        return newChild(new Text(null, text));
    295    }
    296
    297    /**
    298     * Adds and returns a new CDATA node.
    299     *
    300     * @param content the content to be rendered by the node
    301     * @return the newly created node
    302     */
    303    public CData cdata(String content)
    304    {
    305        return newChild(new CData(null, content));
    306    }
    307
    308    /**
    309     * Returns the MIME type of this document.
    310     * @return the MIME type.
    311     * @since 5.4
    312     */
    313    public String getMimeType()
    314    {
    315        return mimeType;
    316    }
    317
    318}