org.dspace.content.packager.PackageUtils Maven / Gradle / Ivy
Show all versions of dspace-api Show documentation
/**
* 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.ByteArrayInputStream;
import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Bitstream;
import org.dspace.content.BitstreamFormat;
import org.dspace.content.Bundle;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.MetadataFieldName;
import org.dspace.content.MetadataSchemaEnum;
import org.dspace.content.MetadataValue;
import org.dspace.content.WorkspaceItem;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.BitstreamFormatService;
import org.dspace.content.service.BitstreamService;
import org.dspace.content.service.BundleService;
import org.dspace.content.service.CollectionService;
import org.dspace.content.service.CommunityService;
import org.dspace.content.service.DSpaceObjectService;
import org.dspace.content.service.InstallItemService;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.SiteService;
import org.dspace.content.service.WorkspaceItemService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.handle.factory.HandleServiceFactory;
import org.dspace.handle.service.HandleService;
import org.dspace.license.service.CreativeCommonsService;
import org.dspace.workflow.WorkflowException;
import org.dspace.workflow.WorkflowService;
import org.dspace.workflow.factory.WorkflowServiceFactory;
/**
* Container class for code that is useful to many packagers.
*
* @author Larry Stone
*/
public class PackageUtils {
/**
* log4j category
*/
private static final Logger log = LogManager.getLogger(PackageUtils.class);
// Map of metadata elements for Communities and Collections
// Format is alternating key/value in a straight array; use this
// to initialize hash tables that convert to and from.
protected static final String ccMetadataMap[] = {
// getMetadata() -> DC element.term
"name", "dc.title",
"introductory_text", "dc.description",
"short_description", "dc.description.abstract",
"side_bar_text", "dc.description.tableofcontents",
"copyright_text", "dc.rights",
"provenance_description", "dc.provenance",
"license", "dc.rights.license"
};
// HashMaps to convert Community/Collection metadata to/from Dublin Core
// (useful when crosswalking Communities/Collections)
protected static final Map ccMetadataToDC = new HashMap();
protected static final Map ccDCToMetadata = new HashMap();
protected static final BitstreamService bitstreamService = ContentServiceFactory.getInstance()
.getBitstreamService();
protected static final BitstreamFormatService bitstreamFormatService =
ContentServiceFactory.getInstance().getBitstreamFormatService();
protected static final BundleService bundleService = ContentServiceFactory.getInstance().getBundleService();
protected static final CommunityService communityService = ContentServiceFactory.getInstance()
.getCommunityService();
protected static final CollectionService collectionService = ContentServiceFactory.getInstance()
.getCollectionService();
protected static final InstallItemService installItemService = ContentServiceFactory.getInstance()
.getInstallItemService();
protected static final HandleService handleService = HandleServiceFactory.getInstance().getHandleService();
protected static final WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance()
.getWorkspaceItemService();
protected static final SiteService siteService = ContentServiceFactory.getInstance().getSiteService();
protected static final ItemService itemService = ContentServiceFactory.getInstance().getItemService();
static {
for (int i = 0; i < ccMetadataMap.length; i += 2) {
ccMetadataToDC.put(ccMetadataMap[i], ccMetadataMap[i + 1]);
ccDCToMetadata.put(ccMetadataMap[i + 1], ccMetadataMap[i]);
}
}
/**
* Default constructor
*/
private PackageUtils() { }
/**
* Translate a Dublin Core metadata field into a Container's (Community or Collection)
* database column for that metadata entry.
*
* e.g. "dc.title" would translate to the "name" database column
*
* This method is of use when crosswalking Community or Collection metadata for ingest,
* as most ingest Crosswalks tend to deal with translating to DC-based metadata.
*
* @param dcField The dublin core metadata field
* @return The Community or Collection DB column where this metadata info is stored.
*/
public static String dcToContainerMetadata(String dcField) {
return ccDCToMetadata.get(dcField);
}
/**
* Translate a Container's (Community or Collection) database column into
* a valid Dublin Core metadata field. This is the opposite of 'dcToContainerMetadata()'.
*
* e.g. the "name" database column would translate to "dc.title"
*
* This method is of use when crosswalking Community or Collection metadata for dissemination,
* as most dissemination Crosswalks tend to deal with translating from DC-based metadata.
*
* @param databaseField The Community or Collection DB column
* @return The Dublin Core metadata field that this metadata translates to.
*/
public static String containerMetadataToDC(String databaseField) {
return ccMetadataToDC.get(databaseField);
}
/**
* Test that item has adequate metadata.
* Check item for the minimal DC metadata required to ingest a
* new item, and throw a PackageValidationException if test fails.
* Used by all SIP processors as a common sanity test.
*
* @param item - item to test.
* @throws PackageValidationException if validation error
*/
public static void checkItemMetadata(Item item)
throws PackageValidationException {
List t = itemService.getMetadata(item, MetadataSchemaEnum.DC.getName(), "title", null, Item.ANY);
if (t == null || t.isEmpty()) {
throw new PackageValidationException("Item cannot be created without the required \"title\" DC metadata.");
}
}
/**
* Add DSpace Deposit License to an Item.
* Utility function to add the a user-supplied deposit license or
* a default one if none was given; creates new bitstream in the
* "LICENSE" bundle and gives it the special license bitstream format.
*
* @param context - dspace context
* @param license - license string to add, may be null to invoke default.
* @param item - the item.
* @param collection - get the default license from here.
* @throws IOException if IO error
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public static void addDepositLicense(Context context, String license,
Item item, Collection collection)
throws SQLException, IOException, AuthorizeException {
if (license == null) {
license = collectionService.getLicense(collection);
}
InputStream lis = new ByteArrayInputStream(license.getBytes());
Bundle lb;
//If LICENSE bundle is missing, create it
List bundles = itemService.getBundles(item, Constants.LICENSE_BUNDLE_NAME);
if (CollectionUtils.isEmpty(bundles)) {
lb = bundleService.create(context, item, Constants.LICENSE_BUNDLE_NAME);
} else {
lb = bundles.get(0);
}
//Create the License bitstream
Bitstream lbs = bitstreamService.create(context, lb, lis);
lis.close();
BitstreamFormat bf = bitstreamFormatService.findByShortDescription(context, "License");
if (bf == null) {
bf = bitstreamFormatService.guessFormat(context, lbs);
}
lbs.setFormat(context, bf);
lbs.setName(context, Constants.LICENSE_BITSTREAM_NAME);
lbs.setSource(context, Constants.LICENSE_BITSTREAM_NAME);
bitstreamService.update(context, lbs);
}
/**
* Find bitstream by its Name, looking in all bundles.
*
* @param item Item whose bitstreams to search.
* @param name Bitstream's name to match.
* @return first bitstream found or null.
* @throws SQLException if database error
*/
public static Bitstream getBitstreamByName(Item item, String name)
throws SQLException {
return getBitstreamByName(item, name, null);
}
/**
* Find bitstream by its Name, looking in specific named bundle.
*
* @param item - dspace item whose bundles to search.
* @param bsName - name of bitstream to match.
* @param bnName - bundle name to match, or null for all.
* @return the format found or null if none found.
* @throws SQLException if database error
*/
public static Bitstream getBitstreamByName(Item item, String bsName, String bnName)
throws SQLException {
List bundles;
if (bnName == null) {
bundles = item.getBundles();
} else {
bundles = itemService.getBundles(item, bnName);
}
for (Bundle bundle : bundles) {
List bitstreams = bundle.getBitstreams();
for (Bitstream bitstream : bitstreams) {
if (bsName.equals(bitstream.getName())) {
return bitstream;
}
}
}
return null;
}
/**
* Find bitstream by its format, looking in a specific bundle.
* Used to look for particularly-typed Package Manifest bitstreams.
*
* @param context context
* @param item - dspace item whose bundles to search.
* @param bsf - BitstreamFormat object to match.
* @param bnName - bundle name to match, or null for all.
* @return the format found or null if none found.
* @throws SQLException if database error
*/
public static Bitstream getBitstreamByFormat(Context context, Item item,
BitstreamFormat bsf, String bnName)
throws SQLException {
int fid = bsf.getID();
List bundles;
if (bnName == null) {
bundles = item.getBundles();
} else {
bundles = itemService.getBundles(item, bnName);
}
for (Bundle bundle : bundles) {
List bitstreams = bundle.getBitstreams();
for (Bitstream bitstream : bitstreams) {
if (bitstream.getFormat(context).getID() == fid) {
return bitstream;
}
}
}
return null;
}
/**
* Predicate, does this bundle container meta-information. I.e.
* does this bundle contain descriptive metadata or other metadata
* such as license bitstreams? If so we probably don't want to put
* it into the "content" section of a package; hence this predicate.
*
* @param bn -- the bundle
* @return true if this bundle name indicates it is a meta-info bundle.
*/
public static boolean isMetaInfoBundle(Bundle bn) {
return (bn.getName().equals(Constants.LICENSE_BUNDLE_NAME) ||
bn.getName().equals(CreativeCommonsService.CC_BUNDLE_NAME) ||
bn.getName().equals(Constants.METADATA_BUNDLE_NAME));
}
/**
* Stream wrapper that does not allow its wrapped stream to be
* closed. This is needed to work around problem when loading
* bitstreams from ZipInputStream. The Bitstream constructor
* invokes close() on the input stream, which would prematurely end
* the ZipInputStream.
* Example:
*
* ZipEntry ze = zip.getNextEntry();
* Bitstream bs = bundle.createBitstream(new PackageUtils.UnclosableInputStream(zipInput));
*
*/
public static class UnclosableInputStream extends FilterInputStream {
public UnclosableInputStream(InputStream in) {
super(in);
}
/**
* Do nothing, to prevent wrapped stream from being closed prematurely.
*/
@Override
public void close() {
}
}
/**
* Find or create a bitstream format to match the given short
* description.
* Used by packager ingesters to obtain a special bitstream
* format for the manifest (and/or metadata) file.
*
* NOTE: When creating a new format, do NOT set any extensions, since
* we don't want any file with the same extension, which may be something
* generic like ".xml", to accidentally get set to this format.
*
* @param context - the context.
* @param shortDesc - short descriptive name, used to locate existing format.
* @param MIMEType - MIME content-type
* @param desc - long description
* @return BitstreamFormat object that was found or created. Never null.
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public static BitstreamFormat findOrCreateBitstreamFormat(Context context,
String shortDesc, String MIMEType, String desc)
throws SQLException, AuthorizeException {
return findOrCreateBitstreamFormat(context, shortDesc, MIMEType, desc, BitstreamFormat.KNOWN, false);
}
/**
* Find or create a bitstream format to match the given short
* description.
* Used by packager ingesters to obtain a special bitstream
* format for the manifest (and/or metadata) file.
*
* NOTE: When creating a new format, do NOT set any extensions, since
* we don't want any file with the same extension, which may be something
* generic like ".xml", to accidentally get set to this format.
*
* @param context - the context.
* @param shortDesc - short descriptive name, used to locate existing format.
* @param MIMEType - mime content-type
* @param desc - long description
* @param supportLevel support level
* @param internal value for the 'internal' flag of a new format if created.
* @return BitstreamFormat object that was found or created. Never null.
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public static BitstreamFormat findOrCreateBitstreamFormat(Context context,
String shortDesc, String MIMEType, String desc,
int supportLevel, boolean internal)
throws SQLException, AuthorizeException {
BitstreamFormat bsf = bitstreamFormatService.findByShortDescription(context,
shortDesc);
// not found, try to create one
if (bsf == null) {
bsf = bitstreamFormatService.create(context);
bsf.setShortDescription(context, shortDesc);
bsf.setMIMEType(MIMEType);
bsf.setDescription(desc);
bsf.setSupportLevel(supportLevel);
bsf.setInternal(internal);
bitstreamFormatService.update(context, bsf);
}
return bsf;
}
/**
* Utility to find the license bitstream from an item
*
* @param context DSpace context
* @param item the item
* @return the license bitstream or null
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
* @throws IOException if the license bitstream can't be read
*/
public static Bitstream findDepositLicense(Context context, Item item)
throws SQLException, IOException, AuthorizeException {
// get license format ID
int licenseFormatId = -1;
BitstreamFormat bf = bitstreamFormatService.findByShortDescription(context,
"License");
if (bf != null) {
licenseFormatId = bf.getID();
}
List bundles = itemService.getBundles(item, Constants.LICENSE_BUNDLE_NAME);
for (Bundle bundle : bundles) {
// Assume license will be in its own bundle
List bitstreams = bundle.getBitstreams();
for (Bitstream bitstream : bitstreams) {
// The License should have a file format of "License"
if (bitstream.getFormat(context).getID() == licenseFormatId) {
//found a bitstream with format "License" -- return it
return bitstream;
}
}
// If we couldn't find a bitstream with format = "License",
// we will just assume the first bitstream is the deposit license
// (usually a safe assumption as it is in the LICENSE bundle)
if (bitstreams.size() > 0) {
return bitstreams.get(0);
}
}
// Oops! No license!
return null;
}
/*=====================================================
* Utility Methods -- may be useful for subclasses
*====================================================*/
/**
* Create the specified DSpace Object, based on the passed
* in Package Parameters (along with other basic info required
* to create the object)
*
* @param context DSpace Context
* @param parent Parent Object
* @param type Type of new Object
* @param handle Handle of new Object (may be null)
* @param uuid
* @param params Properties-style list of options (interpreted by each packager).
* @return newly created DSpace Object (or null)
* @throws AuthorizeException if authorization error
* @throws SQLException if database error
* @throws IOException if IO error
*/
public static DSpaceObject createDSpaceObject(Context context, DSpaceObject parent, int type, String handle,
UUID uuid, PackageParameters params)
throws AuthorizeException, SQLException, IOException {
DSpaceObject dso = null;
switch (type) {
case Constants.COLLECTION:
Collection collection = collectionService.find(context, uuid);
if (collection != null) {
dso = collectionService.create(context, (Community) parent, handle);
} else {
dso = collectionService.create(context, (Community) parent, handle, uuid);
}
return dso;
case Constants.COMMUNITY:
// top-level community?
if (parent == null || parent.getType() == Constants.SITE) {
Community community = communityService.find(context, uuid);
if (community != null) {
dso = communityService.create(null, context, handle);
} else {
dso = communityService.create(null, context, handle, uuid);
}
} else {
Community community = communityService.find(context, uuid);
if (community != null) {
dso = communityService.createSubcommunity(context, ((Community) parent), handle);
} else {
dso = communityService.createSubcommunity(context, ((Community) parent), handle, uuid);
}
}
return dso;
case Constants.ITEM:
//Initialize a WorkspaceItem
//(Note: Handle is not set until item is finished)
Item item = itemService.find(context, uuid);
if (item != null) {
return item;
}
WorkspaceItem wsi = null;
if (!params.replaceModeEnabled()) {
wsi = workspaceItemService.create(context, (Collection)parent, params.useCollectionTemplate());
} else {
wsi = workspaceItemService.create(context, (Collection)parent,
uuid, params.useCollectionTemplate());
}
// Please note that we are returning an Item which is *NOT* yet in the Archive,
// and doesn't yet have a handle assigned.
// This Item will remain "incomplete" until 'PackageUtils.finishCreateItem()' is called
return wsi.getItem();
case Constants.SITE:
return siteService.findSite(context);
default:
return null;
}
}
/**
* Perform any final tasks on a newly created WorkspaceItem in order to finish
* ingestion of an Item.
*
* This may include starting up a workflow for the new item, restoring it,
* or archiving it (based on params passed in)
*
* @param context DSpace Context
* @param wsi Workspace Item that requires finishing
* @param handle Handle to assign to item (may be null)
* @param params Properties-style list of options (interpreted by each packager).
* @return finished Item
* @throws IOException if IO error
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
* @throws WorkflowException if workflow error
*/
public static Item finishCreateItem(Context context, WorkspaceItem wsi, String handle, PackageParameters params)
throws IOException, SQLException, AuthorizeException, WorkflowException {
// if we are restoring/replacing existing object using the package
if (params.restoreModeEnabled()) {
// Restore & install item immediately
//(i.e. skip over any Collection workflows, as we are essentially restoring item from backup)
installItemService.restoreItem(context, wsi, handle);
//return newly restored item
return wsi.getItem();
} else if (params.workflowEnabled()) {
// if we are treating package as a SIP, and we are told to respect workflows
WorkflowService workflowService = WorkflowServiceFactory.getInstance().getWorkflowService();
// Start an item workflow
// (NOTICE: The specified handle is ignored, as Workflows *always* end in a new handle being assigned)
return workflowService.startWithoutNotify(context, wsi).getItem();
} else {
// default: skip workflow, but otherwise normal submission (i.e. package treated like a SIP)
// Install item immediately with the specified handle
installItemService.installItem(context, wsi, handle);
// return newly installed item
return wsi.getItem();
}
} //end finishCreateItem
/**
* Commit all recent changes to DSpaceObject.
*
* This method is necessary as there is no generic 'update()' on a DSpaceObject
*
* @param context context
* @param dso DSpaceObject to update
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
* @throws IOException if IO error
*/
public static void updateDSpaceObject(Context context, DSpaceObject dso)
throws AuthorizeException, SQLException, IOException {
if (dso != null) {
DSpaceObjectService dsoService = ContentServiceFactory.getInstance()
.getDSpaceObjectService(dso);
dsoService.update(context, dso);
}
}
/**
* Utility method to retrieve the file extension off of a filename.
*
* @param filename Full filename
* @return file extension
*/
public static String getFileExtension(String filename) {
// Extract the file extension off of a filename
String extension = filename;
int lastDot = filename.lastIndexOf('.');
if (lastDot != -1) {
extension = filename.substring(lastDot + 1);
}
return extension;
}
/**
* Returns name of a dissemination information package (DIP), based on the
* DSpace object and a provided fileExtension
*
* Format: [dspace-obj-type]@[handle-with-dashes].[fileExtension]
* OR [dspace-obj-type]@internal-id-[dspace-ID].[fileExtension]
*
* @param dso DSpace Object to create file name for
* @param fileExtension file Extension of output file.
* @return filename of a DIP representing the DSpace Object
*/
public static String getPackageName(DSpaceObject dso, String fileExtension) {
String handle = dso.getHandle();
// if Handle is empty, use internal ID for name
if (handle == null || handle.isEmpty()) {
handle = "internal-id-" + dso.getID();
} else {
// if Handle exists, replace '/' with '-' to meet normal file naming conventions
handle = handle.replace("/", "-");
}
//Get type name
int typeID = dso.getType();
String type = Constants.typeText[typeID];
//check if passed in file extension already starts with "."
if (!fileExtension.startsWith(".")) {
fileExtension = "." + fileExtension;
}
//Here we go, here's our magical file name!
//Format: [email protected]
return type + "@" + handle + fileExtension;
}
/**
* Creates the specified file (along with all parent directories) if it doesn't already
* exist. If the file already exists, nothing happens.
*
* @param file file
* @return boolean true if succeeded, false otherwise
* @throws IOException if IO error
*/
public static boolean createFile(File file)
throws IOException {
boolean success = false;
//Check if file exists
if (!file.exists()) {
//file doesn't exist yet, does its parent directory exist?
File parentFile = file.getCanonicalFile().getParentFile();
//create the parent directory structure
if ((null != parentFile) && !parentFile.exists() && !parentFile.mkdirs()) {
log.error("Unable to create parent directory");
}
//create actual file
success = file.createNewFile();
}
return success;
}
/**
* Remove all bitstreams (files) associated with a DSpace object.
*
* If this object is an Item, it removes all bundles and bitstreams. If this
* object is a Community or Collection, it removes all logo bitstreams.
*
* This method is useful for replace functionality.
*
* @param context context
* @param dso The object to remove all bitstreams from
* @throws IOException if IO error
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public static void removeAllBitstreams(Context context, DSpaceObject dso)
throws SQLException, IOException, AuthorizeException {
//If we are dealing with an Item
if (dso.getType() == Constants.ITEM) {
Item item = (Item) dso;
// Get a reference to all Bundles in Item (which contain the bitstreams)
Iterator bunds = item.getBundles().iterator();
// Remove each bundle -- this will in turn remove all bitstreams associated with this Item.
while (bunds.hasNext()) {
Bundle bundle = bunds.next();
bunds.remove();
itemService.removeBundle(context, item, bundle);
}
} else if (dso.getType() == Constants.COLLECTION) {
Collection collection = (Collection) dso;
//clear out the logo for this collection
collectionService.setLogo(context, collection, null);
} else if (dso.getType() == Constants.COMMUNITY) {
Community community = (Community) dso;
//clear out the logo for this community
communityService.setLogo(context, community, null);
}
}
/**
* Removes all metadata associated with a DSpace object.
*
* This method is useful for replace functionality.
*
* @param context context
* @param dso The object to remove all metadata from
* @throws IOException if IO error
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public static void clearAllMetadata(Context context, DSpaceObject dso)
throws SQLException, IOException, AuthorizeException {
//If we are dealing with an Item
if (dso.getType() == Constants.ITEM) {
Item item = (Item) dso;
//clear all metadata entries
itemService.clearMetadata(context, item, Item.ANY, Item.ANY, Item.ANY, Item.ANY);
} else if (dso.getType() == Constants.COLLECTION) {
//Else if collection, clear its database table values
Collection collection = (Collection) dso;
// Use the MetadataToDC map (defined privately in this class)
// to clear out all the Collection database fields.
for (String dbField : ccMetadataToDC.keySet()) {
try {
String[] elements = MetadataFieldName.parse(dbField);
collectionService.clearMetadata(context, collection,
elements[0], elements[1], elements[2], Item.ANY);
} catch (IllegalArgumentException ie) {
// ignore the error -- just means the field doesn't exist in DB
// Communities & Collections don't include the exact same metadata fields
}
}
} else if (dso.getType() == Constants.COMMUNITY) {
//Else if community, clear its database table values
Community community = (Community) dso;
// Use the MetadataToDC map (defined privately in this class)
// to clear out all the Community database fields.
for (String dbField : ccMetadataToDC.keySet()) {
try {
String[] elements = MetadataFieldName.parse(dbField);
communityService.clearMetadata(context, community,
elements[0], elements[1], elements[2], Item.ANY);
} catch (IllegalArgumentException ie) {
// ignore the error -- just means the field doesn't exist in DB
// Communities & Collections don't include the exact same metadata fields
}
}
}
}
/**
* Lookaside list for translations we've already done, so we don't generate
* multiple names for the same group
*/
protected static final Map orphanGroups = new HashMap();
/**
* When DSpace creates Default Group Names they are of a very specific format,
* for example:
*
* - COMMUNITY_[ID]_ADMIN
* - COLLECTION_[ID]_ADMIN
* - COLLECTION_[ID]_SUBMIT
* - COLLECTION_[ID]_WORKFLOW_STEP_#
*
*
* Although these names work fine within DSpace, the DSpace internal ID
* (represented by [ID] above) becomes meaningless when content is exported
* outside of DSpace. In order to make these Group names meaningful outside
* of DSpace, they must be translated into a different format:
* {@code COMMUNITY_[HANDLE]_ADMIN} (e.g. COMMUNITY_hdl:123456789/10_ADMIN), etc.
*
* This format replaces the internal ID with an external Handle identifier
* (which is expected to be more meaningful even when content is exported
* from DSpace).
*
* This method prepares group names for export by replacing any found
* internal IDs with the appropriate external Handle identifier. If
* the group name doesn't have an embedded internal ID, it is returned
* as is. If the group name contains an embedded internal ID, but the
* corresponding Handle cannot be determined, then it will be translated to
* GROUP_[random]_[objectType]_[groupType] and not re-translated on
* import.
*
* This method may be useful to any Crosswalks/Packagers which deal with
* import/export of DSpace Groups.
*
* Also see the translateGroupNameForImport() method which does the opposite
* of this method.
*
* @param context current DSpace Context
* @param groupName Group's name
* @return the group name, with any internal IDs translated to Handles
* @throws PackageException if package error
*/
public static String translateGroupNameForExport(Context context, String groupName)
throws PackageException {
Pattern defaultGroupNamePattern = Pattern.compile("^([^_]+)_([^_]+)_(.+)$");
// Check if this looks like a default Group name
Matcher matcher = defaultGroupNamePattern.matcher(groupName);
if (!matcher.matches()) {
//if this is not a valid default group name, just return group name as-is (no crosswalking necessary)
return groupName;
}
String objTypeText = matcher.group(1);
String objID = matcher.group(2);
String groupType = matcher.group(3);
int objType = Constants.getTypeID(objTypeText);
if (objID == null || objType == -1) {
return groupName;
}
try {
//We'll translate this internal ID into a Handle
//First, get the object via the Internal ID
DSpaceObject dso = ContentServiceFactory.getInstance().getDSpaceLegacyObjectService(objType)
.findByIdOrLegacyId(context, objID);
if (dso == null) {
// No such object. Change the name to something harmless, but predictable.
// NOTE: this name *must* be predictable. If we generate the same AIP
// twice in a row, we must end up with the same group name each time.
String newName;
if (orphanGroups.containsKey(groupName)) {
newName = orphanGroups.get(groupName);
} else {
newName = "ORPHANED_" + objType + "_GROUP_"
+ objID + "_" + groupType;
orphanGroups.put(groupName, newName);
// A given group should only be translated once, since the
// new name contains unique random elements which would be
// different every time.
}
// Just log a warning -- it's possible this Group was not
// cleaned up when the associated DSpace Object was removed.
// So, we don't want to throw an error and stop all other processing.
log.warn("DSpace Object (ID='" + objID
+ "', type ='" + objType
+ "') no longer exists -- translating " + groupName
+ " to " + newName + ".");
return newName;
}
//Create an updated group name, using the Handle to replace the InternalID
// Format: _hdl:_
return objTypeText + "_" + "hdl:" + dso.getHandle() + "_" + groupType;
} catch (SQLException sqle) {
throw new PackageException(
"Database error while attempting to translate group name ('" + groupName + "') for export.", sqle);
}
}
/**
* This method does the exact opposite of the translateGroupNameForExport()
* method. It prepares group names for import by replacing any found
* external Handle identifiers with the appropriate DSpace Internal
* identifier. As a basic example, it would change a group named
* "COLLECTION_hdl:123456789/10_ADMIN" to a name similar to
* "COLLECTION_11_ADMIN (where '11' is the internal ID of that Collection).
*
* If the group name either doesn't have an embedded handle, then it is
* returned as is. If it has an embedded handle, but the corresponding
* internal ID cannot be determined, then an error is thrown. It is up
* to the calling method whether that error should be displayed to the user
* or if the group should just be skipped (since its associated object
* doesn't currently exist).
*
* This method may be useful to any Crosswalks/Packagers which deal with
* import/export of DSpace Groups.
*
* Also see the translateGroupNameForExport() method which does the opposite
* of this method.
*
* @param context current DSpace Context
* @param groupName Group's name
* @return the group name, with any Handles translated to internal IDs
* @throws PackageException if package error
*/
public static String translateGroupNameForImport(Context context, String groupName)
throws PackageException {
// Check if this looks like a default Group name -- must have at LEAST two underscores surrounded by other
// characters
if (!groupName.matches("^.+_.+_.+$")) {
//if this is not a valid default group name, just return group name as-is (no crosswalking necessary)
return groupName;
}
//Pull apart default group name into its three main parts
// Format: __
// (e.g. COLLECTION_123_ADMIN)
String objType = groupName.substring(0, groupName.indexOf('_'));
String tmpEndString = groupName.substring(groupName.indexOf('_') + 1);
String objID = tmpEndString.substring(0, tmpEndString.indexOf('_'));
String groupType = tmpEndString.substring(tmpEndString.indexOf('_') + 1);
try {
if (objID.startsWith("hdl:")) {
//We'll translate this handle into an internal ID
//Format for Handle => "hdl:/"
// (e.g. "hdl:123456789/10")
//First, get the object via the Handle
DSpaceObject dso = handleService.resolveToObject(context, objID.substring(4));
if (dso == null) {
//throw an error as we cannot accurately rename/recreate this Group without its related DSpace
// Object
throw new PackageException(
"Unable to translate Handle to Internal ID in group named '" + groupName + "' as DSpace " +
"Object (Handle='" + objID + "') does not exist.");
}
//verify our group specified object Type corresponds to this object's type
if (Constants.getTypeID(objType) != dso.getType()) {
throw new PackageValidationException(
"DSpace Object referenced by handle '" + objID + "' does not correspond to the object type " +
"specified by Group named '" + groupName + "'. This Group doesn't seem to correspond to " +
"this DSpace Object!");
}
//Create an updated group name, using the Internal ID to replace the Handle
// Format: __
return objType + "_" + dso.getID() + "_" + groupType;
} else {
// default -- return group name as is
return groupName;
}
} catch (SQLException sqle) {
throw new PackageException(
"Database error while attempting to translate group name ('" + groupName + "') for import.", sqle);
}
}
}