1 // Copyright 2006, 2007, 2008 The Apache Software Foundation 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package org.apache.tapestry5.dom; 16 17 import org.apache.tapestry5.internal.TapestryInternalUtils; 18 import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 19 import org.apache.tapestry5.ioc.internal.util.Defense; 20 import org.apache.tapestry5.ioc.internal.util.InternalUtils; 21 22 import java.io.PrintWriter; 23 import java.util; 24 25 /** 26 * An element that will render with a begin tag and attributes, a body, and an end tag. Also acts as a factory for 27 * enclosed Element, Text and Comment nodes. 28 * <p/> 29 * TODO: Support for CDATA nodes. Do we need Entity nodes? 30 */ 31 public final class Element extends Node 32 { 33 class Attribute 34 { 35 private final String namespace; 36 private final String name; 37 private final String value; 38 39 public Attribute(String namespace, String name, String value) 40 { 41 this.namespace = namespace; 42 this.name = name; 43 this.value = value; 44 } 45 46 47 void render(MarkupModel model, StringBuilder builder, Map<String, String> namespaceURIToPrefix) 48 { 49 builder.append(" "); 50 builder.append(toPrefixedName(namespaceURIToPrefix, namespace, name)); 51 builder.append("=\""); 52 model.encodeQuoted(value, builder); 53 builder.append('"'); 54 } 55 } 56 57 private final String name; 58 59 private Map<String, Attribute> attributes; 60 61 private Element parent; 62 63 private final Document document; 64 65 private static final String CLASS_ATTRIBUTE = "class"; 66 67 /** 68 * URI of the namespace which contains the element. A quirk in XML is that the element may be in a namespace it 69 * defines itself, so resolving the namespace to a prefix must wait until render time (since the Element is created 70 * before the namespaces for it are defined). 71 */ 72 private final String namespace; 73 74 private Map<String, String> namespaceToPrefix; 75 76 /** 77 * Constructor for a root element. 78 */ 79 Element(Document container, String namespace, String name) 80 { 81 super(container); 82 83 document = container; 84 this.namespace = namespace; 85 this.name = name; 86 } 87 88 /** 89 * Constructor for a nested element. 90 */ 91 Element(Element parent, String namespace, String name) 92 { 93 super(parent); 94 95 this.parent = parent; 96 this.namespace = namespace; 97 this.name = name; 98 99 document = null; 100 } 101 102 @Override 103 public Document getDocument() 104 { 105 return document != null ? document : super.getDocument(); 106 } 107 108 /** 109 * Returns the containing element for this element. This will be null for the root element of a document. 110 */ 111 public Element getParent() 112 { 113 return parent; 114 } 115 116 /** 117 * Adds an attribute to the element, but only if the attribute name does not already exist. 118 * 119 * @param name the name of the attribute to add 120 * @param value the value for the attribute. A value of null is allowed, and no attribute will be added to the 121 * element. 122 */ 123 public Element attribute(String name, String value) 124 { 125 return attribute(null, name, value); 126 } 127 128 /** 129 * Adds a namespaced attribute to the element, but only if the attribute name does not already exist. 130 * 131 * @param namespace the namespace to contain the attribute, or null 132 * @param name the name of the attribute to add 133 * @param value the value for the attribute. A value of null is allowed, and no attribute will be added to the 134 * element. 135 */ 136 public Element attribute(String namespace, String name, String value) 137 { 138 Defense.notBlank(name, "name"); 139 140 if (value == null) return this; 141 142 if (attributes == null) attributes = CollectionFactory.newMap(); 143 144 if (!attributes.containsKey(name)) attributes.put(name, new Attribute(namespace, name, value)); 145 146 return this; 147 } 148 149 150 /** 151 * Convenience for invoking {@link #attribute(String, String)} multiple times. 152 * 153 * @param namesAndValues alternating attribute names and attribute values 154 */ 155 public Element attributes(String... namesAndValues) 156 { 157 int i = 0; 158 while (i < namesAndValues.length) 159 { 160 String name = namesAndValues[i++]; 161 String value = namesAndValues[i++]; 162 163 attribute(name, value); 164 } 165 166 return this; 167 } 168 169 /** 170 * Forces changes to a number of attributes. The new attributes <em>overwrite</em> previous values. 171 */ 172 public Element forceAttributes(String... namesAndValues) 173 { 174 if (attributes == null) attributes = CollectionFactory.newMap(); 175 176 int i = 0; 177 178 while (i < namesAndValues.length) 179 { 180 String name = namesAndValues[i++]; 181 String value = namesAndValues[i++]; 182 183 if (value == null) 184 { 185 attributes.remove(name); 186 continue; 187 } 188 189 attributes.put(name, new Attribute(null, name, value)); 190 } 191 192 return this; 193 } 194 195 /** 196 * Creates and returns a new Element node as a child of this node. 197 * 198 * @param name the name of the element to create 199 * @param namesAndValues alternating attribute names and attribute values 200 */ 201 public Element element(String name, String... namesAndValues) 202 { 203 Defense.notBlank(name, "name"); 204 205 Element child = newChild(new Element(this, null, name)); 206 207 child.attributes(namesAndValues); 208 209 return child; 210 } 211 212 /** 213 * Creates and returns a new Element within a namespace as a child of this node. 214 * 215 * @param namespace namespace to contain the element, or null 216 * @param name element name to create within the namespace 217 * @return the newly created element 218 */ 219 public Element elementNS(String namespace, String name) 220 { 221 Defense.notBlank(name, "name"); 222 223 return newChild(new Element(this, namespace, name)); 224 } 225 226 public Element elementAt(int index, String name, String... namesAndValues) 227 { 228 Defense.notBlank(name, "name"); 229 230 Element child = new Element(this, null, name); 231 child.attributes(namesAndValues); 232 233 insertChildAt(index, child); 234 235 return child; 236 } 237 238 /** 239 * Adds the comment and returns this element for further construction. 240 */ 241 public Element comment(String text) 242 { 243 newChild(new Comment(this, text)); 244 245 return this; 246 } 247 248 /** 249 * Adds the raw text and returns this element for further construction. 250 */ 251 public Element raw(String text) 252 { 253 newChild(new Raw(this, text)); 254 255 return this; 256 } 257 258 /** 259 * Adds and returns a new text node (the text node is returned so that {@link Text#write(String)} or [@link {@link 260 * Text#writef(String, Object[])} may be invoked . 261 * 262 * @param text initial text for the node 263 * @return the new Text node 264 */ 265 public Text text(String text) 266 { 267 return newChild(new Text(this, text)); 268 } 269 270 /** 271 * Adds an returns a new CDATA node. 272 * 273 * @param content the content to be rendered by the node 274 * @return the newly created node 275 */ 276 public CData cdata(String content) 277 { 278 return newChild(new CData(this, content)); 279 } 280 281 282 private <T extends Node> T newChild(T child) 283 { 284 addChild(child); 285 286 return child; 287 } 288 289 @Override 290 void toMarkup(Document document, PrintWriter writer, Map<String, String> containerNamespacePrefixToURI) 291 { 292 Map<String, String> localNamespacePrefixToURI = createNamespaceURIToPrefix(containerNamespacePrefixToURI); 293 294 MarkupModel markupModel = document.getMarkupModel(); 295 296 StringBuilder builder = new StringBuilder(); 297 298 String prefixedElementName = toPrefixedName(localNamespacePrefixToURI, namespace, name); 299 300 builder.append("<").append(prefixedElementName); 301 302 List<String> keys = InternalUtils.sortedKeys(attributes); 303 304 for (String key : keys) 305 { 306 Attribute attribute = attributes.get(key); 307 308 attribute.render(markupModel, builder, localNamespacePrefixToURI); 309 } 310 311 // Next, emit namespace declarations for each namespace. 312 313 List<String> namespaces = InternalUtils.sortedKeys(namespaceToPrefix); 314 315 for (String namespace : namespaces) 316 { 317 String prefix = namespaceToPrefix.get(namespace); 318 319 builder.append(" xmlns"); 320 321 if (!prefix.equals("")) 322 { 323 builder.append(":").append(prefix); 324 } 325 326 builder.append("=\""); 327 328 markupModel.encodeQuoted(namespace, builder); 329 330 builder.append('"'); 331 } 332 333 EndTagStyle style = markupModel.getEndTagStyle(name); 334 335 boolean hasChildren = hasChildren(); 336 337 String close = (!hasChildren && style == EndTagStyle.ABBREVIATE) ? "/>" : ">"; 338 339 builder.append(close); 340 341 writer.print(builder.toString()); 342 343 if (hasChildren) writeChildMarkup(document, writer, localNamespacePrefixToURI); 344 345 // Dangerous -- perhaps it should be an error for a tag of type OMIT to even have children! 346 // We'll certainly be writing out unbalanced markup in that case. 347 348 if (style == EndTagStyle.OMIT) return; 349 350 if (hasChildren || style == EndTagStyle.REQUIRE) 351 { 352 // TAP5-471: Avoid use of printf(). 353 writer.print("</"); 354 writer.print(prefixedElementName); 355 writer.print(">"); 356 } 357 } 358 359 private String toPrefixedName(Map<String, String> namespaceURIToPrefix, String namespace, String name) 360 { 361 if (namespace == null || namespace.equals("")) return name; 362 363 String prefix = namespaceURIToPrefix.get(namespace); 364 365 // This should never happen, because namespaces are automatically defined as needed. 366 367 if (prefix == null) 368 throw new IllegalArgumentException( 369 String.format("No prefix has been defined for namespace '%s'.", namespace)); 370 371 // The empty string indicates the default namespace which doesn't use a prefix. 372 373 if (prefix.equals("")) return name; 374 375 return prefix + ":" + name; 376 } 377 378 /** 379 * Tries to find an element under this element (including itself) whose id is specified. Performs a width-first 380 * search of the document tree. 381 * 382 * @param id the value of the id attribute of the element being looked for 383 * @return the element if found. null if not found. 384 */ 385 public Element getElementById(String id) 386 { 387 Defense.notNull(id, "id"); 388 389 LinkedList<Element> queue = CollectionFactory.newLinkedList(); 390 391 queue.add(this); 392 393 while (!queue.isEmpty()) 394 { 395 Element e = queue.removeFirst(); 396 397 String elementId = e.getAttribute("id"); 398 399 if (id.equals(elementId)) return e; 400 401 for (Node n : e.getChildren()) 402 { 403 Element child = n.asElement(); 404 405 if (child != null) queue.addLast(child); 406 } 407 } 408 409 // Exhausted the entire tree 410 411 return null; 412 } 413 414 415 /** 416 * Searchs for a child element with a particular name below this element. The path parameter is a slash separated 417 * series of element names. 418 * 419 * @param path 420 * @return 421 */ 422 public Element find(String path) 423 { 424 Defense.notBlank(path, "path"); 425 426 Element search = this; 427 428 for (String name : TapestryInternalUtils.splitPath(path)) 429 { 430 search = search.findChildWithElementName(name); 431 432 if (search == null) break; 433 } 434 435 return search; 436 } 437 438 private Element findChildWithElementName(String name) 439 { 440 for (Node node : getChildren()) 441 { 442 Element child = node.asElement(); 443 444 if (child != null && child.getName().equals(name)) return child; 445 } 446 447 // Not found. 448 449 return null; 450 } 451 452 public String getAttribute(String attributeName) 453 { 454 Attribute attribute = InternalUtils.get(attributes, attributeName); 455 456 return attribute == null ? null : attribute.value; 457 } 458 459 public String getName() 460 { 461 return name; 462 } 463 464 /** 465 * All other implementations of Node return null except this one. 466 */ 467 @Override 468 Element asElement() 469 { 470 return this; 471 } 472 473 /** 474 * Adds one or more CSS class names to the "class" attribute. No check for duplicates is made. Note that CSS class 475 * names are case insensitive on the client. 476 * 477 * @param className one or more CSS class names 478 * @return the element for further configuration 479 */ 480 public Element addClassName(String... className) 481 { 482 String classes = getAttribute(CLASS_ATTRIBUTE); 483 484 StringBuilder builder = new StringBuilder(); 485 486 if (classes != null) builder.append(classes); 487 488 for (String name : className) 489 { 490 if (builder.length() > 0) builder.append(" "); 491 492 builder.append(name); 493 } 494 495 forceAttributes(CLASS_ATTRIBUTE, builder.toString()); 496 497 return this; 498 } 499 500 /** 501 * Defines a namespace for this element, mapping a URI to a prefix. This will affect how namespaced elements and 502 * attributes nested within the element are rendered, and will also cause <code>xmlns:</code> attributes (to define 503 * the namespace and prefix) to be rendered. 504 * 505 * @param namespace URI of the namespace 506 * @param namespacePrefix prefix 507 * @return this element 508 */ 509 public Element defineNamespace(String namespace, String namespacePrefix) 510 { 511 Defense.notNull(namespace, "namespace"); 512 Defense.notNull(namespacePrefix, "namespacePrefix"); 513 514 if (namespaceToPrefix == null) 515 namespaceToPrefix = CollectionFactory.newMap(); 516 517 namespaceToPrefix.put(namespace, namespacePrefix); 518 519 return this; 520 } 521 522 /** 523 * Returns the namespace for this element (which is typically a URL). The namespace may be null. 524 */ 525 public String getNamespace() 526 { 527 return namespace; 528 } 529 530 /** 531 * Removes an element; the element's children take the place of the node within its container. 532 */ 533 public void pop() 534 { 535 // Have to be careful because we'll be modifying the underlying list of children 536 // as we work, so we need a copy of the children. 537 538 List<Node> childrenCopy = CollectionFactory.newList(getChildren()); 539 540 for (Node child : childrenCopy) 541 { 542 child.moveBefore(this); 543 } 544 545 remove(); 546 } 547 548 /** 549 * Removes all children from this element. 550 * 551 * @return the element, for method chaining 552 */ 553 public Element removeChildren() 554 { 555 clearChildren(); 556 557 return this; 558 } 559 560 /** 561 * Creates the URI to namespace prefix map for this element, which reflects namespace mappings from containing 562 * elements. In addition, automatic namespaces are defined for any URIs that are not explicitly mapped (this occurs 563 * sometimes in Ajax partial render scenarios). 564 * 565 * @return a mapping from namespace URI to namespace prefix 566 */ 567 private Map<String, String> createNamespaceURIToPrefix(Map<String, String> containerNamespaceURIToPrefix) 568 { 569 MapHolder holder = new MapHolder(containerNamespaceURIToPrefix); 570 571 holder.putAll(namespaceToPrefix); 572 573 574 // result now contains all the mappings, including this element's. 575 576 // Add a mapping for the element's namespace. 577 578 if (InternalUtils.isNonBlank(namespace)) 579 { 580 581 // Add the namespace for the element as the default namespace. 582 583 if (!holder.getResult().containsKey(namespace)) 584 { 585 defineNamespace(namespace, ""); 586 holder.put(namespace, ""); 587 } 588 } 589 590 // And for any attributes that have a namespace. 591 592 if (attributes != null) 593 { 594 for (Attribute a : attributes.values()) 595 addMappingIfNeeded(holder, a.namespace); 596 } 597 598 return holder.getResult(); 599 } 600 601 private void addMappingIfNeeded(MapHolder holder, String namespace) 602 { 603 if (InternalUtils.isBlank(namespace)) return; 604 605 Map<String, String> current = holder.getResult(); 606 607 if (current.containsKey(namespace)) return; 608 609 // A missing namespace. 610 611 Set<String> prefixes = CollectionFactory.newSet(holder.getResult().values()); 612 613 // A clumsy way to find a unique id for the new namespace. 614 615 int i = 0; 616 while (true) 617 { 618 String prefix = "ns" + i; 619 620 if (!prefixes.contains(prefix)) 621 { 622 defineNamespace(namespace, prefix); 623 holder.put(namespace, prefix); 624 return; 625 } 626 627 i++; 628 } 629 } 630 631 @Override 632 protected Map<String, String> getNamespaceURIToPrefix() 633 { 634 MapHolder holder = new MapHolder(); 635 636 List<Element> elements = CollectionFactory.newList(this); 637 638 Element cursor = parent; 639 640 while (cursor != null) 641 { 642 elements.add(cursor); 643 cursor = cursor.parent; 644 } 645 646 // Reverse the list, so that later elements will overwrite earlier ones. 647 648 Collections.reverse(elements); 649 650 for (Element e : elements) 651 holder.putAll(e.namespaceToPrefix); 652 653 return holder.getResult(); 654 } 655 }