/** * 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.core.ConfigurationManager; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.LogManager; import org.dspace.storage.rdbms.DatabaseManager; import org.dspace.storage.rdbms.TableRow; import org.dspace.utils.DSpace; import org.dspace.versioning.*; import org.springframework.stereotype.Component; import java.io.IOException; import java.sql.SQLException; import java.util.Date; /** * * * @author Fabio Bolognesi (fabio at atmire dot com) * @author Mark Diggory (markd at atmire dot com) * @author Ben Bosman (ben at atmire dot com) */ @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://"}; private VersionDAO versionDAO; private VersionHistoryDAO versionHistoryDAO; @Override public boolean supports(Class<? extends Identifier> identifier) { return Handle.class.isAssignableFrom(identifier); } 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; } public String register(Context context, DSpaceObject dso) { try { String id = mint(context, dso); // move canonical to point the latest version if(dso != null && dso.getType() == Constants.ITEM) { Item item = (Item)dso; VersionHistory history = retrieveVersionHistory(context, (Item)dso); if(history!=null) { String canonical = getCanonical(item); // Modify Canonical: 12345/100 will point to the new item TableRow canonicalRecord = findHandleInternal(context, canonical); modifyHandleRecord(context, dso, canonicalRecord, canonical); // in case of first version we have to modify the previous metadata to be xxxx.1 Version version = history.getVersion(item); Version previous = history.getPrevious(version); if (history.isFirstVersion(previous)) { modifyHandleMetadata(previous.getItem(), (canonical + DOT + 1)); } // Check if our previous item hasn't got a handle anymore. // This only occurs when a switch has been made from the standard handle identifier provider // to the versioned one, in this case no "versioned handle" is reserved so we need to create one if(previous != null && getHandleInternal(context, Constants.ITEM, previous.getItemID()) == null){ makeIdentifierBasedOnHistory(context, previous.getItem(), canonical, history); } } populateHandleMetadata(item); } return 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() : "")); } } public void register(Context context, DSpaceObject dso, String identifier) { try { Item item = (Item) dso; // if for this identifier is already present a record in the Handle table and the corresponding item // has an history someone is trying to restore the latest version for the item. When // trying to restore the latest version the identifier in input doesn't have the for 1234/123.latestVersion // it is the canonical 1234/123 VersionHistory itemHistory = getHistory(context, identifier); if(!identifier.matches(".*/.*\\.\\d+") && itemHistory!=null){ int newVersionNumber = itemHistory.getLatestVersion().getVersionNumber()+1; String canonical = identifier; identifier = identifier.concat(".").concat("" + newVersionNumber); restoreItAsVersion(context, dso, identifier, item, canonical, itemHistory); } // if identifier == 1234.5/100.4 reinstate the version 4 in the version table if absent else if(identifier.matches(".*/.*\\.\\d+")) { // if it is a version of an item is needed to put back the record // in the versionitem table String canonical = getCanonical(identifier); DSpaceObject canonicalItem = this.resolve(context, canonical); if(canonicalItem==null){ restoreItAsCanonical(context, dso, identifier, item, canonical); } else{ VersionHistory history = retrieveVersionHistory(context, (Item)canonicalItem); if(history==null){ restoreItAsCanonical(context, dso, identifier, item, canonical); } else { restoreItAsVersion(context, dso, identifier, item, canonical, history); } } } else { //A regular handle createNewIdentifier(context, dso, identifier); if(dso instanceof Item) { populateHandleMetadata(item); } } }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(), e); } } private VersionHistory getHistory(Context context, String identifier) { DSpaceObject item = this.resolve(context, identifier); if(item!=null){ VersionHistory history = retrieveVersionHistory(context, (Item)item); return history; } return null; } private void restoreItAsVersion(Context context, DSpaceObject dso, String identifier, Item item, String canonical, VersionHistory history) throws SQLException, IOException, AuthorizeException { createNewIdentifier(context, dso, identifier); populateHandleMetadata(item); int versionNumber = Integer.parseInt(identifier.substring(identifier.lastIndexOf(".") + 1)); createVersion(context, history, item, "Restoring from AIP Service", new Date(), versionNumber); Version latest = history.getLatestVersion(); // if restoring the lastest version: needed to move the canonical if(latest.getVersionNumber() < versionNumber){ TableRow canonicalRecord = findHandleInternal(context, canonical); modifyHandleRecord(context, dso, canonicalRecord, canonical); } } private void restoreItAsCanonical(Context context, DSpaceObject dso, String identifier, Item item, String canonical) throws SQLException, IOException, AuthorizeException { createNewIdentifier(context, dso, identifier); populateHandleMetadata(item); int versionNumber = Integer.parseInt(identifier.substring(identifier.lastIndexOf(".")+1)); VersionHistory history=versionHistoryDAO.create(context); createVersion(context, history, item, "Restoring from AIP Service", new Date(), versionNumber); TableRow canonicalRecord = findHandleInternal(context, canonical); modifyHandleRecord(context, dso, canonicalRecord, canonical); } public void reserve(Context context, DSpaceObject dso, String identifier) { try{ TableRow handle = DatabaseManager.create(context, "Handle"); modifyHandleRecord(context, dso, handle, 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 */ 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 = retrieveVersionHistory(context, (Item)dso); } if(history!=null) { handleId = makeIdentifierBasedOnHistory(context, dso, handleId, 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()); } } public DSpaceObject resolve(Context context, String identifier, String... attributes) { // We can do nothing with this, return null try{ TableRow dbhandle = findHandleInternal(context, identifier); if (dbhandle == null) { //Check for an url identifier = retrieveHandleOutOfUrl(identifier); if(identifier != null) { dbhandle = findHandleInternal(context, identifier); } if(dbhandle == null) { return null; } } if ((dbhandle.isColumnNull("resource_type_id")) || (dbhandle.isColumnNull("resource_id"))) { throw new IllegalStateException("No associated resource type"); } // What are we looking at here? int handletypeid = dbhandle.getIntColumn("resource_type_id"); int resourceID = dbhandle.getIntColumn("resource_id"); if (handletypeid == Constants.ITEM) { Item item = Item.find(context, resourceID); if (log.isDebugEnabled()) { log.debug("Resolved handle " + identifier + " to item " + ((item == null) ? (-1) : item.getID())); } return item; } else if (handletypeid == Constants.COLLECTION) { Collection collection = Collection.find(context, resourceID); if (log.isDebugEnabled()) { log.debug("Resolved handle " + identifier + " to collection " + ((collection == null) ? (-1) : collection.getID())); } return collection; } else if (handletypeid == Constants.COMMUNITY) { Community community = Community.find(context, resourceID); if (log.isDebugEnabled()) { log.debug("Resolved handle " + identifier + " to community " + ((community == null) ? (-1) : community.getID())); } return community; } }catch (Exception e){ log.error(LogManager.getHeader(context, "Error while resolving handle to item", "handle: " + identifier), e); } // throw new IllegalStateException("Unsupported Handle Type " // + Constants.typeText[handletypeid]); return null; } @Override public String lookup(Context context, DSpaceObject dso) throws IdentifierNotFoundException, IdentifierNotResolvableException { try { TableRow row = getHandleInternal(context, dso.getType(), dso.getID()); if (row == null) { if (dso.getType() == Constants.SITE) { return Site.getSiteHandle(); } else { return null; } } else { return row.getStringColumn("handle"); } }catch(SQLException sqe){ throw new IdentifierNotResolvableException(sqe.getMessage(),sqe); } } @Override public void delete(Context context, DSpaceObject dso, String identifier) throws IdentifierException { delete(context, dso); } public void delete(Context context, DSpaceObject dso) throws IdentifierException { try { if (dso instanceof Item) { Item item = (Item) dso; // If it is the most current version occurs to move the canonical to the previous version VersionHistory history = retrieveVersionHistory(context, item); if(history!=null && history.getLatestVersion().getItem().equals(item) && history.size() > 1) { Item previous = history.getPrevious(history.getLatestVersion()).getItem(); // Modify Canonical: 12345/100 will point to the new item String canonical = getCanonical(previous); TableRow canonicalRecord = findHandleInternal(context, canonical); modifyHandleRecord(context, previous, canonicalRecord, canonical); } } } catch (Exception e) { log.error(LogManager.getHeader(context, "Error while attempting to register doi", "Item id: " + dso.getID()), e); throw new IdentifierException("Error while moving doi identifier", e); } } 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 { TableRow handle=null; if(handleId != null) { handle = findHandleInternal(context, handleId); if(handle!=null && !handle.isColumnNull("resource_id")) { //Check if this handle is already linked up to this specified DSpace Object int resourceID = handle.getIntColumn("resource_id"); int resourceType = handle.getIntColumn("resource_type_id"); if(resourceID==dso.getID() && resourceType ==dso.getType()) { //This handle already links to this DSpace Object -- so, there's nothing else we need to do return handleId; } else { //handle found in DB table & already in use by another existing resource throw new IllegalStateException("Attempted to create a handle which is already in use: " + handleId); } } } else if(handle!=null && !handle.isColumnNull("resource_type_id")) { //If there is a 'resource_type_id' (but 'resource_id' is empty), then the object using // this handle was previously unbound (see unbindHandle() method) -- likely because object was deleted int previousType = handle.getIntColumn("resource_type_id"); //Since we are restoring an object to a pre-existing handle, double check we are restoring the same *type* of object // (e.g. we will not allow an Item to be restored to a handle previously used by a Collection) if(previousType != dso.getType()) { throw new IllegalStateException("Attempted to reuse a handle previously used by a " + Constants.typeText[previousType] + " for a new " + Constants.typeText[dso.getType()]); } } if(handle==null){ handle = DatabaseManager.create(context, "Handle"); } if(handleId==null) handleId = createId(handle.getIntColumn("handle_id")); modifyHandleRecord(context, dso, handle, handleId); return handleId; } protected String makeIdentifierBasedOnHistory(Context context, DSpaceObject dso, String handleId, VersionHistory history) throws AuthorizeException, SQLException { Item item = (Item)dso; // FIRST time a VERSION is created 2 identifiers will be minted and the canonical will be updated to point to the newer URL: // - id.1-->old URL // - id.2-->new URL Version version = history.getVersion(item); Version previous = history.getPrevious(version); String canonical = getCanonical(previous.getItem()); if (history.isFirstVersion(previous)) { // add a new Identifier for previous item: 12345/100.1 String identifierPreviousItem=canonical + DOT + 1; //Make sure that this hasn't happened already if(findHandleInternal(context, identifierPreviousItem) == null) { TableRow handle = DatabaseManager.create(context, "Handle"); modifyHandleRecord(context, previous.getItem(), handle, identifierPreviousItem); } } // add a new Identifier for this item: 12345/100.x String idNew = canonical + DOT + version.getVersionNumber(); //Make sure we don't have an old handle hanging around (if our previous version was deleted in the workspace) TableRow handleRow = findHandleInternal(context, idNew); if(handleRow == null) { handleRow = DatabaseManager.create(context, "Handle"); } modifyHandleRecord(context, dso, handleRow, idNew); return handleId; } protected String modifyHandleRecord(Context context, DSpaceObject dso, TableRow handle, String handleId) throws SQLException { handle.setColumn("handle", handleId); handle.setColumn("resource_type_id", dso.getType()); handle.setColumn("resource_id", dso.getID()); DatabaseManager.update(context, handle); if (log.isDebugEnabled()) { log.debug("Created new handle for " + Constants.typeText[dso.getType()] + " " + handleId); } return handleId; } protected String getCanonical(Item item) { String canonical = item.getHandle(); if( canonical.matches(".*/.*\\.\\d+") && canonical.lastIndexOf(DOT)!=-1) { canonical = canonical.substring(0, canonical.lastIndexOf(DOT)); } return canonical; } protected String getCanonical(String identifier) { String canonical = identifier; if( canonical.matches(".*/.*\\.\\d+") && canonical.lastIndexOf(DOT)!=-1) { canonical = canonical.substring(0, canonical.lastIndexOf(DOT)); } return canonical; } /** * Find the database row corresponding to handle. * * @param context DSpace context * @param handle The handle to resolve * @return The database row corresponding to the handle * @exception java.sql.SQLException If a database error occurs */ protected static TableRow findHandleInternal(Context context, String handle) throws SQLException { if (handle == null) { throw new IllegalArgumentException("Handle is null"); } return DatabaseManager.findByUnique(context, "Handle", "handle", handle); } /** * Return the handle for an Object, or null if the Object has no handle. * * @param context * DSpace context * @param type * The type of object * @param id * The id of object * @return The handle for object, or null if the object has no handle. * @exception java.sql.SQLException * If a database error occurs */ protected static TableRow getHandleInternal(Context context, int type, int id) throws SQLException { String sql = "SELECT * FROM Handle WHERE resource_type_id = ? AND resource_id = ?"; return DatabaseManager.querySingleTable(context, "Handle", sql, type, id); } /** * Create a new handle id. The implementation uses the PK of the RDBMS * Handle table. * * @return A new handle id * @exception java.sql.SQLException * If a database error occurs */ protected static String createId(int id) throws SQLException { String handlePrefix = getPrefix(); return handlePrefix + (handlePrefix.endsWith("/") ? "" : "/") + id; } protected VersionHistory retrieveVersionHistory(Context c, Item item) { VersioningService versioningService = new DSpace().getSingletonService(VersioningService.class); return versioningService.findVersionHistory(c, item.getID()); } protected void populateHandleMetadata(Item item) throws SQLException, IOException, AuthorizeException { String handleref = getCanonicalForm(getCanonical(item)); // Add handle as identifier.uri DC value. // First check that identifier doesn't already exist. boolean identifierExists = false; Metadatum[] identifiers = item.getDC("identifier", "uri", Item.ANY); for (Metadatum identifier : identifiers) { if (handleref.equals(identifier.value)) { identifierExists = true; } } if (!identifierExists) { item.addDC("identifier", "uri", null, handleref); } } protected void modifyHandleMetadata(Item item, String handle) throws SQLException, IOException, AuthorizeException { String handleref = getCanonicalForm(handle); item.clearMetadata("dc", "identifier", "uri", Item.ANY); item.addDC("identifier", "uri", null, handleref); item.update(); } protected VersionImpl createVersion(Context c, VersionHistory vh, Item item, String summary, Date date, int versionNumber) { try { VersionImpl version = versionDAO.create(c); // check if an equals versionNumber is already present in the DB (at this point it should never happen). if(vh!=null && vh.getVersions()!=null){ for(Version v : vh.getVersions()){ if(v.getVersionNumber()==versionNumber){ throw new RuntimeException("A Version for this versionNumber is already present. Impossible complete the operation."); } } } version.setVersionNumber(versionNumber); version.setVersionDate(date); version.setEperson(item.getSubmitter()); version.setItemID(item.getID()); version.setSummary(summary); version.setVersionHistory(vh.getVersionHistoryId()); versionDAO.update(version); return version; } catch (SQLException e) { throw new RuntimeException(e.getMessage(), e); } } protected int getNextVersionNumer(Version latest){ if(latest==null) return 1; return latest.getVersionNumber()+1; } public void setVersionDAO(VersionDAO versionDAO) { this.versionDAO = versionDAO; } public void setVersionHistoryDAO(VersionHistoryDAO versionHistoryDAO) { this.versionHistoryDAO = versionHistoryDAO; } }