diff --git a/dspace-api/src/main/java/org/dspace/identifier/ClarinCommunityDOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/ClarinCommunityDOIIdentifierProvider.java new file mode 100644 index 000000000000..9176d861c9f9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/identifier/ClarinCommunityDOIIdentifierProvider.java @@ -0,0 +1,177 @@ +/** + * 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.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.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.content.logic.Filter; +import org.dspace.core.Context; +import org.dspace.identifier.doi.ClarinDataCiteConnector; +import org.dspace.identifier.doi.DOIConnector; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * DOI identifier provider for CLARIN communities. + * It uses the ClarinDataCiteConnector to connect to DataCite and requires the DOI prefix + * and the list of CLARIN community IDs to be configured. + * + * @author Milan Kuchtiak (kuchtiak@ufal.mff.cuni.cz) + */ +public class ClarinCommunityDOIIdentifierProvider extends DOIIdentifierProvider { + + private static final Logger log = LogManager.getLogger(ClarinCommunityDOIIdentifierProvider.class); + + protected DOIConnector connector; + + private String doiPrefix; + + private String namespaceSeparator; + + private Set communities = new HashSet<>(); + + public void init() { + if (!(this.connector instanceof ClarinDataCiteConnector)) { + throw new IllegalStateException("ClarinCommunityDOIIdentifierProvider requires ClarinDataCiteConnector"); + } + if (doiPrefix == null) { + throw new IllegalStateException("No DOI prefix configured for ClarinCommunityDOIIdentifierProvider"); + } + if (namespaceSeparator == null) { + namespaceSeparator = ""; + } + + ClarinDataCiteConnector dataCiteConnector = (ClarinDataCiteConnector) this.connector; + + dataCiteConnector.setDoiPrefix(doiPrefix); + + String username = this.configurationService.getProperty("identifier.doi." + doiPrefix + ".user"); + if (username == null) { + log.error("No username configured for DOI prefix '{}'", doiPrefix); + throw new IllegalStateException("No username configured for DOI prefix '" + doiPrefix + "'"); + } + dataCiteConnector.setUsername(username); + + String password = this.configurationService.getProperty("identifier.doi." + doiPrefix + ".password"); + if (password == null) { + log.error("No password configured for DOI prefix '{}'", doiPrefix); + throw new IllegalStateException("No password configured for DOI prefix '" + doiPrefix + "'"); + } + dataCiteConnector.setPassword(password); + } + + @Override + @Autowired(required = true) + public void setDOIConnector(DOIConnector connector) { + super.setDOIConnector(connector); + this.connector = connector; + } + + /** + * Mint a new DOI identifier for a given DSpaceObject. This method cleans up any old DOI metadata + * that may be left on the item if it was versioned and the DOI metadata was + * not removed from the original item before versioning. + * + * @param context - DSpace context + * @param dso - DSpaceObject identified by the new identifier + * @param filter - Logical item filter to determine whether this identifier should be registered + * @return minted DOI identifier for the given DSpaceObject + */ + @Override + public String mint(Context context, DSpaceObject dso, Filter filter) throws IdentifierException { + if (!(dso instanceof Item)) { + throw new IdentifierException("Currently only Items are supported for DOIs."); + } + Item item = (Item) dso; + + try { + String doi = getDOIByObject(context, dso); + if (doi != null) { + return doi; + } + } catch (SQLException e) { + log.error("Error while attempting 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() + ".", e); + } + String doi; + try { + doi = loadOrCreateDOI(context, dso, null, filter).getDoi(); + } catch (SQLException e) { + log.error("Error while creating new DOI for Object of " + + "ResourceType {} with id {}.", dso.getType(), dso.getID()); + throw new RuntimeException("Error while attempting to create a " + + "new DOI for " + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + + " with ID " + dso.getID() + ".", e); + } + + // check whether we have old DOI metadata and if we have - remove it + // this metadata could be left on the item if it was versioned + // and the DOI metadata was not removed from the original item before versioning + String leftPart = doiService.getResolver() + SLASH + getPrefix() + SLASH + getNamespaceSeparator(); + List metadataToRemove = + itemService.getMetadata(item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, Item.ANY) + .stream() + .filter(id -> id.getValue().startsWith(leftPart)) + .collect(Collectors.toList()); + if (!metadataToRemove.isEmpty()) { + log.debug("Found old DOI metadata that needs to be removed."); + try { + itemService.removeMetadataValues(context, item, metadataToRemove); + itemService.update(context, item); + } catch (SQLException e) { + throw new RuntimeException(e); + } catch (AuthorizeException e) { + throw new RuntimeException( + "Trying to remove an old DOI from a versioned item, but wasn't authorized to.", e); + } + } + + return doi.startsWith(DOI.SCHEME) ? doi : DOI.SCHEME + doi; + } + + public void setDoiPrefix(String doiPrefix) { + this.doiPrefix = doiPrefix; + } + + public void setNamespaceSeparator(String namespaceSeparator) { + this.namespaceSeparator = namespaceSeparator; + } + + public void setCommunities(Set communities) { + this.communities = communities; + } + + @Override + protected String getPrefix() { + return this.doiPrefix; + } + + @Override + protected String getNamespaceSeparator() { + return this.namespaceSeparator; + } + + Set getCommunities() { + return this.communities; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/identifier/ClarinDOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/ClarinDOIIdentifierProvider.java new file mode 100644 index 000000000000..000a2f30cd0e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/identifier/ClarinDOIIdentifierProvider.java @@ -0,0 +1,323 @@ +/** + * 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.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.logic.Filter; +import org.dspace.core.Context; +import org.dspace.identifier.doi.DOIConnector; +import org.dspace.identifier.doi.DOIIdentifierException; +import org.dspace.identifier.doi.DOIIdentifierNotApplicableException; +import org.dspace.services.ConfigurationService; + +/** + * DOI identifier provider for CLARIN communities. + * It delegates to individual ClarinCommunityDOIIdentifierProvider instances based on the item's community. + * + * @author Milan Kuchtiak (kuchtiak@ufal.mff.cuni.cz) + */ +public class ClarinDOIIdentifierProvider extends DOIIdentifierProvider { + + private static final Logger log = LogManager.getLogger(ClarinDOIIdentifierProvider.class); + + private List providers; + + protected final SimpleCache simpleCache = new SimpleCache<>(5); + + public void setProviders(List providers) { + this.providers = providers; + } + + @Override + public void setDOIConnector(DOIConnector connector) { + // Do nothing, as the connector is injected into the individual providers + } + + @Override + public void setConfigurationService(ConfigurationService configurationService) { + // Do nothing, as the configuration service is injected into the individual providers + } + + @Override + public void register(Context context, DSpaceObject dso, String identifier, Filter filter) + throws IdentifierException { + if (dso instanceof Item) { + ClarinCommunityDOIIdentifierProvider provider = getProviderForItem(context, (Item) dso); + if (provider != null) { + provider.register(context, dso, identifier, filter); + } else { + logInfoNonConfigurableEntity(dso, "registration"); + } + } else { + logInfoNoItem(dso, "registration"); + } + } + + @Override + public void registerOnline(Context context, DSpaceObject dso, String identifier, Filter filter) + throws IdentifierException, IllegalArgumentException, SQLException { + if (dso instanceof Item) { + ClarinCommunityDOIIdentifierProvider provider = getProviderForItem(context, (Item) dso); + if (provider != null) { + provider.registerOnline(context, dso, identifier, filter); + } else { + logInfoNonConfigurableEntity(dso, "registration"); + } + } else { + logInfoNoItem(dso, "registration"); + } + } + + @Override + public void reserve(Context context, DSpaceObject dso, String identifier, Filter filter) + throws IdentifierException, IllegalArgumentException { + if (dso instanceof Item) { + ClarinCommunityDOIIdentifierProvider provider = getProviderForItem(context, (Item) dso); + if (provider != null) { + provider.reserve(context, dso, identifier, filter); + } else { + logInfoNonConfigurableEntity(dso, "reservation"); + } + } else { + logInfoNoItem(dso, "reservation"); + } + } + + @Override + public void reserveOnline(Context context, DSpaceObject dso, String identifier, Filter filter) + throws IdentifierException, IllegalArgumentException, SQLException { + if (dso instanceof Item) { + ClarinCommunityDOIIdentifierProvider provider = getProviderForItem(context, (Item) dso); + if (provider != null) { + provider.reserveOnline(context, dso, identifier, filter); + } else { + logInfoNonConfigurableEntity(dso, "reservation"); + } + } else { + logInfoNoItem(dso, "reservation"); + } + } + + @Override + public void checkMintable(Context context, Filter filter, DSpaceObject dso) + throws DOIIdentifierNotApplicableException { + if (dso instanceof Item) { + ClarinCommunityDOIIdentifierProvider provider = getProviderForItem(context, (Item) dso); + if (provider != null) { + provider.checkMintable(context, filter, dso); + } else { + log.warn("Item {} is not in a configured CLARIN community", dso.getID()); + throw new DOIIdentifierNotApplicableException("Item " + dso.getID() + + " is not applicable by DOI identifier provider as it is not in a configured CLARIN community"); + } + } else { + log.warn("DSpaceObject {} is not an Item", dso.getID()); + throw new DOIIdentifierNotApplicableException("DSpaceObject " + dso.getID() + + " is not applicable by DOI identifier provider as it is not an Item"); + } + } + + @Override + public void deleteOnline(Context context, String identifier) throws DOIIdentifierException { + getProviderForIdentifier(identifier).deleteOnline(context, identifier); + } + + @Override + public String mint(Context context, DSpaceObject dso, Filter filter) throws IdentifierException { + if (dso instanceof Item) { + ClarinCommunityDOIIdentifierProvider provider = getProviderForItem(context, (Item) dso); + if (provider != null) { + return provider.mint(context, dso, filter); + } else { + logInfoNonConfigurableEntity(dso, "minting"); + } + } else { + logInfoNoItem(dso, "minting"); + } + return null; + } + + @Override + public String getDOIOutOfObject(DSpaceObject dso) throws DOIIdentifierException { + if (!(dso instanceof Item)) { + throw new IllegalArgumentException("We currently support DOIs for Items only, not for " + + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + "."); + } + Item item = (Item) dso; + + List metadata = itemService.getMetadata(item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, Item.ANY); + for (MetadataValue metadataValue : metadata) { + String doiResolver = doiService.getResolver(); + for (ClarinCommunityDOIIdentifierProvider provider : providers) { + String leftPart = doiResolver + "/" + provider.getPrefix() + "/" + provider.getNamespaceSeparator(); + if (metadataValue.getValue().startsWith(leftPart)) { + return doiService.DOIFromExternalFormat(metadataValue.getValue()); + } + } + } + return null; + } + + @Override + public void updateMetadata(Context context, DSpaceObject dso, String identifier) + throws IdentifierException, SQLException { + if (dso instanceof Item) { + ClarinCommunityDOIIdentifierProvider provider = getProviderForItem(context, (Item) dso); + if (provider != null) { + provider.updateMetadata(context, dso, identifier); + } else { + logInfoNonConfigurableEntity(dso, "metadata update"); + } + } else { + logInfoNoItem(dso, "metadata update"); + } + } + + @Override + public void updateMetadataOnline(Context context, DSpaceObject dso, String identifier) + throws IdentifierException, SQLException { + if (dso instanceof Item) { + ClarinCommunityDOIIdentifierProvider provider = getProviderForItem(context, (Item) dso); + if (provider != null) { + provider.updateMetadataOnline(context, dso, identifier); + } else { + logInfoNonConfigurableEntity(dso, "metadata update"); + } + } else { + logInfoNoItem(dso, "metadata update"); + } + } + + private ClarinCommunityDOIIdentifierProvider getProviderForItem(Context context, Item item) { + + if (simpleCache.containsKey(item.getID())) { + return simpleCache.get(item.getID()); + } + + // Check communities of item.getCollections() - this will only see collections if the item is archived + for (Collection collection : item.getCollections()) { + try { + for (ClarinCommunityDOIIdentifierProvider provider : providers) { + if (isCollectionInProvider(provider, collection)) { + simpleCache.put(item.getID(), provider); + return provider; + } + } + } catch (SQLException e) { + log.error("Error while determining DOI provider for item {}", item.getID(), e); + } + } + + // Look for the parent object of the item. This is important as the item.getOwningCollection method + // may return null, even though the item itself does have a parent object, at the point of archival + try { + DSpaceObject parent = itemService.getParentObject(context, item); + if (parent instanceof Collection) { + log.debug("Got parent DSO for item: " + parent.getID().toString()); + log.debug("Parent DSO handle: " + parent.getHandle()); + try { + // Now iterate communities of this parent collection + for (ClarinCommunityDOIIdentifierProvider provider : providers) { + if (isCollectionInProvider(provider, (Collection) parent)) { + simpleCache.put(item.getID(), provider); + return provider; + } + } + // parent collection is not in any of the configured communities, + // cache this fact to avoid future lookups + simpleCache.put(item.getID(), null); + } catch (SQLException e) { + log.error("Error while determining DOI provider for item {} from the parent collection", + item.getID(), e); + } + } else { + log.debug("Parent DSO is null or is not a Collection..."); + } + } catch (SQLException e) { + log.error("Error obtaining parent DSO", e); + } + + return null; + } + + private boolean isCollectionInProvider(ClarinCommunityDOIIdentifierProvider provider, + Collection collection) throws SQLException { + List parentCommunities = collection.getCommunities(); + for (Community parentCommunity : parentCommunities) { + if (provider.getCommunities().contains(parentCommunity.getID().toString())) { + return true; + } + String parentCommunityHandle = parentCommunity.getHandle(); + if (parentCommunityHandle != null && provider.getCommunities().contains(parentCommunityHandle)) { + return true; + } + } + return false; + } + + private ClarinCommunityDOIIdentifierProvider getProviderForIdentifier(String identifier) + throws DOIIdentifierException { + String doi = doiService.formatIdentifier(identifier).substring(DOI.SCHEME.length()); + String prefix = doi.split("/")[0]; + return providers.stream() + .filter(provider -> provider.getPrefix().equals(prefix)) + .findFirst() + .orElseThrow(() -> new DOIIdentifierException("No provider found for DOI prefix: " + prefix)); + } + + private void logInfoNoItem(DSpaceObject dso, String actionName) { + log.info("DSpaceObject {} is not an Item, skipping DOI {}", dso.getID(), actionName); + } + + private void logInfoNonConfigurableEntity(DSpaceObject dso, String actionName) { + log.info("Item {} is not in a configured CLARIN community, skipping DOI {}", dso.getID(), actionName); + } + + protected static class SimpleCache { + private final Map cache; + + public SimpleCache(int maxCapacity) { + this.cache = Collections.synchronizedMap(new LinkedHashMap(maxCapacity, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > maxCapacity; + } + }); + } + + public V get(K key) { + return cache.get(key); + } + + public void put(K key, V value) { + cache.put(key, value); + } + + public boolean containsKey(K key) { + return cache.containsKey(key); + } + + public void clear() { + cache.clear(); + } + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/identifier/ClarinVersionedDOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/ClarinVersionedDOIIdentifierProvider.java deleted file mode 100644 index bd17bd5a4883..000000000000 --- a/dspace-api/src/main/java/org/dspace/identifier/ClarinVersionedDOIIdentifierProvider.java +++ /dev/null @@ -1,271 +0,0 @@ -/** - * 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.List; - -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.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.Context; -import org.dspace.identifier.doi.DOIConnector; -import org.dspace.identifier.doi.DOIIdentifierException; -import org.dspace.services.ConfigurationService; -import org.dspace.versioning.VersionHistory; -import org.dspace.versioning.service.VersionHistoryService; -import org.dspace.versioning.service.VersioningService; -import org.springframework.beans.factory.annotation.Autowired; - -/** - * This class is copied from the VersionedDOIIdentifierProvider. The main difference is that was removed code - * where is created the handle based on the history. - * - * @author Milan Majchrak (milan.majchrak at dataquest.sk) - * @author Marsa Haoua - * @author Pascal-Nicolas Becker (dspace at pascal dash becker dot de) - */ -public class ClarinVersionedDOIIdentifierProvider extends DOIIdentifierProvider { - /** - * log4j category - */ - private static final Logger log = LogManager.getLogger(VersionedDOIIdentifierProvider.class); - - protected DOIConnector connector; - - static final char DOT = '.'; - protected static final String pattern = "\\d+\\" + String.valueOf(DOT) + "\\d+"; - - @Autowired(required = true) - protected VersioningService versioningService; - @Autowired(required = true) - protected 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() + ".", ex); - throw new RuntimeException("Error while attempting to retrieve " - + "information about a DOI for " - + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) - + " with ID " + dso.getID() + ".", ex); - } - - // TODO do not return DOI based on the history - // 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) { - log.error( - "A problem with the database connection occurd while processing DOI " + - versionedDOI + ".", ex); - throw new RuntimeException("A problem with the database connection occured.", ex); - } - return versionedDOI; - } - } - - try { - doi = loadOrCreateDOI(context, dso, null).getDoi(); - } catch (SQLException ex) { - log.error("SQLException 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(), ex); - 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; - } - - protected 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; - } - - 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); - } - } - } - - @Override - @Autowired(required = true) - public void setDOIConnector(DOIConnector connector) { - super.setDOIConnector(connector); - this.connector = connector; - } - - @Override - @Autowired(required = true) - public void setConfigurationService(ConfigurationService configurationService) { - super.setConfigurationService(configurationService); - this.configurationService = configurationService; - } - -} - diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/ClarinDataCiteConnector.java b/dspace-api/src/main/java/org/dspace/identifier/doi/ClarinDataCiteConnector.java new file mode 100644 index 000000000000..7822a2f0e8e5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/ClarinDataCiteConnector.java @@ -0,0 +1,30 @@ +/** + * 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.doi; + +/** + * DataCite connector for CLARIN communities. + * It allows to set the username, password and DOI prefix via setter methods. + * + * @author Milan Kuchtiak (kuchtiak@ufal.mff.cuni.cz) + */ +public class ClarinDataCiteConnector extends DataCiteConnector { + + public void setUsername(String username) { + super.USERNAME = username; + } + + public void setPassword(String password) { + super.PASSWORD = password; + } + + public void setDoiPrefix(String doiPrefix) { + super.doiPrefix = doiPrefix; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java index 8355f2490380..b875f9cc01a1 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java @@ -122,6 +122,8 @@ public class DataCiteConnector protected String USERNAME; protected String PASSWORD; + protected String doiPrefix; + @Autowired protected HandleService handleService; @@ -247,6 +249,12 @@ protected String getPassword() { return this.PASSWORD; } + protected String getDoiPrefix() { + if (this.doiPrefix == null) { + this.doiPrefix = configurationService.getProperty(CFG_PREFIX); + } + return this.doiPrefix; + } @Override public boolean isDOIReserved(Context context, String doi) @@ -375,9 +383,9 @@ public void reserveDOI(Context context, DSpaceObject dso, String doi) // Set the transform's parameters. // XXX Should the actual list be configurable? Map parameters = new HashMap<>(); - if (configurationService.hasProperty(CFG_PREFIX)) { - parameters.put("prefix", - configurationService.getProperty(CFG_PREFIX)); + String doiPrefix = getDoiPrefix(); + if (doiPrefix != null) { + parameters.put("prefix", doiPrefix); } if (configurationService.hasProperty(CFG_PUBLISHER)) { parameters.put("publisher", diff --git a/dspace-api/src/test/java/org/dspace/identifier/ClarinDOIIdentifierProviderIT.java b/dspace-api/src/test/java/org/dspace/identifier/ClarinDOIIdentifierProviderIT.java new file mode 100644 index 000000000000..8f4349eb5422 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/identifier/ClarinDOIIdentifierProviderIT.java @@ -0,0 +1,319 @@ +/** + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.VersionBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; +import org.dspace.identifier.doi.ClarinDataCiteConnector; +import org.dspace.identifier.doi.DOIIdentifierException; +import org.dspace.identifier.doi.DOIIdentifierNotApplicableException; +import org.dspace.identifier.factory.IdentifierServiceFactory; +import org.dspace.identifier.service.DOIService; +import org.dspace.kernel.ServiceManager; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Tests for the ClarinDOIIdentifierProvider class. + * + * @author Milan Kuchtiak (kuchtiak@ufal.mff.cuni.cz) + */ +public class ClarinDOIIdentifierProviderIT extends AbstractIntegrationTestWithDatabase { + + private ConfigurationService configurationService; + private DOIService doiService; + private ItemService itemService; + + private ClarinDOIIdentifierProvider provider; + + private static IdentifierServiceImpl identifierService; + private static List existingIdentifierProviders; + + private Item item1; + private Item item2; + private Item item3; + private Item itemV2; + + @BeforeClass + public static void setUpClass() throws Exception { + ServiceManager serviceManager = DSpaceServicesFactory.getInstance().getServiceManager(); + identifierService = serviceManager.getServicesByType(IdentifierServiceImpl.class).get(0); + + // Clean out providers to avoid any being used for creation of community and collection + existingIdentifierProviders = identifierService.getProviders(); + identifierService.setProviders(new ArrayList<>()); + } + + @AfterClass + public static void tearDownClass() { + identifierService.setProviders(existingIdentifierProviders); + } + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + + configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + doiService = IdentifierServiceFactory.getInstance().getDOIService(); + itemService = ContentServiceFactory.getInstance().getItemService(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community 1") + .build(); + Community parentCommunity2 = CommunityBuilder.createCommunity(context) + .withName("Parent Community 2") + .build(); + Community parentCommunity3 = CommunityBuilder.createCommunity(context) + .withName("Parent Community 3") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + Collection collection2 = CollectionBuilder.createCollection(context, parentCommunity2) + .withName("Collection 2") + .build(); + Collection collection3 = CollectionBuilder.createCollection(context, parentCommunity3) + .withName("Collection 3") + .build(); + + item1 = ItemBuilder.createItem(context, collection) + .withTitle("First Item") + .build(); + item2 = ItemBuilder.createItem(context, collection2) + .withTitle("Second Item") + .build(); + item3 = ItemBuilder.createItem(context, collection3) + .withTitle("Third Item") + .build(); + + context.restoreAuthSystemState(); + + ClarinCommunityDOIIdentifierProvider communityProvider1 = + createCommunityProvider("10.1", "1-", Set.of(parentCommunity.getID().toString())); + + ClarinCommunityDOIIdentifierProvider communityProvider2 = + createCommunityProvider("10.2", "2-", Set.of(parentCommunity2.getID().toString())); + + provider = new ClarinDOIIdentifierProvider(); + provider.setProviders(List.of(communityProvider1, communityProvider2)); + + provider.itemService = itemService; + provider.doiService = doiService; + provider.contentServiceFactory = ContentServiceFactory.getInstance(); + } + + @Test + public void testMint() throws IdentifierException, SQLException, AuthorizeException { + String doi1 = provider.mint(context, item1); + assertTrue(doi1.startsWith("doi:10.1/1-")); + + checkDoi(doi1, DOIIdentifierProvider.MINTED); + + String doi2 = provider.mint(context, item2); + assertTrue(doi2.startsWith("doi:10.2/2-")); + + checkDoi(doi2, DOIIdentifierProvider.MINTED); + + // item3 is not mintable + String doi3 = provider.mint(context, item3); + assertNull(doi3); + + context.turnOffAuthorisationSystem(); + itemV2 = VersionBuilder.createVersion(context, item1, "Second version").build().getItem(); + // simulate itemV2 having a DOI identifier assigned (from previous version) + itemService.addMetadata(context, itemV2, "dc", "identifier", "doi", null, "https://doi.org/10.1/1-1"); + context.restoreAuthSystemState(); + + // mint new DOI for itemV2, which is a new version of item1, should result in entirely new DOI + // (DOI not based on item's history) + String doi4 = provider.mint(context, itemV2); + assertTrue(doi4.startsWith("doi:10.1/1-")); + // check if the DOI for itemV2 is different from the one for item1 + assertFalse(doi4.startsWith(doi1)); + checkDoi(doi2, DOIIdentifierProvider.MINTED); + // check that the old DOI identifier was removed from itemV2 + assertEquals(0, getDoiMetadata(itemV2).size()); + } + + @Test + public void testCheckMintable() throws IdentifierException { + provider.checkMintable(context, item1); + provider.checkMintable(context, item2); + // item3 is not mintable + assertThrows(DOIIdentifierNotApplicableException.class, () -> provider.checkMintable(context, item3)); + } + + @Test + public void testRegister() throws IdentifierException, SQLException, AuthorizeException { + String doi1 = provider.register(context, item1); + assertTrue(doi1.startsWith("doi:10.1/1-")); + + checkDoi(doi1, DOIIdentifierProvider.TO_BE_REGISTERED); + + // item3 is not mintable + String doi3 = provider.register(context, item3); + assertNull(doi3); + + context.turnOffAuthorisationSystem(); + itemV2 = VersionBuilder.createVersion(context, item1, "Second version").build().getItem(); + context.restoreAuthSystemState(); + + String doi4 = provider.register(context, itemV2); + assertTrue(doi4.startsWith("doi:10.1/1-")); + // check if the DOI for itemV2 is different from the one for item1 + assertFalse(doi4.startsWith(doi1)); + checkDoi(doi4, DOIIdentifierProvider.TO_BE_REGISTERED); + } + + @Test + public void testReserve() throws IdentifierException, SQLException { + String doi1 = "doi:10.1/res-1"; + String doi2 = "doi:10.1/res-2"; + provider.reserve(context, item1, doi1); + + checkDoi(doi1, DOIIdentifierProvider.MINTED); + + // trying to reserve the same DOI for another item should fail + assertThrows(DOIIdentifierException.class, () -> provider.reserve(context, item2, doi1)); + + // trying to reserve a DOI with wrong prefix should fail (10.1 prefix is reserved by community1) + assertThrows(DOIIdentifierException.class, () -> provider.reserve(context, item2, doi2)); + + // trying to reserve a DOI for an item3 that belongs to community not configured for DOI should fail + // (object is not stored to DOI table) + provider.reserve(context, item3, doi2); + assertNull(doiService.findByDoi(context, doi2.substring(DOI.SCHEME.length()))); + } + + @Test + public void testUpdateMetadata() throws IdentifierException, SQLException { + String doi1 = "doi:10.1/res-1"; + provider.reserve(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.MINTED); + + provider.updateMetadata(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.MINTED); + + provider.register(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.TO_BE_REGISTERED); + provider.updateMetadata(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.UPDATE_BEFORE_REGISTRATION); + } + + @Test + public void testOnlineOperations() throws IdentifierException, SQLException { + context.setCurrentUser(admin); + String doi1 = "doi:10.1/res-1"; + String resolver = configurationService.getProperty("identifier.doi.resolver", "https://doi.org"); + + provider.reserve(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.MINTED); + provider.reserveOnline(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.IS_RESERVED); + + assertEquals(0, getDoiMetadata(item1).size()); + + provider.updateMetadata(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.UPDATE_RESERVED); + provider.updateMetadataOnline(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.IS_RESERVED); + + provider.register(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.TO_BE_REGISTERED); + provider.registerOnline(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.IS_REGISTERED); + + List doiMetadata = getDoiMetadata(item1); + assertEquals(1, doiMetadata.size()); + assertEquals(resolver + "/" + doi1.substring(DOI.SCHEME.length()), doiMetadata.get(0).getValue()); + + provider.updateMetadata(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.UPDATE_REGISTERED); + provider.updateMetadataOnline(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.IS_REGISTERED); + + // trying to delete a DOI for item2 that belongs to another community should fail + assertThrows(DOIIdentifierException.class, () -> provider.delete(context, item2, doi1)); + + provider.delete(context, item1, doi1); + checkDoi(doi1, DOIIdentifierProvider.TO_BE_DELETED); + provider.deleteOnline(context, doi1); + checkDoi(doi1, DOIIdentifierProvider.DELETED); + + assertEquals(0, getDoiMetadata(item1).size()); + } + + private ClarinCommunityDOIIdentifierProvider createCommunityProvider(String doiPrefix, + String namespaceSeparator, + Set communityIds) { + configurationService.setProperty("identifier.doi." + doiPrefix + ".user", "test_user"); + configurationService.setProperty("identifier.doi." + doiPrefix + ".password", "password"); + + ClarinDataCiteConnector connector = mock(ClarinDataCiteConnector.class); + + ClarinCommunityDOIIdentifierProvider communityProvider = new ClarinCommunityDOIIdentifierProvider(); + communityProvider.setCommunities(communityIds); + communityProvider.setDoiPrefix(doiPrefix); + communityProvider.setNamespaceSeparator(namespaceSeparator); + communityProvider.setConfigurationService(configurationService); + communityProvider.setDOIConnector(connector); + // communityProvider.versionHistoryService = versionHistoryService; + communityProvider.doiService = doiService; + communityProvider.itemService = itemService; + communityProvider.contentServiceFactory = ContentServiceFactory.getInstance(); + + communityProvider.init(); + + return communityProvider; + } + + private void checkDoi(String doi, Integer expectedStatus) throws SQLException { + DOI doiRow2 = doiService.findByDoi(context, doi.substring(DOI.SCHEME.length())); + assertNotNull(doiRow2); + assertEquals(expectedStatus, doiRow2.getStatus()); + } + + private List getDoiMetadata(Item item) throws SQLException { + return itemService.getMetadata(item, + DOIIdentifierProvider.MD_SCHEMA, + DOIIdentifierProvider.DOI_ELEMENT, + DOIIdentifierProvider.DOI_QUALIFIER, + Item.ANY); + } + +} diff --git a/dspace/config/crosswalks/DIM2DataCite.xsl b/dspace/config/crosswalks/DIM2DataCite.xsl index 7add24bfa248..1b0d568d8f02 100644 --- a/dspace/config/crosswalks/DIM2DataCite.xsl +++ b/dspace/config/crosswalks/DIM2DataCite.xsl @@ -59,7 +59,7 @@ The classe named above respects this. --> @@ -235,24 +235,6 @@ --> - - - - - - - - - Other - Other - - - - + + +