/*
 * $Header: /home/cvspublic/jakarta-slide/src/share/org/apache/slide/util/XMLValue.java,v 1.8 2004/11/16 14:39:54 luetzkendorf Exp $
 * $Revision: 1.8 $
 * $Date: 2004/11/16 14:39:54 $
 *
 * ====================================================================
 *
 * Copyright 1999-2002 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package org.apache.slide.util;
import org.jdom.*;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.slide.content.NodeProperty;
import org.apache.slide.content.NodeRevisionDescriptor;
import org.apache.slide.structure.SubjectNode;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.XMLFilterImpl;

/**
 * This class is a container for a list of JDOM Elements.
 * The {@link #toString toString()} method provides a XML document fragment
 * describing the Elements of this XMLValue.
 *
 * @version $Revision: 1.8 $
 *
 **/
public class XMLValue implements Cloneable, Comparable {
    
    /**
     * Constant for the message of the IllegalArgumentException that may be
     * thrown in method {@link #add add()}.
     */
    
    public static final String ELEMENT_MUST_NOT_BE_NULL = "'null' Element is not allowed";
    
    /**
     * Constant for the message of the IllegalArgumentException that may be
     * thrown in constructor {@link #XMLValue(List) XMLValue(List)}.
     */
    public static final String TYPE_MISMATCH = "Only objects of type Element are allowed";
    
    /**
     * The start tag used to create a valid document out of the XML Element list
     * string in constructor {@link #XMLValue(String) XMLValue(String)}.
     */
    protected static final String START_TAG  = "<root>";
    
    /**
     * The end tag used to create a valid document out of the XML Element list
     * string in constructor {@link #XMLValue(String) XMLValue(String)}.
     */
    protected static final String END_TAG  = "</root>";
    
    protected static final String E_HREF = "href";
    
    protected static ThreadLocal saxBuilders = new ThreadLocal(); 
    
    /**
     *  The list of JDOM Elements.
     */
    protected ArrayList elementList = null;
    
    
    /**
     * Creates a XMLValue.
     */
    public XMLValue() {
        this((List)null);
    }
    
    /**
     * Creates a XMLValue from the given <code>element</code>.
     *
     * @param      element  the JDOM Element to add to the list.
     *
     * @throws     IllegalArgumentException if the given <code>element</code>
     *                                      is <code>null</code>.
     */
    public XMLValue(Element element) throws IllegalArgumentException {
        this((List)null);
        add(element);
    }
    
    /**
     * Creates a XMLValue from the given <code>list</code> of JDOM Elements.
     * the given List may be <code>null</code>.
     *
     * @param      elementList  the list of JDOM Elements to add.
     *
     * @throws     IllegalArgumentException if one of the list items
     *                                      is <code>null</code> or not a
     *                                      <code>Element</code>.
     */
    public XMLValue(List elementList) throws IllegalArgumentException{
        this.elementList = new ArrayList();
        add(elementList);
    }
    
    /**
     * Creates a XMLValue from the given String representation of a
     * list of XML Elements.
     *
     * @param      xmlString  a String representation of a list of XML Elements.
     *
     * @throws     JDOMException if parsing the <code>xmlString</code> fails.
     */
    public XMLValue(String xmlString) throws JDOMException {
        this(xmlString, null);
    }
    
    /**
     * Creates a XMLValue from the given String representation of a
     * list of XML Elements. If the given <code>defaultNamespace</code> is not
     * <code>null</code>, all nodes that does not have any namespace will be
     * created with that Namespace.
     *
     * @param      xmlString  a String representation of a list of XML Elements.
     * @param      defaultNamespace  the Namespace to use to create nodes that
     *                               does not have any namespace.
     *                               May be <code>null</code>.
     *
     * @throws     JDOMException if parsing the <code>xmlString</code> fails.
     */
    public XMLValue(String xmlString, Namespace defaultNamespace) throws JDOMException {
        this((List)null);
        add(xmlString, defaultNamespace);
    }
    
    
    /**
     * Adds a JDOM Element.
     *
     * @param      element  the JDOM Element to add.
     *
     * @throws     IllegalArgumentException if the given <code>element</code>
     *                                      is <code>null</code>.
     */
    public void add(Element element) throws IllegalArgumentException {
        if (element == null) {
            throw new IllegalArgumentException(ELEMENT_MUST_NOT_BE_NULL);
        }
        elementList.add(element);
    }
    
    /**
     * Adds a JDOM Text.
     *
     * @param      text  the JDOM Text to add.
     *
     * @throws     IllegalArgumentException if the given <code>text</code>
     *                                      is <code>null</code>.
     */
    public void add(Text text) throws IllegalArgumentException {
        if (text == null) {
            throw new IllegalArgumentException(ELEMENT_MUST_NOT_BE_NULL);
        }
        elementList.add(text);
    }
    
    /**
     * Adds a JDOM Comment.
     *
     * @param      comment  the JDOM Comment to add.
     *
     * @throws     IllegalArgumentException if the given <code>comment</code>
     *                                      is <code>null</code>.
     */
    public void add(Comment comment) throws IllegalArgumentException {
        if (comment == null) {
            throw new IllegalArgumentException(ELEMENT_MUST_NOT_BE_NULL);
        }
        elementList.add(comment);
    }
    
    /**
     * Adds a JDOM ProcessingInstruction.
     *
     * @param      pi  the JDOM ProcessingInstruction to add.
     *
     * @throws     IllegalArgumentException if the given <code>pi</code>
     *                                      is <code>null</code>.
     */
    public void add(ProcessingInstruction pi) throws IllegalArgumentException {
        if (pi == null) {
            throw new IllegalArgumentException(ELEMENT_MUST_NOT_BE_NULL);
        }
        elementList.add(pi);
    }
    
    /**
     * Adds a JDOM CDATA.
     *
     * @param      cdata  the JDOM CDATA to add.
     *
     * @throws     IllegalArgumentException if the given <code>CDATA</code>
     *                                      is <code>null</code>.
     */
    public void add(CDATA cdata) throws IllegalArgumentException {
        if (cdata == null) {
            throw new IllegalArgumentException(ELEMENT_MUST_NOT_BE_NULL);
        }
        elementList.add(cdata);
    }
    
    /**
     * Adds a JDOM EntityRef.
     *
     * @param      eref  the JDOM EntityRef to add.
     *
     * @throws     IllegalArgumentException if the given <code>entity ref</code>
     *                                      is <code>null</code>.
     */
    public void add(EntityRef eref) throws IllegalArgumentException {
        if (eref == null) {
            throw new IllegalArgumentException(ELEMENT_MUST_NOT_BE_NULL);
        }
        elementList.add(eref);
    }
    
    /**
     * Adds a List of JDOM items.
     *
     * @param      content  the list of JDOM items to add.
     *
     * @throws     IllegalArgumentException if one of the list items
     *                                      is <code>null</code> or not a
     *                                      <code>Element</code>.
     */
    public void add(List content) throws IllegalArgumentException {
        
        if (content != null) {
            Iterator iterator = content.iterator();
            while (iterator.hasNext()) {
                Object o = iterator.next();
                if( o == null ) {
                    throw new IllegalArgumentException(ELEMENT_MUST_NOT_BE_NULL);
                }
                else if( o instanceof Element ) {
                    add((Element)o);
                }
                else if( o instanceof Text ) {
                    Text t = (Text)o;
                    t.setText( t.getTextTrim() );
                    add(t);
                }
                else if( o instanceof Comment ) {
                    add((Comment)o);
                }
                else if( o instanceof ProcessingInstruction ) {
                    add((ProcessingInstruction)o);
                }
                else if( o instanceof CDATA ) {
                    add((CDATA)o);
                }
                else if( o instanceof EntityRef ) {
                    add((EntityRef)o);
                }
                else {
                    throw new IllegalArgumentException(TYPE_MISMATCH);
                }
            }
        }
    }
    
    /**
     * Adds the Elements given by an XML string representation.
     *
     * @param      xmlString  a String representation of a list of XML Elements.
     *
     * @throws     JDOMException if parsing the <code>xmlString</code> fails.
     */
    public void add(String xmlString) throws JDOMException {
        add(xmlString, null);
    }
    
    /**
     * Adds the Elements given by an XML string representation. If the given
     * <code>namespace</code> is not <code>null</code>, all nodes that does not
     * have any namespace will be created with that Namespace.
     *
     * @param      xmlString         a String representation of a list of XML Elements.
     * @param      defaultNamespace  the Namespace to use to create nodes that
     *                               does not have any namespace.
     *                               May be <code>null</code>.
     *
     * @throws     JDOMException if parsing the <code>xmlString</code> fails.
     */
    public void add(String xmlString, Namespace defaultNamespace) throws JDOMException {
        
        if (xmlString != null && xmlString.length() > 0) {
            if (NodeRevisionDescriptor.COLLECTION_TYPE.equals(xmlString)) {
                // avoid trivial parsing
                add(new Element("collection", defaultNamespace));
            }
            else {
                StringBuffer buffer = new StringBuffer(START_TAG.length() +
                                                           xmlString.length() +
                                                           END_TAG.length());
                buffer.append(START_TAG);
                buffer.append(xmlString);
                buffer.append(END_TAG);
                SAXBuilder builder = (SAXBuilder)saxBuilders.get();
                if (builder == null) {
                    builder = new SAXBuilder();
                    builder.setReuseParser(true);
                    saxBuilders.set(builder);
                }
                if (defaultNamespace != null) {
                    builder.setXMLFilter(new DefaultNamespaceXMLFilter(defaultNamespace));
                }
                try {
                    Document document = builder.build(new StringReader(buffer.toString()));
                    List content = document.getRootElement().getContent();
                    add( content );
                    // detach elements from parent
                    content.clear();
                }
                catch (Exception e) {
                    // should not happen since the StringReader does not
                    // perform any "real" I/O
                    throw new JDOMException(e.getMessage());
                }
            }
        }
    }
    
    public void addHref(String path) {
        Element href = new Element(E_HREF, NodeProperty.NamespaceCache.DEFAULT_NAMESPACE);
        href.setText(path);
        add(href);
    }
    
    /**
     * Method getHrefs
     *
     * @return   a List of URIs (String)
     *
     */
    public List getHrefStrings() {
        List result = new ArrayList();
        Iterator i = iterator();
        while (i.hasNext()) {
            Object o = i.next();
            if (o instanceof Element && E_HREF.equals(((Element)o).getName())) {
                result.add(((Element)o).getTextTrim());
            }
        }
        return result;
    }
    
    /**
     * Method getHrefs
     *
     * @return   a List of nodes (SubjectNode)
     *
     */
    public List getHrefNodes() {
        List result = new ArrayList();
        Iterator i = iterator();
        while (i.hasNext()) {
            Object o = i.next();
            if (o instanceof Element && E_HREF.equals(((Element)o).getName())) {
                result.add(SubjectNode.getSubjectNode(((Element)o).getTextTrim()));
            }
        }
        return result;
    }
    
    public void stripServletContext(String servletContext) {
        Iterator i = iterator();
        while (i.hasNext()) {
            Object o = i.next();
            if (o instanceof Element && E_HREF.equals(((Element)o).getName())) {
                Element hrefElm = (Element)o;
                String href = hrefElm.getTextTrim();
                if (href.startsWith(servletContext)) {
                    hrefElm.setContent(java.util.Arrays.asList(
                                           new Text[]{new Text(href.substring(servletContext.length()))}));
                }
            }
        }
    }
    
    /**
     * Returns an iterator of JDOM Elements.
     *
     * @return     an iterator of JDOM Elements.
     */
    public Iterator iterator() {
        return elementList.iterator();
    }
    
    /**
     * Returns the amount of JDOM elements.
     *
     * @return     the amount of JDOM elements.
     */
    public int size() {
        return elementList.size();
    }
    
    /**
     * Returns a clone of the List of JDOM Elements.
     *
     * @return     a clone of the List of JDOM Elements.
     */
    public List getList() {
        return (List)elementList.clone();
    }
    
    /**
     * Returns <code>true</code> if the other object is an XMLValue and
     * both their String representations are equal.
     *
     * @param      other  the Object to test for equality.
     *
     * @return     <code>true</code> if the other object is an XMLValue and
     *             both their String representations are equal.
     */
    public boolean equals(Object other) {
        if ( ! (other instanceof XMLValue) ) {
            return false;
        }
        return (this.toString().equals(other.toString()));
    }
    
    /**
     * Returns the hash code of this instance.
     *
     * @return     the hash code of this instance.
     */
    public int hashCode() {
        return toString().hashCode();
    }
    
    /**
     * Returns a clone of this instance.
     *
     * @return     a clone of this instance.
     */
    public Object clone() {
        return new XMLValue(elementList);
    }
    
    /**
     * Returns a String representation of the Elements as a XML document fragment.
     *
     * @return     a String representation of the Elements.
     */
    public String toString() {
        
        XMLOutputter outputter = new XMLOutputter();
        StringWriter stringWriter = new StringWriter();
        Iterator iterator = elementList.iterator();
        while (iterator.hasNext()) {
            try {
                Object o = iterator.next();
                if( o instanceof Element ) {
                    outputter.output((Element)o, stringWriter);
                }
                else if( o instanceof Text ) {
                    outputter.output((Text)o, stringWriter);
                }
                else if( o instanceof Comment ) {
                    outputter.output((Comment)o, stringWriter);
                }
                else if( o instanceof ProcessingInstruction ) {
                    outputter.output((ProcessingInstruction)o, stringWriter);
                }
                else if( o instanceof CDATA ) {
                    outputter.output((CDATA)o, stringWriter);
                }
                else if( o instanceof EntityRef ) {
                    outputter.output((EntityRef)o, stringWriter);
                }
            }
            catch (IOException e) {
                // this should not happen since we're writing to a StringWriter
                // which is no 'real' I/O
                throw new RuntimeException("IOException occurred: " + e.getMessage());
            }
        }
        return stringWriter.toString();
    }
    
    /**
     * This XMLFilter uses the given namespace as the default, means if no
     * namespace is provided.
     */
    protected static class DefaultNamespaceXMLFilter extends XMLFilterImpl {
        
        /**
         * The namespace to use as default.
         */
        Namespace defaultNamespace = null;
        
        /**
         * Creates a DefaultNamespaceXMLFilter which uses the given
         * <code>defaultNamespace</code> as default namespace, means if no
         * namespace is provided.
         *
         * @param      defaultNamespace  the Namespace to use as default.
         *                               Must NOT be <code>null</code>.
         */
        public DefaultNamespaceXMLFilter(Namespace defaultNamespace) {
            this.defaultNamespace = defaultNamespace;
        }
        
        /**
         * Overwrite <code>startElement()</code> in order to provide the
         * <code>defaultNamespace</code> if the current Element does not have
         * any namespace.
         *
         * @param      namespaceURI  the URI of the Namespace.
         * @param      localName     the name of the Element without any namspace
         *                           prefix.
         * @param      qName         the full name of the Element (with the namespace
         *                           prefix if there is one).
         * @param      atts          the Attributes of the element.
         *
         * @throws     SaxException
         */
        public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
            
            if ( (namespaceURI == null) || (namespaceURI.length() == 0) ) {
                namespaceURI = defaultNamespace.getURI();
                qName = defaultNamespace.getPrefix() + ":" + qName;
            }
            super.startElement(namespaceURI, localName, qName, atts);
        }
        
        /**
         * Overwrite <code>endElement()</code> in order to provide the
         * <code>defaultNamespace</code> if the current Element does not have
         * any namespace.
         *
         * @param      namespaceURI  the URI of the Namespace.
         * @param      localName     the name of the Element without any namspace
         *                           prefix.
         * @param      qName         the full name of the Element (with the namespace
         *                           prefix if there is one).
         *
         * @throws     SaxException
         */
        public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
            
            if ( (namespaceURI == null) || (namespaceURI.length() == 0) ) {
                namespaceURI = defaultNamespace.getURI();
                qName = defaultNamespace.getPrefix() + ":" + qName;
            }
            super.endElement(namespaceURI, localName, qName);
        }
        
    }
    
    /**
     * concatenates the text() values of all Elements in elementList
     *
     * @return   a String
     *
     */
    public String getTextValue() {
        Iterator it = elementList.iterator();
        StringBuffer sb = new StringBuffer();
        
        while (it.hasNext()) {
            Object o = it.next();
            if (o instanceof Element) {
                sb.append (((Element)o).getTextTrim());
            }
        }
        return sb.toString();
    }
    
    /**
     * compares the concatenated Text values of all Elements in elementList
     *
     * @param    o                   an Object
     *
     * @return   an int
     *
     */
    public int compareTo (Object o) {
        
        String s1 = getTextValue();
        String s2 = ((XMLValue)o).getTextValue();
        return s1.compareTo (s2);
    }
}

