/* ======================================================================
The Bodington System Software License, Version 1.0
 
Copyright (c) 2001 The University of Leeds.  All rights reserved.
 
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
 
1.  Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
 
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 end-user documentation included with the redistribution, if any,
must include the following acknowledgement:  "This product includes
software developed by the University of Leeds
(http://www.bodington.org/)."  Alternately, this acknowledgement may
appear in the software itself, if and wherever such third-party
acknowledgements normally appear.
 
4.  The names "Bodington", "Nathan Bodington", "Bodington System",
"Bodington Open Source Project", and "The University of Leeds" must not be
used to endorse or promote products derived from this software without
prior written permission. For written permission, please contact
d.gardner@leeds.ac.uk.
 
5.  The name "Bodington" may not appear in the name of products derived
from this software without prior written permission of the University of
Leeds.
 
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO,  TITLE,  THE IMPLIED WARRANTIES
OF QUALITY  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO
EVENT SHALL THE UNIVERSITY OF LEEDS 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.
=========================================================
 
This software was originally created by the University of Leeds and may contain voluntary
contributions from others.  For more information on the Bodington Open Source Project, please
see http://bodington.org/
 
====================================================================== */

package org.bodington.server.resources;

import org.apache.log4j.Logger;



import org.bodington.database.*;
import org.bodington.pool.*;
import org.bodington.server.*;
import org.bodington.server.realm.*;
import java.sql.*;
import java.util.*;

/**
 * Maintains a tree structure for a set of resources.
 * The position of each resource is now defined by the resource id of its parent
 * and by its position in the list of its siblings.  The latter is stored in
 * the column left_index. The column right_index is no longer used.
 * This modification should not require any change in the database schema.
 *
 * @author Jon Maber
 * @author Andrew Booth
 * @version 1.1
 */
public class ResourceTree extends Object
{
    private static Logger log = Logger.getLogger(ResourceTree.class);
    private static ResourceTree the_instance;
    
    
    PrimaryKey root_id;   //, key;
    //Hashtable resources;
    
    int visit;
    
    
    public static synchronized ResourceTree getInstance()
    throws BuildingServerException
    {
        if ( the_instance==null )
        {
            the_instance=new ResourceTree();
            the_instance.loadResources();
        }
        return the_instance;
    }
    
    public Resource findResource( String delimited_name )
    throws BuildingServerException
    {
        if ( root_id == null || delimited_name == null ) return null;
        
        StringTokenizer tok  = new StringTokenizer( delimited_name, "/" );
        String[] names = new String[tok.countTokens()];
        for ( int i=0; tok.hasMoreTokens(); i++ )
            names[i] = tok.nextToken();
        
        return findResource( names );
    }
    
    public Resource findResource( String[] names )
    throws BuildingServerException
    {
        if ( root_id == null || names == null ) return null;
        
        int i;
        boolean found;
        Resource child;
        Resource current = Resource.findResource( root_id );
        String name;
        Enumeration children;
        for ( i=0; i<names.length; i++ )
        {
            name = (String)names[i];
            children = current.findChildren();
            found=false;
            child=null;
            while ( children.hasMoreElements() )
            {
                child = (Resource)children.nextElement();
                if ( name.equals( child.getName() ) )
                {
                    found=true;
                    break;
                }
            }
            if ( !found )
                return null;
            current = child;
        }
        
        return current;
    }
    
    public int countDescendents( PrimaryKey resource_id )
    throws BuildingServerException
    {
       int count=0;
       
        try
        {
            Connection connection = BuildingContext.getContext().getConnection();
            Statement statement = connection.createStatement();
            ResultSet results = statement.executeQuery( "SELECT resource_id FROM resources WHERE parent_resource_id = " + resource_id );
            
            
            while ( results.next() )
            {
                int i = results.getInt(1);
                count++;
                count += countDescendents( new PrimaryKey(i));
            }
            
            return count;
        }
        catch ( ObjectPoolException opex )
        {
            throw new BuildingServerException( opex );
        }
        catch ( SQLException sqlex )
        {
            throw new BuildingServerException( "Database error counting resources "+ sqlex );
        }
    }
    
    
    
  
    /**
     * Loads all the resources and indexes them.
     * No longer uses updateIndices()
     * @exception org.bodington.server.BuildingServerException If data base error.
     */
    public synchronized void loadResources()
    throws BuildingServerException
    {
        //load the root
        Resource root = Resource.findResource( "parent_resource_id IS NULL" );
        if ( root == null )
            root_id = null;
        else
            root_id = root.getResourceId();
    }
    
    
    /**
     * Takes a resource (or whole branch of tree) and moves
     * it to a new place.
     * @param newparent The new location.
     * @param resource The resource to move.
     * @exception org.bodington.server.BuildingServerException If data base error.
     */
    public synchronized void moveResource( Resource newparent, Resource resource )
    throws BuildingServerException
    {
        Connection connection=null;
        int left;
        
        if ( resource==null || resource.getResourceId() == null )
            throw new BuildingServerException( "Error moving resource in table, resource doesn't exist." );
        if ( newparent==null || newparent.getResourceId() == null )
            throw new BuildingServerException( "Error moving resource in table, destination doesn't exist." );
        
        Resource parent = resource.getParent();
        if ( parent == null )
            throw new BuildingServerException( "Error moving resource in table, can't move root resource." );
        left = 0;
        
        if (isInside(newparent, resource))
            throw new BuildingServerException( "Error moving resource in table, the specified destination is inside the resource to be moved." );
        
        try
        {
            connection = BuildingContext.getContext().getConnection();
            Statement statement = connection.createStatement();
            ResultSet results;
            
            results = statement.executeQuery(
            "SELECT max(left_index) FROM resources WHERE parent_resource_id = " +
            newparent.getResourceId() );
            if ( !results.next() )
                throw new BuildingServerException( "Unable to create resource - unable to find current index." );
            left = results.getInt( 1 )+1;
            results.close();
            
            
            connection.setAutoCommit( false );
            
            //change the left_index to negative to avoid clash with any existing value
            statement.executeUpdate(
            "UPDATE resources SET left_index = -left_index WHERE resource_id = "+resource.getResourceId() );
            //change the parent
            statement.executeUpdate(
            "UPDATE resources SET parent_resource_id = "+newparent.getResourceId()+" WHERE resource_id = "+resource.getResourceId() );
            
            //set the left_index to the bottom of the list of resources
            statement.executeUpdate(
            "UPDATE resources SET left_index = "+left+" WHERE resource_id = "+resource.getResourceId() );
            
            statement.close();
            
            // if all that worked update reference to parent in child
            resource.setParentResourceId( newparent.getResourceId() );
            resource.save();
            
            connection.commit();
            connection.setAutoCommit( false );
            
        }
        catch ( ObjectPoolException opex )
        {
            throw new BuildingServerException( opex );
        }
        catch ( SQLException sqlex )
        {
            try
            {
                connection.rollback();
            }
            catch ( SQLException sqlex2 )
            {
            }
            
            throw new BuildingServerException( "Unable to move resource - database error: " + sqlex );
        }
        
        //update linked lists to children
        parent.removeChildResourceId( resource.getResourceId() );
        newparent.addChildResourceId( resource.getResourceId() );
        
        return;
    }
    
    /**
     * Sorts sibling resources.
     * @param parent - the parent of the resources being sorted.
     * @param siblings - vector containing the resources to be sorted (in their final order
     * @exception org.bodington.server.BuildingServerException If data base error.
     */
    public synchronized void sortResources( Resource parent, Vector siblings )
    throws BuildingServerException
    {
        Resource current;
        PrimaryKey key;
        Object thing;
        Connection connection = null;
        int i;
        
        if ( parent==null || parent.getResourceId() == null )
            throw new BuildingServerException( "Error sorting resources, containing resource doesn't exist." );
        
        if ( siblings == null || siblings.size()==0 )
            throw new BuildingServerException( "Error sorting resources, no resources to sort." );
        
        if ( siblings.size() != parent.getChildIds().size() )
            throw new BuildingServerException( "Error sorting resources, not all resources listed." );
        
        
        try
        {
            connection = BuildingContext.getContext().getConnection();
            Statement statement = connection.createStatement();
            ResultSet results;
            
            
            Enumeration enumeration = parent.findChildren();
            while ( enumeration.hasMoreElements() )
            {
        		current = (Resource)enumeration.nextElement();
                if ( !siblings.contains( current.getResourceId() ) )
                    throw new BuildingServerException( "Error sorting resources, sort list omitted a resource." );
            }
            
            
            results = statement.executeQuery(
            "SELECT left_index FROM resources WHERE resource_id = " +
            parent.getResourceId() );
            if ( !results.next() )
                throw new BuildingServerException( "Unable to sort resource - unable to find parent index." );
            results.close();
            
            
            connection.setAutoCommit( false );
            for ( i=0; i< siblings.size(); i++ )
            {
                thing = siblings.elementAt( i );
                if ( !(thing instanceof PrimaryKey) )
                    throw new BuildingServerException( "Invalid paramater to sortResources function" );
                key = (PrimaryKey)thing;
                if ( key == null )
                    throw new BuildingServerException( "Error sorting resources, null resource in list." );
                if ( !parent.getChildIds().contains( key ) )
                    throw new BuildingServerException( "Attempt to sort resource that is not contained in this resource." );
                
                
                statement.executeUpdate(
                "UPDATE resources SET left_index = "+i+" WHERE resource_id = " +
                siblings.elementAt(i) );
            }
            statement.close();
            
            //now change reference to Vector
            parent.setChildIds( siblings );
            
            //commit all the database changes
            connection.commit();
            connection.setAutoCommit( false );
            
        }
        catch ( ObjectPoolException opex )
        {
            throw new BuildingServerException( opex );
        }
        catch ( SQLException sqlex )
        {
            try
            {
                connection.rollback();
            }
            catch ( SQLException sqlex2 )
            {
            }
            
            throw new BuildingServerException( "Unable to sort resources - database error: " + sqlex );
        }
        
        return;
    }
    
    private synchronized void addAclToResource( Resource newresource )
    {
        Acl newacl;
        AclEntry newaclentry;
        try
        {
            User user = (User)BuildingContext.getContext().getUser();
            
            //this constructor does a lot of setting up and saves
            //the resource, the acl and owners group.
            newacl = new Acl( user, newresource );
        }
        catch ( Exception ex )
        {
		    log.error( ex.getMessage(), ex );
            newresource.setAclId( null );
        }
    }
    
    
    
    /**
     * Adds a newly instantiated resource to the tree.
     * The methods will also set up an ACL for the resource
     * too.
     * @param parent The location of the new resource.
     * @param newresource The (unsaved) resource to add in.
     * @exception org.bodington.server.BuildingServerException If data base error.
     */
    public synchronized void addResource( Resource parent, Resource newresource )
    throws BuildingServerException
    {
        try
        {
            Connection connection = BuildingContext.getContext().getConnection();
            Statement statement = connection.createStatement();
            ResultSet results;
            int left;
            
            
            if ( newresource == null )
                throw new BuildingServerException( "Error adding resource to table, null resource can't be added." );
            if ( newresource.getResourceId() != null )
                throw new BuildingServerException( "Error adding resource to table, not a new resource - can only add new resources." );
            if ( newresource.getParentResourceId() != null )
                throw new BuildingServerException( "Error adding resource to table, resource already placed elsewhere." );
            if ( parent!=null && parent.getResourceId() == null )
                throw new BuildingServerException( "Error adding resource to table, unknown/unsaved destination." );
            
            addAclToResource( newresource );
            
            left = 0;
            
            if ( parent==null )
            {
                root_id=newresource.getResourceId();
                newresource.setParentResourceId( null );
                
            }
            else
            {
                results = statement.executeQuery(
                "SELECT max(left_index) FROM resources WHERE parent_resource_id = " +
                parent.getResourceId() );
                
                if ( !results.next() )
                    throw new BuildingServerException( "Unable to create resource - unable to index containing resource." );
                left = results.getInt( 1 );
                
                results.close();     
                
                PreparedStatement preparedStatement = connection
                    .prepareStatement("SELECT resource_id FROM resources WHERE parent_resource_id = ? AND name = ?");
                try
                {
                    preparedStatement.setInt(1, parent.getResourceId()
                        .intValue());
                    preparedStatement.setString(2, newresource.getName());
                    preparedStatement.execute();

                    if (preparedStatement.getResultSet().next())
                    {
                        throw new BuildingServerException(
                            "Unable to create resource - resource with this name already exists");
                    }
                }
                finally
                {
                    preparedStatement.close();
                }
            }
            
            left++;
            
            statement.executeUpdate(
            "UPDATE resources SET left_index = "+left+" WHERE resource_id = " + newresource.getResourceId() );
            statement.close();
            
            if ( parent!=null )
                newresource.setParentResourceId( parent.getResourceId() );
            newresource.save();
            
            
            //if reached here then database work is complete
            //so now put reference to new resource in tables and
            //reference from parent
            
            if ( parent!=null )
            {
                parent.addChildResourceId( newresource.getResourceId() );
            }
        }
        catch ( ObjectPoolException opex )
        {
            throw new BuildingServerException( opex );
        }
        catch ( SQLException sqlex )
        {
			log.error( sqlex.getMessage(), sqlex );
            throw new BuildingServerException( "Unable to add resource - database error: " + sqlex );
        }
        return;
    }
    
    /**
     * Removes resource and all its children from the tree.
     * It must only called when rolling back the addition of
     * a resource because it doesn't attempt to close up the
     * gap in the left and right indices of the resources
     *
     * @param r The resource to remove.
     * @exception org.bodington.server.BuildingServerException
     */
    
    
    public synchronized void removeResource( Resource r )
    throws BuildingServerException
    {
        if ( r.getResourceId() == null )
            return;
        Resource parent = r.getParent();
        if ( parent !=null )
        {
            parent.removeChildResourceId( r.getResourceId() );
        }
    }
    
    
    public Resource findRootResource()
    throws BuildingServerException
    {
        if ( root_id==null ) return null;
        return Resource.findResource( root_id );
    }
    
    /**
     * Uses URLs to check if resource one resource is inside another.
     * @param a The inside Resource.
     * @param b The containing Resource.
     */
    public synchronized boolean isInside( Resource a, Resource b )
    throws BuildingServerException
    {
        
        String nameA = a.getFullName();
        String nameB = b.getFullName();
        
        return nameA.startsWith(nameB);
    }
    
}