/*
 * $Id: ElementNode.java,v 1.11 1999/04/14 20:46:23 mode Exp $
 * 
 * Copyright (c) 1998-1999 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * This software is the confidential and proprietary information of Sun
 * Microsystems, Inc. ("Confidential Information").  You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Sun.
 * 
 * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE
 * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR ANY DAMAGES
 * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING
 * THIS SOFTWARE OR ITS DERIVATIVES.
 */

package com.sun.xml.tree;

import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.Writer;

import java.util.Enumeration;

import org.w3c.dom.*;


/**
 * This class represents XML elements in a parse tree, and is often
 * subclassed to add custom behaviors.  When an XML Document object
 * is built using an <em>XmlDocumentBuilder</em> instance, simple
 * declarative configuration information may be used to control whether
 * this class, or some specialized subclass (e.g. supporting HTML DOM
 * methods) is used for elements in the resulting tree.
 *
 * <P> As well as defining new methods to provide behaviors which are
 * specific to application frameworks, such as Servlets or Swing, such
 * subclasses may also override methods such as <em>doneParse</em>
 * and <em>appendChild</em> to perform some kinds of processing during
 * tree construction.  Such processing can include transforming tree
 * structure to better suit the needs of a given application.  When
 * such transformation is done, the <em>XmlWritable</em> methods
 * may need to be overridden to make elements transform themselves back
 * to XML without losing information.  (One common transformation is
 * eliminating redundant representations of data; attributes of an XML
 * element may correspond to defaultable object properties, and so on.)
 *
 * <P> Element nodes also support a single <em>userObject</em> property,
 * which may be used to bind objects to elements where subclassing is
 * either not possible or is inappropriate.  For example, user interface
 * objects often derive from <code>java.awt.Component</code>, so that
 * they can't extend a different class (<em>ElementNode</em>).
 *
 * @see XmlDocumentBuilder
 *
 * @author David Brownell
 * @version $Revision: 1.11 $
 */
public class ElementNode extends ParentNode implements ElementEx
{
    private String		tag;
    private AttributeSet	attributes;
    private String		idAttributeName;
    private Object		userObject;

    private static final char	tagStart [] = { '<', '/' };
    private static final char	tagEnd [] = { ' ', '/', '>' };
    
    /**
     * Partially constructs an element; its tag will be assigned
     * by the factory (or subclass), while attributes and the
     * parent (and implicitly, siblings) will assigned when it is
     * joined to a DOM document.
     */
    public ElementNode () { }

    public void trimToSize ()
    {
	super.trimToSize ();
	if (attributes != null)
	    attributes.trimToSize ();
    }

    /**
     * Assigns the element's tag, when the element has been
     * constructed using the default constructor.  For use by
     * element factories potentially by custom subclasses. 
     */
    protected void setTag (String t) { tag = t; }
    
    // Assigns the element's attributes.
    void setAttributes (AttributeSet a)
    {
	AttributeSet oldAtts = attributes;

	// Check if the current AttributeSet or any attribute is readonly
	// isReadonly checks if any of the attributes in the AttributeSet
	// is readonly..
	if (oldAtts != null && oldAtts.isReadonly ())
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);

	if (a != null)
	    a.setNameScope (this);
	attributes = a;
	if (oldAtts != null)
	    oldAtts.setNameScope (null);
    }

    // package private -- overrides base class method
    void checkChildType (int type)
    throws DOMException
    {
	switch (type) {
	  case ELEMENT_NODE:
	  case TEXT_NODE:
	  case COMMENT_NODE:
	  case PROCESSING_INSTRUCTION_NODE:
	  case CDATA_SECTION_NODE:
	  case ENTITY_REFERENCE_NODE:
	    return;
	  default:
	    throw new DomEx (DomEx.HIERARCHY_REQUEST_ERR);
	}
    }

    // package private -- overrides base class method
    public void setReadonly (boolean deep)
    {
	if (attributes != null)
	    attributes.setReadonly ();
	super.setReadonly (deep);
    }

    public String getNamespace ()
    {
	String	prefix;
	String	value;

	if ((prefix = getPrefix ()) == null)
	    return getInheritedAttribute ("xmlns");

	//
	// XXX return real URLs for these, both here and
	// in AttributeNode ...
	//
	if ("xml".equals (prefix) || "xmlns".equals (prefix))
	    return null;

	value = getInheritedAttribute ("xmlns:" + prefix);
	if (value == null)
	    throw new IllegalStateException (getMessage ("EN-000",
					     new Object [] { prefix }));

	return value;
    }

    public String getLocalName ()
    {
	int index = tag.indexOf (':');
	if (index < 0)
	    return tag;
	return tag.substring (index + 1);
    }

    public String getPrefix () {
	int	index = tag.indexOf (':');
        return index < 0 ? null : tag.substring (0, index);
    }

    public void setPrefix (String prefix) {
	int index = tag.indexOf (':');
	if (prefix == null) {
	    if (index < 0)
	    	return;
	    else 
	    	tag = tag.substring (index + 1);
	    return;
	}
   	StringBuffer tmp = new StringBuffer (prefix);
	tmp.append (':');
	if (index < 0 )
	    tmp.append (tag);
	else
	    tmp.append (tag.substring (index + 1));
	tag = tmp.toString ();
    }

    /** <b>DOM:</b> Returns the attributes of this element. */
    public NamedNodeMap getAttributes ()
    {
	if (attributes == null)
	    attributes = new AttributeSet (this);
        return attributes;
    }
        
    /**
     * Returns the element and its content as a string, which includes
     * all the markup embedded in this element.  If the element is not
     * fully constructed, the content will not be an XML tag.
     */
    public String toString ()
    {
	try {
	    CharArrayWriter	out = new CharArrayWriter ();
	    XmlWriteContext	x = new XmlWriteContext (out);
	    writeXml (x);
	    return out.toString ();
	} catch (Exception e) {
	    return super.toString ();
	}
    }
    
    
    /**
     * Writes this element and all of its children out, as well
     * formed XML.
     */
    public void writeXml (XmlWriteContext context) throws IOException
    {
	Writer	out = context.getWriter ();

	if (tag == null)
	   throw new IllegalStateException ( getMessage ("EN-002"));
	   
	out.write (tagStart, 0, 1);	// "<"
	out.write (tag);
	
        if (attributes != null)
	    attributes.writeXml (context);

	//
	// Write empty nodes as "<EMPTY />" to make sure version 3
	// and 4 web browsers can read empty tag output as HTML.
	// XML allows "<EMPTY/>" too, of course.
	//
	if (!hasChildNodes ())
	    out.write (tagEnd, 0, 3);	// " />"
	else  {
	    out.write (tagEnd, 2, 1);	// ">"
	    writeChildrenXml (context);
	    out.write (tagStart, 0, 2);	// "</"
	    out.write (tag);
	    out.write (tagEnd, 2, 1);	// ">"
	}
    }

    /**
     * Assigns the name of the element's ID attribute; only one attribute
     * may have the ID type.  XML supports a kind of validatable internal
     * linking using ID attributes, with IDREF attributes identifying
     * specific nodes (and IDREFS attributes identifying sets of them).
     */
    public void setIdAttributeName (String attName)
    {
	if (readonly)
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);
	idAttributeName = attName;
    }

    /**
     * Returns the name of the element's ID attribute, if one is known.
     */
    public String getIdAttributeName ()
	{ return idAttributeName; }

    
    public void setUserObject (Object userObject)
	{ this.userObject = userObject; }

    public Object getUserObject ()
	{ return userObject; }

    // DOM support

    /** <b>DOM:</b> Returns the ELEMENT_NODE node type. */
    public short getNodeType ()  { return ELEMENT_NODE; }

    /** <b>DOM:</b> Returns the name of the XML tag for this element. */
    public String getTagName () { return tag; }
    
    /** <b>DOM:</b> Returns the name of the XML tag for this element. */
    public String getNodeName () { return tag; }

    /** <b>DOM:</b> Returns the value of the named attribute, or null */
    public String getAttribute (String name)
    {
	return (attributes == null)
	    ? ""
	    : attributes.getValue (name);
    }

    public String getAttribute (String uri, String name)
    {
	Attr	attr;

	if (attributes == null)
	    return "";
	attr = getAttributeNode (uri, name);
	if (attr == null)
	    return "";
	return attr.getValue ();
    }

    public Attr getAttributeNode (String uri, String name)
    {
	if (name == null)
	    return null;
	if (attributes != null) {
	    for (int i = 0; ; i++) {
		AttributeNode	attr;
		String		ns;

		attr = (AttributeNode) attributes.item (i);
		if (attr == null)
		    return null;
		if (!name.equals (attr.getName ()))
		    continue;
		ns = attr.getNamespace ();
		if (ns != null && ns.equals (uri))
		    return attr;
	    }
	}
	return null;
    }

    
    /**
     * <b>DOM:</b> Assigns or modifies the value of the specified attribute.
     */
    public void setAttribute (String name, String value)
    throws DOMException
    {
	AttributeNode	att;

	if (readonly)
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);
	if (attributes == null)
	    attributes = new AttributeSet (this);
	if ((att = (AttributeNode) attributes.getNamedItem (name)) != null)
	    att.setNodeValue (value);
	else {
	    att = new AttributeNode (name, value, true, null);
	    att.setOwnerDocument ((XmlDocument) getOwnerDocument ());
	    attributes.setNamedItem (att);
	}
    }
    
    /** <b>DOM:</b> Remove the named attribute. */
    public void removeAttribute (String name)
    throws DOMException
    {
	if (readonly)
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);
	if (attributes == null)
	    throw new DomEx (DomEx.NOT_FOUND_ERR);
        attributes.removeNamedItem (name);
    }

    /** <b>DOM:</b>  returns the attribute */
    public Attr getAttributeNode (String name)
    {
	if (attributes != null)
	    return (Attr) attributes.getNamedItem (name);
	else
	    return null;
    }
    
    /** <b>DOM:</b> assigns the attribute */
    public Attr setAttributeNode (Attr newAttr)
    throws DOMException
    {
	if (readonly)
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);
	if (!(newAttr instanceof AttributeNode))
	    throw new DomEx (DomEx.WRONG_DOCUMENT_ERR);

	if (attributes == null) 
	    attributes = new AttributeSet (this);

	return (Attr)attributes.setNamedItem (newAttr);
    }
    
    /** <b>DOM:</b> removes the attribute with the same name as this one */
    public Attr removeAttributeNode (Attr oldAttr)
    throws DOMException
    {
	if (isReadonly ())
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);

	Attr	attr = getAttributeNode (oldAttr.getNodeName ());
	if (attr == null)
	    throw new DomEx (DomEx.NOT_FOUND_ERR);
	removeAttribute (attr.getNodeName ());
	return attr;
    }

    
    /**
     * <b>DOM:</b> Merges all adjacent Text nodes in the tree rooted by this
     * element.  Avoid using this on large blocks of text not separated
     * by markup such as elements or processing instructions, since it
     * can require arbitrarily large blocks of contiguous memory.
     *
     * <P> As a compatible extension to DOM, this normalizes treatment
     * of whitespace except when the <em>xml:space='preserve'</em>
     * attribute value applies to a node.  All whitespace is normalized
     * to one space.  This ensures that text which is pretty-printed and
     * then reread (and normalized) retains the same content. </P>
     */
    public void normalize ()
    {
	Node	node;
	boolean	preserve = false;
	boolean	knowPreserve = false;

	if (readonly)
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);
	for (int i = 0; true; i++) {
	    if ((node = item (i)) == null)
		break;
	    switch (node.getNodeType ()) {
	      case ELEMENT_NODE:
		((Element)node).normalize ();
		continue;
	      // case CDATA_SECTION_NODE:
	      case TEXT_NODE:
		{
		    Node	node2 = item (i + 1);

		    if (node2 == null || node2.getNodeType () != TEXT_NODE) {
			// See if xml:space='preserve' is set...
			if (!knowPreserve) {
			    preserve = "preserve".equals (
				    getInheritedAttribute ("xml:space"));
			    knowPreserve = true;
			}

			// ... and if not, normalize whitespace
			if (!preserve) {
			    char buf [] = ((TextNode)node).data;

			    // XXX this isn't supposed to happen
			    if (buf == null || buf.length == 0) {
				removeChild (node);
				i--;
				continue;
			    }

			    int current = removeWhiteSpaces (buf);

			    // compact if it shrank
			    if (current != buf.length) {
				char tmp [] = new char [current];
				System.arraycopy (buf, 0,
				    tmp, 0,
				    current);
				((TextNode)node).data = tmp;
			    }
			}
			continue;
		    }
		    
		    ((TextNode) node).joinNextText ();
		    i--;
		    continue;
		}
	      default:
		continue;
	    }
	}
    }

    /*
     * removes white leading, trailing and extra white spaces from the buffer.
     * returns the size of the new buf after the white spaces are removed.
     */
    public int removeWhiteSpaces (char [] buf) {
	    int current = 0, j = 0;

	    // copy to beginning, normalizing whitespace
	    // (including leading, trailing) to one space
	    while (j < buf.length) {
		boolean	sawSpace = false;
		char	c = buf [j++];

		if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
			c = ' ';
			sawSpace = true;
		}
		buf [current++] = c;
		if (sawSpace) {
			while (j < buf.length) {
				c = buf [j];
			        if (c == ' ' || c == '\t'
				   || c == '\n' || c == '\r') {
				     j++;
				     continue;
				} else
				    break;
			}
		}
	    }
	    return current;
    }


    /**
     * Creates a new unparented node whose attributes are the same as
     * this node's attributes; if <em>deep</em> is true, the children
     * of this node are cloned as children of the new node.
     */
    public Node cloneNode (boolean deep)
    {
	try {
	    ElementNode	retval;

	    retval = (ElementNode) getOwnerDocument().createElement (tag);
	    if (attributes != null)
		retval.setAttributes (new AttributeSet (attributes, true));
	    if (deep) {
		for (int i = 0; true; i++) {
		    Node	node = item (i);
		    if (node == null)
			break;
		    retval.appendChild (node.cloneNode (true));
		}
	    }
	    return retval;
	} catch (DOMException e) {
	    throw new RuntimeException (getMessage ("EN-001"));
	}
    }

    /**
     * Convenience method to construct a non-prettyprinting XML write
     * context and call writeXml with it.  Subclasses may choose to
     * to override this method to generate non-XML text, 
     *
     * @param out where to emit the XML content of this node
     */
    public void write (Writer out) throws IOException
    {
	writeXml (new XmlWriteContext (out));
    }
}
