All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dspace.identifier.EZIDIdentifierProvider Maven / Gradle / Ivy

There is a newer version: 8.0
Show newest version
/**
 * 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.io.IOException;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.dspace.authorize.AuthorizeException;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.DSpaceObjectService;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.identifier.ezid.EZIDRequest;
import org.dspace.identifier.ezid.EZIDRequestFactory;
import org.dspace.identifier.ezid.EZIDResponse;
import org.dspace.identifier.ezid.Transform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Provide service for DOIs through DataCite using the EZID service.
 *
 * 

Configuration of this class is is in two parts.

* *

Installation-specific configuration (credentials and the "shoulder" value * which forms a prefix of the site's DOIs) is supplied from property files in * [DSpace]/config**.

* *
*
identifier.doi.ezid.shoulder
*
base of the site's DOIs. Example: 10.5072/FK2
*
identifier.doi.ezid.user
*
EZID username.
*
identifier.doi.ezid.password
*
EZID password.
*
identifier.doi.ezid.publisher
*
A default publisher, for Items not previously published. EZID requires a publisher.
*
* *

Then there are properties injected using Spring:

*
    *
  • There is a Map (with the property name "crosswalk") from EZID metadata * field names into DSpace field names, injected by Spring. Specify the * fully-qualified names of all metadata fields to be looked up on a DSpace * object and their values set on mapped fully-qualified names in the object's * DataCite metadata.
  • * *
  • A second map ("crosswalkTransform") provides Transform instances mapped * from EZID metadata field names. This allows the crosswalk to rewrite field * values where the form maintained by DSpace is not directly usable in EZID * metadata.
  • * *
  • Optional: A boolean property ("generateDataciteXML") that controls the * creation and inclusion of DataCite xml schema during the metadata * crosswalking. The default "DataCite" dissemination plugin uses * DIM2DataCite.xsl for crosswalking. Default value: false.
  • * *
  • Optional: A string property ("disseminationCrosswalkName") that can be * used to set the name of the dissemination crosswalk plugin for metadata * crosswalking. Default value: "DataCite".
  • *
* * @author mwood */ public class EZIDIdentifierProvider extends IdentifierProvider { private static final Logger log = LoggerFactory.getLogger(EZIDIdentifierProvider.class); // Configuration property names static final String CFG_SHOULDER = "identifier.doi.ezid.shoulder"; static final String CFG_USER = "identifier.doi.ezid.user"; static final String CFG_PASSWORD = "identifier.doi.ezid.password"; static final String CFG_PUBLISHER = "identifier.doi.ezid.publisher"; // DataCite metadata field names static final String DATACITE_PUBLISHER = "datacite.publisher"; static final String DATACITE_PUBLICATION_YEAR = "datacite.publicationyear"; // DSpace metadata field name elements // XXX move these to MetadataSchema or some such public static final String MD_SCHEMA = "dc"; public static final String DOI_ELEMENT = "identifier"; public static final String DOI_QUALIFIER = null; private static final String DOI_SCHEME = "doi:"; protected boolean GENERATE_DATACITE_XML = false; protected String DATACITE_XML_CROSSWALK = "DataCite"; /** * Map DataCite metadata into local metadata. */ private Map crosswalk = new HashMap<>(); /** * Converters to be applied to specific fields. */ private static Map transforms = new HashMap<>(); /** * Factory for EZID requests. */ private EZIDRequestFactory requestFactory; @Autowired(required = true) protected ContentServiceFactory contentServiceFactory; @Autowired(required = true) protected ItemService itemService; @Override public boolean supports(Class identifier) { return DOI.class.isAssignableFrom(identifier); } @Override public boolean supports(String identifier) { if (null == identifier) { return false; } else { return identifier.startsWith(DOI_SCHEME); } // XXX more thorough test? } @Override public String register(Context context, DSpaceObject dso) throws IdentifierException { log.debug("register {}", dso); if (!(dso instanceof Item)) { // DOI are currently assigned only to Item return null; } DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); List identifiers = dsoService.getMetadata(dso, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); for (MetadataValue identifier : identifiers) { if ((null != identifier.getValue()) && (identifier.getValue().startsWith(DOI_SCHEME))) { return identifier.getValue(); } } String id = mint(context, dso); try { dsoService.addMetadata(context, dso, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, id); dsoService.update(context, dso); } catch (SQLException | AuthorizeException ex) { throw new IdentifierException("New identifier not stored", ex); } log.info("Registered {}", id); return id; } @Override public void register(Context context, DSpaceObject object, String identifier) { log.debug("register {} as {}", object, identifier); if (!(object instanceof Item)) { // DOI are currently assigned only to Item return; } EZIDResponse response; try { EZIDRequest request = requestFactory.getInstance(loadAuthority(), loadUser(), loadPassword()); response = request.create(identifier, crosswalkMetadata(context, object)); } catch (IdentifierException | IOException | URISyntaxException e) { log.error("Identifier '{}' not registered: {}", identifier, e.getMessage()); return; } if (response.isSuccess()) { try { DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(object); dsoService.addMetadata(context, object, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, idToDOI(identifier)); dsoService.update(context, object); log.info("registered {}", identifier); } catch (SQLException | AuthorizeException | IdentifierException ex) { // TODO throw new IdentifierException("New identifier not stored", ex); log.error("New identifier not stored", ex); } } else { log.error("Identifier '{}' not registered -- EZID returned: {}", identifier, response.getEZIDStatusValue()); } } @Override public void reserve(Context context, DSpaceObject dso, String identifier) throws IdentifierException { log.debug("reserve {}", identifier); EZIDResponse response; try { EZIDRequest request = requestFactory.getInstance(loadAuthority(), loadUser(), loadPassword()); Map metadata = crosswalkMetadata(context, dso); metadata.put("_status", "reserved"); response = request.create(identifier, metadata); } catch (IOException | URISyntaxException e) { log.error("Identifier '{}' not registered: {}", identifier, e.getMessage()); return; } if (response.isSuccess()) { DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); try { dsoService.addMetadata(context, dso, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, idToDOI(identifier)); dsoService.update(context, dso); log.info("reserved {}", identifier); } catch (SQLException | AuthorizeException ex) { throw new IdentifierException("New identifier not stored", ex); } } else { log.error("Identifier '{}' not registered -- EZID returned: {}", identifier, response.getEZIDStatusValue()); } } @Override public String mint(Context context, DSpaceObject dso) throws IdentifierException { log.debug("mint for {}", dso); // Compose the request EZIDRequest request; try { request = requestFactory.getInstance(loadAuthority(), loadUser(), loadPassword()); } catch (URISyntaxException ex) { log.error(ex.getMessage()); throw new IdentifierException("DOI request not sent: " + ex.getMessage()); } // Send the request EZIDResponse response; try { response = request.mint(crosswalkMetadata(context, dso)); } catch (IOException | URISyntaxException ex) { log.error("Failed to send EZID request: {}", ex.getMessage()); throw new IdentifierException("DOI request not sent: " + ex.getMessage()); } // Good response? if (HttpURLConnection.HTTP_CREATED != response.getHttpStatusCode()) { log.error("EZID server responded: {} {}: {}", new String[] { String.valueOf(response.getHttpStatusCode()), response.getHttpReasonPhrase(), response.getEZIDStatusValue() }); throw new IdentifierException("DOI not created: " + response.getHttpReasonPhrase() + ": " + response.getEZIDStatusValue()); } // Extract the DOI from the content blob if (response.isSuccess()) { String value = response.getEZIDStatusValue(); int end = value.indexOf('|'); // Following pipe is "shadow ARK" if (end < 0) { end = value.length(); } String doi = value.substring(0, end).trim(); log.info("Created {}", doi); return doi; } else { log.error("EZID responded: {}", response.getEZIDStatusValue()); throw new IdentifierException("No DOI returned"); } } @Override public DSpaceObject resolve(Context context, String identifier, String... attributes) throws IdentifierNotFoundException, IdentifierNotResolvableException { log.debug("resolve {}", identifier); Iterator found; try { found = itemService.findByMetadataField(context, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, idToDOI(identifier)); } catch (IdentifierException | SQLException | AuthorizeException | IOException ex) { log.error(ex.getMessage()); throw new IdentifierNotResolvableException(ex); } if (!found.hasNext()) { throw new IdentifierNotFoundException("No object bound to " + identifier); } Item found1 = found.next(); if (found.hasNext()) { log.error("More than one object bound to {}!", identifier); } log.debug("Resolved to {}", found1); return found1; } @Override public String lookup(Context context, DSpaceObject object) throws IdentifierNotFoundException, IdentifierNotResolvableException { log.debug("lookup {}", object); MetadataValue found = null; DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(object); for (MetadataValue candidate : dsoService.getMetadata(object, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null)) { if (candidate.getValue().startsWith(DOI_SCHEME)) { found = candidate; break; } } if (null != found) { log.debug("Found {}", found.getValue()); return found.getValue(); } else { throw new IdentifierNotFoundException(dsoService.getTypeText(object) + " " + object.getID() + " has no DOI"); } } @Override public void delete(Context context, DSpaceObject dso) throws IdentifierException { log.debug("delete {}", dso); // delete from EZID DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); List metadata = dsoService.getMetadata(dso, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); List remainder = new ArrayList<>(); int skipped = 0; for (MetadataValue id : metadata) { if (!id.getValue().startsWith(DOI_SCHEME)) { remainder.add(id.getValue()); continue; } EZIDResponse response; try { EZIDRequest request = requestFactory.getInstance(loadAuthority(), loadUser(), loadPassword()); response = request.delete(DOIToId(id.getValue())); } catch (URISyntaxException e) { log.error("Bad URI in metadata value: {}", e.getMessage()); remainder.add(id.getValue()); skipped++; continue; } catch (IOException e) { log.error("Failed request to EZID: {}", e.getMessage()); remainder.add(id.getValue()); skipped++; continue; } if (!response.isSuccess()) { log.error("Unable to delete {} from DataCite: {}", id.getValue(), response.getEZIDStatusValue()); remainder.add(id.getValue()); skipped++; continue; } log.info("Deleted {}", id.getValue()); } // delete from item try { dsoService.clearMetadata(context, dso, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); dsoService.addMetadata(context, dso, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, remainder); dsoService.update(context, dso); } catch (SQLException | AuthorizeException e) { log.error("Failed to re-add identifiers: {}", e.getMessage()); } if (skipped > 0) { throw new IdentifierException(skipped + " identifiers could not be deleted."); } } @Override public void delete(Context context, DSpaceObject dso, String identifier) throws IdentifierException { log.debug("delete {} from {}", identifier, dso); DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); List metadata = dsoService.getMetadata(dso, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); List remainder = new ArrayList<>(); int skipped = 0; for (MetadataValue id : metadata) { if (!id.getValue().equals(idToDOI(identifier))) { remainder.add(id.getValue()); continue; } EZIDResponse response; try { EZIDRequest request = requestFactory.getInstance(loadAuthority(), loadUser(), loadPassword()); response = request.delete(DOIToId(id.getValue())); } catch (URISyntaxException e) { log.error("Bad URI in metadata value {}: {}", id.getValue(), e.getMessage()); remainder.add(id.getValue()); skipped++; continue; } catch (IOException e) { log.error("Failed request to EZID: {}", e.getMessage()); remainder.add(id.getValue()); skipped++; continue; } if (!response.isSuccess()) { log.error("Unable to delete {} from DataCite: {}", id.getValue(), response.getEZIDStatusValue()); remainder.add(id.getValue()); skipped++; continue; } log.info("Deleted {}", id.getValue()); } // delete from item try { dsoService.clearMetadata(context, dso, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); dsoService.addMetadata(context, dso, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, remainder); dsoService.update(context, dso); } catch (SQLException | AuthorizeException e) { log.error("Failed to re-add identifiers: {}", e.getMessage()); } if (skipped > 0) { throw new IdentifierException(identifier + " could not be deleted."); } } /** * Format a naked identifier as a DOI with our configured authority prefix. * * @throws IdentifierException if authority prefix is not configured. */ String idToDOI(String id) throws IdentifierException { return "doi:" + loadAuthority() + id; } /** * Remove scheme and our configured authority prefix from a doi: URI string. * * @return naked local identifier. * @throws IdentifierException if authority prefix is not configured. */ String DOIToId(String DOI) throws IdentifierException { String prefix = "doi:" + loadAuthority(); if (DOI.startsWith(prefix)) { return DOI.substring(prefix.length()); } else { return DOI; } } /** * Get configured value of EZID username. * * @throws IdentifierException if identifier error */ private String loadUser() throws IdentifierException { String user = configurationService.getProperty(CFG_USER); if (null != user) { return user; } else { throw new IdentifierException("Unconfigured: define " + CFG_USER); } } /** * Get configured value of EZID password. * * @throws IdentifierException if identifier error */ private String loadPassword() throws IdentifierException { String password = configurationService.getProperty(CFG_PASSWORD); if (null != password) { return password; } else { throw new IdentifierException("Unconfigured: define " + CFG_PASSWORD); } } /** * Get configured value of EZID "shoulder". * * @throws IdentifierException if identifier error */ private String loadAuthority() throws IdentifierException { String shoulder = configurationService.getProperty(CFG_SHOULDER); if (null != shoulder) { return shoulder; } else { throw new IdentifierException("Unconfigured: define " + CFG_SHOULDER); } } /** * Map selected DSpace metadata to fields recognized by DataCite. */ Map crosswalkMetadata(Context context, DSpaceObject dso) { if ((null == dso) || !(dso instanceof Item)) { throw new IllegalArgumentException("Must be an Item"); } Item item = (Item) dso; // TODO generalize to DSO when all DSOs have metadata. Map mapped = new HashMap<>(); for (Entry datum : crosswalk.entrySet()) { List values = itemService.getMetadataByMetadataString(item, datum.getValue()); if (null != values) { for (MetadataValue value : values) { String key = datum.getKey(); String mappedValue; Transform xfrm = transforms.get(key); if (null != xfrm) { try { mappedValue = xfrm.transform(value.getValue()); } catch (Exception ex) { log.error("Unable to transform '{}' from {} to {}: {}", new String[] { value.getValue(), value.toString(), key, ex.getMessage() }); continue; } } else { mappedValue = value.getValue(); } mapped.put(key, mappedValue); } } } if (GENERATE_DATACITE_XML == true) { DataCiteXMLCreator xmlGen = new DataCiteXMLCreator(); xmlGen.setDisseminationCrosswalkName(DATACITE_XML_CROSSWALK); String xmlString = xmlGen.getXMLString(context, dso); log.debug("Generated DataCite XML: {}", xmlString); mapped.put("datacite", xmlString); } // Supply a default publisher, if the Item has none. if (!mapped.containsKey(DATACITE_PUBLISHER) && !mapped.containsKey("datacite")) { String publisher = configurationService.getPropertyAsType(CFG_PUBLISHER, "unknown"); log.info("Supplying default publisher: {}", publisher); mapped.put(DATACITE_PUBLISHER, publisher); } // Supply current year as year of publication, if the Item has none. if (!mapped.containsKey(DATACITE_PUBLICATION_YEAR) && !mapped.containsKey("datacite")) { String year = String.valueOf(Calendar.getInstance().get(Calendar.YEAR)); log.info("Supplying default publication year: {}", year); mapped.put(DATACITE_PUBLICATION_YEAR, year); } // Supply _target link back to this object String handle = dso.getHandle(); if (null == handle) { log.warn("{} #{} has no handle -- location not set.", contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso), dso.getID()); } else { String url = configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle(); log.info("Supplying location: {}", url); mapped.put("_target", url); } return mapped; } /** * Provide a map from DSO metadata keys to EZID keys. This will drive the * generation of EZID metadata for the minting of new identifiers. * * @param aCrosswalk map of metadata fields to EZID keys */ @Autowired(required = true) public void setCrosswalk(Map aCrosswalk) { crosswalk = aCrosswalk; } public Map getCrosswalk() { return crosswalk; } /** * Provide a map from DSO metadata keys to classes which can transform their * values to something acceptable to EZID. * * @param transformMap map of metadata fields to EZID transformation classes */ public void setCrosswalkTransform(Map transformMap) { transforms = transformMap; } public void setGenerateDataciteXML(boolean GENERATE_DATACITE_XML) { this.GENERATE_DATACITE_XML = GENERATE_DATACITE_XML; } public void setDisseminationCrosswalkName(String DATACITE_XML_CROSSWALK) { this.DATACITE_XML_CROSSWALK = DATACITE_XML_CROSSWALK; } @Autowired(required = true) public void setRequestFactory(EZIDRequestFactory aRequestFactory) { requestFactory = aRequestFactory; } public EZIDRequestFactory getRequestFactory() { return requestFactory; } /** * Method should never be used aside from the unit tests where we can cannot autowire this class. * * @param itemService itemService instance */ protected void setItemService(ItemService itemService) { this.itemService = itemService; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy