/** * 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.services.factory.DSpaceServicesFactory; 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; import org.apache.commons.lang3.StringUtils; /** * * * @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 = '.'; @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) { String prefix = handleService.getPrefix(); String canonicalPrefix = DSpaceServicesFactory.getInstance().getConfigurationService().getProperty("handle.canonical.prefix"); if (identifier == null) { return false; } // return true if handle has valid starting pattern if (identifier.startsWith(prefix + "/") || identifier.startsWith(canonicalPrefix) || identifier.startsWith("hdl:") || identifier.startsWith("info:hdl") || identifier.matches("^https?://hdl\\.handle\\.net/.*") || identifier.matches("^https?://.+/handle/.*")) { return true; } //Check additional prefixes supported in the config file String[] additionalPrefixes = DSpaceServicesFactory.getInstance().getConfigurationService().getArrayProperty("handle.additional.prefixes"); for(String additionalPrefix: additionalPrefixes) { if (identifier.startsWith(additionalPrefix + "/")) { return true; } } // otherwise, assume invalid handle 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); // do we have to register a versioned handle? 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); } // do we have a version history? if (history != null) { // get the version Version version = null; try { versionHistoryService.getVersion(context, history, item); } catch (SQLException ex) { throw new RuntimeException("Problem with the database connection occurd.", ex); } // did we found a version? if (version != null) { // do the version's number and the handle versionnumber match? if (version.getVersionNumber() != versionNumber) { throw new IdentifierException("Trying to register a handle without matching its item's version number."); } // 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 item 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 handle) throws SQLException, IOException, AuthorizeException { String handleref = getCanonicalForm(handle); // we want to remove the old handle and insert the new. To do so, we // load all identifiers, clear the metadata field, re add all // identifiers which are not from type handle and add the new handle. List<MetadataValue> identifiers = itemService.getMetadata(item, MetadataSchema.DC_SCHEMA, "identifier", "uri", Item.ANY); itemService.clearMetadata(context, item, MetadataSchema.DC_SCHEMA, "identifier", "uri", Item.ANY); for (MetadataValue identifier : identifiers) { if (this.supports(identifier.getValue())) { // ignore handles log.debug("Removing identifier " + identifier.getValue()); continue; } log.debug("Preserving identifier " + identifier.getValue()); itemService.addMetadata(context, item, identifier.getMetadataField(), identifier.getLanguage(), identifier.getValue(), identifier.getAuthority(), identifier.getConfidence()); } // Add handle as identifier.uri DC value. if (StringUtils.isNotBlank(handleref)) { itemService.addMetadata(context, item, MetadataSchema.DC_SCHEMA, "identifier", "uri", null, handleref); } itemService.update(context, item); } }