/* ======================================================================
   Parts Copyright 2006 University of Leeds, Oxford University, University of the Highlands and Islands.

   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.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 );
        }
    }
    
}
