DS-1348: Removing the use of "canonical handles"

The previous VersionedHandleIdentifierProvider introduced "canonical
handles". The idea was to keep the current handle always linked to the
newest version of an item. The downside is that this limits the
versioning to version changes that do not derogate the citation of the
items (e.g. no changes in pagination of any bitstreams). This can be
used as a special feature to manually track metadata changes. In this
case administrators might want to show only the newest version publicly
and hide older versions.

Another way to use a versioning feature is to create new versions of an
item which may even change existing citations. The old versions should
be kept with there former handles so that citing a handle with a page
number still results in the same citation. A newer version should get a
new handle.

This commit introduces a VersionedHandleIdentifierProvider that does not
change the link between a handle and an item. A new version of an item
gets a new handle. This behaviour ensures that every version is always
citable even if a newer version will be release at any time.
This commit is contained in:
Pascal-Nicolas Becker
2015-11-03 18:55:50 +01:00
parent 0e34ed8d9d
commit bff93d6686
2 changed files with 460 additions and 8 deletions

View File

@@ -0,0 +1,445 @@
/**
* 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.identifier;
import org.apache.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.*;
import org.dspace.content.service.ItemService;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.LogManager;
import org.dspace.handle.service.HandleService;
import org.dspace.versioning.*;
import org.dspace.versioning.service.VersionHistoryService;
import org.dspace.versioning.service.VersioningService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
*
* @author Fabio Bolognesi (fabio at atmire dot com)
* @author Mark Diggory (markd at atmire dot com)
* @author Ben Bosman (ben at atmire dot com)
* @author Pascal-Nicolas Becker (dspace at pascal dash becker dot de)
*/
@Component
public class VersionedHandleIdentifierProvider extends IdentifierProvider {
/** log4j category */
private static Logger log = Logger.getLogger(VersionedHandleIdentifierProvider.class);
/** Prefix registered to no one */
static final String EXAMPLE_PREFIX = "123456789";
private static final char DOT = '.';
private String[] supportedPrefixes = new String[]{"info:hdl", "hdl", "http://"};
@Autowired(required = true)
private VersioningService versionService;
@Autowired(required = true)
private VersionHistoryService versionHistoryService;
@Autowired(required = true)
private HandleService handleService;
@Autowired(required = true)
private ItemService itemService;
@Override
public boolean supports(Class<? extends Identifier> identifier)
{
return Handle.class.isAssignableFrom(identifier);
}
@Override
public boolean supports(String identifier)
{
for(String prefix : supportedPrefixes)
{
if(identifier.startsWith(prefix))
{
return true;
}
}
try {
String outOfUrl = retrieveHandleOutOfUrl(identifier);
if(outOfUrl != null)
{
return true;
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
return false;
}
@Override
public String register(Context context, DSpaceObject dso)
{
String id = mint(context, dso);
try
{
if (dso instanceof Item)
{
populateHandleMetadata(context, (Item) dso, id);
}
}catch (Exception e){
log.error(LogManager.getHeader(context, "Error while attempting to create handle", "Item id: " + (dso != null ? dso.getID() : "")), e);
throw new RuntimeException("Error while attempting to create identifier for Item id: " + (dso != null ? dso.getID() : ""));
}
return id;
}
@Override
public void register(Context context, DSpaceObject dso, String identifier)
throws IdentifierException
{
if (dso instanceof Item && identifier != null)
{
Item item = (Item) dso;
// if identifier == 1234.5/100.4 reinstate the version 4 in the
// version table if absent
Matcher versionHandleMatcher = Pattern.compile("^.*/.*\\.(\\d+)$").matcher(identifier);
if(versionHandleMatcher.matches())
{
// parse the version number from the handle
int versionNumber = -1;
try {
versionNumber = Integer.valueOf(versionHandleMatcher.group(1));
} catch (NumberFormatException ex) {
throw new IllegalStateException("Cannot detect the interger value of a digit.", ex);
}
// get history
VersionHistory history = null;
try {
history = versionHistoryService.findByItem(context, item);
} catch (SQLException ex) {
throw new RuntimeException("Unable to create handle '"
+ identifier + "' for "
+ Constants.typeText[dso.getType()] + " " + dso.getID()
+ " in cause of a problem with the database: ", ex);
}
if (history != null)
{
// get version
Version version = null;
try {
versionHistoryService.getVersion(context, history, item);
} catch (SQLException ex) {
throw new RuntimeException("Problem with the database connection occurd.", ex);
}
if (version != null)
{
if (version.getVersionNumber() != versionNumber)
{
throw new IdentifierException("Trying to register a handle without matching its item's version number.");
}
// version numbers matches, just create the handle
try {
handleService.createHandle(context, dso, identifier);
populateHandleMetadata(context, item, identifier);
return;
} catch (AuthorizeException ex) {
throw new IdentifierException("Current user does not "
+ "have the privileges to add the handle "
+ identifier + " to the item's ("
+ dso.getID() + ") metadata.", ex);
} catch (SQLException | IOException ex) {
throw new RuntimeException("Unable to create handle '"
+ identifier + "' for "
+ Constants.typeText[dso.getType()] + " " + dso.getID()
+ ".", ex);
}
}
} else {
try {
// either no VersionHistory or no Version exists.
// Restore item with the appropriate version number.
restoreItAsVersion(context, item, identifier, versionNumber);
} catch (SQLException | IOException ex) {
throw new RuntimeException("Unable to restore a versioned "
+ "handle as there was a problem in creating a "
+ "neccessary item version: ", ex);
} catch (AuthorizeException ex) {
throw new RuntimeException("Unable to restore a versioned "
+ "handle as the current user was not allowed to "
+ "create a neccessary item version: ", ex);
}
return;
}
}
}
try {
// either we have a DSO not of type object or the handle was not a
// versioned (e.g. 123456789/100) one
// just register it.
createNewIdentifier(context, dso, identifier);
if (dso instanceof Item) {
populateHandleMetadata(context, (Item) dso, identifier);
}
} catch (SQLException ex) {
throw new RuntimeException("Unable to create handle '"
+ identifier + "' for "
+ Constants.typeText[dso.getType()] + " " + dso.getID()
+ " in cause of a problem with the database: ", ex);
} catch (AuthorizeException ex) {
throw new IdentifierException("Current user does not "
+ "have the privileges to add the handle "
+ identifier + " to the item's ("
+ dso.getID() + ") metadata.", ex);
} catch (IOException ex) {
throw new RuntimeException("Unable add the handle '"
+ identifier + "' for "
+ Constants.typeText[dso.getType()] + " " + dso.getID()
+ " in the object's metadata.", ex);
}
}
// get VersionHistory by handle
protected VersionHistory getHistory(Context context, String identifier) throws SQLException {
DSpaceObject item = this.resolve(context, identifier);
if(item!=null){
VersionHistory history = versionHistoryService.findByItem(context, (Item) item);
return history;
}
return null;
}
protected void restoreItAsVersion(Context context, Item item, String identifier, int versionNumber)
throws SQLException, AuthorizeException, IOException
{
createNewIdentifier(context, item, identifier);
populateHandleMetadata(context, item, identifier);
VersionHistory vh = versionHistoryService.findByItem(context, item);
if (vh == null)
{
vh = versionHistoryService.create(context);
}
Version version = versionHistoryService.getVersion(context, vh, item);
if (version == null)
{
version = versionService.createNewVersion(context, vh, item, "Restoring from AIP Service", new Date(), versionNumber);
}
versionHistoryService.update(context, vh);
}
@Override
public void reserve(Context context, DSpaceObject dso, String identifier)
{
try{
handleService.createHandle(context, dso, identifier);
}catch(Exception e){
log.error(LogManager.getHeader(context, "Error while attempting to create handle", "Item id: " + dso.getID()), e);
throw new RuntimeException("Error while attempting to create identifier for Item id: " + dso.getID());
}
}
/**
* Creates a new handle in the database.
*
* @param context DSpace context
* @param dso The DSpaceObject to create a handle for
* @return The newly created handle
*/
@Override
public String mint(Context context, DSpaceObject dso)
{
if(dso.getHandle() != null)
{
return dso.getHandle();
}
try{
String handleId = null;
VersionHistory history = null;
if(dso instanceof Item)
{
history = versionHistoryService.findByItem(context, (Item) dso);
}
if(history!=null)
{
handleId = makeIdentifierBasedOnHistory(context, dso, history);
}else{
handleId = createNewIdentifier(context, dso, null);
}
return handleId;
}catch (Exception e){
log.error(LogManager.getHeader(context, "Error while attempting to create handle", "Item id: " + dso.getID()), e);
throw new RuntimeException("Error while attempting to create identifier for Item id: " + dso.getID());
}
}
@Override
public DSpaceObject resolve(Context context, String identifier, String... attributes)
{
// We can do nothing with this, return null
try{
return handleService.resolveToObject(context, identifier);
}catch (Exception e){
log.error(LogManager.getHeader(context, "Error while resolving handle to item", "handle: " + identifier), e);
}
return null;
}
@Override
public String lookup(Context context, DSpaceObject dso) throws IdentifierNotFoundException, IdentifierNotResolvableException {
try
{
return handleService.findHandle(context, dso);
}catch(SQLException sqe){
throw new IdentifierNotResolvableException(sqe.getMessage(),sqe);
}
}
@Override
public void delete(Context context, DSpaceObject dso, String identifier) throws IdentifierException {
delete(context, dso);
}
@Override
public void delete(Context context, DSpaceObject dso) throws IdentifierException {
try{
handleService.unbindHandle(context, dso);
} catch(SQLException sqe) {
throw new RuntimeException(sqe.getMessage(),sqe);
}
}
public static String retrieveHandleOutOfUrl(String url) throws SQLException
{
// We can do nothing with this, return null
if (!url.contains("/")) return null;
String[] splitUrl = url.split("/");
return splitUrl[splitUrl.length - 2] + "/" + splitUrl[splitUrl.length - 1];
}
/**
* Get the configured Handle prefix string, or a default
* @return configured prefix or "123456789"
*/
public static String getPrefix()
{
String prefix = ConfigurationManager.getProperty("handle.prefix");
if (null == prefix)
{
prefix = EXAMPLE_PREFIX; // XXX no good way to exit cleanly
log.error("handle.prefix is not configured; using " + prefix);
}
return prefix;
}
protected static String getCanonicalForm(String handle)
{
// Let the admin define a new prefix, if not then we'll use the
// CNRI default. This allows the admin to use "hdl:" if they want to or
// use a locally branded prefix handle.myuni.edu.
String handlePrefix = ConfigurationManager.getProperty("handle.canonical.prefix");
if (handlePrefix == null || handlePrefix.length() == 0)
{
handlePrefix = "http://hdl.handle.net/";
}
return handlePrefix + handle;
}
protected String createNewIdentifier(Context context, DSpaceObject dso, String handleId) throws SQLException {
if(handleId == null)
{
return handleService.createHandle(context, dso);
}else{
return handleService.createHandle(context, dso, handleId);
}
}
protected String makeIdentifierBasedOnHistory(Context context, DSpaceObject dso, VersionHistory history) throws AuthorizeException, SQLException
{
if (!(dso instanceof Item))
{
throw new IllegalStateException("Cannot create versioned handle for "
+ "objects other then item: Currently versioning supports "
+ "items only.");
}
Item item = (Item)dso;
// The first version will have a handle like 12345/100 to be backward compatible
// to DSpace installation that started without versioning.
// Mint foreach new VERSION an identifier like: 12345/100.versionNumber.
Version version = versionService.getVersion(context, item);
Version firstVersion = versionHistoryService.getFirstVersion(context, history);
String bareHandle = firstVersion.getItem().getHandle();
if(bareHandle.matches(".*/.*\\.\\d+"))
{
bareHandle = bareHandle.substring(0, bareHandle.lastIndexOf(DOT));
}
// add a new Identifier for new item: 12345/100.x
int versionNumber = version.getVersionNumber();
String identifier = bareHandle;
if (versionNumber > 1)
{
identifier = identifier.concat(String.valueOf(DOT)).concat(String.valueOf(versionNumber));
}
// Ensure this handle does not exist already.
if (handleService.resolveToObject(context, identifier) == null)
{
handleService.createHandle(context, dso, identifier);
}
else
{
throw new IllegalStateException("A versioned handle is used for another version already!");
}
return identifier;
}
protected void populateHandleMetadata(Context context, Item item, String handleref)
throws SQLException, IOException, AuthorizeException
{
// Add handle as identifier.uri DC value.
// First check that identifier doesn't already exist.
boolean identifierExists = false;
List<MetadataValue> identifiers = itemService.getMetadata(item, MetadataSchema.DC_SCHEMA, "identifier", "uri", Item.ANY);
for (MetadataValue identifier : identifiers)
{
if (handleref.equals(identifier.getValue()))
{
identifierExists = true;
}
}
if (!identifierExists)
{
itemService.addMetadata(context, item, MetadataSchema.DC_SCHEMA, "identifier", "uri", null, handleref);
}
}
}