/**
* 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 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.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService;
import org.jdom.Namespace;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.*;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* 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";
protected final EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
protected final GroupService groupService = EPersonServiceFactory.getInstance().getGroupService();
/*
* (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 IO error
* 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
*/
protected 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 current Context
* @param object DSpaceObject
* @param stream receives the output. Is not closed by this method.
* @param emitPasswords true if password hashes should be included.
* @throws PackageException if error
*/
protected void writeToStream(Context context, DSpaceObject object, OutputStream stream,
boolean emitPasswords)
throws PackageException
{
try
{
//First, find all Groups/People associated with our current Object
List<Group> groups = findAssociatedGroups(context, object);
List<EPerson> people = findAssociatedPeople(context, object);
//Only continue if we've found Groups or People which we need to disseminate
if((groups!=null && groups.size()>0) ||
(people!=null && people.size()>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
* @param relatedObject
* the DSpaceObject related to this group (if any)
* @param group
* the Group to describe
* @param writer
* the description to this stream
* @throws XMLStreamException if XML error
* @throws PackageException if packaging error
*/
protected 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().size()>0)
{
writer.writeStartElement(MEMBERS);
for (EPerson member : group.getMembers())
{
writer.writeEmptyElement(MEMBER);
writer.writeAttribute(ID, String.valueOf(member.getID()));
if (null != member.getName())
writer.writeAttribute(NAME, member.getName());
}
writer.writeEndElement();
}
//Add Groups as Member Groups (if any belong to this group)
if(group.getMemberGroups().size()>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
*/
protected 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.getWorkflowStep1()))
{
//Check if workflow step 1 group
return GROUP_TYPE_WORKFLOW_STEP_1;
}
else if (group.equals(collection.getWorkflowStep2()))
{
//check if workflow step 2 group
return GROUP_TYPE_WORKFLOW_STEP_2;
}
else if (group.equals(collection.getWorkflowStep3()))
{
//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 writer
* the description to this stream
* @param emitPassword
* do not export the password hash unless true
* @throws XMLStreamException if XML error
*/
protected void writeEPerson(EPerson eperson, XMLStreamWriter writer,
boolean emitPassword) throws XMLStreamException
{
writer.writeStartElement(EPERSON);
writer.writeAttribute(ID, String.valueOf(eperson.getID()));
if (eperson.getEmail()!=null)
{
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 = ePersonService.getPasswordHash(eperson);
if (null != password)
{
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
* @throws SQLException if database error
*/
protected List<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 groupService.findAll(context, null);
}
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)
List<Group> matchingGroups = groupService.search(context, "COMMUNITY\\_" + community.getID() + "\\_");
for(Group g : matchingGroups)
{
if(!list.contains(g))
{
list.add(g);
}
}
if(list.size()>0)
{
return list;
}
}
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.getWorkflowStep1()!=null)
{
list.add(collection.getWorkflowStep1());
}
//check for workflow step 2 group
if(collection.getWorkflowStep2()!=null)
{
list.add(collection.getWorkflowStep2());
}
//check for workflow step 3 group
if(collection.getWorkflowStep3()!=null)
{
list.add(collection.getWorkflowStep3());
}
// 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)
List<Group> matchingGroups = groupService.search(context, "COLLECTION\\_" + collection.getID() + "\\_");
for(Group g : matchingGroups)
{
if(!list.contains(g))
{
list.add(g);
}
}
if(list.size()>0)
{
return list;
}
}
//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
* @throws SQLException if database error
*/
protected List<EPerson> findAssociatedPeople(Context context, DSpaceObject object)
throws SQLException
{
if(object.getType()==Constants.SITE)
{
return ePersonService.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)";
}
}