View Javadoc
1   /*
2    * Redistribution and use of this software and associated documentation ("Software"), with or
3    * without modification, are permitted provided that the following conditions are met:
4    *
5    * 1. Redistributions of source code must retain copyright statements and notices. Redistributions
6    * must also contain a copy of this document.
7    *
8    * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
9    * conditions and the following disclaimer in the documentation and/or other materials provided with
10   * the distribution.
11   *
12   * 3. The name "Exolab" must not be used to endorse or promote products derived from this Software
13   * without prior written permission of Intalio, Inc. For written permission, please contact
14   * info@exolab.org.
15   *
16   * 4. Products derived from this Software may not be called "Exolab" nor may "Exolab" appear in
17   * their names without prior written permission of Intalio, Inc. Exolab is a registered trademark of
18   * Intalio, Inc.
19   *
20   * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
21   *
22   * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR
23   * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
24   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS
25   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26   * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28   * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
29   * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30   *
31   * Copyright 2001 (C) Intalio, Inc. All Rights Reserved.
32   *
33   * $Id$ Date Author Changes 04/24/2001 Arnaud Blandin Rewrited from scratch 04/22/2001 Arnaud
34   * Blandin Clean-up and support of comments 04/04/2001 Arnaud Blandin Created
35   */
36  package org.exolab.castor.types;
37  
38  import java.io.StringWriter;
39  import java.util.Stack;
40  
41  import org.castor.xml.BackwardCompatibilityContext;
42  import org.exolab.castor.xml.Serializer;
43  import org.exolab.castor.xml.util.AnyNode2SAX;
44  
45  /**
46   * A class used to represent an XML node. This is an alternative to DOM which is too heavy for our
47   * purpose (mainly handle XML Fragment when {@literal <any>} is used in an XML schema). The model is
48   * based on XPath Node. An AnyNode can be a:
49   * <ul>
50   * <li>ELEMENT</li>
51   * <li>ATTRIBUTE</li>
52   * <li>NAMESPACE</li>
53   * <li>COMMENT</li>
54   * <li>TEXT</li>
55   * <li>PROCESSING INSTRUCTION</li>
56   * </ul>
57   *
58   * @author <a href="mailto:arkin@intalio.com">Assaf Arkin</a>
59   * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
60   * @author <a href="mailto:kvisco@intalio.com">Keith Visco</a>
61   * @version $Revision$ $Date: 2006-04-25 15:08:23 -0600 (Tue, 25 Apr 2006) $
62   */
63  public final class AnyNode implements java.io.Serializable {
64    // TODO Processing Instructions
65    // TODO Full handling of namespaces
66  
67    /** SerialVersionUID */
68    private static final long serialVersionUID = -4104117996051705975L;
69  
70    /**
71     * The prefix for XML namespace
72     */
73    private final static String XMLNS_PREFIX = "xmlns";
74  
75    /**
76     * Representation for an element node.
77     */
78    public static final short ELEMENT = 1;
79  
80    /**
81     * Representation for an attribute node.
82     */
83    public static final short ATTRIBUTE = 2;
84  
85    /**
86     * Representation for a Namespace node.
87     */
88    public static final short NAMESPACE = 3;
89  
90    /**
91     * Representation for a processing instruction node.
92     */
93    public static final short PI = 4;
94  
95    /**
96     * Representation for a comment node.
97     */
98    public static final short COMMENT = 5;
99  
100   /**
101    * Representation for a text node.
102    */
103   public static final short TEXT = 6;
104 
105   /**
106    * The type of the current node. ELEMENT is the default value.
107    */
108   private short _nodeType = ELEMENT;
109 
110   /**
111    * The next sibling of this AnyNode
112    */
113   private AnyNode _nextSiblingNode = null;
114 
115   /**
116    * The first child of this AnyNode
117    */
118   private AnyNode _firstChildNode = null;
119 
120 
121   /**
122    * the local name of the current node.
123    */
124   private String _localName;
125 
126   /**
127    * the Namespace URI of the current node
128    */
129   private String _uri;
130 
131   /**
132    * The prefix of the Namespace
133    */
134   private String _prefix;
135 
136   /**
137    * A stack used for avoiding endless loop in toString()
138    */
139   private static Stack<AnyNode> _elements;
140 
141   /**
142    * The namespace context used in the toString()
143    */
144 
145   /**
146    * The value of this node defined as follow:
147    * <ul>
148    * <li>for an element the value is its TEXT NODE value (if any)</li>
149    * <li>for an attribute the value is the value of the attribute</li>
150    * <li>for a text node it is the character data</li>
151    * <li>for a namespace it is the namespace URI that is being bound to the namespace prefix</li>
152    * <li>for a comment it is the content of the comment not including the opening &lt;!-- and the
153    * closing --&gt;.</li>
154    * </ul>
155    */
156   private String _value;
157 
158   /**
159    * Default constructor: creates an empty element node
160    */
161   public AnyNode() {
162     this(ELEMENT, null, null, null, null);
163   }
164 
165   /**
166    * Creates a node given all the necessary information: type, localName, prefix, uri and value.
167    * This constructor is not user-friendly and launched RunTime exception is you try to instantiate
168    * non-valid node.
169    * 
170    * @param type the node type.
171    * @param localName the name of the node.
172    * @param prefix the prefix if any of the namespace.
173    * @param uri the namespace uri of this node.
174    * @param value the value of this node.
175    */
176   public AnyNode(short type, String localName, String prefix, String uri, String value) {
177     if ((type > 6) && (type < 1)) {
178       throw new IllegalArgumentException("Illegal node type");
179     }
180     _nodeType = type;
181 
182     // comment and text nodes don't have name
183     if ((type > PI) && (localName != null)) {
184       String err = "This node can not have a local name";
185       throw new IllegalArgumentException(err);
186     }
187     _localName = localName;
188 
189     // for comment, pi or text we should have no namespaces
190     if ((type > NAMESPACE) && ((uri != null) || (prefix != null))) {
191       String err = "This node can not handle namespace";
192       throw new IllegalArgumentException(err);
193     }
194     _uri = uri;
195     _prefix = prefix;
196 
197     // attributes can not be namespaces
198     if (type == AnyNode.ATTRIBUTE)
199       if (localName.startsWith(XMLNS_PREFIX)) {
200         String err = "Namespaces can't be used as attributes.";
201         throw new IllegalArgumentException(err);
202       }
203 
204     // you can't set value for element
205     if ((type == ELEMENT) && (value != null)) {
206       String err = "You can't set a value for this node type";
207       throw new IllegalArgumentException(err);
208     }
209     _value = value;
210   }
211 
212   /**
213    * Adds an AnyNode to the current node
214    * 
215    * @param node the node to append
216    */
217   public void addAnyNode(AnyNode node) {
218     if (node == null) {
219       throw new IllegalArgumentException("null argument in addAnyNode");
220     }
221 
222     switch (node.getNodeType()) {
223       case ATTRIBUTE:
224         addAttribute(node);
225         break;
226       case NAMESPACE:
227         addNamespace(node);
228         break;
229       default:
230         addChild(node);
231         break;
232     }
233   }
234 
235   /**
236    * <p>
237    * Adds a child AnyNode to this node. A 'child' can be either an ELEMENT node, a COMMENT node, a
238    * TEXT node or a PROCESSING INSTRUCTION. If the current node already has a child then the node to
239    * add will be append as a sibling.
240    * <p>
241    * Note: you cannot add a child to a TEXT node.
242    *
243    * @param node the node to add.
244    */
245   public void addChild(AnyNode node) {
246     if (node == null) {
247       throw new IllegalArgumentException("null argument in appendChild");
248     }
249 
250     if (node.getNodeType() == ATTRIBUTE || node.getNodeType() == NAMESPACE) {
251       throw new IllegalArgumentException("An Attribute or an Namespace can't be added as a child");
252     }
253 
254     if (this.getNodeType() == TEXT) {
255       throw new IllegalArgumentException("a TEXT node can't have children.");
256     }
257 
258     if (_firstChildNode == null) {
259       _firstChildNode = node;
260     } else if (_firstChildNode.getNodeType() == ATTRIBUTE
261         || _firstChildNode.getNodeType() == NAMESPACE) {
262       _firstChildNode.addChild(node);
263     } else {
264       _firstChildNode.appendSibling(node);
265     }
266   }
267 
268   /**
269    * Adds an attribute to the current node.
270    * 
271    * @param node the attribute to add.
272    */
273   public void addAttribute(AnyNode node) {
274     if (node == null) {
275       throw new IllegalArgumentException("null argument in addAttribute");
276     }
277 
278     if (node.getNodeType() != ATTRIBUTE) {
279       throw new IllegalArgumentException("Only attribute can be added as an attribute");
280     }
281 
282     if (_firstChildNode == null) {
283       _firstChildNode = node;
284     } else {
285       if (_firstChildNode.getNodeType() == ATTRIBUTE) {
286         // if we reach an attribute then we add the node as its sibling
287         _firstChildNode.appendSibling(node);
288       } else if (_firstChildNode.getNodeType() == NAMESPACE) {
289         // if we reach an namespace the attributre should be added to
290         // the first child of the namespace
291         _firstChildNode.addAttribute(node);
292       } else {
293         // unplug the current firstNode to add a new one
294         node.addChild(_firstChildNode);
295         _firstChildNode = node;
296       }
297     }
298   } // addAttribute
299 
300   /**
301    * Appends an namespace to the current node.
302    * 
303    * @param node the attribute to add.
304    */
305   public void addNamespace(AnyNode node) {
306     if (node == null) {
307       throw new IllegalArgumentException("null argument in addNamespace");
308     }
309 
310     if (node.getNodeType() != NAMESPACE) {
311       throw new IllegalArgumentException("Only namespace can be added as an namespace");
312     }
313 
314     if (_firstChildNode == null) {
315       _firstChildNode = node;
316     } else {
317       if (_firstChildNode.getNodeType() == NAMESPACE) {
318         // if we reach an namepace then we add the node as its sibling
319         _firstChildNode.appendSibling(node);
320       } else if (_firstChildNode.getNodeType() == ATTRIBUTE) {
321         // if we reach an attribute the attributre should be added to
322         // the first child of the attribute
323         _firstChildNode.addNamespace(node);
324       } else {
325         // unplug the current firstNode to add a new one
326         node.addChild(_firstChildNode);
327         _firstChildNode = node;
328       }
329     }
330   } // addNamespace
331 
332   /**
333    * Returns the first attribute of the current ELEMENT node or null. The next attribute,if any,is
334    * the sibling of the returned node.
335    */
336   public AnyNode getFirstAttribute() {
337     if (this.getNodeType() != ELEMENT) {
338       String err = "This node type can not contain attributes";
339       throw new UnsupportedOperationException(err);
340     }
341 
342     boolean found = false;
343     AnyNode tempNode = this.getFirstChildNode();
344     while (tempNode != null && !found) {
345       short type = tempNode.getNodeType();
346       // if the child is not an attribute or a namespace
347       // this element does not have any attribute
348       if (type == ELEMENT || type == COMMENT || type == TEXT || type == PI) {
349         tempNode = null;
350       } else if (type == NAMESPACE) {
351         tempNode = tempNode.getFirstChildNode();
352       } else {
353         found = true;
354       }
355     }
356     return tempNode;
357   }
358 
359   /**
360    * Returns the first namespace of the current ELEMENT node or null. The next attribute if any is
361    * the sibling of the returned node.
362    * 
363    * @return the first namespace of the current ELEMENT node or null.
364    */
365   public AnyNode getFirstNamespace() {
366     if (this.getNodeType() != ELEMENT) {
367       String err = "This node type can not contain namespaces";
368       throw new UnsupportedOperationException(err);
369     }
370 
371     AnyNode tempNode = this.getFirstChildNode();
372     boolean found = false;
373     while (tempNode != null && !found) {
374       short type = tempNode.getNodeType();
375       // if the child is not an attribute or a namespace
376       // this element does not have any namespace
377       if (type == ELEMENT || type == COMMENT || type == TEXT || type == PI) {
378         tempNode = null;
379       } else if (type == ATTRIBUTE) {
380         tempNode = tempNode.getFirstChildNode();
381       } else {
382         found = true;
383       }
384     }
385     return tempNode;
386   }
387 
388   /**
389    * Returns the first Child node of this node. A 'child' can be either an ELEMENT node, a COMMENT
390    * node, a TEXT node or a PROCESSING INSTRUCTION.
391    *
392    * @return the first child of this node
393    */
394   public AnyNode getFirstChild() {
395     // an ATTRIBUTE or a NAMESPACE can not
396     // have children
397     if (this.getNodeType() == ATTRIBUTE || this.getNodeType() == NAMESPACE) {
398       return null;
399     }
400 
401     // loop througth the first two (in the worst case) nodes
402     // and then return the firstChild if any
403     AnyNode tempNode = this.getFirstChildNode();
404     boolean found = false;
405     while (tempNode != null && !found) {
406       short type = tempNode.getNodeType();
407       if (type == ELEMENT || type == COMMENT || type == TEXT || type == PI) {
408         found = true;
409       } else if (type == ATTRIBUTE || type == NAMESPACE) {
410         tempNode = tempNode.getFirstChildNode();
411       }
412     }
413     return tempNode;
414   }
415 
416   /**
417    * Returns the next sibling of the current node. When the AnyNode is an ATTRIBUTE, it will return
418    * the next ATTRIBUTE node. When the AnyNode is a NAMESPACE, it will return the next NAMESPACE
419    * node.
420    *
421    * @return the next sibling of the current node
422    */
423   public AnyNode getNextSibling() {
424     return _nextSiblingNode;
425   }
426 
427   /**
428    * Returns the type of this node.
429    * 
430    * @return The type of this node
431    */
432   public short getNodeType() {
433     return _nodeType;
434   }
435 
436   /**
437    * Returns the local name of the node. Returns the local name of an element or attribute, the
438    * prefix of a namespace node, the target of a processing instruction, or null for all other node
439    * types.
440    *
441    * @return The local name of the node, or null if the node has no name
442    */
443   public String getLocalName() {
444     return _localName;
445   }
446 
447   /**
448    * Returns the namespace URI of the node. Returns the namespace URI of an element, attribute or
449    * namespace node, or null for all other node types.
450    *
451    * @return The namespace URI of the node, or null if the node has no namespace URI
452    */
453   public String getNamespaceURI() {
454     return _uri;
455   }
456 
457   /**
458    * Returns the string value of the node. The string value of a text node or an attribute node is
459    * its text value. The string value of an element or a root node is the concatenation of the
460    * string value of all its child nodes. The string value of a namespace node is its namespace URI.
461    * The string value of a processing instruction is the instruction, and the string value of a
462    * comment is the comment text.
463    *
464    * @return The string value of the node
465    */
466   public String getStringValue() {
467     switch (_nodeType) {
468       case ATTRIBUTE:
469       case TEXT:
470         return _value;
471       case NAMESPACE:
472         return _uri;
473       // not yet supported
474       case PI:
475         return "";
476       case COMMENT:
477         return _value;
478       case ELEMENT:
479         StringBuffer result = new StringBuffer(4096);
480         AnyNode tempNode = this.getNextSibling();
481         while (tempNode != null && tempNode.getNodeType() == TEXT) {
482           result.append(tempNode.getStringValue());
483           tempNode = tempNode.getNextSibling();
484         }
485 
486         tempNode = this.getFirstChild();
487         while (tempNode != null) {
488           result.append(tempNode.getStringValue());
489           tempNode = tempNode.getNextSibling();
490         }
491         tempNode = null;
492         return result.toString();
493       default:
494         return null;
495     }
496   }
497 
498   /**
499    * Returns the namespace prefix associated with the namespace URI of this node. Returns null if no
500    * prefix. is defined for this namespace URI. Returns an empty string if the default prefix is
501    * associated with this namespace URI.
502    *
503    * @return The namespace prefix, or null
504    */
505   public String getNamespacePrefix() {
506     return _prefix;
507   }
508 
509   /**
510    * Returns the String representation of this AnyNode. The String representation is a xml
511    * well-formed fragment corresponding to the representation of this node.
512    * 
513    * @return the String representation of this AnyNode.
514    */
515   public String toString() {
516     Serializer serializer = new BackwardCompatibilityContext().getSerializer();
517     if (serializer == null) {
518       throw new RuntimeException("Unable to obtain serializer");
519     }
520 
521     StringWriter writer = new StringWriter();
522 
523     serializer.setOutputCharStream(writer);
524 
525     try {
526       AnyNode2SAX.fireEvents(this, serializer.asDocumentHandler());
527     } catch (java.io.IOException ioe) {
528       return privateToString();
529     } catch (org.xml.sax.SAXException saxe) {
530       throw new RuntimeException(saxe.getMessage());
531     }
532 
533     return writer.toString();
534   }
535 
536   private String privateToString() {
537     StringBuilder sb = new StringBuilder(4096);
538 
539     if (_elements == null) {
540       _elements = new Stack<>();
541     }
542 
543     // check the Stack too see if we have
544     // already proceed the node
545     if (_elements.search(this) == -1) {
546       _elements.push(this);
547 
548       if (this.getNodeType() == ELEMENT) {
549         // open the tag
550         sb.append('<');
551         String prefix = getNamespacePrefix();
552         if (prefix != null) {
553           sb.append(prefix).append(':');
554         }
555         prefix = null;
556         sb.append(getLocalName());
557 
558         // append the attributes
559         AnyNode tempNode = this.getFirstAttribute();
560         while (tempNode != null) {
561           sb.append(' ').append(tempNode.getLocalName()).append("='")
562               .append(tempNode.getStringValue()).append('\'');
563           tempNode = tempNode.getNextSibling();
564         }
565 
566         // append the namespaces
567         tempNode = this.getFirstNamespace();
568         while (tempNode != null) {
569           sb.append(' ').append(XMLNS_PREFIX);
570           prefix = tempNode.getNamespacePrefix();
571           if (prefix != null && prefix.length() != 0) {
572             sb.append(':').append(prefix);
573           }
574           sb.append("='").append(tempNode.getNamespaceURI()).append('\'');
575           tempNode = tempNode.getNextSibling();
576         } // namespaceNode
577 
578         tempNode = this.getFirstChild();
579         if (tempNode != null) {
580           sb.append('>');
581           while (tempNode != null) {
582             sb.append(tempNode.privateToString());
583             tempNode = tempNode.getNextSibling();
584           }
585           // close the tag
586           sb.append("</").append(getLocalName()).append('>');
587         } else {
588           sb.append("/>");
589         }
590       } else {
591         sb.append(this.getStringValue());
592       }
593     }
594     return sb.toString();
595   }// toString()
596 
597   /**
598    * Appends a sibling AnyNode to the current node. The node to append will be added at the end of
599    * the sibling branch.
600    *
601    * @param node the node to add
602    */
603   protected void appendSibling(AnyNode node) {
604     if (node == null) {
605       throw new IllegalArgumentException();
606     }
607 
608     if (((node.getNodeType() == ATTRIBUTE) || (node.getNodeType() == NAMESPACE))
609         && (this.getNodeType() != node.getNodeType())) {
610       String err =
611           "a NAMESPACE or an ATTRIBUTE can only be add as a sibling to a node of the same type";
612       throw new UnsupportedOperationException(err);
613     }
614 
615     if (_nextSiblingNode == null) {
616       // if we already have a TEXT node -> merge
617       if ((node.getNodeType() == TEXT) && (this.getNodeType() == TEXT)) {
618         mergeTextNode(this, node);
619       } else {
620         _nextSiblingNode = node;
621       }
622     } else {
623       _nextSiblingNode.appendSibling(node);
624     }
625   }
626 
627   /**
628    * Returns the first child node in the tree.
629    * 
630    * @return the first child node in the tree.
631    */
632   protected AnyNode getFirstChildNode() {
633     return _firstChildNode;
634   }
635 
636   /**
637    * Adds the text value of a TEXT node to another TEXT node.
638    * 
639    * @param node1 the AnyNode that receives the text value
640    * @param node2 the AnyNode that needs to be merges with node1.
641    */
642   private void mergeTextNode(AnyNode node1, AnyNode node2) {
643     if (node1.getNodeType() != node2.getNodeType()) {
644       return;
645     }
646     if (node1.getNodeType() != AnyNode.TEXT) {
647       return;
648     }
649 
650     node1._value = node1.getStringValue() + node2.getStringValue();
651     node2 = null;
652   }
653 
654 }