/* ======================================================================
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.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");
    }
    
	public Integer getSpecialGroup()
		{
		return special_group;
		}
	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) )
			{
			Exception ex = new Exception( "Attempt to check membership on principal other than a user." );
			log.error(ex.getMessage(), ex);	       
			return false;
			}
		User user = (User)p;
		return user.isMember( this );
		}
	
	/**
	 * 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();
            int mask;
            if (s < 0 || s >= 128)
            {
            	throw new ArrayIndexOutOfBoundsException();
            }
            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 ");
        }
   		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 );
    }
    
    }


	
	