diff --git a/dspace-api/src/main/java/org/dspace/identifier/DOI.java b/dspace-api/src/main/java/org/dspace/identifier/DOI.java index 589321dfc8..72a5c46661 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/DOI.java +++ b/dspace-api/src/main/java/org/dspace/identifier/DOI.java @@ -40,6 +40,9 @@ public class DOI @JoinColumn(name = "dspace_object") private DSpaceObject dSpaceObject; + @Column(name = "resource_type_id") + private Integer resourceTypeId; + @Column(name = "status") private Integer status; @@ -67,11 +70,20 @@ public class DOI public DSpaceObject getDSpaceObject() { return dSpaceObject; } - + public void setDSpaceObject(DSpaceObject dSpaceObject) { this.dSpaceObject = dSpaceObject; } - + + public Integer getResourceTypeId() { + return this.resourceTypeId; + } + + public void setResourceTypeId(Integer resourceTypeId) + { + this.resourceTypeId = resourceTypeId; + } + public Integer getStatus() { return status; } diff --git a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java index fb2cb58062..5eccc1abdc 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java +++ b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java @@ -20,6 +20,7 @@ import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.identifier.doi.DOIConnector; import org.dspace.identifier.doi.DOIIdentifierException; @@ -63,6 +64,7 @@ public class DOIIdentifierProvider static final String CFG_PREFIX = "identifier.doi.prefix"; static final String CFG_NAMESPACE_SEPARATOR = "identifier.doi.namespaceseparator"; + static final char SLASH = '/'; // Metadata field name elements // TODO: move these to MetadataSchema or some such? @@ -83,11 +85,11 @@ public class DOIIdentifierProvider @Autowired(required = true) protected DOIService doiService; @Autowired(required = true) - protected ItemService itemService; - @Autowired(required = true) protected ContentServiceFactory contentServiceFactory; + @Autowired(required = true) + protected ItemService itemService; - private DOIIdentifierProvider() { + protected DOIIdentifierProvider() { } /** @@ -165,10 +167,7 @@ public class DOIIdentifierProvider { try { doiService.formatIdentifier(identifier); - } catch (IdentifierException e) { - return false; - } catch (IllegalArgumentException e) - { + } catch (IdentifierException | IllegalArgumentException ex) { return false; } return true; @@ -182,7 +181,7 @@ public class DOIIdentifierProvider String doi = mint(context, dso); // register tries to reserve doi if it's not already. // So we don't have to reserve it here. - this.register(context, dso, doi); + register(context, dso, doi); return doi; } @@ -347,9 +346,7 @@ public class DOIIdentifierProvider throws IdentifierException, IllegalArgumentException, SQLException { String doi = doiService.formatIdentifier(identifier); - DOI doiRow = null; - - doiRow = loadOrCreateDOI(context, dso, doi); + DOI doiRow = loadOrCreateDOI(context, dso, doi); if (DELETED.equals(doiRow.getStatus()) || TO_BE_DELETED.equals(doiRow.getStatus())) @@ -768,68 +765,97 @@ public class DOIIdentifierProvider * * @param context * @param dso The DSpaceObject the DOI should be loaded or created for. - * @param doi A DOI or null if a DOI should be generated. The generated DOI + * @param doiIdentifier A DOI or null if a DOI should be generated. The generated DOI * can be found in the appropriate column for the TableRow. * @return The database row of the object. * @throws SQLException In case of an error using the database. * @throws DOIIdentifierException If {@code doi} is not part of our prefix or * DOI is registered for another object already. */ - protected DOI loadOrCreateDOI(Context context, DSpaceObject dso, String doi) + protected DOI loadOrCreateDOI(Context context, DSpaceObject dso, String doiIdentifier) throws SQLException, DOIIdentifierException { - DOI doiRow = null; - if (null != doi) + DOI doi = null; + if (null != doiIdentifier) { // we expect DOIs to have the DOI-Scheme except inside the doi table: - doi = doi.substring(DOI.SCHEME.length()); + doiIdentifier = doiIdentifier.substring(DOI.SCHEME.length()); // check if DOI is already in Database - doiRow = doiService.findByDoi(context, doi); - if (null != doiRow) + doi = doiService.findByDoi(context, doiIdentifier); + if (null != doi) { - // check if DOI already belongs to dso - if (ObjectUtils.equals(doiRow.getDSpaceObject(), dso)) + if (doi.getDSpaceObject() == null) { - return doiRow; - } - else - { - throw new DOIIdentifierException("Trying to create a DOI " + - "that is already reserved for another object.", - DOIIdentifierException.DOI_ALREADY_EXISTS); + // doi was deleted, check resource type + if (doi.getResourceTypeId() != null + && doi.getResourceTypeId() != dso.getType()) + { + // doi was assigend to another resource type. Don't + // reactivate it + throw new DOIIdentifierException("Cannot reassing " + + "previously deleted DOI " + doiIdentifier + + " as the resource types of the object it was " + + "previously assigned to and the object it " + + "shall be assigned to now divert (was: " + + Constants.typeText[doi.getResourceTypeId()] + + ", trying to assign to " + + Constants.typeText[dso.getType()] + ").", + DOIIdentifierException.DOI_IS_DELETED); + } else { + // reassign doi + // nothing to do here, doi will br reassigned after this + // if-else-if-else-...-block + } + } else { + // doi is assigned to a DSO; is it assigned to our specific dso? + // check if DOI already belongs to dso + if (dso.getID().equals(doi.getDSpaceObject().getID())) + { + return doi; + } + else + { + throw new DOIIdentifierException("Trying to create a DOI " + + "that is already reserved for another object.", + DOIIdentifierException.DOI_ALREADY_EXISTS); + } } } // check prefix - if (!doi.startsWith(this.getPrefix() + "/")) + if (!doiIdentifier.startsWith(this.getPrefix() + "/")) { throw new DOIIdentifierException("Trying to create a DOI " + "that's not part of our Namespace!", DOIIdentifierException.FOREIGN_DOI); } - // prepare new doiRow - doiRow = doiService.create(context); + if (doi == null) + { + // prepare new doiRow + doi = doiService.create(context); + } } else { // We need to generate a new DOI. - doiRow = doiService.create(context); - - doi = this.getPrefix() + "/" + this.getNamespaceSeparator() + - doiRow.getId(); + doi = doiService.create(context); + doiIdentifier = this.getPrefix() + "/" + this.getNamespaceSeparator() + + doi.getId(); } - doiRow.setDoi(doi); - doiRow.setDSpaceObject(dso); - doiRow.setStatus(null); + // prepare new doiRow + doi.setDoi(doiIdentifier); + doi.setDSpaceObject(dso); + doi.setResourceTypeId(dso.getType()); + doi.setStatus(null); try { - doiService.update(context, doiRow); + doiService.update(context, doi); } catch (SQLException e) { throw new RuntimeException("Cannot save DOI to databse for unkown reason."); } - return doiRow; + return doi; } /** @@ -850,7 +876,7 @@ public class DOIIdentifierProvider List metadata = itemService.getMetadata(item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); for (MetadataValue id : metadata) { - if (id.getValue().startsWith(DOI.RESOLVER + "/10.")) { + if (id.getValue().startsWith(DOI.RESOLVER + String.valueOf(SLASH) + PREFIX + String.valueOf(SLASH) + NAMESPACE_SEPARATOR)) { return doiService.DOIFromExternalFormat(id.getValue()); } } @@ -922,12 +948,6 @@ public class DOIIdentifierProvider itemService.clearMetadata(context, item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); itemService.addMetadata(context, item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, remainder); - try { - itemService.update(context, item); - } catch (SQLException e) { - throw e; - } catch (AuthorizeException e) { - throw e; - } + itemService.update(context, item); } } diff --git a/dspace-api/src/main/java/org/dspace/identifier/DOIServiceImpl.java b/dspace-api/src/main/java/org/dspace/identifier/DOIServiceImpl.java index 558aa14b2a..3e69fac11a 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/DOIServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/identifier/DOIServiceImpl.java @@ -115,4 +115,11 @@ public class DOIServiceImpl implements DOIService { public List getDOIsByStatus(Context context, List statuses) throws SQLException{ return doiDAO.findByStatus(context, statuses); } + + @Override + public List getSimilarDOIsNotInState(Context context, String doiPattern, List statuses, boolean dsoIsNotNull) + throws SQLException + { + return doiDAO.findSimilarNotInState(context, doiPattern, statuses, dsoIsNotNull); + } } diff --git a/dspace-api/src/main/java/org/dspace/identifier/VersionedDOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/VersionedDOIIdentifierProvider.java new file mode 100644 index 0000000000..7dc2f81db8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/identifier/VersionedDOIIdentifierProvider.java @@ -0,0 +1,355 @@ +/** + * 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 java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.identifier.doi.DOIConnector; +import org.dspace.identifier.doi.DOIIdentifierException; +import org.dspace.services.ConfigurationService; +import org.dspace.versioning.Version; +import org.dspace.versioning.VersionHistory; +import org.dspace.versioning.service.VersionHistoryService; +import org.dspace.versioning.service.VersioningService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Required; + +/** + * + * @author Marsa Haoua + * @author Pascal-Nicolas Becker (dspace at pascal dash becker dot de) + */ +public class VersionedDOIIdentifierProvider extends DOIIdentifierProvider +{ + /** log4j category */ + private static Logger log = Logger.getLogger(VersionedDOIIdentifierProvider.class); + + private DOIConnector connector; + + static final char DOT = '.'; + private static final String pattern = "\\d+\\" + String.valueOf(DOT) +"\\d+"; + + @Autowired(required = true) + private VersioningService versioningService; + @Autowired(required = true) + private VersionHistoryService versionHistoryService; + + @Override + public String mint(Context context, DSpaceObject dso) + throws IdentifierException + { + if (!(dso instanceof Item)) + { + throw new IdentifierException("Currently only Items are supported for DOIs."); + } + Item item = (Item) dso; + + VersionHistory history = null; + try { + history = versionHistoryService.findByItem(context, item); + } catch (SQLException ex) { + throw new RuntimeException("A problem occured while accessing the database.", ex); + } + + String doi = null; + try + { + doi = getDOIByObject(context, dso); + if (doi != null) + { + return doi; + } + } + catch (SQLException ex) + { + log.error("Error while attemping to retrieve information about a DOI for " + + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + + " with ID " + dso.getID() + "."); + throw new RuntimeException("Error while attempting to retrieve " + + "information about a DOI for " + + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + + " with ID " + dso.getID() + ".", ex); + } + + // check whether we have a DOI in the metadata and if we have to remove it + String metadataDOI = getDOIOutOfObject(dso); + if (metadataDOI != null) + { + // check whether doi and version number matches + String bareDOI = getBareDOI(metadataDOI); + int versionNumber; + try { + versionNumber = versionHistoryService.getVersion(context, history, item).getVersionNumber(); + } catch (SQLException ex) { + throw new RuntimeException(ex); + } + String versionedDOI = bareDOI; + if (versionNumber > 1) + { + versionedDOI = bareDOI + .concat(String.valueOf(DOT)) + .concat(String.valueOf(versionNumber)); + } + if (!metadataDOI.equalsIgnoreCase(versionedDOI)) + { + log.debug("Will remove DOI " + metadataDOI + + " from item metadata, as it should become " + versionedDOI + "."); + // remove old versioned DOIs + try { + removePreviousVersionDOIsOutOfObject(context, item, metadataDOI); + } catch (AuthorizeException ex) { + throw new RuntimeException("Trying to remove an old DOI from a versioned item, but wasn't authorized to.", ex); + } + } else { + log.debug("DOI " + doi + " matches version number " + versionNumber + "."); + // ensure DOI exists in our database as well and return. + // this also checks that the doi is not assigned to another dso already. + try { + loadOrCreateDOI(context, dso, versionedDOI); + } catch (SQLException ex) { + throw new RuntimeException("A problem with the database connection occured.", ex); + } + return versionedDOI; + } + } + + try{ + if(history != null) + { + // versioning is currently supported for items only + // if we have a history, we have a item + doi = makeIdentifierBasedOnHistory(context, dso, history); + } else { + doi = loadOrCreateDOI(context, dso, null).getDoi(); + } + } catch(SQLException ex) { + log.error("SQLException while creating a new DOI: ", ex); + throw new IdentifierException(ex); + } catch (AuthorizeException ex) { + log.error("AuthorizationException while creating a new DOI: ", ex); + throw new IdentifierException(ex); + } + return doi; + } + + @Override + public void register(Context context, DSpaceObject dso, String identifier) + throws IdentifierException + { + if (!(dso instanceof Item)) + { + throw new IdentifierException("Currently only Items are supported for DOIs."); + } + Item item = (Item) dso; + + if (StringUtils.isEmpty(identifier)) + { + identifier = mint(context, dso); + } + String doiIdentifier = doiService.formatIdentifier(identifier); + + DOI doi = null; + + // search DOI in our db + try + { + doi = loadOrCreateDOI(context, dso, doiIdentifier); + } catch (SQLException ex) { + log.error("Error in databse connection: " + ex.getMessage()); + throw new RuntimeException("Error in database conncetion.", ex); + } + + if (DELETED.equals(doi.getStatus()) || + TO_BE_DELETED.equals(doi.getStatus())) + { + throw new DOIIdentifierException("You tried to register a DOI that " + + "is marked as DELETED.", DOIIdentifierException.DOI_IS_DELETED); + } + + // Check status of DOI + if (IS_REGISTERED.equals(doi.getStatus())) + { + return; + } + + String metadataDOI = getDOIOutOfObject(dso); + if (!StringUtils.isEmpty(metadataDOI) + && !metadataDOI.equalsIgnoreCase(doiIdentifier)) + { + // remove doi of older version from the metadata + try { + removePreviousVersionDOIsOutOfObject(context, item, metadataDOI); + } catch (AuthorizeException ex) { + throw new RuntimeException("Trying to remove an old DOI from a versioned item, but wasn't authorized to.", ex); + } + } + + // change status of DOI + doi.setStatus(TO_BE_REGISTERED); + try { + doiService.update(context, doi); + } + catch (SQLException ex) + { + log.warn("SQLException while changing status of DOI {} to be registered.", ex); + throw new RuntimeException(ex); + } + } + + protected String getBareDOI(String identifier) + throws DOIIdentifierException + { + doiService.formatIdentifier(identifier); + String doiPrefix = DOI.SCHEME.concat(getPrefix()) + .concat(String.valueOf(SLASH)) + .concat(getNamespaceSeparator()); + String doiPostfix = identifier.substring(doiPrefix.length()); + if (doiPostfix.matches(pattern) && doiPostfix.lastIndexOf(DOT) != -1) + { + return doiPrefix.concat(doiPostfix.substring(0, doiPostfix.lastIndexOf(DOT))); + } + // if the pattern does not match, we are already working on a bare handle. + return identifier; + } + + private String getDOIPostfix(String identifier) + throws DOIIdentifierException{ + + String doiPrefix = DOI.SCHEME.concat(getPrefix()).concat(String.valueOf(SLASH)).concat(getNamespaceSeparator()); + String doiPostfix = null; + if(null != identifier){ + doiPostfix = identifier.substring(doiPrefix.length()); + } + return doiPostfix; + } + + // Should never return null! + protected String makeIdentifierBasedOnHistory(Context context, DSpaceObject dso, VersionHistory history) + throws AuthorizeException, SQLException, DOIIdentifierException + { + // Mint foreach new version an identifier like: 12345/100.versionNumber + // use the bare handle (g.e. 12345/100) for the first version. + + // currently versioning is supported for items only + if (!(dso instanceof Item)) + { + throw new IllegalArgumentException("Cannot create versioned handle for objects other then item: Currently versioning supports items only."); + } + Item item = (Item)dso; + Version version = versionHistoryService.getVersion(context, history, item); + + String previousVersionDOI = null; + for (Version v : versioningService.getVersionsByHistory(context, history)) + { + previousVersionDOI = getDOIByObject(context, v.getItem()); + if (null != previousVersionDOI) + { + break; + } + } + + if (previousVersionDOI == null) + { + // We need to generate a new DOI. + DOI doi = doiService.create(context); + + // as we reuse the DOI ID, we do not have to check whether the DOI exists already. + String identifier = this.getPrefix() + "/" + this.getNamespaceSeparator() + + doi.getId().toString(); + + if (version.getVersionNumber() > 1) + { + identifier.concat(String.valueOf(DOT).concat(String.valueOf(version.getVersionNumber()))); + } + + doi.setDoi(identifier); + doi.setResourceTypeId(dso.getType()); + doi.setDSpaceObject(dso); + doi.setStatus(null); + doiService.update(context, doi); + return doi.getDoi(); + } + assert(previousVersionDOI != null); + + String identifier = getBareDOI(previousVersionDOI); + + if (version.getVersionNumber() > 1) + { + identifier = identifier.concat(String.valueOf(DOT)).concat(String.valueOf(versionHistoryService.getVersion(context, history, item).getVersionNumber())); + } + + loadOrCreateDOI(context, dso, identifier); + return identifier; + } + + void removePreviousVersionDOIsOutOfObject(Context c, Item item, String oldDoi) + throws IdentifierException, AuthorizeException + { + if (StringUtils.isEmpty(oldDoi)) + { + throw new IllegalArgumentException("Old DOI must be neither empty nor null!"); + } + + String bareDoi = getBareDOI(doiService.formatIdentifier(oldDoi)); + String bareDoiRef = doiService.DOIToExternalForm(bareDoi); + + List identifiers = itemService.getMetadata(item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, Item.ANY); + // We have to remove all DOIs referencing previous versions. To do that, + // we store all identifiers we do not know in an array list, clear + // dc.identifier.uri and add the safed identifiers. + // The list of identifiers to safe won't get larger then the number of + // existing identifiers. + ArrayList newIdentifiers = new ArrayList(identifiers.size()); + boolean changed = false; + for (MetadataValue identifier : identifiers) + { + if (!StringUtils.startsWithIgnoreCase(identifier.getValue(), bareDoiRef)) + { + newIdentifiers.add(identifier.getValue()); + } else { + changed = true; + } + } + // reset the metadata if neccessary. + if (changed) + { + try + { + itemService.clearMetadata(c, item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, Item.ANY); + itemService.addMetadata(c, item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, newIdentifiers); + itemService.update(c, item); + } catch (SQLException ex) { + throw new RuntimeException("A problem with the database connection occured.", ex); + } + } + } + + @Required + public void setDOIConnector(DOIConnector connector) + { + super.setDOIConnector(connector); + this.connector = connector; + } + + @Required + public void setConfigurationService(ConfigurationService configurationService) { + super.setConfigurationService(configurationService); + this.configurationService = configurationService; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/identifier/dao/DOIDAO.java b/dspace-api/src/main/java/org/dspace/identifier/dao/DOIDAO.java index 8b63da621b..6e447aaca8 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/dao/DOIDAO.java +++ b/dspace-api/src/main/java/org/dspace/identifier/dao/DOIDAO.java @@ -27,6 +27,9 @@ public interface DOIDAO extends GenericDAO public DOI findByDoi(Context context, String doi) throws SQLException; public DOI findDOIByDSpaceObject(Context context, DSpaceObject dso, List statusToExclude) throws SQLException; + + public List findSimilarNotInState(Context context, String doi, List statuses, boolean dsoNotNull) + throws SQLException; public List findByStatus(Context context, List statuses) throws SQLException; diff --git a/dspace-api/src/main/java/org/dspace/identifier/dao/impl/DOIDAOImpl.java b/dspace-api/src/main/java/org/dspace/identifier/dao/impl/DOIDAOImpl.java index 5a94509890..e4c588168b 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/dao/impl/DOIDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/identifier/dao/impl/DOIDAOImpl.java @@ -69,6 +69,28 @@ public class DOIDAOImpl extends AbstractHibernateDAO implements DOIDAO for (Integer status : statuses) { statusQuery.add(Restrictions.eq("status", status)); } + criteria.add(statusQuery); + return list(criteria); + } + + @Override + public List findSimilarNotInState(Context context, String doi, List excludedStatuses, boolean dsoNotNull) + throws SQLException + { + // SELECT * FROM Doi WHERE doi LIKE ? AND resource_type_id = ? AND resource_id IS NOT NULL AND status != ? AND status != ? + Criteria criteria = createCriteria(context, DOI.class); + Conjunction conjunctionAnd = Restrictions.and(); + Disjunction statusQuery = Restrictions.or(); + for (Integer status : excludedStatuses) { + statusQuery.add(Restrictions.ne("status", status)); + } + conjunctionAnd.add(Restrictions.like("doi", doi)); + conjunctionAnd.add(statusQuery); + if (dsoNotNull) + { + conjunctionAnd.add(Restrictions.isNotNull("dSpaceObject")); + } + criteria.add(conjunctionAnd); return list(criteria); } diff --git a/dspace-api/src/main/java/org/dspace/identifier/service/DOIService.java b/dspace-api/src/main/java/org/dspace/identifier/service/DOIService.java index 8860071d49..f8ce56d2fb 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/service/DOIService.java +++ b/dspace-api/src/main/java/org/dspace/identifier/service/DOIService.java @@ -68,4 +68,16 @@ public interface DOIService { throws DOIIdentifierException; public List getDOIsByStatus(Context context, List statuses) throws SQLException; + + /** + * Find all DOIs that are similar to the specified pattern ant not in the specified states. + * @param context DSpace context + * @param doiPattern The pattern, e.g. "10.5072/123.%" + * @param statuses The statuses the DOI should not be in, @{link DOIIdentifierProvider.DELETED}. + * @param dsoIsNotNull Boolean whether all DOIs should be excluded where the DSpaceObject is NULL. + * @return null or a list of DOIs + * @throws SQLException + */ + public List getSimilarDOIsNotInState(Context context, String doiPattern, List statuses, boolean dsoIsNotNull) + throws SQLException; } diff --git a/dspace/config/spring/api/identifier-service.xml b/dspace/config/spring/api/identifier-service.xml index 049463557d..acef8d1c00 100644 --- a/dspace/config/spring/api/identifier-service.xml +++ b/dspace/config/spring/api/identifier-service.xml @@ -22,10 +22,11 @@ scope="singleton"/> - - - - + - + +