001    // Copyright 2006, 2007, 2008 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    
015    package org.apache.tapestry.dom;
016    
017    import org.apache.tapestry.internal.TapestryInternalUtils;
018    import org.apache.tapestry.ioc.internal.util.CollectionFactory;
019    import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newLinkedList;
020    import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
021    import org.apache.tapestry.ioc.internal.util.Defense;
022    import static org.apache.tapestry.ioc.internal.util.Defense.notBlank;
023    import org.apache.tapestry.ioc.internal.util.InternalUtils;
024    
025    import java.io.PrintWriter;
026    import java.util.LinkedList;
027    import java.util.List;
028    import java.util.Map;
029    
030    /**
031     * An element that will render with a begin tag and attributes, a body, and an end tag. Also acts as a factory for
032     * enclosed Element, Text and Comment nodes.
033     * <p/>
034     * TODO: Support for CDATA nodes. Do we need Entity nodes?
035     */
036    public final class Element extends Node
037    {
038        class Attribute
039        {
040            private final String _namespace;
041            private final String _name;
042            private final String _value;
043    
044            public Attribute(String namespace, String name, String value)
045            {
046                _namespace = namespace;
047                _name = name;
048                _value = value;
049            }
050    
051            public String getValue()
052            {
053                return _value;
054            }
055    
056            void render(MarkupModel model, StringBuilder builder)
057            {
058                builder.append(" ");
059                builder.append(toPrefixedName(_namespace, _name));
060                builder.append("=\"");
061                model.encodeQuoted(_value, builder);
062                builder.append('"');
063            }
064        }
065    
066        private final String _name;
067    
068        private Map<String, Attribute> _attributes;
069    
070        private Element _parent;
071    
072        private final Document _document;
073    
074        private static final String CLASS_ATTRIBUTE = "class";
075    
076        /**
077         * URI of the namespace which contains the element.  A quirk in XML is that the element may be in a namespace it
078         * defines itself, so resolving the namespace to a prefix must wait until render time (since the Element is created
079         * before the namespaces for it are defined).
080         */
081        private final String _namespace;
082    
083        private Map<String, String> _namespaceToPrefix;
084    
085        /**
086         * Constructor for a root element.
087         */
088        Element(Document container, String namespace, String name)
089        {
090            super(container);
091    
092            _document = container;
093            _namespace = namespace;
094            _name = name;
095        }
096    
097        /**
098         * Constructor for a nested element.
099         */
100        Element(Element parent, String namespace, String name)
101        {
102            super(parent);
103    
104            _parent = parent;
105            _namespace = namespace;
106            _name = name;
107    
108            _document = parent.getDocument();
109        }
110    
111        public Document getDocument()
112        {
113            return _document;
114        }
115    
116        /**
117         * Returns the containing element for this element. This will be null for the root element of a document.
118         */
119        public Element getParent()
120        {
121            return _parent;
122        }
123    
124        /**
125         * Adds an attribute to the element, but only if the attribute name does not already exist.
126         *
127         * @param name  the name of the attribute to add
128         * @param value the value for the attribute. A value of null is allowed, and no attribute will be added to the
129         *              element.
130         */
131        public void attribute(String name, String value)
132        {
133            attribute(null, name, value);
134        }
135    
136        /**
137         * Adds a namespaced attribute to the element, but only if the attribute name does not already exist.
138         *
139         * @param namespace the namespace to contain the attribute, or null
140         * @param name      the name of the attribute to add
141         * @param value     the value for the attribute. A value of null is allowed, and no attribute will be added to the
142         *                  element.
143         */
144        public void attribute(String namespace, String name, String value)
145        {
146            notBlank(name, "name");
147    
148            if (value == null) return;
149    
150            if (_attributes == null) _attributes = newMap();
151    
152            if (!_attributes.containsKey(name)) _attributes.put(name, new Attribute(namespace, name, value));
153        }
154    
155    
156        /**
157         * Convenience for invoking {@link #attribute(String, String)} multiple times.
158         *
159         * @param namesAndValues alternating attribute names and attribute values
160         */
161        public void attributes(String... namesAndValues)
162        {
163            int i = 0;
164            while (i < namesAndValues.length)
165            {
166                String name = namesAndValues[i++];
167                String value = namesAndValues[i++];
168    
169                attribute(name, value);
170            }
171        }
172    
173        /**
174         * Forces changes to a number of attributes. The new attributes <em>overwrite</em> previous values.
175         */
176        public void forceAttributes(String... namesAndValues)
177        {
178            if (_attributes == null) _attributes = newMap();
179    
180            int i = 0;
181    
182            while (i < namesAndValues.length)
183            {
184                String name = namesAndValues[i++];
185                String value = namesAndValues[i++];
186    
187                if (value == null)
188                {
189                    _attributes.remove(name);
190                    continue;
191                }
192    
193                _attributes.put(name, new Attribute(null, name, value));
194            }
195        }
196    
197        /**
198         * Creates and returns a new Element node as a child of this node.
199         *
200         * @param name           the name of the element to create
201         * @param namesAndValues alternating attribute names and attribute values
202         */
203        public Element element(String name, String... namesAndValues)
204        {
205            notBlank(name, "name");
206    
207            Element child = newChild(new Element(this, null, name));
208    
209            child.attributes(namesAndValues);
210    
211            return child;
212        }
213    
214        /**
215         * Creates and returns a new Element within a namespace as a child of this node.
216         *
217         * @param namespace namespace to contain the element, or null
218         * @param name      element name to create within the namespace
219         * @return the newly created element
220         */
221        public Element elementNS(String namespace, String name)
222        {
223            notBlank(name, "name");
224    
225            return newChild(new Element(this, namespace, name));
226        }
227    
228        public Element elementAt(int index, String name, String... namesAndValues)
229        {
230            notBlank(name, "name");
231    
232            Element child = new Element(this, null, name);
233            child.attributes(namesAndValues);
234    
235            insertChildAt(index, child);
236    
237            return child;
238        }
239    
240        /**
241         * Adds the comment and returns this element for further construction.
242         */
243        public Element comment(String text)
244        {
245            newChild(new Comment(this, text));
246    
247            return this;
248        }
249    
250        /**
251         * Adds the raw text and returns this element for further construction.
252         */
253        public Element raw(String text)
254        {
255            newChild(new Raw(this, text));
256    
257            return this;
258        }
259    
260        /**
261         * Adds and returns a new text node (the text node is returned so that {@link Text#write(String)} or [@link {@link
262         * Text#writef(String, Object[])} may be invoked .
263         *
264         * @param text initial text for the node
265         * @return the new Text node
266         */
267        public Text text(String text)
268        {
269            return newChild(new Text(this, _document, text));
270        }
271    
272        /**
273         * Adds an returns a new CDATA node.
274         *
275         * @param content the content to be rendered by the node
276         * @return the newly created node
277         */
278        public CData cdata(String content)
279        {
280            return newChild(new CData(this, _document, content));
281        }
282    
283    
284        private <T extends Node> T newChild(T child)
285        {
286            addChild(child);
287    
288            return child;
289        }
290    
291        @Override
292        public void toMarkup(PrintWriter writer)
293        {
294            StringBuilder builder = new StringBuilder();
295    
296            String prefixedElementName = toPrefixedName(_namespace, _name);
297    
298            builder.append("<").append(prefixedElementName);
299    
300            MarkupModel markupModel = _document.getMarkupModel();
301    
302            if (_attributes != null)
303            {
304                List<String> keys = InternalUtils.sortedKeys(_attributes);
305    
306                for (String key : keys)
307                {
308                    Attribute attribute = _attributes.get(key);
309    
310                    attribute.render(markupModel, builder);
311                }
312            }
313    
314            // Next, emit namespace declarations for each namespace.
315    
316            if (_namespaceToPrefix != null)
317            {
318                List<String> namespaces = InternalUtils.sortedKeys(_namespaceToPrefix);
319    
320                for (String namespace : namespaces)
321                {
322                    String prefix = _namespaceToPrefix.get(namespace);
323    
324                    builder.append(" xmlns");
325    
326                    if (!prefix.equals(""))
327                    {
328                        builder.append(":").append(prefix);
329                    }
330    
331                    builder.append("=\"");
332    
333                    markupModel.encodeQuoted(namespace, builder);
334    
335                    builder.append('"');
336                }
337            }
338    
339            EndTagStyle style = markupModel.getEndTagStyle(_name);
340    
341            boolean hasChildren = hasChildren();
342    
343            String close = (!hasChildren && style == EndTagStyle.ABBREVIATE) ? "/>" : ">";
344    
345            builder.append(close);
346    
347            writer.print(builder.toString());
348    
349            if (hasChildren) writeChildMarkup(writer);
350    
351            // Dangerous -- perhaps it should be an error for a tag of type OMIT to even have children!
352            // We'll certainly be writing out unbalanced markup in that case.
353    
354            if (style == EndTagStyle.OMIT) return;
355    
356            if (hasChildren || style == EndTagStyle.REQUIRE) writer.printf("</%s>", prefixedElementName);
357        }
358    
359        private String toPrefixedName(String namespace, String name)
360        {
361            if (namespace == null || namespace.equals("")) return name;
362    
363            String prefix = toNamespacePrefix(namespace);
364    
365            // The empty string indicates the default namespace which doesn't use a prefix.
366    
367            if (prefix.equals("")) return name;
368    
369            return prefix + ":" + name;
370        }
371    
372        /**
373         * Tries to find an element under this element (including itself) whose id is specified. Performs a width-first
374         * search of the document tree.
375         *
376         * @param id the value of the id attribute of the element being looked for
377         * @return the element if found. null if not found.
378         */
379        public Element getElementById(String id)
380        {
381            Defense.notNull(id, "id");
382    
383            LinkedList<Element> queue = newLinkedList();
384    
385            queue.add(this);
386    
387            while (!queue.isEmpty())
388            {
389                Element e = queue.removeFirst();
390    
391                String elementId = e.getAttribute("id");
392    
393                if (id.equals(elementId)) return e;
394    
395                for (Node n : e.getChildren())
396                {
397                    Element child = n.asElement();
398    
399                    if (child != null) queue.addLast(child);
400                }
401            }
402    
403            // Exhausted the entire tree
404    
405            return null;
406        }
407    
408        /**
409         * Searchs for a child element with a particular name below this element. The path parameter is a slash separated
410         * series of element names.
411         *
412         * @param path
413         * @return
414         */
415        public Element find(String path)
416        {
417            notBlank(path, "path");
418    
419            Element search = this;
420    
421            for (String name : TapestryInternalUtils.splitPath(path))
422            {
423                search = search.findChildWithElementName(name);
424    
425                if (search == null) break;
426            }
427    
428            return search;
429        }
430    
431        private Element findChildWithElementName(String name)
432        {
433            for (Node node : getChildren())
434            {
435                Element child = node.asElement();
436    
437                if (child != null && child.getName().equals(name)) return child;
438            }
439    
440            // Not found.
441    
442            return null;
443        }
444    
445        public String getAttribute(String attributeName)
446        {
447            Attribute attribute = InternalUtils.get(_attributes, attributeName);
448    
449            return attribute == null ? null : attribute.getValue();
450        }
451    
452        public String getName()
453        {
454            return _name;
455        }
456    
457        /**
458         * All other implementations of Node return null except this one.
459         */
460        @Override
461        Element asElement()
462        {
463            return this;
464        }
465    
466        /**
467         * Adds one or more CSS class names to the "class" attribute. No check for duplicates is made. Note that CSS class
468         * names are case insensitive on the client.
469         *
470         * @param className one or more CSS class names
471         * @return the element for further configuration
472         */
473        public Element addClassName(String... className)
474        {
475            String classes = getAttribute(CLASS_ATTRIBUTE);
476    
477            StringBuilder builder = new StringBuilder();
478    
479            if (classes != null) builder.append(classes);
480    
481            for (String name : className)
482            {
483                if (builder.length() > 0) builder.append(" ");
484    
485                builder.append(name);
486            }
487    
488            forceAttributes(CLASS_ATTRIBUTE, builder.toString());
489    
490            return this;
491        }
492    
493        String toNamespacePrefix(String namespaceURI)
494        {
495            String prefix = InternalUtils.get(_namespaceToPrefix, namespaceURI);
496    
497            if (prefix != null) return prefix;
498    
499            if (_parent == null) throw new RuntimeException(DomMessages.namespaceURINotMappedToPrefix(namespaceURI));
500    
501            return _parent.toNamespacePrefix(namespaceURI);
502        }
503    
504        /**
505         * Defines a namespace for this element, mapping a URI to a prefix.   This will affect how namespaced elements and
506         * attributes nested within the element are rendered, and will also cause <code>xmlns:</code> attributes (to define
507         * the namespace and prefix) to be rendered.
508         *
509         * @param namespace       URI of the namespace
510         * @param namespacePrefix prefix
511         * @return this element
512         */
513        public Element defineNamespace(String namespace, String namespacePrefix)
514        {
515            Defense.notNull(namespace, "namespace");
516            Defense.notNull(namespacePrefix, "namespacePrefix");
517    
518            if (_namespaceToPrefix == null) _namespaceToPrefix = CollectionFactory.newMap();
519    
520            _namespaceToPrefix.put(namespace, namespacePrefix);
521    
522            return this;
523        }
524    
525    }