001// Copyright 2006, 2007, 2008, 2009, 2010 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    /**
049     * Non-element content that comes between the DOCTYPE and the root element.
050     */
051    private List<Node> preamble;
052
053    public Document(MarkupModel model)
054    {
055        this(model, null);
056    }
057
058    public Document(MarkupModel model, String encoding)
059    {
060        super(null);
061
062        assert model != null;
063
064        this.model = model;
065        this.encoding = encoding;
066    }
067
068    @Override
069    public Document getDocument()
070    {
071        return this;
072    }
073
074    /**
075     * Finds an element based on a path of element names.
076     *
077     * @param path slash separated series of element names
078     * @return the matching element, or null if not found
079     * @see Element#find(String)
080     */
081    public Element find(String path)
082    {
083        assert InternalUtils.isNonBlank(path);
084
085        if (rootElement == null)
086            return null;
087
088        int slashx = path.indexOf("/");
089
090        String rootElementName = slashx < 0 ? path : path.substring(0, slashx);
091
092        if (!rootElement.getName().equals(rootElementName))
093            return null;
094
095        return slashx < 0 ? rootElement : rootElement.find(path.substring(slashx + 1));
096    }
097
098    /**
099     * Builds with an instance of {@link DefaultMarkupModel}.
100     */
101    public Document()
102    {
103        this(new DefaultMarkupModel());
104    }
105
106    public MarkupModel getMarkupModel()
107    {
108        return model;
109    }
110
111    /**
112     * Creates the root element for this document, replacing any previous root element.
113     */
114    public Element newRootElement(String name)
115    {
116        rootElement = new Element(this, null, name);
117
118        return rootElement;
119    }
120
121    /**
122     * Creates a new root element within a namespace.
123     *
124     * @param namespace URI of namespace containing the element
125     * @param name      name of element with namespace
126     * @return the root element
127     */
128    public Element newRootElement(String namespace, String name)
129    {
130        rootElement = new Element(this, namespace, name);
131
132        return rootElement;
133    }
134
135    @Override
136    public void toMarkup(Document document, PrintWriter writer, Map<String, String> namespaceURIToPrefix)
137    {
138        if (model.isXML())
139        {
140            writer.print("<?xml version=\"1.0\"");
141
142            if (encoding != null)
143                writer.printf(" encoding=\"%s\"", encoding);
144
145            writer.print("?>\n");
146        }
147        if (dtd != null)
148        {
149            dtd.toMarkup(writer);
150        }
151
152        if (preamble != null)
153        {
154            for (Node n : preamble)
155                n.toMarkup(this, writer, namespaceURIToPrefix);
156        }
157
158        if (rootElement == null)
159            return;
160
161        Map<String, String> initialNamespaceMap = CollectionFactory.newMap();
162
163        initialNamespaceMap.put("xml", "http://www.w3.org/XML/1998/namespace");
164        initialNamespaceMap.put("xmlns", "http://www.w3.org/2000/xmlns/");
165
166        rootElement.toMarkup(document, writer, initialNamespaceMap);
167    }
168
169    public Element getRootElement()
170    {
171        return rootElement;
172    }
173
174    /**
175     * Tries to find an element in this document whose id is specified.
176     *
177     * @param id the value of the id attribute of the element being looked for
178     * @return the element if found. null if not found.
179     */
180    public Element getElementById(String id)
181    {
182        return rootElement.getElementById(id);
183    }
184
185    /**
186     * Sets the DTD for the document, overriding any prior DTD.
187     *
188     * @param name     non-blank name of document type (i.e., "html")
189     * @param publicId optional
190     * @param systemId optional
191     */
192    public void dtd(String name, String publicId, String systemId)
193    {
194        dtd = new DTD(name, publicId, systemId);
195    }
196
197    /**
198     * Returns true if the document has an explicit DTD (set via {@link #dtd(String, String, String)}).
199     *
200     * @since 5.3
201     */
202    public boolean hasDTD()
203    {
204        return dtd != null;
205    }
206
207    @Override
208    protected Map<String, String> getNamespaceURIToPrefix()
209    {
210        if (rootElement == null)
211        {
212            return Collections.emptyMap();
213        }
214
215        return rootElement.getNamespaceURIToPrefix();
216    }
217
218    /**
219     * Visits the root element of the document.
220     *
221     * @param visitor callback
222     * @since 5.1.0.0
223     */
224    void visit(Visitor visitor)
225    {
226        rootElement.visit(visitor);
227    }
228
229    private <T extends Node> T newChild(T child)
230    {
231        if (preamble == null)
232            preamble = CollectionFactory.newList();
233
234        preamble.add(child);
235
236        return child;
237    }
238
239    /**
240     * Adds the comment and returns this document for further construction.
241     *
242     * @since 5.1.0.0
243     */
244    public Document comment(String text)
245    {
246        newChild(new Comment(null, text));
247
248        return this;
249    }
250
251    /**
252     * Adds the raw text and returns this document for further construction.
253     *
254     * @since 5.1.0.0
255     */
256    public Document raw(String text)
257    {
258        newChild(new Raw(null, text));
259
260        return this;
261    }
262
263    /**
264     * Adds and returns a new text node (the text node is returned so that {@link Text#write(String)} or [@link
265     * {@link Text#writef(String, Object[])} may be invoked.
266     *
267     * @param text initial text for the node
268     * @return the new Text node
269     */
270    public Text text(String text)
271    {
272        return newChild(new Text(null, text));
273    }
274
275    /**
276     * Adds and returns a new CDATA node.
277     *
278     * @param content the content to be rendered by the node
279     * @return the newly created node
280     */
281    public CData cdata(String content)
282    {
283        return newChild(new CData(null, content));
284    }
285}