/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.content.packager;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.apache.log4j.Logger;

import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DSpaceObject;
import org.dspace.content.crosswalk.CrosswalkException;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.PasswordHash;

import org.jdom.Namespace;

/**
 * Plugin to export all Group and EPerson objects in XML, perhaps for reloading.
 * 
 * @author Mark Wood
 */
public class RoleDisseminator implements PackageDisseminator
{

    /** log4j category */
    private static final Logger log = Logger.getLogger(RoleDisseminator.class);

    /**
     * DSpace Roles XML Namespace in JDOM form.
     */
    public static final Namespace DSROLES_NS =
        Namespace.getNamespace("dsroles", "http://www.dspace.org/xmlns/dspace/dspace-roles");

    public static final String DSPACE_ROLES = "DSpaceRoles";
    public static final String ID = "ID";
    public static final String GROUPS = "Groups";
    public static final String GROUP = "Group";
    public static final String NAME = "Name";
    public static final String TYPE = "Type";
    public static final String MEMBERS = "Members";
    public static final String MEMBER = "Member";
    public static final String MEMBER_GROUPS = "MemberGroups";
    public static final String MEMBER_GROUP = "MemberGroup";
    public static final String EPERSONS = "People";
    public static final String EPERSON = "Person";
    public static final String EMAIL = "Email";
    public static final String NETID = "Netid";
    public static final String FIRST_NAME = "FirstName";
    public static final String LAST_NAME = "LastName";
    public static final String LANGUAGE = "Language";
    public static final String PASSWORD_HASH = "PasswordHash";
    public static final String PASSWORD_DIGEST = "digest";
    public static final String PASSWORD_SALT = "salt";
    public static final String CAN_LOGIN = "CanLogin";
    public static final String REQUIRE_CERTIFICATE = "RequireCertificate";
    public static final String SELF_REGISTERED = "SelfRegistered";

    // Valid type values for Groups (only used when Group is associated with a Community or Collection)
    public static final String GROUP_TYPE_ADMIN = "ADMIN";
    public static final String GROUP_TYPE_SUBMIT = "SUBMIT";
    public static final String GROUP_TYPE_WORKFLOW_STEP_1 = "WORKFLOW_STEP_1";
    public static final String GROUP_TYPE_WORKFLOW_STEP_2 = "WORKFLOW_STEP_2";
    public static final String GROUP_TYPE_WORKFLOW_STEP_3 = "WORKFLOW_STEP_3";

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.dspace.content.packager.PackageDisseminator#disseminate(org.dspace
     * .core.Context, org.dspace.content.DSpaceObject,
     * org.dspace.content.packager.PackageParameters, java.io.File)
     */
    @Override
    public void disseminate(Context context, DSpaceObject object,
                     PackageParameters params, File pkgFile)
        throws PackageException, CrosswalkException,
               AuthorizeException, SQLException, IOException
    {
        boolean emitPasswords = params.containsKey("passwords");

        FileOutputStream fileOut = null;
        try
        {
            //open file stream for writing
            fileOut = new FileOutputStream(pkgFile);
            writeToStream(context, object, fileOut, emitPasswords);
        }
        finally
        {
            //close file stream & save
            if (fileOut != null)
            {
                fileOut.close();
            }
        }
    }

    /**
     * Make serialized users and groups available on an InputStream, for code
     * which wants to read one.
     * 
     * @param emitPasswords true if password hashes should be included.
     * @return the stream of XML representing users and groups.
     * @throws IOException
     *             if a PipedOutputStream or PipedInputStream cannot be created.
     */
    InputStream asStream(Context context, DSpaceObject object, boolean emitPasswords)
            throws IOException
    {
        // Create a PipedOutputStream to which to write some XML
        PipedOutputStream outStream = new PipedOutputStream();
        PipedInputStream inStream = new PipedInputStream(outStream);

        // Create a new Thread to push serialized objects into the pipe
        Serializer serializer = new Serializer(context, object, outStream,
                emitPasswords);
        new Thread(serializer).start();

        return inStream;
    }

    /**
     * Embody a thread for serializing users and groups.
     * 
     * @author mwood
     */
    private class Serializer implements Runnable
    {
        private Context context;
        private DSpaceObject object;
        private OutputStream stream;
        private boolean emitPasswords;
        
        @SuppressWarnings("unused")
        private Serializer() {}

        /**
         * @param context
         * @param object the DSpaceObject
         * @param stream receives serialized user and group objects.  Will be
         *          closed when serialization is complete.
         * @param emitPasswords true if password hashes should be included.
         */
        Serializer(Context context, DSpaceObject object, OutputStream stream, boolean emitPasswords)
        {
            this.context = context;
            this.object = object;
            this.stream = stream;
            this.emitPasswords = emitPasswords;
        }

        @Override
        public void run()
        {
            try
            {
                writeToStream(context, object, stream, emitPasswords);
                stream.close();
            }
            catch (IOException e)
            {
                log.error(e);
            }
            catch (PackageException e)
            {
                log.error(e);
            }
        }
    }

    /**
     * Serialize users and groups to a stream.
     * 
     * @param context
     * @param stream receives the output.  Is not closed by this method.
     * @param emitPasswords true if password hashes should be included.
     * @throws XMLStreamException
     * @throws SQLException
     */
    private void writeToStream(Context context, DSpaceObject object, OutputStream stream,
            boolean emitPasswords)
    throws PackageException
    {
        try
        {
            //First, find all Groups/People associated with our current Object
            Group[] groups = findAssociatedGroups(context, object);
            EPerson[] people = findAssociatedPeople(context, object);

            //Only continue if we've found Groups or People which we need to disseminate
            if((groups!=null && groups.length>0) ||
               (people!=null && people.length>0))
            {
                XMLOutputFactory factory = XMLOutputFactory.newInstance();
                XMLStreamWriter writer;

                writer = factory.createXMLStreamWriter(stream, "UTF-8");
                writer.setDefaultNamespace(DSROLES_NS.getURI());
                writer.writeStartDocument("UTF-8", "1.0");
                writer.writeStartElement(DSPACE_ROLES);

                //Only disseminate a <Groups> element if some groups exist
                if(groups!=null)
                {
                    writer.writeStartElement(GROUPS);

                    for (Group group : groups)
                    {
                        writeGroup(context, object, group, writer);
                    }

                    writer.writeEndElement(); // GROUPS
                }

                //Only disseminate an <People> element if some people exist
                if(people!=null)
                {
                    writer.writeStartElement(EPERSONS);

                    for (EPerson eperson : people)
                    {
                        writeEPerson(eperson, writer, emitPasswords);
                    }

                    writer.writeEndElement(); // EPERSONS
                }

                writer.writeEndElement(); // DSPACE_ROLES
                writer.writeEndDocument();
                writer.close();
            }//end if Groups or People exist
        }
        catch (Exception e)
        {
            throw new PackageException(e);
        }
    }

    /* (non-Javadoc)
     * 
     * @see
     * org.dspace.content.packager.PackageDisseminator#disseminateAll(org.dspace
     * .core.Context, org.dspace.content.DSpaceObject,
     * org.dspace.content.packager.PackageParameters, java.io.File)
     */
    @Override
     public List<File> disseminateAll(Context context, DSpaceObject dso,
                     PackageParameters params, File pkgFile)
        throws PackageException, CrosswalkException,
               AuthorizeException, SQLException, IOException
    {
        throw new PackageException("disseminateAll() is not implemented, as disseminate() method already handles dissemination of all roles to an external file.");
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.dspace.content.packager.PackageDisseminator#getMIMEType(org.dspace
     * .content.packager.PackageParameters)
     */
    @Override
    public String getMIMEType(PackageParameters params)
    {
        return "application/xml";
    }

    /**
     * Emit XML describing a single Group.
     *
     * @param context
     *            the DSpace Context
     * @parm relatedObject
     *            the DSpaceObject related to this group (if any)
     * @param group
     *            the Group to describe
     * @param write
     *            the description to this stream
     */
    private void writeGroup(Context context, DSpaceObject relatedObject, Group group, XMLStreamWriter writer)
            throws XMLStreamException, PackageException
    {
        //Translate the Group name for export.  This ensures that groups with Internal IDs in their names
        // (e.g. COLLECTION_1_ADMIN) are properly translated using the corresponding Handle or external identifier.
        String exportGroupName = PackageUtils.translateGroupNameForExport(context, group.getName());

        //If translated group name is returned as "null", this means the Group name 
        // had an Internal Collection/Community ID embedded, which could not be
        // translated properly to a Handle.  We will NOT export these groups,
        // as they could cause conflicts or data integrity problems if they are
        // imported into another DSpace system.
        if(exportGroupName==null)
        {
            return;
        }

        writer.writeStartElement(GROUP);
        writer.writeAttribute(ID, String.valueOf(group.getID()));
        writer.writeAttribute(NAME, exportGroupName);

        String groupType = getGroupType(relatedObject, group);
        if(groupType!=null && !groupType.isEmpty())
        {
            writer.writeAttribute(TYPE, groupType);
        }

        //Add People to Group (if any belong to this group)
        if(group.getMembers().length>0)
        {
            writer.writeStartElement(MEMBERS);
            for (EPerson member : group.getMembers())
            {
                writer.writeEmptyElement(MEMBER);
                writer.writeAttribute(ID, String.valueOf(member.getID()));
                writer.writeAttribute(NAME, member.getName());
            }
            writer.writeEndElement();
        }

        //Add Groups as Member Groups (if any belong to this group)
        if(group.getMemberGroups().length>0)
        {
            writer.writeStartElement(MEMBER_GROUPS);
            for (Group member : group.getMemberGroups())
            {
                String exportMemberName = PackageUtils.translateGroupNameForExport(context, member.getName());
                //Only export member group if its name can be properly translated for export.  As noted above,
                // we don't want groups that are *unable* to be accurately translated causing issues on import.
                if(exportMemberName!=null)
                {
                    writer.writeEmptyElement(MEMBER_GROUP);
                    writer.writeAttribute(ID, String.valueOf(member.getID()));
                    writer.writeAttribute(NAME, exportMemberName);
                }
            }
            writer.writeEndElement();
        }

        writer.writeEndElement();
    }

    /**
     * Return a Group Type string (see RoleDisseminator.GROUP_TYPE_* constants)
     * which describes the type of group and its relation to the given object.
     * <P>
     * As a basic example, if the Group is a Collection Administration group,
     * the Group Type string returned should be "ADMIN"
     * <P>
     * If type string cannot be determined, null is returned.
     *
     * @param dso
     *          the related DSpaceObject
     * @param group
     *          the group
     * @return a group type string or null
     */
    private String getGroupType(DSpaceObject dso, Group group)
    {
        if (dso == null || group == null)
        {
            return null;
        }

        if( dso.getType()==Constants.COMMUNITY)
        {
            Community community = (Community) dso;

            //Check if this is the ADMIN group for this community
            if (group.equals(community.getAdministrators()))
            {
                return GROUP_TYPE_ADMIN;
            }
        }
        else if(dso.getType() == Constants.COLLECTION)
        {
            Collection collection = (Collection) dso;

            if (group.equals(collection.getAdministrators()))
            {
                //Check if this is the ADMIN group for this collection
                return GROUP_TYPE_ADMIN;
            }
            else if (group.equals(collection.getSubmitters()))
            {
                //Check if Submitters group
                return GROUP_TYPE_SUBMIT;
            }
            else if (group.equals(collection.getWorkflowGroup(1)))
            {
                //Check if workflow step 1 group
                return GROUP_TYPE_WORKFLOW_STEP_1;
            }
            else if (group.equals(collection.getWorkflowGroup(2)))
            {
                //check if workflow step 2 group
                return GROUP_TYPE_WORKFLOW_STEP_2;
            }
            else if (group.equals(collection.getWorkflowGroup(3)))
            {
                //check if workflow step 3 group
                return GROUP_TYPE_WORKFLOW_STEP_3;
            }
        }

        //by default, return null
        return null;
    }

    /**
     * Emit XML describing a single EPerson.
     * 
     * @param eperson
     *            the EPerson to describe
     * @param write
     *            the description to this stream
     * @param emitPassword
     *            do not export the password hash unless true
     */
    private void writeEPerson(EPerson eperson, XMLStreamWriter writer,
            boolean emitPassword) throws XMLStreamException
    {
        writer.writeStartElement(EPERSON);
        writer.writeAttribute(ID, String.valueOf(eperson.getID()));

        writer.writeStartElement(EMAIL);
        writer.writeCharacters(eperson.getEmail());
        writer.writeEndElement();

        if(eperson.getNetid()!=null)
        {
            writer.writeStartElement(NETID);
            writer.writeCharacters(eperson.getNetid());
            writer.writeEndElement();
        }

        if(eperson.getFirstName()!=null)
        {
            writer.writeStartElement(FIRST_NAME);
            writer.writeCharacters(eperson.getFirstName());
            writer.writeEndElement();
        }

        if(eperson.getLastName()!=null)
        {
            writer.writeStartElement(LAST_NAME);
            writer.writeCharacters(eperson.getLastName());
            writer.writeEndElement();
        }

        if(eperson.getLanguage()!=null)
        {
            writer.writeStartElement(LANGUAGE);
            writer.writeCharacters(eperson.getLanguage());
            writer.writeEndElement();
        }
        
        if (emitPassword)
        {
            PasswordHash password = eperson.getPasswordHash();

            writer.writeStartElement(PASSWORD_HASH);

            String algorithm = password.getAlgorithm();
            if (null != algorithm)
                writer.writeAttribute(PASSWORD_DIGEST, algorithm);

            String salt = password.getSaltString();
            if (null != salt)
                writer.writeAttribute(PASSWORD_SALT, salt);

            writer.writeCharacters(password.getHashString());
            writer.writeEndElement();
        }

        if (eperson.canLogIn())
        {
            writer.writeEmptyElement(CAN_LOGIN);
        }

        if (eperson.getRequireCertificate())
        {
            writer.writeEmptyElement(REQUIRE_CERTIFICATE);
        }

        if (eperson.getSelfRegistered())
        {
            writer.writeEmptyElement(SELF_REGISTERED);
        }

        writer.writeEndElement();
    }

    /**
     * Find all Groups associated with this DSpace Object.
     * <P>
     * If object is SITE, all groups are returned.
     * <P>
     * If object is COMMUNITY or COLLECTION, only groups associated with
     * those objects are returned (if any).
     * <P>
     * For all other objects, null is returned.
     *
     * @param context The DSpace context
     * @param object the DSpace object
     * @return array of all associated groups
     */
    private Group[] findAssociatedGroups(Context context, DSpaceObject object)
            throws SQLException
    {
        if(object.getType()==Constants.SITE)
        {
            // @TODO FIXME -- if there was a way to ONLY export Groups which are NOT
            // associated with a Community or Collection, we should be doing that instead!
            return Group.findAll(context, Group.NAME);
        }
        else if(object.getType()==Constants.COMMUNITY)
        {
            Community community = (Community) object;

            ArrayList<Group> list = new ArrayList<Group>();

            //check for admin group
            if(community.getAdministrators()!=null)
            {
                list.add(community.getAdministrators());
            }

            // FINAL CATCH-ALL -> Find any other groups where name begins with "COMMUNITY_<ID>_"
            // (There should be none, but this code is here just in case)
            Group[] matchingGroups = Group.search(context, "COMMUNITY\\_" + community.getID() + "\\_");
            for(Group g : matchingGroups)
            {
                if(!list.contains(g))
                {
                    list.add(g);
                }
            }

            if(list.size()>0)
            {
                Group[] groupArray = new Group[list.size()];
                groupArray = (Group[]) list.toArray(groupArray);
                return groupArray;
            }
        }
        else if(object.getType()==Constants.COLLECTION)
        {
            Collection collection = (Collection) object;
            
            ArrayList<Group> list = new ArrayList<Group>();
            
            //check for admin group
            if(collection.getAdministrators()!=null)
            {
                list.add(collection.getAdministrators());
            }
            //check for submitters group
            if(collection.getSubmitters()!=null)
            {
                list.add(collection.getSubmitters());
            }
            //check for workflow step 1 group
            if(collection.getWorkflowGroup(1)!=null)
            {
                list.add(collection.getWorkflowGroup(1));
            }
            //check for workflow step 2 group
            if(collection.getWorkflowGroup(2)!=null)
            {
                list.add(collection.getWorkflowGroup(2));
            }
            //check for workflow step 3 group
            if(collection.getWorkflowGroup(3)!=null)
            {
                list.add(collection.getWorkflowGroup(3));
            }

            // FINAL CATCH-ALL -> Find any other groups where name begins with "COLLECTION_<ID>_"
            // (Necessary cause XMLUI allows you to generate a 'COLLECTION_<ID>_DEFAULT_READ' group)
            Group[] matchingGroups = Group.search(context, "COLLECTION\\_" + collection.getID() + "\\_");
            for(Group g : matchingGroups)
            {
                if(!list.contains(g))
                {
                    list.add(g);
                }
            }

            if(list.size()>0)
            {
                Group[] groupArray = new Group[list.size()];
                groupArray = (Group[]) list.toArray(groupArray);
                return groupArray;
            }
        }

        //by default, return nothing
        return null;
    }


    /**
     * Find all EPeople associated with this DSpace Object.
     * <P>
     * If object is SITE, all people are returned.
     * <P>
     * For all other objects, null is returned.
     *
     * @param context The DSpace context
     * @param object the DSpace object
     * @return array of all associated EPerson objects
     */
    private EPerson[] findAssociatedPeople(Context context, DSpaceObject object)
            throws SQLException
    {
        if(object.getType()==Constants.SITE)
        {
            return EPerson.findAll(context, EPerson.EMAIL);
        }

        //by default, return nothing
        return null;
    }

    /**
     * Returns a user help string which should describe the
     * additional valid command-line options that this packager
     * implementation will accept when using the <code>-o</code> or
     * <code>--option</code> flags with the Packager script.
     *
     * @return a string describing additional command-line options available
     * with this packager
     */
    @Override
    public String getParameterHelp()
    {
        return  "* passwords=[boolean]      " +
                   "If true, user password hashes are also exported (so that they can be later restored).  If false, user passwords are not exported. (Default is false)";
    }

}
