/* ======================================================================
   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.realm;

import org.bodington.server.*;
import org.bodington.server.realm.*;
import org.bodington.server.resources.*;
import org.bodington.sqldatabase.SqlDatabase;
import org.bodington.database.*;

import java.util.Hashtable;
import java.util.Enumeration;

import org.apache.log4j.Logger;

/**
 * Based on a specification in java.security.acl package.
 * This class represents a set of users.  It will only work with users defined
 * with the class User from the same package.  It is designed to work with very large
 * groups without loading vast amounts of data from storage.  The main use of the group
 * is to test if a specified user is a member or not.  It is rare that a complete
 * list of members is required.  So, membership is actually stored in the User object.
 * When a User is loaded from store a list of the groups the user belongs to is
 * also loaded.  Membership of some special groups is stored as a bit within the User
 * database record but most use a Member record.
 * 
 * @author Jon Maber
 */
public class Group extends org.bodington.server.realm.Principal implements java.security.acl.Group
	{
    
    private static Logger log = Logger.getLogger(Group.class);
    
	private PrimaryKey group_id;
	private PrimaryKey resource_id;
	private Integer special_group;
	private String name;
	private String description;
	
	private Hashtable unsaved_users;
	private Hashtable members;
	boolean members_up_to_date;
    private GroupNameIndexKey nameKey;

	public static Group findGroup( PrimaryKey key )
	    throws BuildingServerException
	    {
	    return (Group)findPersistentObject( key, "org.bodington.server.realm.Group" );
	    }
	
	public static Group findGroup( String where )
	    throws BuildingServerException
	    {
	    return (Group)findPersistentObject( where, "org.bodington.server.realm.Group" );
	    }
    
    public static Group findGroupByName( String name )
        throws BuildingServerException
        {
        return (Group)findPersistentObject(new GroupNameIndexKey(name), "org.bodington.server.realm.Group");
        }
	
	public static Enumeration findGroups( String where )
	    throws BuildingServerException
	    {
	    return findPersistentObjects( where, "org.bodington.server.realm.Group" );
	    }
	
	public static Enumeration findGroups( String where, String order )
	    throws BuildingServerException
	    {
	    return findPersistentObjects( where, order, "org.bodington.server.realm.Group" );
	    }
	

	
	/**
	 * Some intialisation of storage.
	 */
	public Group()
		{
		unsaved_users=null;
		members = null;
		members_up_to_date=false;
        nameKey = new GroupNameIndexKey();
		}
	
	/**
	 * Create a group. Passing all the information required by the database.
	 * @param name The name of the group.
	 * @param description The description of the group.
	 * @param resource The resource this group if associated with.
	 */
	public Group(String name, String description, Resource resource)
	{
	    this();
	    setName(name);
	    setDescription(description);
	    setResourceId(resource.getResourceId());
	}
	
	/**
	 * This is a required method for implementation of Principal
	 * 
	 * @return Returns the name of the group.
	 */

	public String toString()
		{
		String n = getName();
		if ( n==null )
			return "(null)";
		return n;
		}
		

	/**
	 * This is a required method for implementation of PersistentObject.
	 * 
	 * @return The PrimaryKey for this object or null if it hasn't been stored yet.
	 */
		
	// implementation of PersistentObject
	public PrimaryKey getPrimaryKey()
		{
		return getGroupId();
		}

	/**
	 * This is a required method for implementation of PersistentObject.
	 * 
	 * @param key The new primary key for this object.
	 */
	public void setPrimaryKey( PrimaryKey key )
		{
		setGroupId( key );
		}
		
	public PrimaryKey getGroupId()
		{
		return group_id;
		}
	public void setGroupId( PrimaryKey  x )
		{
		group_id=x;
    	setUnsaved();
		}
		
	public PrimaryKey getResourceId()
		{
		return resource_id;
		}
	public void setResourceId( PrimaryKey  x )
		{
		resource_id=x;
    	setUnsaved();
		}
    public Resource getResource()
        throws BuildingServerException
        {
        if ( resource_id==null ) return null;
        return Resource.findResource( resource_id );
        }
        
        
    /**
     * Checks to see if this is a special group.
     * @return True if this is a special group.
     */
    public boolean isSpecialGroup()
    {
        return special_group != null;
    }
    
    /**
     * Checks to see if this is a group local to a resource.
     * Doesn't really belong here (application level thing) but good enough for now.
     * @return True if this group is local to a resource.
     */
    public boolean isLocalGroup()
    {
        return getName().startsWith("localgroup.");
    }
    
    public String getLocalName()
    {
        return getName().substring( name.lastIndexOf('.') +1 );
    }
    
    /**
     * Checks to see if this is the owners group.
     * @return True if this is the owners group.
     */
    public boolean isOwnersGroup()
    {
        return getName().equals("localgroup."+getResourceId()+ ".owners");
    }
    
    /**
     * Checks to see if this is the adhoc group.
     * @return True if this is the adhoc group.
     */
    public boolean isAdhocGroup()
    {
        return getName().equals("localgroup."+getResourceId()+ ".adhoc");
    }
    
    /**
     * Gets the special group value. The special special value is zero which is
     * treated as everyone in the system.
     */
	public Integer getSpecialGroup()
		{
		return special_group;
		}
    /**
     * Sets the special group value.
     * @see #getSpecialGroup()
     */
	public void setSpecialGroup( Integer g )
		{
		special_group=g;
    	setUnsaved();
		}
		
	public String getName()
		{
		return name;
		}
	public void setName( String x )
		{
		name=x;
        nameKey.setName(x);
		if ( description==null )
		    {
            StringBuffer buffer = new StringBuffer( "Temporary description of the group called " );
            buffer.append( getName() );
            buffer.append( "." );
            setDescription( buffer.toString() );
		    }
    	setUnsaved();
		}

	public String getDescription()
		{
		return description;
		}
	public void setDescription( String x )
		{
		description=x;
    	setUnsaved();
		}
        
	/**
	 * Attempt to add a new member.
	 * Check that the current user has permission first.
	 * @param p The Pricipal to add. Should be a User.
	 * @return True if the new member was added.
	 */
	public boolean addMemberChecked( java.security.Principal p )
	{
	    if ( !checkPermission( Permission.MANAGE ) )
	        return false;
	    return addMember( p );
	}
        
	/**
	 * Attempts to add a new member to this group.
	 * Hard work is done in the User class.
	 * @param p The Principal to add. Should be a User.
	 * @return True if the new member was added.
	 */
	public boolean addMember( java.security.Principal p )
		{
		if ( !(p instanceof User) )
			return false;
		User user = (User)p;

    	setUnsaved();
    	members_up_to_date=false;
   	    boolean b = user.addToGroup( this );
   	    if ( b )
   	        {
   	        if ( unsaved_users==null )
   	            unsaved_users=new Hashtable();
   	        unsaved_users.put( user.getPrimaryKey(), user );
   	        }
    	return b;
		}

	/**
	 * The most important method.  Checks membership.
	 * This should always be allowed.
	 * @param p The user to check.
	 * @return If the user is a member returns true.
	 */
	public boolean isMember( java.security.Principal p )
		{
		if ( p instanceof User )
			{
            return ((User)p).isMember( this );
			}
        if (p instanceof Group)
            {
            return equals(p);
            }
        Exception ex = new Exception( "Attempt to check membership on principal other than a user." );
        log.error(ex.getMessage(), ex);        
        return false;
		}
	
	/**
	 * Gets all the members of this group after checking if the current user has permission.
	 * @see #members()
	 */
	public Enumeration membersChecked()
		{
	    if ( !checkPermission( Permission.VIEW ) )
	        return null;
	    return members();
		}
	
		/**
		 * Gets all the members of this group.
		 * @see #whereMembers()
		 * @see #isSpecialGroup()
		 */   
	    public Enumeration members()
	    {
		if ( members_up_to_date )
		    {
    		if ( members==null ) return new Hashtable().elements();
    		return members.elements();
		    }

    	members = new Hashtable();
		Enumeration enumeration;
		try
		    {
		    enumeration = User.findUsers( whereMembers() );

		    if ( enumeration == null )
			    return members.elements();
		    
		    // We don't want to cache the special groups as they are very large.
		    if (isSpecialGroup())
		        return enumeration;
    		
		    Member member;
		    User user;
		    while ( enumeration.hasMoreElements() )
			    {
			    user = (User)enumeration.nextElement();
			    members.put( user.getUserId(), user );
			    }
    		members_up_to_date=true;
		    }
		catch ( BuildingServerException ex )
		    {
		    log.error( ex.getMessage(), ex );	       
		    enumeration=null;
		    members=new Hashtable();
		    }

		
		return members.elements();
		}
	    
	    public int size()
	    {
	        if (members_up_to_date)
	        {
	            return members.size();
	        }
	        else
	        {
	            try 
	            {
	            return PersistentObject.countPersistentObjects( this.whereMembers(), "org.bodington.server.realm.User" );
	            }
	            catch (BuildingServerException bse)
	            {
	                log.warn("Failed to count objects");
	            }
	        }
	        return 0;
	    }
		
	public boolean removeMemberChecked( java.security.Principal p )
		{
	    if ( !checkPermission( Permission.MANAGE ) )
	        return false;
        return removeMember( p );
        }

	public boolean removeMember( java.security.Principal p )
        {
		if ( !(p instanceof User) )
			return false;
		User user = (User)p;
		
    	setUnsaved();
    	members_up_to_date=false;
    	members=null;
   	    boolean b = user.removeFromGroup( this );
   	    if ( b )
   	        {
   	        if ( unsaved_users==null )
   	            unsaved_users=new Hashtable();
   	        unsaved_users.put( user.getPrimaryKey(), user );
   	        }
    	return b;
		}


	public void removeAllMembersChecked() throws BuildingServerException
	{
	    if ( !checkPermission( Permission.MANAGE ) )
	        return;
	    removeAllMembers();
	}
	
	public void removeAllMembers()
	    throws BuildingServerException
		{

        Enumeration users = PersistentObject.findPrimaryKeys( this.whereMembers(), "org.bodington.server.realm.User" );

		PrimaryKey user_id;
        User user;
        while ( users.hasMoreElements() )
            {
            user_id = (PrimaryKey)users.nextElement();
            user = User.findUser( user_id );
            removeMember( user );
            }
		}


	/**
	 * When methods of the group that modify membership are called a references to
	 * Users are kept.  This save method makes sure that modified users are saved
	 * and then dereferenced.
	 * 
	 * @exception org.bodington.server.BuildingServerException Thrown is there is a database error.
	 */
	public void save()
		throws BuildingServerException
		{
		if ( unsaved_users!=null )
			{
			User user;
			for ( Enumeration enumeration=unsaved_users.elements(); enumeration.hasMoreElements(); )
				{
				user = (User)enumeration.nextElement();
				if ( user.isUnsaved() )
					user.save();
				}
    	    unsaved_users.clear();
			}
		super.save();
		}
		
	public boolean checkPermission( java.security.acl.Permission permission )
        {
        try
            {
            Resource r = getResource();
            if ( r==null ) return false;
            return r.checkPermission( permission );
            }
         catch ( Exception ex )
            {
            return false;
            }
        }
        
	/**
	 * Builds a string that can be used as a WHERE clause on a user search.
	 * @return The string to append to your SQL statement. 
	 */
    public String whereMembers()
    	{
    	if ( group_id ==null )
    		return null;
    		
    	StringBuffer where = new StringBuffer();
    	
    	if ( ! isSpecialGroup() )
    		{
    		where.append( "user_id IN (SELECT user_id FROM members WHERE group_id = " );
    		where.append( group_id.toString() );
    		where.append( " )" );
    		}
    	else 
        {
            int s = special_group.intValue();
            if (s < 0 || s >= 128)
            {
            	throw new ArrayIndexOutOfBoundsException();
            }
            // The zero special group is everyone.
            if ( s != 0)
            {
                // Only use 0 - 31 as the mask.
                int mask;
                mask = 1 << (s & 0x1f);
                switch (s & 0x60)
                {
                    case 0x00:
                        where.append("special_groups_a & (");
                        break;
                    case 0x20:
                        where.append("special_groups_b & (");
                        break;
                    case 0x40:
                        where.append("special_groups_c & (");
                        break;
                    case 0x60:
                        where.append("special_groups_d & (");
                }
                where.append(mask);
                where.append(") <> 0 ");
            }
            else
            {
                where.append("TRUE");
            }
        }
   		return where.toString();
    	}
    
    /**
     * Attempt to remove this group and remove all the members.
     * Doesn't check if the caller has permission.
     * @throws BuildingServerException
     */
    public void remove() throws BuildingServerException
    {
        removeAllMembers();
        save();
        delete();
    }
    
    public IndexKey[] getIndexKeys()
    {
        if (nameKey.getName() == null)
            return null;  
        return new IndexKey[]{nameKey};
    }
    
    public boolean matchesKey( IndexKey key )
    {
        return nameKey.equals(key );
    }
    
    }


	
	
