Home » tapestry-src-5.0.19 » org.apache.tapestry5.dom » [javadoc | source]

    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   }

Home » tapestry-src-5.0.19 » org.apache.tapestry5.dom » [javadoc | source]