/* ======================================================================
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.
 * <p>
 * This class depends on BuildingContext begin correctly setup as it uses a
 * database connection and needs to be able to find the current user for setting
 * the ACL up.
 * <p>
 * The quota checking is done in this class and relies on the synchronized methods
 * to perform the locking over the quota objects.
 *
 * @author Jon Maber
 * @author Andrew Booth
 * @author buckett
 * @version 1.1
 */
public class ResourceTreeImpl implements ResourceTree
{
    private static Logger log = Logger.getLogger(ResourceTreeImpl.class);
    /**
     * The root resource ID for the tree.
     */
    private PrimaryKey root_id;
    
    private Resource recycler;
    
    
    public Resource findResource( String delimited_name )
    throws BuildingServerException
    {
        if ( checkState() || 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 ( checkState() || names == null )
            return null;
        
        Resource child = null;
        Resource current = Resource.findResource( root_id );
        for ( int i=0; i<names.length; i++ )
        {
            child = current.findChild( names[i]);
            if ( child == null)
                return null;
            current = child;
        }
        return current;
    }
    
    public int countDescendents( PrimaryKey resource_id )
    throws BuildingServerException
    {
       int count=0;
       Enumeration results = Resource.findResourceIds("parent_resource_id = " + resource_id, null);

       while ( results.hasMoreElements() )
       {
           count++;
           count += countDescendents( (PrimaryKey)results.nextElement());
       }

       return count;
    }
    
    /**
     * Loads resources needed by the resource tree to function.
     * If you change the root resource while Bodington is running you should 
     * call this method the reset the ResourceTree.
     * @exception BuildingServerException If data base error or if more than one
     * root resource is found.
     */
    public synchronized void loadResources()
    throws BuildingServerException
    {
        Resource root = Resource.findResource( "parent_resource_id IS NULL" );
        if ( root == null )
        {
            root_id = null;
            log.warn("Could not find root resource.");
        }
        else
        {
            root_id = root.getResourceId();
            log.debug("Root resource set to: "+ root_id);
        }
    }
    
    
    /**
     * 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 )
            {
                log.warn("Failed to rollback: "+ sqlex2.getMessage(), sqlex2);
            }
            
            throw new BuildingServerException( "Unable to sort resources - database error: " + sqlex );
        }
        
        return;
    }
    
    private synchronized void addAclToResource( Resource newresource )
    {
        try
        {
            User user = (User)BuildingContext.getContext().getUser();
            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. It gets saved as part
     * of the method.
     * @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;
            QuotaMetadatum quota = null;
            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 );
                log.info("Reset root ID to :"+ root_id);
            }
            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 name already exists: "
                                + newresource.getName());
                    }
                }
                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 ( checkState() ) 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);
    }
    
    private boolean checkState()
    {
        if ( root_id == null)
        {
            log.warn("We don't have a root ID.");
            return true;
        }
        return false;
    }
    
    public boolean isDeleted( Resource resource )
    throws BuildingServerException
    {
        if (recycler == null)
            loadRecycleBuilding();
        if (recycler == null)
            throw new BuildingServerException("Couldn't load the recycle building");           
        return isInside( resource, recycler );
    }
    
    /**
     * Sets a single resource as the recycling building.
     * @throws BuildingServerException if recycling building not found.
     */
    
    private void loadRecycleBuilding()
    throws BuildingServerException
    {
        try
        {
            String property = BuildingContext.getProperty( "buildingservlet.facility.recycler" );
            String httpFacilityNo = new StringTokenizer( property, "," ).nextToken();
            
            Enumeration enumeration = Resource.findResources( "http_facility_no = "
                + httpFacilityNo, "left_index" );
            
            recycler = (Resource)enumeration.nextElement();
            log.info( "Recycling Building set to: " + recycler.getFullName() );
        }
        catch ( Exception e )
        {
            log.error(
            "Unable to locate the recycling building", e );
        }
    }
    
}
