/*
* Redistribution and use of this software and associated documentation
* ("Software"), with or without modification, are permitted provided
* that the following conditions are met:
*
* 1. Redistributions of source code must retain copyright
* statements and notices. Redistributions must also contain a
* copy of this document.
*
* 2. Redistributions in binary form must reproduce the
* above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. The name "Exolab" must not be used to endorse or promote
* products derived from this Software without prior written
* permission of Intalio, Inc. For written permission,
* please contact info@exolab.org.
*
* 4. Products derived from this Software may not be called "Exolab"
* nor may "Exolab" appear in their names without prior written
* permission of Intalio, Inc. Exolab is a registered
* trademark of Intalio, Inc.
*
* 5. Due credit should be given to the Exolab Project
* (http://www.exolab.org/).
*
* THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
* NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Copyright 2001 (C) Intalio, Inc. All Rights Reserved.
*
* $Id$
* Date Author Changes
* 04/24/2001 Arnaud Blandin Rewrited from scratch
* 04/22/2001 Arnaud Blandin Clean-up and support of comments
* 04/04/2001 Arnaud Blandin Created
*/
package org.exolab.castor.types;
import java.io.StringWriter;
import java.util.Stack;
import org.castor.xml.BackwardCompatibilityContext;
import org.exolab.castor.xml.Serializer;
import org.exolab.castor.xml.util.AnyNode2SAX;
/**
* A class used to represent an XML node. This is an alternative to DOM which is
* too heavy for our purpose (mainly handle XML Fragment when {@literal <any>}
* is used in an XML schema). The model is based on XPath Node. An AnyNode can
* be a:
* <ul>
* <li>ELEMENT</li>
* <li>ATTRIBUTE</li>
* <li>NAMESPACE</li>
* <li>COMMENT</li>
* <li>TEXT</li>
* <li>PROCESSING INSTRUCTION</li>
* </ul>
*
* @author <a href="mailto:arkin@intalio.com">Assaf Arkin</a>
* @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
* @author <a href="mailto:kvisco@intalio.com">Keith Visco</a>
* @version $Revision$ $Date: 2006-04-25 15:08:23 -0600 (Tue, 25 Apr 2006) $
*/
public final class AnyNode implements java.io.Serializable {
//TODO Processing Instructions
//TODO Full handling of namespaces
/** SerialVersionUID */
private static final long serialVersionUID = -4104117996051705975L;
/**
* The prefix for XML namespace
*/
private final static String XMLNS_PREFIX = "xmlns";
/**
* Representation for an element node.
*/
public static final short ELEMENT = 1;
/**
* Representation for an attribute node.
*/
public static final short ATTRIBUTE = 2;
/**
* Representation for a Namespace node.
*/
public static final short NAMESPACE = 3;
/**
* Representation for a processing instruction node.
*/
public static final short PI = 4;
/**
* Representation for a comment node.
*/
public static final short COMMENT = 5;
/**
* Representation for a text node.
*/
public static final short TEXT = 6;
/**
* The type of the current node.
* ELEMENT is the default value.
*/
private short _nodeType = ELEMENT;
/**
* The next sibling of this AnyNode
*/
private AnyNode _nextSiblingNode = null;
/**
* The first child of this AnyNode
*/
private AnyNode _firstChildNode = null;
/**
* the local name of the current node.
*/
private String _localName;
/**
* the Namespace URI of the current node
*/
private String _uri;
/**
* The prefix of the Namespace
*/
private String _prefix;
/**
* A stack used for avoiding endless loop
* in toString()
*/
private static Stack _elements;
/**
* The namespace context used in the toString()
*/
/**
* The value of this node defined as follow:
* <ul>
* <li>for an element the value is its TEXT NODE value (if any)</li>
* <li>for an attribute the value is the value of the attribute</li>
* <li>for a text node it is the character data</li>
* <li>for a namespace it is the namespace URI that is being bound
* to the namespace prefix</li>
* <li>for a comment it is the content of the comment not including the
* opening <!-- and the closing -->. </li>
* </ul>
*/
private String _value;
/**
* Default constructor: creates an empty element node
*/
public AnyNode() {
this(ELEMENT, null, null, null, null);
}
/**
* Creates a node given all the necessary information:
* type, localName, prefix, uri and value.
* This constructor is not user-friendly and launched
* RunTime exception is you try to instantiate non-valid node.
* @param type the node type.
* @param localName the name of the node.
* @param prefix the prefix if any of the namespace.
* @param uri the namespace uri of this node.
* @param value the value of this node.
*/
public AnyNode(short type, String localName, String prefix, String uri, String value) {
if ( (type > 6) && (type<1) ) {
throw new IllegalArgumentException("Illegal node type");
}
_nodeType = type;
//comment and text nodes don't have name
if ( (type>PI) && (localName != null) ) {
String err = "This node can not have a local name";
throw new IllegalArgumentException(err);
}
_localName = localName;
//for comment, pi or text we should have no namespaces
if ((type>NAMESPACE) && ( (uri != null) || (prefix != null)) ) {
String err = "This node can not handle namespace";
throw new IllegalArgumentException(err);
}
_uri = uri;
_prefix = prefix;
//attributes can not be namespaces
if (type == AnyNode.ATTRIBUTE)
if (localName.startsWith(XMLNS_PREFIX)) {
String err = "Namespaces can't be used as attributes.";
throw new IllegalArgumentException(err);
}
//you can't set value for element
if ( (type == ELEMENT) && (value != null) ) {
String err = "You can't set a value for this node type";
throw new IllegalArgumentException(err);
}
_value = value;
}
/**
* Adds an AnyNode to the current node
* @param node the node to append
*/
public void addAnyNode(AnyNode node) {
if (node == null) {
throw new IllegalArgumentException("null argument in addAnyNode");
}
switch(node.getNodeType()) {
case ATTRIBUTE:
addAttribute(node);
break;
case NAMESPACE:
addNamespace(node);
break;
default:
addChild(node);
break;
}
}
/**
* <p>
* Adds a child AnyNode to this node. A 'child' can be either an ELEMENT
* node, a COMMENT node, a TEXT node or a PROCESSING INSTRUCTION. If the
* current node already has a child then the node to add will be append as a
* sibling.
* <p>
* Note: you cannot add a child to a TEXT node.
*
* @param node
* the node to add.
*/
public void addChild(AnyNode node) {
if (node == null) {
throw new IllegalArgumentException("null argument in appendChild");
}
if (node.getNodeType() == ATTRIBUTE || node.getNodeType() == NAMESPACE) {
throw new IllegalArgumentException("An Attribute or an Namespace can't be added as a child");
}
if (this.getNodeType() == TEXT) {
throw new IllegalArgumentException("a TEXT node can't have children.");
}
if (_firstChildNode == null) {
_firstChildNode = node;
} else if (_firstChildNode.getNodeType() == ATTRIBUTE || _firstChildNode.getNodeType() == NAMESPACE) {
_firstChildNode.addChild(node);
} else {
_firstChildNode.appendSibling(node);
}
}
/**
* Adds an attribute to the current node.
* @param node the attribute to add.
*/
public void addAttribute(AnyNode node) {
if (node == null) {
throw new IllegalArgumentException("null argument in addAttribute");
}
if (node.getNodeType() != ATTRIBUTE) {
throw new IllegalArgumentException("Only attribute can be added as an attribute");
}
if (_firstChildNode == null) {
_firstChildNode = node;
} else {
if (_firstChildNode.getNodeType() == ATTRIBUTE) {
//if we reach an attribute then we add the node as its sibling
_firstChildNode.appendSibling(node);
} else if (_firstChildNode.getNodeType() == NAMESPACE) {
//if we reach an namespace the attributre should be added to
//the first child of the namespace
_firstChildNode.addAttribute(node);
} else {
//unplug the current firstNode to add a new one
node.addChild(_firstChildNode);
_firstChildNode = node;
}
}
} //addAttribute
/**
* Appends an namespace to the current node.
* @param node the attribute to add.
*/
public void addNamespace(AnyNode node){
if (node == null) {
throw new IllegalArgumentException("null argument in addNamespace");
}
if (node.getNodeType() != NAMESPACE) {
throw new IllegalArgumentException("Only namespace can be added as an namespace");
}
if (_firstChildNode == null) {
_firstChildNode = node;
} else {
if (_firstChildNode.getNodeType() == NAMESPACE) {
//if we reach an namepace then we add the node as its sibling
_firstChildNode.appendSibling(node);
} else if (_firstChildNode.getNodeType() == ATTRIBUTE) {
//if we reach an attribute the attributre should be added to
//the first child of the attribute
_firstChildNode.addNamespace(node);
} else {
//unplug the current firstNode to add a new one
node.addChild(_firstChildNode);
_firstChildNode = node;
}
}
} //addNamespace
/**
* Returns the first attribute of the current ELEMENT node
* or null.
* The next attribute,if any,is the sibling of the returned node.
*/
public AnyNode getFirstAttribute() {
if (this.getNodeType() != ELEMENT) {
String err = "This node type can not contain attributes";
throw new UnsupportedOperationException(err);
}
boolean found = false;
AnyNode tempNode = this.getFirstChildNode();
while (tempNode != null && !found) {
short type = tempNode.getNodeType();
//if the child is not an attribute or a namespace
//this element does not have any attribute
if (type == ELEMENT || type == COMMENT || type == TEXT || type == PI) {
tempNode = null;
} else if (type == NAMESPACE) {
tempNode = tempNode.getFirstChildNode();
} else {
found = true;
}
}
return tempNode;
}
/**
* Returns the first namespace of the current ELEMENT node or null. The next
* attribute if any is the sibling of the returned node.
* @return the first namespace of the current ELEMENT node or null.
*/
public AnyNode getFirstNamespace() {
if (this.getNodeType() != ELEMENT) {
String err = "This node type can not contain namespaces";
throw new UnsupportedOperationException(err);
}
AnyNode tempNode = this.getFirstChildNode();
boolean found = false;
while (tempNode != null && !found) {
short type = tempNode.getNodeType();
//if the child is not an attribute or a namespace
//this element does not have any namespace
if (type == ELEMENT || type == COMMENT || type == TEXT || type == PI) {
tempNode = null;
} else if (type == ATTRIBUTE) {
tempNode = tempNode.getFirstChildNode();
} else {
found = true;
}
}
return tempNode;
}
/**
* Returns the first Child node of this node. A 'child' can be either an
* ELEMENT node, a COMMENT node, a TEXT node or a PROCESSING INSTRUCTION.
*
* @return the first child of this node
*/
public AnyNode getFirstChild() {
//an ATTRIBUTE or a NAMESPACE can not
//have children
if (this.getNodeType() == ATTRIBUTE || this.getNodeType() == NAMESPACE) {
return null;
}
//loop througth the first two (in the worst case) nodes
//and then return the firstChild if any
AnyNode tempNode = this.getFirstChildNode();
boolean found = false;
while (tempNode != null && !found) {
short type = tempNode.getNodeType();
if (type == ELEMENT || type == COMMENT || type == TEXT || type == PI) {
found = true;
} else if (type == ATTRIBUTE || type == NAMESPACE) {
tempNode = tempNode.getFirstChildNode();
}
}
return tempNode;
}
/**
* Returns the next sibling of the current node. When the AnyNode is an
* ATTRIBUTE, it will return the next ATTRIBUTE node. When the AnyNode is a
* NAMESPACE, it will return the next NAMESPACE node.
*
* @return the next sibling of the current node
*/
public AnyNode getNextSibling() {
return _nextSiblingNode;
}
/**
* Returns the type of this node.
* @return The type of this node
*/
public short getNodeType() {
return _nodeType;
}
/**
* Returns the local name of the node. Returns the local name of an element
* or attribute, the prefix of a namespace node, the target of a processing
* instruction, or null for all other node types.
*
* @return The local name of the node, or null if the node has no name
*/
public String getLocalName(){
return _localName;
}
/**
* Returns the namespace URI of the node. Returns the namespace URI of an
* element, attribute or namespace node, or null for all other node types.
*
* @return The namespace URI of the node, or null if the node has no
* namespace URI
*/
public String getNamespaceURI() {
return _uri;
}
/**
* Returns the string value of the node. The string value of a text node or
* an attribute node is its text value. The string value of an element or a
* root node is the concatenation of the string value of all its child
* nodes. The string value of a namespace node is its namespace URI. The
* string value of a processing instruction is the instruction, and the
* string value of a comment is the comment text.
*
* @return The string value of the node
*/
public String getStringValue() {
switch(_nodeType) {
case ATTRIBUTE:
case TEXT:
return _value;
case NAMESPACE:
return _uri;
//not yet supported
case PI:
return "";
case COMMENT:
return _value;
case ELEMENT:
StringBuffer result = new StringBuffer(4096);
AnyNode tempNode = this.getNextSibling();
while (tempNode != null && tempNode.getNodeType() == TEXT) {
result.append(tempNode.getStringValue());
tempNode = tempNode.getNextSibling();
}
tempNode = this.getFirstChild();
while (tempNode != null) {
result.append(tempNode.getStringValue());
tempNode = tempNode.getNextSibling();
}
tempNode = null;
return result.toString();
default:
return null;
}
}
/**
* Returns the namespace prefix associated with the namespace URI of this
* node. Returns null if no prefix. is defined for this namespace URI.
* Returns an empty string if the default prefix is associated with this
* namespace URI.
*
* @return The namespace prefix, or null
*/
public String getNamespacePrefix() {
return _prefix;
}
/**
* Returns the String representation of this AnyNode. The String
* representation is a xml well-formed fragment corresponding to the
* representation of this node.
* @return the String representation of this AnyNode.
*/
public String toString() {
Serializer serializer = new BackwardCompatibilityContext().getSerializer();
if (serializer == null) {
throw new RuntimeException("Unable to obtain serializer");
}
StringWriter writer = new StringWriter();
serializer.setOutputCharStream(writer);
try {
AnyNode2SAX.fireEvents(this, serializer.asDocumentHandler());
} catch (java.io.IOException ioe) {
return privateToString();
} catch (org.xml.sax.SAXException saxe) {
throw new RuntimeException(saxe.getMessage());
}
return writer.toString();
}
private String privateToString() {
StringBuffer sb = new StringBuffer(4096);
if (_elements == null) {
_elements = new Stack();
}
//check the Stack too see if we have
//already proceed the node
if (_elements.search(this) == -1) {
_elements.push(this);
if (this.getNodeType() == ELEMENT) {
//open the tag
sb.append("<");
String prefix = getNamespacePrefix();
if (prefix != null) {
sb.append(prefix+":");
}
prefix = null;
sb.append(getLocalName());
//append the attributes
AnyNode tempNode = this.getFirstAttribute();
while (tempNode != null) {
sb.append(" ");
sb.append(tempNode.getLocalName());
sb.append("='"+tempNode.getStringValue()+"'");
tempNode = tempNode.getNextSibling();
}
//append the namespaces
tempNode = this.getFirstNamespace();
while (tempNode != null) {
sb.append(" ");
sb.append(XMLNS_PREFIX);
prefix = tempNode.getNamespacePrefix();
if (prefix != null && prefix.length() != 0) {
sb.append(":"+prefix);
}
sb.append("='"+tempNode.getNamespaceURI()+"'");
tempNode = tempNode.getNextSibling();
}//namespaceNode
tempNode = this.getFirstChild();
if (tempNode != null) {
sb.append(">");
while (tempNode != null) {
sb.append(tempNode.privateToString());
tempNode = tempNode.getNextSibling();
}
//close the tag
sb.append("</"+getLocalName()+">");
} else {
sb.append("/>");
}
} else {
sb.append(this.getStringValue());
}
return sb.toString();
}
return sb.toString();
}//toString()
/**
* Appends a sibling AnyNode to the current node. The node to append will be
* added at the end of the sibling branch.
*
* @param node
* the node to add
*/
protected void appendSibling(AnyNode node) {
if (node == null) {
throw new IllegalArgumentException();
}
if ( ((node.getNodeType() == ATTRIBUTE) || (node.getNodeType()== NAMESPACE)) &&
(this.getNodeType() != node.getNodeType())) {
String err = "a NAMESPACE or an ATTRIBUTE can only be add as a sibling to a node of the same type";
throw new UnsupportedOperationException(err);
}
if (_nextSiblingNode == null) {
//if we already have a TEXT node -> merge
if ((node.getNodeType() == TEXT) && (this.getNodeType() == TEXT)) {
mergeTextNode(this,node);
} else {
_nextSiblingNode = node;
}
} else {
_nextSiblingNode.appendSibling(node);
}
}
/**
* Returns the first child node in the tree.
* @return the first child node in the tree.
*/
protected AnyNode getFirstChildNode() {
return _firstChildNode;
}
/**
* Adds the text value of a TEXT node to another
* TEXT node.
* @param node1 the AnyNode that receives the text value
* @param node2 the AnyNode that needs to be merges with node1.
*/
private void mergeTextNode(AnyNode node1, AnyNode node2) {
if (node1.getNodeType() != node2.getNodeType()) {
return;
}
if (node1.getNodeType() != AnyNode.TEXT) {
return;
}
StringBuffer temp = new StringBuffer(node1.getStringValue());
temp.append(node2.getStringValue());
node1._value = temp.toString();
node2 = null;
}
}