/*
 * $Header: /home/cvspublic/jakarta-slide/src/share/org/apache/slide/structure/ObjectNode.java,v 1.30 2004/11/24 16:20:42 luetzkendorf Exp $
 * $Revision: 1.30 $
 * $Date: 2004/11/24 16:20:42 $
 *
 * ====================================================================
 *
 * 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.structure;

import java.io.Serializable;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.Vector;

import org.apache.slide.common.ObjectValidationFailedException;
import org.apache.slide.common.UriPath;
import org.apache.slide.util.EmptyEnumeration;
import org.apache.slide.util.Messages;

/**
 * Represents any kind of object node.
 * <p/>
 * This includes actors, activities and collections.
 *
 * @version $Revision: 1.30 $
 */
public abstract class ObjectNode
    implements Serializable, Cloneable {
    

    /**
     * For tracking which bindings need to be updated.
     */
	private Set updatedBindings = null;

    /**
     * Uniform ressource identifier (URI) of the object.
     */
    protected String uri;
    
    /**
     * Unique URI of the object. Enables clients to determine
     * whether two bindings are to the same resource.
     * The  UURI is a URI and may use any registered URI scheme.
     * If binding is enabled, several object URIs may represent the same resource,
     * while the UURI must be really unique.
     */
    private String uuri;
    
    /**
     * Vector of inbound links' Uris. Before modifying this vector you must check
     * wheter {@link #linksShared} is true. In this case clone the vector
     * and set the shared state to false.
     */
    private Vector links = null;
    
    /*
     * If true then the {@link #links} vector is shared between multiple
     * ObjectNode-instances and thus must not be modified.
     * 
     *FIXME Never read! Do we need to implement linkSharing
     */
    //private boolean linksShared;
    
    /**
     * Vector of bindings. Before modifying this vector you must check
     * whether {@link #bindingsShared} is true. In this case clone the vector
     * and set the shared state to false.
     */
    private BindingList bindings;
    private ParentBindingList parentBindings;
    
    /**
     * If true then the {@link #bindings} vector is shared between multiple
     * ObjectNode-instances and thus must not be modified.
     */
    private boolean bindingsShared;
    
    private Vector childrenCache = null;
    
    private transient UriPath path = null;

    
    /**
     * Default constructor.
     */
    public ObjectNode() {
        this.bindings = new BindingList();
        this.parentBindings = new ParentBindingList();
        this.updatedBindings = new HashSet();
    }
    
    /**
     * Default constructor.
     */
    public ObjectNode(String uri) {
        this();
        this.uri = uri;
    }
    
    /**
     * Default constructor.
     * NOTE: this constructor should not be used if binding is enabled for the
     *       store associated to the specified URI.
     */
    public ObjectNode(String uri, Vector children, Vector links) {
        this( uri );
        this.links = links;
        addChildren(children);
    }
    
    /**
     * Contructor to be used by stores supporting binding.
     */
    public ObjectNode(String uuri, Vector bindings, Vector parentBindings, Vector links) {
        this();
        this.uuri = uuri;
        this.bindings = new BindingList(bindings);
        this.parentBindings = new ParentBindingList(parentBindings);
        this.links = links;
        Enumeration e = bindings.elements();
        if (e.hasMoreElements()) this.updatedBindings = new HashSet();
        while(e.hasMoreElements()) {
            updatedBindings.add(((ObjectNode.Binding)e.nextElement()).getUuri());
        }
    }
    
    /**
     * Get object's unique resource identifier.
     *
     * @return String Uri
     */
    public String getUri() {
        return this.uri;
    }
    
    /**
     * Set object's unique identifier.
     *
     * @param uri Object Uri
     */
    public void setUri(String uri) {
        this.uri = uri;
        this.path = null;
    }
    
    /**
     * Get the unique URI.
     *
     * @return   an URI
     *
     */
    public String getUuri() {
        if (uuri != null) {
            return uuri;
        } else {
            if (uri == null) {
                throw new IllegalStateException(toString());
            }
            return uri;
        }
    }
    
    /**
     * Set the unique URI.
     */
    public void setUuri(String uuri) {
        if (uuri == null) {
            throw new IllegalArgumentException();
        }
        this.uuri = uuri;
    }
    
    public Set getUpdatedBindings() {
        if (this.updatedBindings == null) {
            return Collections.EMPTY_SET;
        } else {
            return Collections.unmodifiableSet(updatedBindings);
        }
    }

    public void resetUpdatedBindings() {
        this.updatedBindings = null;
    }

    /**
     * Return this object's children
     *
     * @return Enumeration Children's uris
     */
    public Vector getChildren() {
        if (childrenCache == null) {
            computeChildren();
        }
        return childrenCache;
    }
    
    /**
     * Return this object's children
     *
     * @return Enumeration Children's uris
     */
    public Enumeration enumerateChildren() {
        return getChildren().elements();
    }
    
    /**
     * Return this object's bindings
     *
     * @return Enumeration of the bindings (Binding instances)
     */
    public Enumeration enumerateBindings() {
        return bindings.elements();
    }
    
    /**
     * Return this object's binding parent-set
     *
     * @return Enumeration of the binding parent-set (Binding instances)
     */
    public Enumeration enumerateParentBindings() {
        return parentBindings.elements();
    }
    
    /**
     * Return the UURI of the specified binding.
     *
     * @param    bindingName         a  String
     * @return   UURI of the bound member
     */
    public String getBindingUuri( String bindingName ) {
        String result = null;
        Binding b = bindings.get(bindingName);
        if (b != null) {
            result = b.getUuri();
        }
        return result;
    }
    
    /**
     * Test if object has the specified child.
     *
     * @param uri Child's uri
     * @return boolean true if this object has the specified child,
     * false otherwise
     */
    public boolean hasChild(String uri) {
        return getChildren().contains(uri);
    }
    
    /**
     * Test if this object has the specified child.
     *
     * @param child Child object
     * @return boolean true if this object has the specified child,
     * false otherwise
     */
    public boolean hasChild(ObjectNode child) {
        boolean result = false;
        if (child != null) {
            result = getChildren().contains(child.getUri());
        }
        return result;
    }
    
    /**
     * Test if this object has the specified binding.
     *
     * @param bindingName the binding name
     * @return boolean true if this object has the specified binding,
     * false otherwise
     */
    public boolean hasBinding( String bindingName ) {
        boolean result = false;
        if (bindingName != null) {
            result = (bindings.get(bindingName) != null);
        }
        return result;
    }
    
    /**
     * Test if this object has the specified parent binding.
     *
     * @param bindingName the binding name
     * @return boolean true if this object has the specified binding,
     * false otherwise
     */
    public boolean hasParentBinding( String bindingName ) {
        boolean result = false;
        if (bindingName != null) {
            result = (parentBindings.get(bindingName) != null);
        }
        return result;
    }
    
    /**
     * Test if this object has the specified binding.
     *
     * @param    child               an ObjectNode
     *
     * @return   true if this object has the specified binding,
     * false otherwise
     */
    public boolean hasBinding( ObjectNode child ) {
        if (child != null) {
            Enumeration benum = enumerateBindings();
            while (benum.hasMoreElements()) {
                Binding b = (Binding)benum.nextElement();
                if (b.getUuri().equals(child.getUuri())) {
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * Return the number of binding parents (i.e. of nodes having a binding to this node)
     */
    public int numberOfParentBindings() {
        return parentBindings.size();
    }
    
    /**
     * Test if object has children.
     *
     * @return boolean true if this object has children, false otherwise
     */
    public boolean hasChildren() {
        return !(getChildren().isEmpty());
    }

    
    /**
     * Test if object has links.
     *
     * @return boolean true if this object has links, false otherwise
     */
    public boolean hasLinks() {
        if (this.links == null) {
            return false;
        } else {
            return !( links.isEmpty());
        }
    }
    /**
     * Return this object's inbound links
     *
     * @return Enumeration Inbound links uris
     */
    public Enumeration enumerateLinks() {
        if (this.links == null) {
            return EmptyEnumeration.INSTANCE;
        } else {
            return links.elements();
        }
    }
    
    
    // --------------------------------------------------------- Object Methods
    /**
     * Equals.
     *
     * @param obj Object to test
     * @return boolean True if the two object are equal :
     * <li>obj is of type ObjectNode and is not null</li>
     * <li>The Uris are equal</li>
     */
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj instanceof ObjectNode) {
            return getPath().equals(((ObjectNode)obj).getPath());
        }
        else {
            return false;
        }
    }
    
    public int hashCode() {
        return  getPath().hashCode();
    }
    
    /**
     * Clone.
     *
     * @return Object clone
     */
    public ObjectNode cloneObject() {
        ObjectNode result = null;
        
        try {
            // init the shared fields to let clone() copy them
            //this.linksShared=true;
            this.bindingsShared=true;
            result = (ObjectNode) super.clone();
        } catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
        
        return result;
    }
    
    /**
     * Copy.
     *
     * @return Object copy
     */
    public ObjectNode copyObject() {
        ObjectNode result = null;
        childrenCache = null;
        
        try {
            result = (ObjectNode) super.clone();
            //result.linksShared=false;
            result.bindingsShared=false;
            result.links = new Vector();
            result.bindings = new BindingList();
            result.parentBindings = new ParentBindingList();
        } catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }
        
        return result;
    }
    
    /**
     * Validate an ObjectNode.
     *
     * @param expectedUri Uri
     */
    public void validate(String expectedUri) {
        
        if (uri == null)
            throw new ObjectValidationFailedException
                (expectedUri, Messages.message
                     (ObjectNode.class.getName() + ".nullUri"));
        
        if (!uri.equals(expectedUri) && !uuri.equals(expectedUri))
            throw new ObjectValidationFailedException
                (expectedUri, Messages.message
                     (ObjectNode.class.getName() + ".incorrectUri"));
        
        if (bindings == null || parentBindings == null)
            throw new ObjectValidationFailedException
                (uri, Messages.message
                     (ObjectNode.class.getName() + ".nullBindingsVector"));
                
    }
    
    /**
     * Add a child.
     * @param    child               an ObjectNode
     */
    public void addChild( ObjectNode child ) {
        addBinding( child.getPath().lastSegment(), child );
    }

    
    /**
     * Add a link.
     * @param   link               an LinkNode
     */
    public void addLink( LinkNode link ) {
        if (this.links == null) this.links = new Vector();
        links.add(link.getUri());
    }
    
    /**
     * Add a new binding.
     * @param    bindingName         a  String
     * @param    source              the child ObjectNode
     */
    public void addBinding( String bindingName, ObjectNode source ) {
        if (this.updatedBindings == null) this.updatedBindings = new HashSet();
        updatedBindings.add(source.getUri());
          
        if (!hasBinding(bindingName)) {
            if(bindingsShared) {
                // Lazy cloning on first write access
                bindings=(BindingList)bindings.clone();
                parentBindings=(ParentBindingList)parentBindings.clone();
                bindingsShared=false;
            }
            bindings.put(bindingName, source);
            childrenCache = null;
            source.addParentBinding(bindingName, this);
        }
        else {
            throw new IllegalStateException(
                "Existing binding "+bindingName+" at "+this.uri+" has to be removed first");
        }
    }
    
    /**
     * Remove child.
     *
     * @param child The child to remove
     */
    public void removeChild(ObjectNode child) {
        if (this.updatedBindings == null) this.updatedBindings = new HashSet();
        updatedBindings.add(child.getUri());
        
        if (child == null) {
            return;
        }
        
        if(bindingsShared) {
            // Lazy cloning on first write access
            bindings=(BindingList)bindings.clone();
            bindingsShared=false;
        }
        String bindingName = UriPath.getLastSegment( child.getUri() );
        bindings.remove(bindingName);
        childrenCache = null;
        child.removeParentBinding(bindingName, this);
    }

    /**
     * Remove link.
     *
     * @param link
     */
    public void removeLink(LinkNode link) {
        if (this.links != null) {
            links.remove(link.getUri());
        }
    }
    
    /**
     * Get the path of this object node.
     *
     * @return   an UriPath
     */
    public UriPath getPath() {
        if (path == null) {
            path = new UriPath(getUri());
        }
        return path;
    }
    
    private void computeChildren() {
        childrenCache = new Vector();
        Enumeration e = bindings.elements();
        while (e.hasMoreElements()) {
            Binding b = (Binding)e.nextElement();
            StringBuffer buf = new StringBuffer(uri);
            if (!uri.endsWith("/")) {
                buf.append("/");
            }
            buf.append(b.getName());
            childrenCache.add( buf.toString() );
        }
    }
    
    /**
     * Method computeBindings
     * NOTE: should not be used if binding is enabled for the
     *       store associated to this object node.
     */
    private void addChildren( Vector children ) {
        Enumeration ch = children.elements();
        while (ch.hasMoreElements()) {
            String c = (String)ch.nextElement();
            ObjectNode s = new SubjectNode(c);
            s.setUuri( s.getUri() );
            addBinding( UriPath.getLastSegment(c), s );
        }
        ObjectNode p = null;
        String pUri = UriPath.getParentUri(uri);
        if (pUri != null) {
            p = new SubjectNode( pUri );
            p.setUuri( p.getUri() );
        }
        addParentBinding( getPath().lastSegment(), p );
    }
    
    public void addParentBinding( String bindingName, ObjectNode parent ) {
        if(bindingsShared) {
            // Lazy cloning on first write access
            bindings=(BindingList)bindings.clone();
            parentBindings=(ParentBindingList)parentBindings.clone();
            bindingsShared=false;
        }
        parentBindings.put(bindingName, parent);
    }
    
    
    private void removeParentBinding( String bindingName, ObjectNode parent ) {
        if(bindingsShared) {
            // Lazy cloning on first write access
            bindings=(BindingList)bindings.clone();
            parentBindings=(ParentBindingList)parentBindings.clone();
            bindingsShared=false;
        }
        parentBindings.remove(bindingName, parent.getUuri() );
    }
    
    public String toString() {
        StringBuffer b = new StringBuffer(getUri());
        if (!getUri().equals(getUuri())) {
            b.append(" [").append(getUuri()).append("]");
        }
        return b.toString();
    }
    
    /**
     * Represents an binding bindingName -> UURI
     */
    public static class Binding implements Serializable, Cloneable {
        
        protected final String bName;
        protected final String bUuri;
        
        public Binding(String bindingName, String bindingUuri) {
            this.bName = bindingName;
            this.bUuri = bindingUuri;
        }
        
        public String getName() {
            return bName;
        }
        
        public String getUuri() {
            return bUuri;
        }
        
        public boolean equals( Object o ) {
            boolean result = false;
            if (o instanceof Binding) {
                Binding b = (Binding)o;
                result = getName().equals( b.getName() );
            }
            return result;
        }
        
        public String toString() {
            return bName+"->"+bUuri;
        }
    }
    
    /**
     * Represents an binding bindingName -> UURI
     */
    public static class ParentBinding extends Binding {
        
        public ParentBinding(String bindingName, String bindingUuri) {
            super(bindingName, bindingUuri);
        }
        
        public boolean equals( Object o ) {
            boolean result = false;
            if (o instanceof ParentBinding) {
                ParentBinding b = (ParentBinding)o;
                result =
                    getName().equals(b.getName()) &&
                    this.getUuri().equals(b.getUuri());
            }
            return result;
        }
        
        public String toString() {
            return bName+":"+bUuri;
        }
    }
    
    public static class ParentBindingList extends BindingList {
        
        public ParentBindingList() {
            super();
        }
        
        public ParentBindingList(Vector container) {
            super(container);
        }
        
        protected Enumeration elements() {
            return container.elements();
        }
        
        protected Binding put(String bindingName, ObjectNode source) {
            String uuri = "";
            if (source != null && source.getUuri() != null) {
                uuri = source.getUuri();
            }
            int i = container.indexOf( new ParentBinding(bindingName, uuri) );
            ParentBinding result = null;
            if (i >= 0) {
                result = (ParentBinding)container.get(i);
                container.set( i, new ParentBinding(bindingName, uuri) );
            }
            else {
                container.add( new ParentBinding(bindingName, uuri) );
            }
            return result;
        }
        
        protected void remove(String bindingName, String uuri) {
            container.removeElement( new ParentBinding(bindingName, uuri) );
        }
        
        public synchronized Object clone() {
            return new ParentBindingList( (Vector)container.clone() );
        }
        
        public String toString() {
            return String.valueOf(container);
        }
    }
    
    public static class BindingList implements Serializable, Cloneable {
        
        protected Vector container;
        
        public BindingList() {
            this.container = new Vector();
        }
        
        public BindingList(Vector container) {
            this.container = container;
        }
        
        protected Enumeration elements() {
            return container.elements();
        }
        
        public Binding get(String bindingName) {
            Binding b = null;
            int i = container.indexOf( new Binding(bindingName, null) );
            if (i >= 0) {
                b = (Binding)container.get(i);
            }
            return b;
        }
        
        protected Binding put(String bindingName, ObjectNode source) {
            Binding result = null;
            String uuri = "";
            if (source != null && source.getUuri() != null) {
                uuri = source.getUuri();
            }
            int i = container.indexOf( new Binding(bindingName, null) );
            if (i >= 0) {
                result = (Binding)container.get(i);
                container.set( i, new Binding(bindingName, uuri) );
            }
            else {
                container.add( new Binding(bindingName, uuri) );
            }
            return result;
        }
        
        protected void remove(String bindingName) {
            container.removeElement( new Binding(bindingName, null) );
        }
        
        public synchronized Object clone() {
            return new BindingList( (Vector)container.clone() );
        }
        
        public String toString() {
            return String.valueOf(container);
        }
        
        public int size() {
            return container.size();
        }
    }
}

