diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java index 29169363c7..b871d6b4aa 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java @@ -1,663 +1,663 @@ -/** - * 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 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 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 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. - *

- * As a basic example, if the Group is a Collection Administration group, - * the Group Type string returned should be "ADMIN" - *

- * 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. - *

- * If object is SITE, all groups are returned. - *

- * If object is COMMUNITY or COLLECTION, only groups associated with - * those objects are returned (if any). - *

- * 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 list = new ArrayList(); - - //check for admin group - if(community.getAdministrators()!=null) - { - list.add(community.getAdministrators()); - } - - // FINAL CATCH-ALL -> Find any other groups where name begins with "COMMUNITY__" - // (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 list = new ArrayList(); - - //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__" - // (Necessary cause XMLUI allows you to generate a 'COLLECTION__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. - *

- * If object is SITE, all people are returned. - *

- * 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 -o or - * --option 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)"; - } - -} +/** + * 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 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 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 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. + *

+ * As a basic example, if the Group is a Collection Administration group, + * the Group Type string returned should be "ADMIN" + *

+ * 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. + *

+ * If object is SITE, all groups are returned. + *

+ * If object is COMMUNITY or COLLECTION, only groups associated with + * those objects are returned (if any). + *

+ * 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 list = new ArrayList(); + + //check for admin group + if(community.getAdministrators()!=null) + { + list.add(community.getAdministrators()); + } + + // FINAL CATCH-ALL -> Find any other groups where name begins with "COMMUNITY__" + // (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 list = new ArrayList(); + + //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__" + // (Necessary cause XMLUI allows you to generate a 'COLLECTION__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. + *

+ * If object is SITE, all people are returned. + *

+ * 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 -o or + * --option 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)"; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java index 5015433bd0..90e1cc0c37 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java @@ -1,545 +1,545 @@ -/** - * 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.IOException; -import java.io.InputStream; -import java.sql.SQLException; -import java.util.List; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import org.apache.commons.codec.DecoderException; - -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.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.w3c.dom.*; -import org.xml.sax.SAXException; - -/** - * Create EPersons and Groups from a file of external representations. - * - * @author mwood - */ -public class RoleIngester implements PackageIngester -{ - private static final Logger log = LoggerFactory - .getLogger(RoleIngester.class); - - /** - * Common code to ingest roles from a Document. - * - * @param context - * DSpace Context - * @param parent - * the Parent DSpaceObject - * @param document - * the XML Document - * @throws SQLException - * @throws AuthorizeException - * @throws PackageException - */ - static void ingestDocument(Context context, DSpaceObject parent, - PackageParameters params, Document document) - throws SQLException, AuthorizeException, PackageException - { - String myEmail = context.getCurrentUser().getEmail(); - String myNetid = context.getCurrentUser().getNetid(); - - // Ingest users (EPersons) first so Groups can use them - NodeList users = document - .getElementsByTagName(RoleDisseminator.EPERSON); - for (int i = 0; i < users.getLength(); i++) - { - Element user = (Element) users.item(i); - // int userID = Integer.valueOf(user.getAttribute("ID")); // FIXME - // no way to set ID! - NodeList emails = user.getElementsByTagName(RoleDisseminator.EMAIL); - NodeList netids = user.getElementsByTagName(RoleDisseminator.NETID); - EPerson eperson; - EPerson collider; - String email = null; - String netid = null; - String identity; - if (emails.getLength() > 0) - { - email = emails.item(0).getTextContent(); - if (email.equals(myEmail)) - { - continue; // Cannot operate on my own EPerson! - } - identity = email; - collider = EPerson.findByEmail(context, identity); - // collider = EPerson.find(context, userID); - } - else if (netids.getLength() > 0) - { - netid = netids.item(0).getTextContent(); - if (netid.equals(myNetid)) - { - continue; // Cannot operate on my own EPerson! - } - identity = netid; - collider = EPerson.findByNetid(context, identity); - } - else - { - throw new PackageException("EPerson has neither email nor netid."); - } - - if (null != collider) - if (params.replaceModeEnabled()) // -r -f - { - eperson = collider; - } - else if (params.keepExistingModeEnabled()) // -r -k - { - log.warn("Existing EPerson {} was not restored from the package.", identity); - continue; - } - else - { - throw new PackageException("EPerson " + identity + " already exists."); - } - else - { - eperson = EPerson.create(context); - log.info("Created EPerson {}.", identity); - } - - eperson.setEmail(email); - eperson.setNetid(netid); - - NodeList data; - - data = user.getElementsByTagName(RoleDisseminator.FIRST_NAME); - if (data.getLength() > 0) - { - eperson.setFirstName(data.item(0).getTextContent()); - } - else - { - eperson.setFirstName(null); - } - - data = user.getElementsByTagName(RoleDisseminator.LAST_NAME); - if (data.getLength() > 0) - { - eperson.setLastName(data.item(0).getTextContent()); - } - else - { - eperson.setLastName(null); - } - - data = user.getElementsByTagName(RoleDisseminator.LANGUAGE); - if (data.getLength() > 0) - { - eperson.setLanguage(data.item(0).getTextContent()); - } - else - { - eperson.setLanguage(null); - } - - data = user.getElementsByTagName(RoleDisseminator.CAN_LOGIN); - eperson.setCanLogIn(data.getLength() > 0); - - data = user.getElementsByTagName(RoleDisseminator.REQUIRE_CERTIFICATE); - eperson.setRequireCertificate(data.getLength() > 0); - - data = user.getElementsByTagName(RoleDisseminator.SELF_REGISTERED); - eperson.setSelfRegistered(data.getLength() > 0); - - data = user.getElementsByTagName(RoleDisseminator.PASSWORD_HASH); - if (data.getLength() > 0) - { - Node element = data.item(0); - NamedNodeMap attributes = element.getAttributes(); - - Node algorithm = attributes.getNamedItem(RoleDisseminator.PASSWORD_DIGEST); - String algorithmText; - if (null != algorithm) - algorithmText = algorithm.getNodeValue(); - else - algorithmText = null; - - Node salt = attributes.getNamedItem(RoleDisseminator.PASSWORD_SALT); - String saltText; - if (null != salt) - saltText = salt.getNodeValue(); - else - saltText = null; - - PasswordHash password; - try { - password = new PasswordHash(algorithmText, saltText, element.getTextContent()); - } catch (DecoderException ex) { - throw new PackageValidationException("Unable to decode hexadecimal password hash or salt", ex); - } - eperson.setPasswordHash(password); - } - else - { - eperson.setPasswordHash(null); - } - - // Actually write Eperson info to DB - // NOTE: this update() doesn't call a commit(). So, Eperson info - // may still be rolled back if a subsequent error occurs - eperson.update(); - } - - // Now ingest the Groups - NodeList groups = document.getElementsByTagName(RoleDisseminator.GROUP); - - // Create the groups and add their EPerson members - for (int groupx = 0; groupx < groups.getLength(); groupx++) - { - Element group = (Element) groups.item(groupx); - String name = group.getAttribute(RoleDisseminator.NAME); - - try - { - //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) - // TODO: is this necessary? can we leave it in format with Handle in place of ? - // For now, this is necessary, because we don't want to accidentally - // create a new group COLLECTION_hdl:123/34_ADMIN, which is equivalent - // to an existing COLLECTION_45_ADMIN group - name = PackageUtils.translateGroupNameForImport(context, name); - } - catch(PackageException pe) - { - // If an error is thrown, then this Group corresponds to a - // Community or Collection that doesn't currently exist in the - // system. So, log a warning & skip it for now. - log.warn("Skipping group named '" + name + "' as it seems to correspond to a Community or Collection that does not exist in the system. " + - "If you are performing an AIP restore, you can ignore this warning as the Community/Collection AIP will likely create this group once it is processed."); - continue; - } - - Group groupObj = null; // The group to restore - Group collider = Group.findByName(context, name); // Existing group? - if (null != collider) - { // Group already exists, so empty it - if (params.replaceModeEnabled()) // -r -f - { - for (Group member : collider.getMemberGroups()) - { - collider.removeMember(member); - } - for (EPerson member : collider.getMembers()) - { - // Remove all group members *EXCEPT* we don't ever want - // to remove the current user from the list of Administrators - // (otherwise remainder of ingest will fail) - if(!(collider.equals(Group.find(context, 1)) && - member.equals(context.getCurrentUser()))) - { - collider.removeMember(member); - } - } - log.info("Existing Group {} was cleared. Its members will be replaced.", name); - groupObj = collider; - } - else if (params.keepExistingModeEnabled()) // -r -k - { - log.warn("Existing Group {} was not replaced from the package.", - name); - continue; - } - else - { - throw new PackageException("Group " + name + " already exists"); - } - } - else - { // No such group exists -- so, we'll need to create it! - - // First Check if this is a "typed" group (i.e. Community or Collection associated Group) - // If so, we'll create it via the Community or Collection - String type = group.getAttribute(RoleDisseminator.TYPE); - if(type!=null && !type.isEmpty() && parent!=null) - { - //What type of dspace object is this group associated with - if(parent.getType()==Constants.COLLECTION) - { - Collection collection = (Collection) parent; - - // Create this Collection-associated group, based on its group type - if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) - { - groupObj = collection.createAdministrators(); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_SUBMIT)) - { - groupObj = collection.createSubmitters(); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_1)) - { - groupObj = collection.createWorkflowGroup(1); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_2)) - { - groupObj = collection.createWorkflowGroup(2); - } - else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_3)) - { - groupObj = collection.createWorkflowGroup(3); - } - } - else if(parent.getType()==Constants.COMMUNITY) - { - Community community = (Community) parent; - - // Create this Community-associated group, based on its group type - if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) - { - groupObj = community.createAdministrators(); - } - } - //Ignore all other dspace object types - } - - //If group not yet created, create it with the given name - if(groupObj==null) - { - groupObj = Group.create(context); - } - - // Always set the name: parent.createBlop() is guessing - groupObj.setName(name); - - log.info("Created Group {}.", groupObj.getName()); - } - - // Add EPeople to newly created Group - NodeList members = group.getElementsByTagName(RoleDisseminator.MEMBER); - for (int memberx = 0; memberx < members.getLength(); memberx++) - { - Element member = (Element) members.item(memberx); - String memberName = member.getAttribute(RoleDisseminator.NAME); - EPerson memberEPerson = EPerson.findByEmail(context, memberName); - if (null != memberEPerson) - groupObj.addMember(memberEPerson); - else - throw new PackageValidationException("EPerson " + memberName - + " not found, not added to " + name); - } - - // Actually write Group info to DB - // NOTE: this update() doesn't call a commit(). So, Group info - // may still be rolled back if a subsequent error occurs - groupObj.update(); - - } - - // Go back and add Group members, now that all groups exist - for (int groupx = 0; groupx < groups.getLength(); groupx++) - { - Element group = (Element) groups.item(groupx); - String name = group.getAttribute(RoleDisseminator.NAME); - try - { - // Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) - name = PackageUtils.translateGroupNameForImport(context, name); - } - catch(PackageException pe) - { - // If an error is thrown, then this Group corresponds to a - // Community or Collection that doesn't currently exist in the - // system. So,skip it for now. - // (NOTE: We already logged a warning about this group earlier as - // this is the second time we are looping through all groups) - continue; - } - - // Find previously created group - Group groupObj = Group.findByName(context, name); - NodeList members = group - .getElementsByTagName(RoleDisseminator.MEMBER_GROUP); - for (int memberx = 0; memberx < members.getLength(); memberx++) - { - Element member = (Element) members.item(memberx); - String memberName = member.getAttribute(RoleDisseminator.NAME); - //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) - memberName = PackageUtils.translateGroupNameForImport(context, memberName); - // Find previously created group - Group memberGroup = Group.findByName(context, memberName); - groupObj.addMember(memberGroup); - } - // Actually update Group info in DB - // NOTE: Group info may still be rolled back if a subsequent error occurs - groupObj.update(); - } - } - - /** - * Ingest roles from an InputStream. - * - * @param context - * DSpace Context - * @param parent - * the Parent DSpaceObject - * @param stream - * the XML Document InputStream - * @throws PackageException - * @throws SQLException - * @throws AuthorizeException - */ - public static void ingestStream(Context context, DSpaceObject parent, - PackageParameters params, InputStream stream) - throws PackageException, SQLException, AuthorizeException - { - Document document; - - try - { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - dbf.setIgnoringComments(true); - dbf.setCoalescing(true); - DocumentBuilder db = dbf.newDocumentBuilder(); - document = db.parse(stream); - } - catch (ParserConfigurationException e) - { - throw new PackageException(e); - } - catch (SAXException e) - { - throw new PackageException(e); - } - catch (IOException e) - { - throw new PackageException(e); - } - /* - * TODO ? finally { close(stream); } - */ - ingestDocument(context, parent, params, document); - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#ingest(org.dspace.core.Context - * , org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters, java.lang.String) - */ - @Override - public DSpaceObject ingest(Context context, DSpaceObject parent, - File pkgFile, PackageParameters params, String license) - throws PackageException, CrosswalkException, AuthorizeException, - SQLException, IOException - { - Document document; - - try - { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - dbf.setIgnoringComments(true); - dbf.setCoalescing(true); - DocumentBuilder db = dbf.newDocumentBuilder(); - document = db.parse(pkgFile); - } - catch (ParserConfigurationException e) - { - throw new PackageException(e); - } - catch (SAXException e) - { - throw new PackageException(e); - } - ingestDocument(context, parent, params, document); - - /* Does not create a DSpaceObject */ - return null; - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#ingestAll(org.dspace.core - * .Context, org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters, java.lang.String) - */ - @Override - public List ingestAll(Context context, DSpaceObject parent, - File pkgFile, PackageParameters params, String license) - throws PackageException, UnsupportedOperationException, - CrosswalkException, AuthorizeException, SQLException, IOException - { - throw new PackageException( - "ingestAll() is not implemented, as ingest() method already handles ingestion of all roles from an external file."); - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#replace(org.dspace.core.Context - * , org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters) - */ - @Override - public DSpaceObject replace(Context context, DSpaceObject dso, - File pkgFile, PackageParameters params) throws PackageException, - UnsupportedOperationException, CrosswalkException, - AuthorizeException, SQLException, IOException - { - //Just call ingest() -- this will perform a replacement as necessary - return ingest(context, dso, pkgFile, params, null); - } - - /* - * (non-Javadoc) - * - * @see - * org.dspace.content.packager.PackageIngester#replaceAll(org.dspace.core - * .Context, org.dspace.content.DSpaceObject, java.io.File, - * org.dspace.content.packager.PackageParameters) - */ - @Override - public List replaceAll(Context context, DSpaceObject dso, - File pkgFile, PackageParameters params) throws PackageException, - UnsupportedOperationException, CrosswalkException, - AuthorizeException, SQLException, IOException - { - throw new PackageException( - "replaceAll() is not implemented, as replace() method already handles replacement of all roles from an external file."); - } - - /** - * Returns a user help string which should describe the - * additional valid command-line options that this packager - * implementation will accept when using the -o or - * --option flags with the Packager script. - * - * @return a string describing additional command-line options available - * with this packager - */ - @Override - public String getParameterHelp() - { - return "No additional options available."; - } -} +/** + * 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.IOException; +import java.io.InputStream; +import java.sql.SQLException; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.apache.commons.codec.DecoderException; + +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.*; +import org.xml.sax.SAXException; + +/** + * Create EPersons and Groups from a file of external representations. + * + * @author mwood + */ +public class RoleIngester implements PackageIngester +{ + private static final Logger log = LoggerFactory + .getLogger(RoleIngester.class); + + /** + * Common code to ingest roles from a Document. + * + * @param context + * DSpace Context + * @param parent + * the Parent DSpaceObject + * @param document + * the XML Document + * @throws SQLException + * @throws AuthorizeException + * @throws PackageException + */ + static void ingestDocument(Context context, DSpaceObject parent, + PackageParameters params, Document document) + throws SQLException, AuthorizeException, PackageException + { + String myEmail = context.getCurrentUser().getEmail(); + String myNetid = context.getCurrentUser().getNetid(); + + // Ingest users (EPersons) first so Groups can use them + NodeList users = document + .getElementsByTagName(RoleDisseminator.EPERSON); + for (int i = 0; i < users.getLength(); i++) + { + Element user = (Element) users.item(i); + // int userID = Integer.valueOf(user.getAttribute("ID")); // FIXME + // no way to set ID! + NodeList emails = user.getElementsByTagName(RoleDisseminator.EMAIL); + NodeList netids = user.getElementsByTagName(RoleDisseminator.NETID); + EPerson eperson; + EPerson collider; + String email = null; + String netid = null; + String identity; + if (emails.getLength() > 0) + { + email = emails.item(0).getTextContent(); + if (email.equals(myEmail)) + { + continue; // Cannot operate on my own EPerson! + } + identity = email; + collider = EPerson.findByEmail(context, identity); + // collider = EPerson.find(context, userID); + } + else if (netids.getLength() > 0) + { + netid = netids.item(0).getTextContent(); + if (netid.equals(myNetid)) + { + continue; // Cannot operate on my own EPerson! + } + identity = netid; + collider = EPerson.findByNetid(context, identity); + } + else + { + throw new PackageException("EPerson has neither email nor netid."); + } + + if (null != collider) + if (params.replaceModeEnabled()) // -r -f + { + eperson = collider; + } + else if (params.keepExistingModeEnabled()) // -r -k + { + log.warn("Existing EPerson {} was not restored from the package.", identity); + continue; + } + else + { + throw new PackageException("EPerson " + identity + " already exists."); + } + else + { + eperson = EPerson.create(context); + log.info("Created EPerson {}.", identity); + } + + eperson.setEmail(email); + eperson.setNetid(netid); + + NodeList data; + + data = user.getElementsByTagName(RoleDisseminator.FIRST_NAME); + if (data.getLength() > 0) + { + eperson.setFirstName(data.item(0).getTextContent()); + } + else + { + eperson.setFirstName(null); + } + + data = user.getElementsByTagName(RoleDisseminator.LAST_NAME); + if (data.getLength() > 0) + { + eperson.setLastName(data.item(0).getTextContent()); + } + else + { + eperson.setLastName(null); + } + + data = user.getElementsByTagName(RoleDisseminator.LANGUAGE); + if (data.getLength() > 0) + { + eperson.setLanguage(data.item(0).getTextContent()); + } + else + { + eperson.setLanguage(null); + } + + data = user.getElementsByTagName(RoleDisseminator.CAN_LOGIN); + eperson.setCanLogIn(data.getLength() > 0); + + data = user.getElementsByTagName(RoleDisseminator.REQUIRE_CERTIFICATE); + eperson.setRequireCertificate(data.getLength() > 0); + + data = user.getElementsByTagName(RoleDisseminator.SELF_REGISTERED); + eperson.setSelfRegistered(data.getLength() > 0); + + data = user.getElementsByTagName(RoleDisseminator.PASSWORD_HASH); + if (data.getLength() > 0) + { + Node element = data.item(0); + NamedNodeMap attributes = element.getAttributes(); + + Node algorithm = attributes.getNamedItem(RoleDisseminator.PASSWORD_DIGEST); + String algorithmText; + if (null != algorithm) + algorithmText = algorithm.getNodeValue(); + else + algorithmText = null; + + Node salt = attributes.getNamedItem(RoleDisseminator.PASSWORD_SALT); + String saltText; + if (null != salt) + saltText = salt.getNodeValue(); + else + saltText = null; + + PasswordHash password; + try { + password = new PasswordHash(algorithmText, saltText, element.getTextContent()); + } catch (DecoderException ex) { + throw new PackageValidationException("Unable to decode hexadecimal password hash or salt", ex); + } + eperson.setPasswordHash(password); + } + else + { + eperson.setPasswordHash(null); + } + + // Actually write Eperson info to DB + // NOTE: this update() doesn't call a commit(). So, Eperson info + // may still be rolled back if a subsequent error occurs + eperson.update(); + } + + // Now ingest the Groups + NodeList groups = document.getElementsByTagName(RoleDisseminator.GROUP); + + // Create the groups and add their EPerson members + for (int groupx = 0; groupx < groups.getLength(); groupx++) + { + Element group = (Element) groups.item(groupx); + String name = group.getAttribute(RoleDisseminator.NAME); + + try + { + //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) + // TODO: is this necessary? can we leave it in format with Handle in place of ? + // For now, this is necessary, because we don't want to accidentally + // create a new group COLLECTION_hdl:123/34_ADMIN, which is equivalent + // to an existing COLLECTION_45_ADMIN group + name = PackageUtils.translateGroupNameForImport(context, name); + } + catch(PackageException pe) + { + // If an error is thrown, then this Group corresponds to a + // Community or Collection that doesn't currently exist in the + // system. So, log a warning & skip it for now. + log.warn("Skipping group named '" + name + "' as it seems to correspond to a Community or Collection that does not exist in the system. " + + "If you are performing an AIP restore, you can ignore this warning as the Community/Collection AIP will likely create this group once it is processed."); + continue; + } + + Group groupObj = null; // The group to restore + Group collider = Group.findByName(context, name); // Existing group? + if (null != collider) + { // Group already exists, so empty it + if (params.replaceModeEnabled()) // -r -f + { + for (Group member : collider.getMemberGroups()) + { + collider.removeMember(member); + } + for (EPerson member : collider.getMembers()) + { + // Remove all group members *EXCEPT* we don't ever want + // to remove the current user from the list of Administrators + // (otherwise remainder of ingest will fail) + if(!(collider.equals(Group.find(context, 1)) && + member.equals(context.getCurrentUser()))) + { + collider.removeMember(member); + } + } + log.info("Existing Group {} was cleared. Its members will be replaced.", name); + groupObj = collider; + } + else if (params.keepExistingModeEnabled()) // -r -k + { + log.warn("Existing Group {} was not replaced from the package.", + name); + continue; + } + else + { + throw new PackageException("Group " + name + " already exists"); + } + } + else + { // No such group exists -- so, we'll need to create it! + + // First Check if this is a "typed" group (i.e. Community or Collection associated Group) + // If so, we'll create it via the Community or Collection + String type = group.getAttribute(RoleDisseminator.TYPE); + if(type!=null && !type.isEmpty() && parent!=null) + { + //What type of dspace object is this group associated with + if(parent.getType()==Constants.COLLECTION) + { + Collection collection = (Collection) parent; + + // Create this Collection-associated group, based on its group type + if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) + { + groupObj = collection.createAdministrators(); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_SUBMIT)) + { + groupObj = collection.createSubmitters(); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_1)) + { + groupObj = collection.createWorkflowGroup(1); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_2)) + { + groupObj = collection.createWorkflowGroup(2); + } + else if(type.equals(RoleDisseminator.GROUP_TYPE_WORKFLOW_STEP_3)) + { + groupObj = collection.createWorkflowGroup(3); + } + } + else if(parent.getType()==Constants.COMMUNITY) + { + Community community = (Community) parent; + + // Create this Community-associated group, based on its group type + if(type.equals(RoleDisseminator.GROUP_TYPE_ADMIN)) + { + groupObj = community.createAdministrators(); + } + } + //Ignore all other dspace object types + } + + //If group not yet created, create it with the given name + if(groupObj==null) + { + groupObj = Group.create(context); + } + + // Always set the name: parent.createBlop() is guessing + groupObj.setName(name); + + log.info("Created Group {}.", groupObj.getName()); + } + + // Add EPeople to newly created Group + NodeList members = group.getElementsByTagName(RoleDisseminator.MEMBER); + for (int memberx = 0; memberx < members.getLength(); memberx++) + { + Element member = (Element) members.item(memberx); + String memberName = member.getAttribute(RoleDisseminator.NAME); + EPerson memberEPerson = EPerson.findByEmail(context, memberName); + if (null != memberEPerson) + groupObj.addMember(memberEPerson); + else + throw new PackageValidationException("EPerson " + memberName + + " not found, not added to " + name); + } + + // Actually write Group info to DB + // NOTE: this update() doesn't call a commit(). So, Group info + // may still be rolled back if a subsequent error occurs + groupObj.update(); + + } + + // Go back and add Group members, now that all groups exist + for (int groupx = 0; groupx < groups.getLength(); groupx++) + { + Element group = (Element) groups.item(groupx); + String name = group.getAttribute(RoleDisseminator.NAME); + try + { + // Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) + name = PackageUtils.translateGroupNameForImport(context, name); + } + catch(PackageException pe) + { + // If an error is thrown, then this Group corresponds to a + // Community or Collection that doesn't currently exist in the + // system. So,skip it for now. + // (NOTE: We already logged a warning about this group earlier as + // this is the second time we are looping through all groups) + continue; + } + + // Find previously created group + Group groupObj = Group.findByName(context, name); + NodeList members = group + .getElementsByTagName(RoleDisseminator.MEMBER_GROUP); + for (int memberx = 0; memberx < members.getLength(); memberx++) + { + Element member = (Element) members.item(memberx); + String memberName = member.getAttribute(RoleDisseminator.NAME); + //Translate Group name back to internal ID format (e.g. COLLECTION__ADMIN) + memberName = PackageUtils.translateGroupNameForImport(context, memberName); + // Find previously created group + Group memberGroup = Group.findByName(context, memberName); + groupObj.addMember(memberGroup); + } + // Actually update Group info in DB + // NOTE: Group info may still be rolled back if a subsequent error occurs + groupObj.update(); + } + } + + /** + * Ingest roles from an InputStream. + * + * @param context + * DSpace Context + * @param parent + * the Parent DSpaceObject + * @param stream + * the XML Document InputStream + * @throws PackageException + * @throws SQLException + * @throws AuthorizeException + */ + public static void ingestStream(Context context, DSpaceObject parent, + PackageParameters params, InputStream stream) + throws PackageException, SQLException, AuthorizeException + { + Document document; + + try + { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setIgnoringComments(true); + dbf.setCoalescing(true); + DocumentBuilder db = dbf.newDocumentBuilder(); + document = db.parse(stream); + } + catch (ParserConfigurationException e) + { + throw new PackageException(e); + } + catch (SAXException e) + { + throw new PackageException(e); + } + catch (IOException e) + { + throw new PackageException(e); + } + /* + * TODO ? finally { close(stream); } + */ + ingestDocument(context, parent, params, document); + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#ingest(org.dspace.core.Context + * , org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters, java.lang.String) + */ + @Override + public DSpaceObject ingest(Context context, DSpaceObject parent, + File pkgFile, PackageParameters params, String license) + throws PackageException, CrosswalkException, AuthorizeException, + SQLException, IOException + { + Document document; + + try + { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setIgnoringComments(true); + dbf.setCoalescing(true); + DocumentBuilder db = dbf.newDocumentBuilder(); + document = db.parse(pkgFile); + } + catch (ParserConfigurationException e) + { + throw new PackageException(e); + } + catch (SAXException e) + { + throw new PackageException(e); + } + ingestDocument(context, parent, params, document); + + /* Does not create a DSpaceObject */ + return null; + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#ingestAll(org.dspace.core + * .Context, org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters, java.lang.String) + */ + @Override + public List ingestAll(Context context, DSpaceObject parent, + File pkgFile, PackageParameters params, String license) + throws PackageException, UnsupportedOperationException, + CrosswalkException, AuthorizeException, SQLException, IOException + { + throw new PackageException( + "ingestAll() is not implemented, as ingest() method already handles ingestion of all roles from an external file."); + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#replace(org.dspace.core.Context + * , org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters) + */ + @Override + public DSpaceObject replace(Context context, DSpaceObject dso, + File pkgFile, PackageParameters params) throws PackageException, + UnsupportedOperationException, CrosswalkException, + AuthorizeException, SQLException, IOException + { + //Just call ingest() -- this will perform a replacement as necessary + return ingest(context, dso, pkgFile, params, null); + } + + /* + * (non-Javadoc) + * + * @see + * org.dspace.content.packager.PackageIngester#replaceAll(org.dspace.core + * .Context, org.dspace.content.DSpaceObject, java.io.File, + * org.dspace.content.packager.PackageParameters) + */ + @Override + public List replaceAll(Context context, DSpaceObject dso, + File pkgFile, PackageParameters params) throws PackageException, + UnsupportedOperationException, CrosswalkException, + AuthorizeException, SQLException, IOException + { + throw new PackageException( + "replaceAll() is not implemented, as replace() method already handles replacement of all roles from an external file."); + } + + /** + * Returns a user help string which should describe the + * additional valid command-line options that this packager + * implementation will accept when using the -o or + * --option flags with the Packager script. + * + * @return a string describing additional command-line options available + * with this packager + */ + @Override + public String getParameterHelp() + { + return "No additional options available."; + } +}