org.dspace.identifier.EZIDIdentifierProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.dspace.identifier.EZIDIdentifierProvider.java

Source

/**
 * This file is a modified version of a DSpace file.
 * All modifications are subject to the following copyright and license.
 * 
 * Copyright 2016 University of Chicago. All Rights Reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * 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.DCValue;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.ItemIterator;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.globus.configuration.BaseConfigurable;
import org.dspace.globus.configuration.Configurable;
import org.dspace.globus.configuration.ConfigurableClass;
import org.dspace.globus.configuration.ConfigurableField;
import org.dspace.globus.configuration.GlobusConfigurationManager;
import org.dspace.globus.identifier.GlobusIdentifierService;
import org.dspace.handle.HandleServerIdentifierProvider;
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.dspace.storage.rdbms.DatabaseManager;
import org.dspace.storage.rdbms.TableRow;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

/**
 * Provide service for DOIs through DataCite using the EZID service.
 *
 * <p>
 * Configuration of this class is is in two parts.
 * </p>
 *
 * <p>
 * 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**.
 * </p>
 *
 * <dl>
 * <dt>identifier.doi.ezid.shoulder</dt>
 * <dd>base of the site's DOIs. Example: 10.5072/FK2</dd>
 * <dt>identifier.doi.ezid.user</dt>
 * <dd>EZID username.</dd>
 * <dt>identifier.doi.ezid.password</dt>
 * <dd>EZID password.</dd>
 * <dt>identifier.doi.ezid.publisher</dt>
 * <dd>A default publisher, for Items not previously published. EZID requires a
 * publisher.</dd>
 * </dl>
 *
 * <p>
 * Then there are properties injected using Spring:
 * </p>
 * <ul>
 * <li>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.</li>
 *
 * <li>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.</li>
 * </ul>
 *
 * @author mwood
 */
@ConfigurableClass(name = "EZID")
public class EZIDIdentifierProvider extends IdentifierProvider implements Configurable {
    /**
     *
     */
    private static final String EZID_TARGET = "_target";

    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";

    private static final String DOI_RESOLVER = "http://dx.doi.org/";

    private static final ConfigurableProperty[] configProps = { IdentifierProvider.IdentifierConfigProp,
            new ConfigurableProperty(CFG_USER, "EZID User name", null, DataType.STRING, "Username for EZID API",
                    true),
            new ConfigurableProperty(CFG_PASSWORD, "Password", null, DataType.PASSWORD, "Password for EZID API",
                    true),
            new ConfigurableProperty(CFG_PUBLISHER, "Publisher name", null, DataType.STRING,
                    "Default Datacite publisher", true),
            new ConfigurableProperty(CFG_SHOULDER, "Shoulder", "10.5072/FK2", DataType.STRING, "Datacite shoulder",
                    true),
            new ConfigurableProperty(HandleServerIdentifierProvider.CFG_RESOLVER, "Resolver Base URL", DOI_RESOLVER,
                    DataType.STRING, "Base URL for resolving ID", true), };

    @ConfigurableField(displayName = "EZID User name", propId = CFG_USER)
    public String userName;

    @ConfigurableField(type = DataType.PASSWORD, displayName = "Password", propId = CFG_PASSWORD)
    public String password;

    @ConfigurableField(displayName = "Publisher Name", defaultValue = "Globus.org", propId = CFG_PUBLISHER)
    public String publisherName;

    @ConfigurableField(displayName = "Shoulder", defaultValue = "10.5072/FK2", propId = CFG_SHOULDER)
    public String shoulder;

    // 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 = "uri";

    private static final String DOI_SCHEME = "doi:";

    /** Map DataCite metadata into local metadata. */
    private static Map<String, String> crosswalk = new HashMap<String, String>();

    /** Converters to be applied to specific fields. */
    private static Map<String, Transform> transforms = new HashMap<String, Transform>();

    /** Factory for EZID requests. */
    private static EZIDRequestFactory requestFactory;

    private BaseConfigurable config;

    /**
     *
     */
    public EZIDIdentifierProvider() {
        config = new BaseConfigurable("EZID", configProps);
    }

    /*
     * (non-Javadoc)
     *
     * @see org.dspace.globus.configuration.Configurable#getName()
     */
    @Override
    public String getName() {
        return config.getName();
    }

    /*
     * (non-Javadoc)
     *
     * @see org.dspace.globus.configuration.Configurable#getPropIds()
     */
    @Override
    public Iterator<String> getPropIds() {
        return config.getPropIds();
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.dspace.globus.configuration.Configurable#getProperty(java.lang.String
     * )
     */
    @Override
    public ConfigurableProperty getProperty(String id) {
        return config.getProperty(id);
    }

    @Override
    public boolean supports(Class<? extends Identifier> identifier) {
        return DOI.class.isAssignableFrom(identifier);
    }

    @Override
    public boolean supports(String identifier) {
        if (null == identifier) {
            return false;
        } else {
            return identifier.startsWith(DOI_SCHEME) || identifier.startsWith(DOI_RESOLVER);
        } // XXX more thorough test?
    }

    @Override
    public String register(Context context, DSpaceObject dso) throws IdentifierException {
        log.debug("register {}", dso);

        if (!(dso instanceof Item)) {
            throw new IdentifierException("Unsupported object type " + dso.getTypeText());
        }

        Item item = (Item) dso;
        DCValue[] identifiers = item.getMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null);
        for (DCValue identifier : identifiers) {
            if ((null != identifier.value) && (supports(identifier.value))) {
                return identifier.value;
            }
        }

        String id = mint(context, item);
        item.clearDC(DOI_ELEMENT, DOI_QUALIFIER, null);
        String uri = uriForId(context, dso, id);
        item.addMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, uri);
        try {
            item.update();
            context.commit();
        } catch (SQLException ex) {
            throw new IdentifierException("New identifier not stored", ex);
        } catch (AuthorizeException ex) {
            throw new IdentifierException("New identifier not stored", ex);
        }
        log.info("Registered {}", id);
        try {
            modifyHandleRecord(context, dso, id);
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return id;
    }

    /**
     * Given the id portion create the full Uri to be stored in the metadata for
     * the item
     *
     * @param id
     * @return
     */
    private String uriForId(Context ctx, DSpaceObject dso, String id) {
        String resolver = (String) getConfigProperty(ctx, HandleServerIdentifierProvider.CFG_RESOLVER, dso);
        if (resolver == null) {
            resolver = DOI_RESOLVER;
        }
        if (id.startsWith(resolver)) {
            return id;
        } else {
            return resolver + id;
        }
    }

    protected String modifyHandleRecord(Context context, DSpaceObject dso, String handleId) throws SQLException {
        TableRow handle = getHandleInternal(context, dso.getType(), dso.getID());
        if (handle == null) {
            handle = DatabaseManager.create(context, "Handle");
        }

        handle.setColumn("handle", handleId);
        handle.setColumn("resource_type_id", dso.getType());
        handle.setColumn("resource_id", dso.getID());
        DatabaseManager.update(context, handle);

        if (log.isDebugEnabled()) {
            log.debug("Created new handle for " + Constants.typeText[dso.getType()] + " " + handleId);
        }
        return handleId;
    }

    /**
     * Return the handle for an Object, or null if the Object has no handle.
     *
     * @param context
     *            DSpace context
     * @param type
     *            The type of object
     * @param id
     *            The id of object
     * @return The handle for object, or null if the object has no handle.
     * @exception java.sql.SQLException
     *                If a database error occurs
     */
    protected static TableRow getHandleInternal(Context context, int type, int id) throws SQLException {
        String sql = "SELECT * FROM Handle WHERE resource_type_id = ? " + "AND resource_id = ?";

        return DatabaseManager.querySingleTable(context, "Handle", sql, type, id);
    }

    @Override
    public void register(Context context, DSpaceObject object, String identifier) {
        log.debug("register {} as {}", object, identifier);

        if (!(object instanceof Item)) {
            // TODO throw new IdentifierException("Unsupported object type " +
            // object.getTypeText());
            log.error("Unsupported object type " + object.getTypeText());
            return;
        }

        EZIDResponse response;
        try {
            EZIDRequest request = createRequest(context, object);
            response = request.create(identifier, fillMetadataDefaults(crosswalkMetadata(object), context, object));
        } catch (IdentifierException e) {
            log.error("Identifier '{}' not registered:  {}", identifier, e.getMessage());
            return;
        } catch (IOException e) {
            log.error("Identifier '{}' not registered:  {}", identifier, e.getMessage());
            return;
        } catch (URISyntaxException e) {
            log.error("Identifier '{}' not registered:  {}", identifier, e.getMessage());
            return;
        }

        if (response.isSuccess()) {
            Item item = (Item) object;
            try {
                String doiString = idToDOI(identifier, context, object);
                String uri = uriForId(context, item, doiString);
                item.addMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, uri);
                item.update();
                context.commit();
                log.info("registered {}", identifier);
            } catch (SQLException ex) {
                // TODO throw new
                // IdentifierException("New identifier not stored", ex);
                log.error("New identifier not stored", ex);
            } 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("Identifier '{}' not registered -- EZID returned: {}", identifier,
                    response.getEZIDStatusValue());
        }
    }

    /**
     * @param context
     * @param dso
     * @return
     * @throws URISyntaxException
     * @throws IdentifierException
     */
    private EZIDRequest createRequest(Context context, DSpaceObject dso)
            throws URISyntaxException, IdentifierException {
        EZIDRequest request = requestFactory.getInstance(loadAuthority(context, dso), loadUser(context, dso),
                loadPassword(context, dso));
        return request;
    }

    @Override
    public void reserve(Context context, DSpaceObject dso, String identifier) throws IdentifierException {
        log.debug("reserve {}", identifier);

        EZIDResponse response;
        try {
            EZIDRequest request = createRequest(context, dso);
            Map<String, String> metadata = crosswalkMetadata(dso);
            metadata = fillMetadataDefaults(metadata, context, dso);
            metadata.put("_status", "reserved");
            response = request.create(identifier, metadata);
        } catch (IOException e) {
            log.error("Identifier '{}' not registered:  {}", identifier, e.getMessage());
            return;
        } catch (URISyntaxException e) {
            log.error("Identifier '{}' not registered:  {}", identifier, e.getMessage());
            return;
        }

        if (response.isSuccess()) {
            Item item = (Item) dso;
            String doiString = idToDOI(identifier, context, dso);
            String uri = uriForId(context, dso, doiString);
            item.addMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, uri);
            try {
                item.update();
                context.commit();
                log.info("reserved {}", identifier);
            } catch (SQLException ex) {
                throw new IdentifierException("New identifier not stored", ex);
            } catch (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 = createRequest(context, dso);
        } catch (URISyntaxException ex) {
            log.error(ex.getMessage());
            throw new IdentifierException("DOI request not sent:  " + ex.getMessage());
        }

        // Send the request
        EZIDResponse response;
        try {
            Map<String, String> ezidMetadata = fillMetadataDefaults(crosswalkMetadata(dso), context, dso);
            response = request.mint(ezidMetadata);
        } 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());
        }

        // 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();
            String uri = uriForId(context, dso, doi);
            return uri;
        } 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);

        ItemIterator found;
        try {
            // TODO: We don't know how to do this right now since
            // we're looking for the
            // item but we don't know the shoulder of the DOI without having the
            // object to find the
            // right config for...
            found = Item.findByMetadataField(context, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER,
                    idToDOI(identifier, context, null));
        } 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 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;
        } catch (SQLException ex) {
            log.error(ex.getMessage());
            throw new IdentifierNotResolvableException(ex);
        }
    }

    @Override
    public String lookup(Context context, DSpaceObject object)
            throws IdentifierNotFoundException, IdentifierNotResolvableException {
        log.debug("lookup {}", object);

        if (!(object instanceof Item)) {
            throw new IllegalArgumentException("Unsupported type " + object.getTypeText());
        }

        Item item = (Item) object;
        DCValue found = null;
        for (DCValue candidate : item.getMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null)) {
            if (supports(candidate.value)) {
                found = candidate;
                break;
            }
        }
        if (null != found) {
            log.debug("Found {}", found.value);
            return found.value;
        } else {
            throw new IdentifierNotFoundException(object.getTypeText() + " " + object.getID() + " has no DOI");
        }
    }

    @Override
    public void delete(Context context, DSpaceObject dso) throws IdentifierException {
        log.debug("delete {}", dso);

        if (!(dso instanceof Item)) {
            throw new IllegalArgumentException("Unsupported type " + dso.getTypeText());
        }

        Item item = (Item) dso;

        // delete from EZID
        DCValue[] metadata = item.getMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null);
        List<String> remainder = new ArrayList<String>();
        int skipped = 0;
        for (DCValue id : metadata) {
            if (!supports(id.value)) {
                remainder.add(id.value);
                continue;
            }

            EZIDResponse response;
            try {
                EZIDRequest request = createRequest(context, dso);
                response = request.delete(DOIToId(id.value, context, dso));
            } catch (URISyntaxException e) {
                log.error("Bad URI in metadata 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(skipped + " identifiers could not be deleted.");
        }
    }

    @Override
    public void delete(Context context, DSpaceObject dso, String identifier) throws IdentifierException {
        log.debug("delete {} from {}", identifier, dso);

        if (!(dso instanceof Item)) {
            throw new IllegalArgumentException("Unsupported type " + dso.getTypeText());
        }

        Item item = (Item) dso;

        DCValue[] metadata = item.getMetadata(MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null);
        List<String> remainder = new ArrayList<String>();
        int skipped = 0;
        for (DCValue id : metadata) {
            if (!id.value.equals(idToDOI(identifier, context, dso))) {
                remainder.add(id.value);
                continue;
            }

            EZIDResponse response;
            try {
                EZIDRequest request = createRequest(context, dso);
                response = request.delete(DOIToId(id.value, context, dso));
            } 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.");
        }
    }

    /**
     * Format a naked identifier as a DOI with our configured authority prefix.
     *
     * @throws IdentifierException
     *             if authority prefix is not configured.
     */
    // Adding the context, dso params so that we can use the proper
    // authority for this DSO
    String idToDOI(String id, Context context, DSpaceObject dso) throws IdentifierException {
        return "doi:" + loadAuthority(context, dso) + 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, Context context, DSpaceObject dso) throws IdentifierException {
        String prefix = "doi:" + loadAuthority(context, dso);
        if (DOI.startsWith(prefix)) {
            return DOI.substring(prefix.length());
        } else {
            return DOI;
        }
    }

    /**
     * Get configured value of EZID username.
     *
     * @throws IdentifierException
     */
    private String loadUser(Context context, DSpaceObject dso) throws IdentifierException {
        String user = getConfigProperty(context, CFG_USER, dso);
        if (null != user) {
            return user;
        } else {
            throw new IdentifierException("Unconfigured:  define " + CFG_USER);
        }
    }

    /**
     * Get configured value of EZID password.
     *
     * @throws IdentifierException
     */
    private String loadPassword(Context context, DSpaceObject dso) throws IdentifierException {

        String password = (String) getConfigProperty(context, CFG_PASSWORD, dso);
        if (null != password) {
            return password;
        } else {
            throw new IdentifierException("Unconfigured:  define " + CFG_PASSWORD);
        }
    }

    /**
     * Get configured value of EZID "shoulder".
     *
     * @throws IdentifierException
     */
    // Changed to provide context and dso so that we can resolve via the
    // per-dspace-object
    // level settings for these parameters
    private String loadAuthority(Context context, DSpaceObject dso) throws IdentifierException {
        String shoulder = (String) getConfigProperty(context, CFG_SHOULDER, dso);
        if (null != shoulder) {
            return shoulder;
        } else {
            throw new IdentifierException("Unconfigured:  define " + CFG_SHOULDER);
        }
    }

    /**
     * Map selected DSpace metadata to fields recognized by DataCite.
     */
    private Map<String, String> crosswalkMetadata(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<String, String> mapped = new HashMap<String, String>();

        for (Entry<String, String> datum : crosswalk.entrySet()) {
            DCValue[] values = item.getMetadata(datum.getValue());
            if (null != values) {
                for (DCValue value : values) {
                    String key = datum.getKey();
                    String mappedValue;
                    Transform xfrm = transforms.get(key);
                    if (null != xfrm) {
                        try {
                            mappedValue = xfrm.transform(value.value);
                        } catch (Exception ex) {
                            log.error("Unable to transform '{}' from {} to {}:  {}",
                                    new String[] { value.value, value.toString(), key, ex.getMessage() });
                            continue;
                        }
                    } else {
                        mappedValue = value.value;
                    }
                    mapped.put(key, mappedValue);
                }
            }
        }

        return mapped;
    }

    /**
     * @param mapped
     */
    private Map<String, String> fillMetadataDefaults(Map<String, String> mapped, Context context,
            DSpaceObject dso) {
        // Supply a default publisher, if the Item has none.
        if (!mapped.containsKey(DATACITE_PUBLISHER)) {
            String publisher = (String) getConfigProperty(context, CFG_PUBLISHER, dso);
            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)) {
            String year = String.valueOf(Calendar.getInstance().get(Calendar.YEAR));
            log.info("Supplying default publication year:  {}", year);
            mapped.put(DATACITE_PUBLICATION_YEAR, year);
        }

        // Provide an initial best guess target
        if (!mapped.containsKey(EZID_TARGET)) {
            mapped.put(EZID_TARGET, GlobusIdentifierService.getLandingUrlForDso(context, dso));
        }
        return mapped;
    }

    @Required
    public void setCrosswalk(Map<String, String> aCrosswalk) {
        crosswalk = aCrosswalk;
    }

    public void setCrosswalkTransform(Map<String, Transform> transformMap) {
        transforms = transformMap;
    }

    @Required
    public static void setRequestFactory(EZIDRequestFactory aRequestFactory) {
        requestFactory = aRequestFactory;
    }
}