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 }