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 5ab72d9488..8ab15998f3 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/DOI.java +++ b/dspace-api/src/main/java/org/dspace/identifier/DOI.java @@ -16,4 +16,5 @@ package org.dspace.identifier; public class DOI implements Identifier { + public static final String SCHEME = "doi:"; } diff --git a/dspace-api/src/main/java/org/dspace/identifier/DataCiteIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/DataCiteIdentifierProvider.java index 015a1afd32..4343fbc5c6 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/DataCiteIdentifierProvider.java +++ b/dspace-api/src/main/java/org/dspace/identifier/DataCiteIdentifierProvider.java @@ -11,10 +11,11 @@ package org.dspace.identifier; import java.io.IOException; import java.net.*; import java.sql.SQLException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.logging.Level; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DCValue; import org.dspace.content.DSpaceObject; @@ -60,15 +61,17 @@ public class DataCiteIdentifierProvider private static final Logger log = LoggerFactory.getLogger(DataCiteIdentifierProvider.class); // Configuration property names - private static final String CFG_SHOULDER = "identifier.doi.ezid.shoulder"; - private static final String CFG_USER = "identifier.doi.ezid.user"; - private static final String CFG_PASSWORD = "identifier.doi.ezid.password"; + 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"; // Metadata field name elements // XXX move these to MetadataSchema or some such - public static final String MD_SCHEMA_DSPACE = "dspace"; - public static final String DSPACE_DOI_ELEMENT = "identifier"; - public static final String DSPACE_DOI_QUALIFIER = "doi"; + 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:"; /** Map DataCite metadata into local metadata. */ private static Map crosswalk = new HashMap(); @@ -85,7 +88,10 @@ public class DataCiteIdentifierProvider @Override public boolean supports(String identifier) { - return identifier.startsWith("doi:"); // XXX more thorough test? + if (null == identifier) + return false; + else + return identifier.startsWith(DOI_SCHEME); // XXX more thorough test? } @Override @@ -94,22 +100,20 @@ public class DataCiteIdentifierProvider { log.debug("register {}", dso); - Item item; - - if (dso instanceof Item) - item = (Item)dso; - else + if (!(dso instanceof Item)) throw new IdentifierException("Unsupported object type " + dso.getTypeText()); - String id; - DCValue[] previous = item.getMetadata(MD_SCHEMA_DSPACE, DSPACE_DOI_ELEMENT, DSPACE_DOI_QUALIFIER, null); - if ((previous.length > 0) && (null != previous[0].value)) - return previous[0].value; + Item item = (Item)dso; + DCValue[] identifiers = item.getMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); + for (DCValue identifier : identifiers) + if ((null != identifier.value) && (identifier.value.startsWith(DOI_SCHEME))) + return identifier.value; - id = mint(context, item); - item.addMetadata(MD_SCHEMA_DSPACE, DSPACE_DOI_ELEMENT, DSPACE_DOI_QUALIFIER, null, id); + String id = mint(context, item); + item.addMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, id); try { item.update(); + context.commit(); } catch (SQLException ex) { throw new IdentifierException("New identifier not stored", ex); } catch (AuthorizeException ex) { @@ -132,30 +136,29 @@ public class DataCiteIdentifierProvider } EZIDResponse response; - String doi = "unknown"; // In case we can't even build a name try { - doi = getShoulder() + identifier; - EZIDRequest request = requestFactory.getInstance(doi, - getUser(), getPassword()); - response = request.create(crosswalkMetadata(object)); + EZIDRequest request = requestFactory.getInstance(loadAuthority(), + loadUser(), loadPassword()); + response = request.create(identifier, crosswalkMetadata(object)); } catch (IdentifierException e) { - log.error("doi:{} not registered: {}", doi, e.getMessage()); + log.error("Identifier '{}' not registered: {}", identifier, e.getMessage()); return; } catch (IOException e) { - log.error("doi:{} not registered: {}", doi, e.getMessage()); + log.error("Identifier '{}' not registered: {}", identifier, e.getMessage()); return; } catch (URISyntaxException e) { - log.error("doi:{} not registered: {}", doi, e.getMessage()); + log.error("Identifier '{}' not registered: {}", identifier, e.getMessage()); return; } if (response.isSuccess()) { Item item = (Item)object; - item.addMetadata(MD_SCHEMA_DSPACE, DSPACE_DOI_ELEMENT, - DSPACE_DOI_QUALIFIER, null, identifier); try { + item.addMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, + idToDOI(identifier)); item.update(); + context.commit(); log.info("registered {}", identifier); } catch (SQLException ex) { // TODO throw new IdentifierException("New identifier not stored", ex); @@ -163,12 +166,14 @@ public class DataCiteIdentifierProvider } catch (AuthorizeException ex) { // TODO throw new IdentifierException("New identifier not stored", ex); log.error("New identifier not stored", ex); + } catch (IdentifierException ex) { + log.error("New identifier not stored", ex); } } else { - log.error("doi:{} not registered -- EZID returned: {}", doi, - response.getEZIDStatusValue()); + log.error("Identifier '{}' not registered -- EZID returned: {}", + identifier, response.getEZIDStatusValue()); } } @@ -179,29 +184,27 @@ public class DataCiteIdentifierProvider log.debug("reserve {}", identifier); EZIDResponse response; - String doi = "unknown"; // In case we can't even build a name try { - doi = getShoulder() + identifier; - EZIDRequest request = requestFactory.getInstance(doi, - getUser(), getPassword()); + EZIDRequest request = requestFactory.getInstance(loadAuthority(), + loadUser(), loadPassword()); Map metadata = crosswalkMetadata(dso); metadata.put("_status", "reserved"); - response = request.create(metadata); + response = request.create(identifier, metadata); } catch (IOException e) { - log.error("doi:{} not registered: {}", doi, e.getMessage()); + log.error("Identifier '{}' not registered: {}", identifier, e.getMessage()); return; } catch (URISyntaxException e) { - log.error("doi:{} not registered: {}", doi, e.getMessage()); + log.error("Identifier '{}' not registered: {}", identifier, e.getMessage()); return; } if (response.isSuccess()) { Item item = (Item)dso; - item.addMetadata(MD_SCHEMA_DSPACE, DSPACE_DOI_ELEMENT, - DSPACE_DOI_QUALIFIER, null, identifier); + item.addMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, idToDOI(identifier)); try { item.update(); + context.commit(); log.info("reserved {}", identifier); } catch (SQLException ex) { throw new IdentifierException("New identifier not stored", ex); @@ -211,8 +214,8 @@ public class DataCiteIdentifierProvider } else { - log.error("doi:{} not registered -- EZID returned: {}", doi, - response.getEZIDStatusValue()); + log.error("Identifier '{}' not registered -- EZID returned: {}", + identifier, response.getEZIDStatusValue()); } } @@ -225,7 +228,7 @@ public class DataCiteIdentifierProvider // Compose the request EZIDRequest request; try { - request = requestFactory.getInstance(getShoulder(), getUser(), getPassword()); + request = requestFactory.getInstance(loadAuthority(), loadUser(), loadPassword()); } catch (URISyntaxException ex) { log.error(ex.getMessage()); throw new IdentifierException("DOI request not sent: " + ex.getMessage()); @@ -236,8 +239,10 @@ public class DataCiteIdentifierProvider try { response = request.mint(crosswalkMetadata(dso)); - } catch (IOException ex) - { + } catch (IOException ex) { + log.error("Failed to send EZID request: {}", ex.getMessage()); + throw new IdentifierException("DOI request not sent: " + ex.getMessage()); + } catch (URISyntaxException ex) { log.error("Failed to send EZID request: {}", ex.getMessage()); throw new IdentifierException("DOI request not sent: " + ex.getMessage()); } @@ -275,27 +280,33 @@ public class DataCiteIdentifierProvider { log.debug("resolve {}", identifier); - try - { - ItemIterator found = Item.findByMetadataField(context, MD_SCHEMA_DSPACE, DSPACE_DOI_ELEMENT, DSPACE_DOI_QUALIFIER, - identifier); + ItemIterator found; + try { + found = Item.findByMetadataField(context, + MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, + idToDOI(identifier)); + } catch (IdentifierException ex) { + log.error(ex.getMessage()); + throw new IdentifierNotResolvableException(ex); + } catch (SQLException ex) { + log.error(ex.getMessage()); + throw new IdentifierNotResolvableException(ex); + } catch (AuthorizeException ex) { + log.error(ex.getMessage()); + throw new IdentifierNotResolvableException(ex); + } catch (IOException ex) { + log.error(ex.getMessage()); + throw new IdentifierNotResolvableException(ex); + } + try { if (!found.hasNext()) - throw new IdentifierNotFoundException("No Item bound to DOI " + identifier); + throw new IdentifierNotFoundException("No object bound to " + identifier); Item found1 = found.next(); if (found.hasNext()) - log.error("DOI {} multiply bound!", identifier); + log.error("More than one object bound to {}!", identifier); log.debug("Resolved to {}", found1); return found1; - } catch (SQLException ex) - { - log.error(ex.getMessage()); - throw new IdentifierNotResolvableException(ex); - } catch (AuthorizeException ex) - { - log.error(ex.getMessage()); - throw new IdentifierNotResolvableException(ex); - } catch (IOException ex) - { + } catch (SQLException ex) { log.error(ex.getMessage()); throw new IdentifierNotResolvableException(ex); } @@ -307,16 +318,21 @@ public class DataCiteIdentifierProvider { log.debug("lookup {}", object); - Item item; if (!(object instanceof Item)) throw new IllegalArgumentException("Unsupported type " + object.getTypeText()); - item = (Item)object; - DCValue[] metadata = item.getMetadata(MD_SCHEMA_DSPACE, DSPACE_DOI_ELEMENT, DSPACE_DOI_QUALIFIER, null); - if (metadata.length > 0) + Item item = (Item)object; + DCValue found = null; + for (DCValue candidate : item.getMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null)) + if (candidate.value.startsWith(DOI_SCHEME)) + { + found = candidate; + break; + } + if (null != found) { - log.debug("Found {}", metadata[0].value); - return metadata[0].value; + log.debug("Found {}", found.value); + return found.value; } else throw new IdentifierNotFoundException(object.getTypeText() + " " @@ -332,34 +348,62 @@ public class DataCiteIdentifierProvider if (!(dso instanceof Item)) throw new IllegalArgumentException("Unsupported type " + dso.getTypeText()); - String username = configurationService.getProperty(CFG_USER); - String password = configurationService.getProperty(CFG_PASSWORD); - if (null == username || null == password) - throw new IdentifierException("Unconfigured: define " + CFG_USER - + " and " + CFG_PASSWORD); - Item item = (Item)dso; // delete from EZID - for (DCValue id : item.getMetadata(MD_SCHEMA_DSPACE, DSPACE_DOI_ELEMENT, - DSPACE_DOI_QUALIFIER, null)) + DCValue[] metadata = item.getMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); + List remainder = new ArrayList(); + int skipped = 0; + for (DCValue id : metadata) { + if (!id.value.startsWith(DOI_SCHEME)) + { + remainder.add(id.value); + continue; + } + EZIDResponse response; try { - EZIDRequest request = requestFactory.getInstance(id.value, username, password); - response = request.delete(); + EZIDRequest request = requestFactory.getInstance(loadAuthority(), + loadUser(), loadPassword()); + response = request.delete(DOIToId(id.value)); } catch (URISyntaxException e) { - throw new IdentifierException("Bad URI in metadata value", e); + log.error("Bad URI in metadata value: {}", e.getMessage()); + remainder.add(id.value); + skipped++; + continue; } catch (IOException e) { - throw new IdentifierException("Failed request to EZID", e); + log.error("Failed request to EZID: {}", e.getMessage()); + remainder.add(id.value); + skipped++; + continue; } if (!response.isSuccess()) - throw new IdentifierException("Unable to delete " + id.value - + "from DataCite: " + response.getEZIDStatusValue()); + { + log.error("Unable to delete {} from DataCite: {}", id.value, + response.getEZIDStatusValue()); + remainder.add(id.value); + skipped++; + continue; + } + log.info("Deleted {}", id.value); } // delete from item - item.clearMetadata(MD_SCHEMA_DSPACE, DSPACE_DOI_ELEMENT, DSPACE_DOI_QUALIFIER, null); + item.clearMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); + item.addMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, + remainder.toArray(new String[remainder.size()])); + try { + item.update(); + context.commit(); + } catch (SQLException e) { + log.error("Failed to re-add identifiers: {}", e.getMessage()); + } catch (AuthorizeException e) { + log.error("Failed to re-add identifiers: {}", e.getMessage()); + } + + if (skipped > 0) + throw new IdentifierException(skipped + " identifiers could not be deleted."); } @Override @@ -368,14 +412,98 @@ public class DataCiteIdentifierProvider { log.debug("delete {} from {}", identifier, dso); - throw new UnsupportedOperationException("Not supported yet."); // TODO implement delete(specific) - // TODO find metadata value == identifier - // TODO delete from EZID + if (!(dso instanceof Item)) + throw new IllegalArgumentException("Unsupported type " + dso.getTypeText()); - // TODO delete from item NOTE!!! can't delete single MD values! + Item item = (Item)dso; + + DCValue[] metadata = item.getMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); + List remainder = new ArrayList(); + int skipped = 0; + for (DCValue id : metadata) + { + if (!id.value.equals(idToDOI(identifier))) + { + remainder.add(id.value); + continue; + } + + EZIDResponse response; + try { + EZIDRequest request = requestFactory.getInstance(loadAuthority(), + loadUser(), loadPassword()); + response = request.delete(DOIToId(id.value)); + } catch (URISyntaxException e) { + log.error("Bad URI in metadata value {}: {}", id.value, e.getMessage()); + remainder.add(id.value); + skipped++; + continue; + } catch (IOException e) { + log.error("Failed request to EZID: {}", e.getMessage()); + remainder.add(id.value); + skipped++; + continue; + } + + if (!response.isSuccess()) + { + log.error("Unable to delete {} from DataCite: {}", id.value, + response.getEZIDStatusValue()); + remainder.add(id.value); + skipped++; + continue; + } + log.info("Deleted {}", id.value); + } + + // delete from item + item.clearMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null); + item.addMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, + remainder.toArray(new String[remainder.size()])); + try { + item.update(); + context.commit(); + } catch (SQLException e) { + log.error("Failed to re-add identifiers: {}", e.getMessage()); + } catch (AuthorizeException e) { + log.error("Failed to re-add identifiers: {}", e.getMessage()); + } + + if (skipped > 0) + throw new IdentifierException(identifier + " could not be deleted."); } - private String getUser() + /** + * 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 + */ + private String loadUser() throws IdentifierException { String user = configurationService.getProperty(CFG_USER); @@ -385,7 +513,11 @@ public class DataCiteIdentifierProvider throw new IdentifierException("Unconfigured: define " + CFG_USER); } - private String getPassword() + /** + * Get configured value of EZID password. + * @throws IdentifierException + */ + private String loadPassword() throws IdentifierException { String password = configurationService.getProperty(CFG_PASSWORD); @@ -395,7 +527,11 @@ public class DataCiteIdentifierProvider throw new IdentifierException("Unconfigured: define " + CFG_PASSWORD); } - private String getShoulder() + /** + * Get configured value of EZID "shoulder". + * @throws IdentifierException + */ + private String loadAuthority() throws IdentifierException { String shoulder = configurationService.getProperty(CFG_SHOULDER); @@ -423,21 +559,18 @@ public class DataCiteIdentifierProvider for (DCValue value : values) mapped.put(datum.getKey(), value.value); } + + // TODO find a way to get a current direct URL to the object and set _target + // mapped.put("_target", url); + return mapped; } - /** - * @param aCrosswalk the crosswalk to set - */ @Required public void setCrosswalk(Map aCrosswalk) { crosswalk = aCrosswalk; } - - /** - * @param aRequestFactory the requestFactory to set - */ @Required public static void setRequestFactory(EZIDRequestFactory aRequestFactory) { diff --git a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java index 33d734d6f3..6a39833530 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java +++ b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java @@ -11,7 +11,6 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; -import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; @@ -25,6 +24,7 @@ import org.apache.http.client.methods.HttpPut; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.AbstractHttpClient; import org.apache.http.impl.client.DefaultHttpClient; +import org.dspace.identifier.DOI; import org.dspace.identifier.IdentifierException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,42 +38,63 @@ public class EZIDRequest { private static final Logger log = LoggerFactory.getLogger(EZIDRequest.class); - private URI url; + private static final String ID_PATH = "/ezid/id/" + DOI.SCHEME; - private AbstractHttpClient client; + private static final String SHOULDER_PATH = "/ezid/shoulder/" + DOI.SCHEME; + + private static final String UTF_8 = "UTF-8"; + + private static final String MD_KEY_STATUS = "_status"; + + private final AbstractHttpClient client; + + private final String scheme; + + private final String host; + + private final String authority; /** * Prepare a context for requests concerning a specific identifier or * authority prefix. - * - * @param url EZID API service point (and object) for this request. + * + * @param scheme + * @param host + * @param authority DOI authority prefix. * @param username an EZID user identity. - * @param password user's password, or null for none. + * @param password user's password, or {@code null} for none. + * @throws URISyntaxException if host or authority is bad. */ - EZIDRequest(URI url, String username, String password) + EZIDRequest(String scheme, String host, String authority, String username, String password) throws URISyntaxException { - this.url = url; + this.scheme = scheme; + this.host = host; + this.authority = authority; client = new DefaultHttpClient(); if (null != username) + { + URI uri = new URI(scheme, host, null, null); client.getCredentialsProvider().setCredentials( - new AuthScope(url.getHost(), url.getPort()), + new AuthScope(uri.getHost(), uri.getPort()), new UsernamePasswordCredentials(username, password)); + } } /** - * Fetch an identifier's metadata. + * Fetch the metadata bound to an identifier. * - * @return * @throws IdentifierException if the response is error or body malformed. * @throws IOException if the HTTP request fails. + * @throws URISyntaxException */ - public EZIDResponse lookup() - throws IdentifierException, IOException + public EZIDResponse lookup(String name) + throws IdentifierException, IOException, URISyntaxException { // GET path HttpGet request; - request = new HttpGet(url); + URI uri = new URI(scheme, host, ID_PATH + authority + name, null); + request = new HttpGet(uri); HttpResponse response = client.execute(request); return new EZIDResponse(response); } @@ -86,16 +107,17 @@ public class EZIDRequest * @param metadata ANVL-encoded key/value pairs. * @return */ - public EZIDResponse create(Map metadata) - throws IOException, IdentifierException + public EZIDResponse create(String name, Map metadata) + throws IOException, IdentifierException, URISyntaxException { // PUT path [+metadata] HttpPut request; - request = new HttpPut(url); + URI uri = new URI(scheme, host, ID_PATH + authority + name, null); + request = new HttpPut(uri); if (null != metadata) { try { - request.setEntity(new StringEntity(formatMetadata(metadata), "UTF-8")); + request.setEntity(new StringEntity(formatMetadata(metadata), UTF_8)); } catch (UnsupportedEncodingException ex) { /* SNH */ } } HttpResponse response = client.execute(request); @@ -110,30 +132,30 @@ public class EZIDRequest * @return */ public EZIDResponse mint(Map metadata) - throws IOException, IdentifierException + throws IOException, IdentifierException, URISyntaxException { // POST path [+metadata] HttpPost request; - request = new HttpPost(url); + URI uri = new URI(scheme, host, SHOULDER_PATH + authority, null); + request = new HttpPost(uri); if (null != metadata) { - request.setEntity(new StringEntity(formatMetadata(metadata), "UTF-8")); + request.setEntity(new StringEntity(formatMetadata(metadata), UTF_8)); } HttpResponse response = client.execute(request); EZIDResponse myResponse = new EZIDResponse(response); - // TODO add the identifier to the path for subsequent operations? return myResponse; } /** - * Alter the identifier's metadata. + * Alter the metadata bound to an identifier. * - * @param metadata fields to be altered. Leave a field's value empty to - * delete the field. + * @param metadata fields to be altered. Leave the value of a field's empty + * to delete the field. * @return */ - public EZIDResponse modify(Map metadata) - throws IOException, IdentifierException + public EZIDResponse modify(String name, Map metadata) + throws IOException, IdentifierException, URISyntaxException { if (null == metadata) { @@ -141,8 +163,9 @@ public class EZIDRequest } // POST path +metadata HttpPost request; - request = new HttpPost(url); - request.setEntity(new StringEntity(formatMetadata(metadata), "UTF-8")); + URI uri = new URI(scheme, host, ID_PATH + authority + name, null); + request = new HttpPost(uri); + request.setEntity(new StringEntity(formatMetadata(metadata), UTF_8)); HttpResponse response = client.execute(request); return new EZIDResponse(response); } @@ -150,12 +173,13 @@ public class EZIDRequest /** * Destroy a reserved identifier. Fails if ID was ever public. */ - public EZIDResponse delete() - throws IOException, IdentifierException + public EZIDResponse delete(String name) + throws IOException, IdentifierException, URISyntaxException { // DELETE path HttpDelete request; - request = new HttpDelete(url); + URI uri = new URI(scheme, host, ID_PATH + authority + name, null); + request = new HttpDelete(uri); HttpResponse response = client.execute(request); return new EZIDResponse(response); } @@ -163,12 +187,12 @@ public class EZIDRequest /** * Remove a public identifier from view. */ - public EZIDResponse withdraw() - throws IOException, IdentifierException + public EZIDResponse withdraw(String name) + throws IOException, IdentifierException, URISyntaxException { Map metadata = new HashMap(); - metadata.put("_status", "unavailable"); - return modify(metadata); + metadata.put(MD_KEY_STATUS, "unavailable"); + return modify(name, metadata); } /** @@ -176,38 +200,39 @@ public class EZIDRequest * * @param reason annotation for the item's unavailability. */ - public EZIDResponse withdraw(String reason) - throws IOException, IdentifierException + public EZIDResponse withdraw(String name, String reason) + throws IOException, IdentifierException, URISyntaxException { - String reasonEncoded = null; - try { - reasonEncoded = URLEncoder.encode(reason, "UTF-8"); - } catch (UnsupportedEncodingException e) { /* XXX SNH */ } Map metadata = new HashMap(); - metadata.put("_status", "unavailable | " + reasonEncoded); - return modify(metadata); + metadata.put(MD_KEY_STATUS, "unavailable | " + escape(reason)); + return modify(name, metadata); } /** * Create ANVL-formatted name/value pairs from a Map. */ - private String formatMetadata(Map raw) + private static String formatMetadata(Map raw) { StringBuilder formatted = new StringBuilder(); for (Entry entry : raw.entrySet()) - formatted.append(entry.getKey()) + { + formatted.append(escape(entry.getKey())) .append(": ") - .append(entry.getValue()) + .append(escape(entry.getValue())) .append('\n'); - - // Body should be percent-encoded - String body = null; - try { - body = URLEncoder.encode(formatted.toString(), "UTF-8"); - } catch (UnsupportedEncodingException ex) { // XXX SNH - log.error(ex.getMessage()); - } finally { - return body; } + + return formatted.toString(); + } + + /** + * Percent-encode a few EZID-specific characters. + */ + private static String escape(String s) + { + return s.replace("%", "%25") + .replace("\n", "%0A") + .replace("\r", "%0D") + .replace(":", "%3A"); } } diff --git a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequestFactory.java b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequestFactory.java index 2dd46c934a..14f2b116b1 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequestFactory.java +++ b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequestFactory.java @@ -9,7 +9,6 @@ package org.dspace.identifier.ezid; import java.net.URISyntaxException; -import org.apache.http.client.utils.URIBuilder; import org.springframework.beans.factory.annotation.Required; /** @@ -32,42 +31,19 @@ public class EZIDRequestFactory { private static String EZID_SCHEME; private static String EZID_HOST; - private static String EZID_PATH; /** * Configure an EZID request. * - * @param requestPath specific request (DOI, shoulder). - * @param username - * @param password + * @param authority our DOI authority number. + * @param username EZID user name. + * @param password {@code username}'s password. * @throws URISyntaxException */ - public EZIDRequest getInstance(String requestPath, String username, String password) + public EZIDRequest getInstance(String authority, String username, String password) throws URISyntaxException { - URIBuilder uri = new URIBuilder(); - - uri.setScheme(EZID_SCHEME); - - uri.setHost(EZID_HOST); - - String head, tail; - if (EZID_PATH.endsWith("/")) - head = EZID_PATH.substring(0, EZID_PATH.length() - 1); - else - head = EZID_PATH; - if (requestPath.startsWith("/")) - tail = requestPath.substring( 0, requestPath.length() - 1); - else - tail = requestPath; - - StringBuilder path = new StringBuilder(); - path.append(head); - path.append('/'); - path.append(tail); - uri.setPath(path.toString()); - - return new EZIDRequest(uri.build(), username, password); + return new EZIDRequest(EZID_SCHEME, EZID_HOST, authority, username, password); } /** @@ -87,13 +63,4 @@ public class EZIDRequestFactory { EZID_HOST = aEZID_HOST; } - - /** - * @param aEZID_PATH the EZID path to set - */ - @Required - public static void setEZID_PATH(String aEZID_PATH) - { - EZID_PATH = aEZID_PATH; - } } diff --git a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDResponse.java b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDResponse.java index b3f7a2c276..1f2af8d523 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDResponse.java +++ b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDResponse.java @@ -29,6 +29,8 @@ public class EZIDResponse { private static final Logger log = LoggerFactory.getLogger(EZIDResponse.class); + private static final String UTF_8 = "UTF-8"; + private final String status; private final String statusValue; @@ -48,7 +50,7 @@ public class EZIDResponse String body; try { - body = EntityUtils.toString(responseBody, "UTF-8"); + body = EntityUtils.toString(responseBody, UTF_8); } catch (IOException ex) { log.error(ex.getMessage()); @@ -83,10 +85,10 @@ public class EZIDResponse parts = lines[i].split(":", 2); String key = null, value = null; try { - key = URLDecoder.decode(parts[0], "UTF-8").trim(); + key = URLDecoder.decode(parts[0], UTF_8).trim(); if (parts.length > 1) { - value = URLDecoder.decode(parts[1], "UTF-8").trim(); + value = URLDecoder.decode(parts[1], UTF_8).trim(); } else { @@ -120,7 +122,7 @@ public class EZIDResponse } /** - * Value associated with the EZID status (identifier, error text, etc.) + * Value associated with the EZID status (identifier, error text, etc.). */ public String getEZIDStatusValue() { diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-identifier-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-identifier-services.xml index 51394e5326..8cc1faf7e7 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-identifier-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-identifier-services.xml @@ -22,12 +22,11 @@ - - + diff --git a/dspace-api/src/test/java/org/dspace/identifier/DataCiteIdentifierProviderTest.java b/dspace-api/src/test/java/org/dspace/identifier/DataCiteIdentifierProviderTest.java index e3542c1294..fdf86df6a6 100644 --- a/dspace-api/src/test/java/org/dspace/identifier/DataCiteIdentifierProviderTest.java +++ b/dspace-api/src/test/java/org/dspace/identifier/DataCiteIdentifierProviderTest.java @@ -8,16 +8,17 @@ package org.dspace.identifier; -import java.util.List; +import java.io.IOException; +import java.sql.SQLException; +import java.util.UUID; import org.dspace.AbstractUnitTest; -import org.dspace.content.Collection; -import org.dspace.content.Community; -import org.dspace.content.DSpaceObject; -import org.dspace.content.Item; -import org.dspace.content.WorkspaceItem; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.*; import org.dspace.core.Context; import org.dspace.kernel.ServiceManager; import org.dspace.services.ConfigurationService; +import org.dspace.workflow.WorkflowItem; +import org.dspace.workflow.WorkflowManager; import org.junit.*; import static org.junit.Assert.*; @@ -28,29 +29,85 @@ import static org.junit.Assert.*; public class DataCiteIdentifierProviderTest extends AbstractUnitTest { - private static final String TEST_SHOULDER = "doi:10.5072/FK2"; + /** Name of the reserved EZID test authority */ + private static final String TEST_SHOULDER = "10.5072/FK2"; private static ServiceManager sm = null; private static ConfigurationService config = null; - private static Item item = null; + private static Community community; + + private static Collection collection; + + /** The most recently created test Item's ID */ + private static int itemID; public DataCiteIdentifierProviderTest() { } + private static void dumpMetadata(Item eyetem) + { + DCValue[] metadata = eyetem.getMetadata("dc", Item.ANY, Item.ANY, Item.ANY); + for (DCValue metadatum : metadata) + System.out.printf("Metadata: %s.%s.%s(%s) = %s\n", + metadatum.schema, + metadatum.element, + metadatum.qualifier, + metadatum.language, + metadatum.value); + } + + /** + * Create a fresh Item, installed in the repository. + * + * @throws SQLException + * @throws AuthorizeException + * @throws IOException + */ + private Item newItem(Context ctx) + throws SQLException, AuthorizeException, IOException + { + ctx.turnOffAuthorisationSystem(); + ctx.setCurrentUser(eperson); + + WorkspaceItem wsItem = WorkspaceItem.create(ctx, collection, false); + + WorkflowItem wfItem = WorkflowManager.start(ctx, wsItem); + WorkflowManager.advance(ctx, wfItem, ctx.getCurrentUser()); + + Item item = wfItem.getItem(); + item.addMetadata("dc", "contributor", "author", null, "Author, A. N."); + item.addMetadata("dc", "title", null, null, "A Test Object"); + item.addMetadata("dc", "publisher", null, null, "DSpace Test Harness"); + item.update(); + itemID = item.getID(); + + ctx.commit(); + ctx.restoreAuthSystemState(); + + return item; + } + @BeforeClass public static void setUpClass() throws Exception { - // Create an object to work with Context ctx = new Context(); ctx.turnOffAuthorisationSystem(); - Community community = Community.create(null, ctx); - Collection collection = community.createCollection(); - WorkspaceItem wsItem = WorkspaceItem.create(ctx, collection, false); - item = wsItem.getItem(); + + ctx.setCurrentUser(eperson); + + // Create an environment for our test objects to live in. + community = Community.create(null, ctx); + community.setMetadata("name", "A Test Community"); + community.update(); + + collection = community.createCollection(); + collection.setMetadata("name", "A Test Collection"); + collection.update(); + ctx.complete(); // Find the usual kernel services @@ -58,26 +115,35 @@ public class DataCiteIdentifierProviderTest config = kernelImpl.getConfigurationService(); - // Configure the service under test - config.setProperty("identifier.doi.ezid.shoulder", TEST_SHOULDER); - config.setProperty("identifier.doi.ezid.user", "apitest"); - config.setProperty("identifier.doi.ezid.password", "apitest"); + // Configure the service under test. + config.setProperty(DataCiteIdentifierProvider.CFG_SHOULDER, TEST_SHOULDER); + config.setProperty(DataCiteIdentifierProvider.CFG_USER, "apitest"); + config.setProperty(DataCiteIdentifierProvider.CFG_PASSWORD, "apitest"); + + // Don't try to send mail. + config.setProperty("mail.server.disabled", "true"); } @AfterClass public static void tearDownClass() throws Exception { + System.out.print("Tearing down\n\n"); + Context ctx = new Context(); + dumpMetadata(Item.find(ctx, itemID)); } @Before public void setUp() { + context.setCurrentUser(eperson); + context.turnOffAuthorisationSystem(); } @After public void tearDown() { + context.restoreAuthSystemState(); } /** @@ -103,7 +169,7 @@ public class DataCiteIdentifierProviderTest System.out.println("supports"); DataCiteIdentifierProvider instance = new DataCiteIdentifierProvider(); - String identifier = TEST_SHOULDER; + String identifier = "doi:" + TEST_SHOULDER; boolean result = instance.supports(identifier); assertTrue(identifier + " should be supported", result); } @@ -115,16 +181,16 @@ public class DataCiteIdentifierProviderTest public void testRegister_Context_DSpaceObject() throws Exception { - System.out.println("register"); + System.out.println("register 2"); List instance = (List) sm.getServicesByType(DataCiteIdentifierProvider.class); - DSpaceObject dso = item; + DSpaceObject dso = newItem(context); - String result = instance.get(0).register(context, dso); - assertTrue("Didn't get a DOI back", result.startsWith("doi:10.5072/")); + String result = instance.register(context, dso); + assertTrue("Didn't get a DOI back", result.startsWith("doi:" + TEST_SHOULDER)); System.out.println(" got identifier: " + result); } @@ -133,18 +199,17 @@ public class DataCiteIdentifierProviderTest */ @Test public void testRegister_3args() + throws SQLException, AuthorizeException, IOException { - System.out.println("register"); - // TODO review the generated test code and remove the default call to fail. - fail("The test case is a prototype."); + System.out.println("register 3"); List instances = (List) sm.getServicesByType(DataCiteIdentifierProvider.class); - DSpaceObject object = item; + DSpaceObject object = newItem(context); - String identifier = TEST_SHOULDER + "blarg"; // TODO a unique value + String identifier = UUID.randomUUID().toString(); instances.get(0).register(context, object, identifier); } @@ -157,13 +222,11 @@ public class DataCiteIdentifierProviderTest throws Exception { System.out.println("reserve"); - // TODO review the generated test code and remove the default call to fail. - fail("The test case is a prototype."); DataCiteIdentifierProvider instance = new DataCiteIdentifierProvider(); - DSpaceObject dso = item; - String identifier = ""; + DSpaceObject dso = newItem(context); + String identifier = UUID.randomUUID().toString(); instance.reserve(context, dso, identifier); } @@ -180,8 +243,7 @@ public class DataCiteIdentifierProviderTest DataCiteIdentifierProvider instance = new DataCiteIdentifierProvider(); - DSpaceObject dso = item; - String expResult = ""; + DSpaceObject dso = newItem(context); String result = instance.mint(context, dso); assertEquals(expResult, result); } @@ -194,18 +256,18 @@ public class DataCiteIdentifierProviderTest throws Exception { System.out.println("resolve"); - // TODO review the generated test code and remove the default call to fail. - fail("The test case is a prototype."); DataCiteIdentifierProvider instance = new DataCiteIdentifierProvider(); - String identifier = ""; + String identifier = UUID.randomUUID().toString(); + DSpaceObject expResult = newItem(context); + instance.register(context, expResult, identifier); + String[] attributes = null; - DSpaceObject expResult = null; DSpaceObject result = instance.resolve(context, identifier, attributes); assertEquals(expResult, result); } - + /** * Test of lookup method, of class DataCiteIdentifierProvider. */ @@ -219,8 +281,10 @@ public class DataCiteIdentifierProviderTest DataCiteIdentifierProvider instance = new DataCiteIdentifierProvider(); - DSpaceObject object = item; - String expResult = ""; + String identifier = UUID.randomUUID().toString(); + DSpaceObject object = newItem(context); + instance.register(context, object, identifier); + String result = instance.lookup(context, object); assertEquals(expResult, result); } @@ -232,31 +296,66 @@ public class DataCiteIdentifierProviderTest public void testDelete_Context_DSpaceObject() throws Exception { - System.out.println("delete"); - // TODO review the generated test code and remove the default call to fail. - fail("The test case is a prototype."); + System.out.println("delete 2"); DataCiteIdentifierProvider instance = new DataCiteIdentifierProvider(); - DSpaceObject dso = item; - instance.delete(context, dso); + DSpaceObject dso = newItem(context); + + // Ensure that it has multiple DOIs (ooo, bad boy!) + String id1 = UUID.randomUUID().toString(); + String id2 = UUID.randomUUID().toString(); + instance.reserve(context, dso, id1); + instance.reserve(context, dso, id2); + + // Test deletion + try { + instance.delete(context, dso); + } catch (IdentifierException e) { + // Creation of the Item registers a "public" identifier, which can't be deleted. + assertEquals("Unexpected exception", "1 identifiers could not be deleted.", e.getMessage()); + } + + // See if those identifiers were really deleted. + ItemIterator found; + found = Item.findByMetadataField(context, + DataCiteIdentifierProvider.MD_SCHEMA, + DataCiteIdentifierProvider.DOI_ELEMENT, + DataCiteIdentifierProvider.DOI_QUALIFIER, id1); + assertFalse("A test identifier is still present", found.hasNext()); + + found = Item.findByMetadataField(context, + DataCiteIdentifierProvider.MD_SCHEMA, + DataCiteIdentifierProvider.DOI_ELEMENT, + DataCiteIdentifierProvider.DOI_QUALIFIER, id2); + assertFalse("A test identifier is still present", found.hasNext()); } /** * Test of delete method, of class DataCiteIdentifierProvider. */ - @Test + @Test() public void testDelete_3args() throws Exception { - System.out.println("delete"); - // TODO review the generated test code and remove the default call to fail. - fail("The test case is a prototype."); + System.out.println("delete 3"); DataCiteIdentifierProvider instance = new DataCiteIdentifierProvider(); - DSpaceObject dso = item; - String identifier = ""; + DSpaceObject dso = newItem(context); + String identifier = UUID.randomUUID().toString(); + + // Set a known identifier on the object + instance.reserve(context, dso, identifier); + + // Test deletion instance.delete(context, dso, identifier); + + // See if it is gone + ItemIterator found = Item.findByMetadataField(context, + DataCiteIdentifierProvider.MD_SCHEMA, + DataCiteIdentifierProvider.DOI_ELEMENT, + DataCiteIdentifierProvider.DOI_QUALIFIER, identifier); + assertFalse("Test identifier is still present", found.hasNext()); } }