/*******************************************************************************
* Australian National University Data Commons
* Copyright (C) 2013 The Australian National University
*
* This file is part of Australian National University Data Commons.
*
* Australian National University Data Commons is free software: you
* can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation,
* either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package au.edu.anu.datacommons.doi;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.Properties;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriBuilder;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import org.datacite.schema.kernel_2.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import au.edu.anu.datacommons.config.Config;
import au.edu.anu.datacommons.config.PropertiesFile;
import au.edu.anu.datacommons.doi.logging.ExtWebResourceLog;
import au.edu.anu.datacommons.doi.logging.ExtWebResourceLogDao;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.filter.LoggingFilter;
import com.sun.jersey.client.apache.ApacheHttpClient;
import com.sun.jersey.client.apache.config.DefaultApacheHttpClientConfig;
import com.sun.jersey.core.util.MultivaluedMapImpl;
/**
* A Client that sends Digital Object Identifier (DOI) requests to a relevant service. Refer to <a
* href="http://ands.org.au/resource/r9-cite-my-data-v1.1-tech-doco.pdf">Cite My Data M2M Service</a>
*
* @author Rahul Khanna
*
*/
public class DoiClient {
private static final Logger LOGGER = LoggerFactory.getLogger(DoiClient.class);
private static Client noProxyClient = null;
private static Client proxyClient = null;
private static Client client = null;
private static JAXBContext resourceContext;
private static JAXBContext doiResponseContext;
private Marshaller resourceMarshaller;
private Unmarshaller resourceUnmarshaller;
private Unmarshaller doiResponseUnmarshaller;
private DoiConfig doiConfig;
static {
try {
resourceContext = JAXBContext.newInstance(Resource.class);
doiResponseContext = JAXBContext.newInstance(DoiResponse.class);
} catch (JAXBException e) {
// Exception thrown only when class DoiResponse isn't properly coded. NPE expected in that case.
LOGGER.warn(e.getMessage(), e);
}
}
private ResponseFormat respFmt = ResponseFormat.XML;
private DoiResponse doiResponse = null;
private String doiResponseAsString = null;
public enum ResponseFormat {
XML, JSON, STRING;
@Override
public String toString() {
return super.toString().toLowerCase();
}
}
/**
* Constructor for DoiClient. A DoiConfig object is created from the doi.properties file in the default conf
* location.
*/
public DoiClient() {
Properties doiProps;
try {
doiProps = new PropertiesFile(new File(Config.getAppHome(), "config/doi.properties"));
} catch (IOException e) {
throw new RuntimeException("doi.properties not found or unreadable.");
}
this.doiConfig = new DoiConfigImpl(doiProps);
setupClients(this.doiConfig);
setupMarshallers();
}
/**
* Constructor for when a DoiConfig object is provided
*
* @param doiConfig
* DoiConfig object
*/
public DoiClient(DoiConfig doiConfig) {
this.doiConfig = doiConfig;
setupClients(this.doiConfig);
setupMarshallers();
}
public ResponseFormat getRespFmt() {
return respFmt;
}
public void setRespFmt(ResponseFormat respFmt) {
this.respFmt = respFmt;
}
public DoiResponse getDoiResponse() {
return doiResponse;
}
public String getDoiResponseAsString() {
return doiResponseAsString;
}
/**
* Sends a request to mint a DOI. Refer to section 3.9 of Cite My Data Technical Documentation. URL request is in
* format: <code>https://services.ands.org.au/doi/1.1/mint.{response_type}/?app_id={app_id}&url={url}</code>
*
* @param pid
* Pid of the record for which DOI is to be minted.
* @param metadata
* DataCite Resource object containing metadata about the record.
* @throws DoiException
* If unable to mint a DOI.
*/
public void mint(String pid, Resource metadata) throws DoiException {
if (pid == null || pid.trim().length() <= 0)
throw new DoiException("URL not specified.");
if (metadata == null)
throw new DoiException("Metadata for DOI not provided.");
ExtWebResourceLogDao logDao = null;
try {
String url = generateLandingUri(pid).toString();
String xml = getMetadataAsStr(metadata);
LOGGER.trace("Minting url={}, xml={}.", new Object[] { url, xml });
// Build URI.
UriBuilder doiUriBuilder = UriBuilder.fromUri(doiConfig.getBaseUri()).path(
"mint." + this.respFmt.toString() + "/");
doiUriBuilder = appendAppId(doiUriBuilder);
doiUriBuilder = appendDebug(doiUriBuilder);
doiUriBuilder = doiUriBuilder.queryParam("url", URLEncoder.encode(url, "UTF-8"));
URI doiUri = doiUriBuilder.build();
LOGGER.debug("Minting DOI using {}", doiUri.toString());
WebResource mintDoiResource = client.resource(doiUri);
MultivaluedMap<String, String> formData = new MultivaluedMapImpl();
formData.add("xml", xml);
logDao = new ExtWebResourceLogDao();
ExtWebResourceLog extResLog = null;
try {
extResLog = generateExtWebResourceLog(doiUri, pid, xml);
logDao.create(extResLog);
} catch (Exception e) {
LOGGER.warn("Unable to create log record to external service.");
}
ClientResponse resp = mintDoiResource.accept(getMediaTypeForResp())
.type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).post(ClientResponse.class, formData);
processResponse(resp);
try {
updateExtWebResourceLog(extResLog, this.doiResponseAsString);
logDao.update(extResLog);
} catch (Exception e) {
LOGGER.warn("Unable to update log record to external service.");
}
if (!doiResponse.getType().equalsIgnoreCase("success"))
throw new DoiException("DOI Service request failed. Server response: " + doiResponseAsString);
} catch (DoiException e) {
throw e;
} catch (Exception e) {
throw new DoiException("Unable to mint DOI", e);
} finally {
if (logDao != null)
logDao.close();
}
}
/**
* Updates the metadata associated with a DOI. Refer to section 3.9 of Cite My Data Technical Documentation. URL is
* in format <code>https://services.ands.org.au/doi/1.1/mint.{response_type}/?app_id={app_id}&url={url}</code>
*
* @param doi
* DOI whose metadata to be updated
* @param pid
* Pid of the record to which the DOI belongs
* @param metadata
* Updated metadata as DataCite Resource object
* @throws DoiException
* If unable to update DOI
*/
public void update(String doi, String pid, Resource metadata) throws DoiException {
ExtWebResourceLogDao logDao = null;
try {
String url = generateLandingUri(pid).toString();
String xml = getMetadataAsStr(metadata);
LOGGER.trace("Updating doi={}, url={}, xml={}.", new Object[] { doi, url, xml });
// Build URI.
UriBuilder doiUriBuilder = UriBuilder.fromUri(doiConfig.getBaseUri()).path(
"update." + this.respFmt.toString() + "/");
doiUriBuilder = appendAppId(doiUriBuilder);
doiUriBuilder = appendDoi(doiUriBuilder, doi);
doiUriBuilder = appendDebug(doiUriBuilder);
if (url != null && url.trim().length() > 0)
doiUriBuilder = doiUriBuilder.queryParam("url", URLEncoder.encode(url, "UTF-8"));
URI doiUri = doiUriBuilder.build();
LOGGER.debug("Updating DOI using {}", doiUri.toString());
WebResource updateDoiResource = client.resource(doiUri);
ClientResponse resp;
logDao = new ExtWebResourceLogDao();
ExtWebResourceLog extResLog = null;
try {
extResLog = generateExtWebResourceLog(doiUri, pid, xml);
logDao.create(extResLog);
} catch (Exception e) {
LOGGER.warn("Unable to create log record to external service.");
}
if (xml != null && xml.trim().length() > 0) {
MultivaluedMap<String, String> formData = new MultivaluedMapImpl();
formData.add("xml", xml);
resp = updateDoiResource.accept(getMediaTypeForResp()).type(MediaType.APPLICATION_FORM_URLENCODED_TYPE)
.post(ClientResponse.class, formData);
} else
resp = updateDoiResource.accept(getMediaTypeForResp()).get(ClientResponse.class);
processResponse(resp);
if (!doiResponse.getType().equalsIgnoreCase("success"))
throw new DoiException("DOI Service request failed. Server response: " + doiResponseAsString);
} catch (Exception e) {
throw new DoiException("Unable to update DOI", e);
} finally {
if (logDao != null)
logDao.close();
}
}
/**
* Deactivates a DOI. Refer to section 3.9 of Cite My Data Technical Documentation. URL is in format
* <code>https://services.ands.org.au/doi/1.1/mint.{response_type}/?app_id={app_id}&url={url}</code>
*
* @param doi
* DOI to deactivate.
*
* @throws DoiException
* If unable to deactivate DOI
*/
public void deactivate(String doi) throws DoiException {
ExtWebResourceLogDao logDao = null;
try {
LOGGER.trace("Deactivating doi={}", doi);
// Build URI.
UriBuilder doiUriBuilder = UriBuilder.fromUri(doiConfig.getBaseUri()).path(
"deactivate." + this.respFmt.toString() + "/");
doiUriBuilder = appendAppId(doiUriBuilder);
doiUriBuilder = appendDoi(doiUriBuilder, doi);
doiUriBuilder = appendDebug(doiUriBuilder);
URI doiUri = doiUriBuilder.build();
logDao = new ExtWebResourceLogDao();
ExtWebResourceLog extResLog = null;
try {
extResLog = generateExtWebResourceLog(doiUri);
logDao.create(extResLog);
} catch (Exception e) {
LOGGER.warn("Unable to create log record to external service.");
}
LOGGER.debug("Deactivating DOI using {}", doiUri.toString());
WebResource deactivateDoiResouce = client.resource(doiUri);
ClientResponse resp = deactivateDoiResouce.accept(getMediaTypeForResp()).get(ClientResponse.class);
processResponse(resp);
} catch (Exception e) {
throw new DoiException("Unable to deactivate DOI", e);
} finally {
if (logDao != null)
logDao.close();
}
}
/**
* Activates a deactivated DOI. Refer to section 3.9 of Cite My Data Technical Documentation. URL is in format
* <code>https://services.ands.org.au/doi/1.1/deactivate.{response_type}/?app_id={app_id}&doi={doi}</code>
*
* @param doi
* DOI to activate
* @throws DoiException
* If unable to activate a DOI
*/
public void activate(String doi) throws DoiException {
ExtWebResourceLogDao logDao = null;
try {
LOGGER.trace("Activating doi={}", doi);
// Build URI.
UriBuilder doiUriBuilder = UriBuilder.fromUri(doiConfig.getBaseUri()).path(
"activate." + this.respFmt.toString() + "/");
doiUriBuilder = appendAppId(doiUriBuilder);
doiUriBuilder = appendDoi(doiUriBuilder, doi);
doiUriBuilder = appendDebug(doiUriBuilder);
URI doiUri = doiUriBuilder.build();
ExtWebResourceLog extResLog = null;
logDao = new ExtWebResourceLogDao();
try {
extResLog = generateExtWebResourceLog(doiUri);
logDao.create(extResLog);
} catch (Exception e) {
LOGGER.warn("Unable to create log record to external service.");
}
LOGGER.debug("Activating DOI using {}", doiUri.toString());
WebResource activateDoiResource = client.resource(doiUri);
ClientResponse resp = activateDoiResource.accept(getMediaTypeForResp()).get(ClientResponse.class);
processResponse(resp);
} catch (Exception e) {
throw new DoiException("Unable to activate DOI", e);
} finally {
if (logDao != null)
logDao.close();
}
}
/**
* Get the metadata associated with a DOI.
*
* @param doi
* DOI whose metadata to receive.
* @return Metadata as a DataCite Resource Object
* @throws DoiException
* If unable to retrieve DOI metadata.
*/
public Resource getMetadata(String doi) throws DoiException {
Resource res;
String respStr;
ExtWebResourceLogDao logDao = null;
try {
LOGGER.trace("Getting metadata for doi={}", doi);
// Build URI.
UriBuilder doiUriBuilder = UriBuilder.fromUri(doiConfig.getBaseUri()).path(
"xml." + this.respFmt.toString() + "/");
doiUriBuilder = appendDoi(doiUriBuilder, doi);
doiUriBuilder = appendDebug(doiUriBuilder);
URI doiUri = doiUriBuilder.build();
ExtWebResourceLog extResLog = null;
logDao = new ExtWebResourceLogDao();
try {
extResLog = generateExtWebResourceLog(doiUri);
logDao.create(extResLog);
} catch (Exception e) {
LOGGER.warn("Unable to create log record to external service.");
}
LOGGER.debug("Getting DOI Metadata using {}", doiUri.toString());
WebResource getDoiMetadataResource = client.resource(doiUri);
ClientResponse resp = getDoiMetadataResource.accept(getMediaTypeForResp()).get(ClientResponse.class);
respStr = resp.getEntity(String.class);
LOGGER.trace("Response from server: {}", respStr);
res = (Resource) resourceUnmarshaller.unmarshal(new StringReader(respStr));
} catch (Exception e) {
throw new DoiException("Unable to retrieve DOI metadata.", e);
} finally {
if (logDao != null)
logDao.close();
}
return res;
}
/**
* Appends the app ID to a URIBuilder object as query parameter. App ID is required to make a DOI change request.
*
* @param ub
* UriBuilder to which app ID will be appended.
*
* @return UriBuilder with app ID appended as a query parameter.
*/
private UriBuilder appendAppId(UriBuilder ub) {
// App Id is alphanumeric so no need to URLEncode.
return ub.queryParam("app_id", (doiConfig.useTestPrefix() ? "TEST" : "") + doiConfig.getAppId());
}
/**
* Appends a DOI to a URIBuilder object as a query parameter.
*
* @param ub
* UriBuilder object to which DOI is to be appended.
* @param doi
* DOI to be appended.
* @return UriBuilder
*/
private UriBuilder appendDoi(UriBuilder ub, String doi) {
String encodedDoi;
try {
encodedDoi = URLEncoder.encode(doi, "UTF-8");
return ub.queryParam("doi", encodedDoi);
} catch (UnsupportedEncodingException e) {
// This exception should never be thrown if the charset is a valid one.
LOGGER.error(e.getMessage(), e);
return null;
}
}
/**
* Appends debug query parameter to a URIBuilder.
*
* @param ub
* UriBuilder to which debug query parameter will be appended.
*
* @return UriBuilder with appended query parameter.
*/
private UriBuilder appendDebug(UriBuilder ub) {
if (doiConfig.isDebug())
return ub.queryParam("debug", true);
else
return ub;
}
private MediaType getMediaTypeForResp() {
if (this.respFmt == ResponseFormat.XML)
return MediaType.APPLICATION_XML_TYPE;
else if (this.respFmt == ResponseFormat.JSON)
return MediaType.APPLICATION_JSON_TYPE;
else if (this.respFmt == ResponseFormat.STRING)
return MediaType.TEXT_HTML_TYPE;
else
throw new IllegalArgumentException("Invalid response type.");
}
/**
* Processes the response received from the DOI service.
*
* @param resp
* Response received from DOI service as ClientResponse.
*/
private void processResponse(ClientResponse resp) {
this.doiResponseAsString = resp.getEntity(String.class);
LOGGER.trace("Server response: {}", doiResponseAsString);
if (this.getRespFmt() == ResponseFormat.XML) {
try {
this.doiResponse = (DoiResponse) doiResponseUnmarshaller.unmarshal(new StreamSource(new StringReader(
this.doiResponseAsString.substring(this.doiResponseAsString.indexOf("<?xml")))));
} catch (StringIndexOutOfBoundsException e) {
this.doiResponse = null;
} catch (JAXBException e) {
LOGGER.warn(e.getMessage(), e);
this.doiResponse = null;
}
}
LOGGER.debug("Response from server: ({}) {}", resp.getStatus(), this.doiResponseAsString);
}
/**
* Marshals a DataCite Resource object containing metadata about a record into a String.
*
* @param metadata
* DataCite Resource object
* @return Returns DataCite Resource Object XML as String
*
* @throws JAXBException
* If unable to marshall DataCite Resource into XML
*/
private String getMetadataAsStr(Resource metadata) throws JAXBException {
StringWriter xmlSw = new StringWriter();
resourceMarshaller.marshal(metadata, xmlSw);
return xmlSw.toString();
}
/**
* Generates the landing page URI for a record.
*
* @param pid
* Pid of the record for which a URI is to be created.
* @return URI of the record
*/
private URI generateLandingUri(String pid) {
return doiConfig.getLandingUri().build(pid);
}
/**
* Sets up the marshallers and unmarshallers required to marshal/unmarshall requests to and responses from the DOI
* service.
*/
private void setupMarshallers() {
try {
resourceMarshaller = resourceContext.createMarshaller();
resourceMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
resourceMarshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION,
"http://datacite.org/schema/kernel-2.2 http://schema.datacite.org/meta/kernel-2.2/metadata.xsd");
} catch (JAXBException e) {
LOGGER.error(e.getMessage(), e);
resourceMarshaller = null;
}
try {
resourceUnmarshaller = resourceContext.createUnmarshaller();
} catch (JAXBException e) {
LOGGER.error(e.getMessage(), e);
resourceUnmarshaller = null;
}
try {
doiResponseUnmarshaller = doiResponseContext.createUnmarshaller();
} catch (JAXBException e) {
LOGGER.error(e.getMessage(), e);
doiResponseUnmarshaller = null;
}
}
/**
* Adds the response object to the External Resource Log object. The log object would already contain the request
* when this method is called.
*
* @param extResLog
* @param respAsStr
*/
private void updateExtWebResourceLog(ExtWebResourceLog extResLog, String respAsStr) {
extResLog.addResponse(respAsStr);
}
private ExtWebResourceLog generateExtWebResourceLog(URI doiUri, String pid, String xml) {
StringBuilder reqStr = new StringBuilder();
reqStr.append(doiUri.toString());
reqStr.append(Config.NEWLINE);
reqStr.append(Config.NEWLINE);
if (xml != null)
reqStr.append(xml);
ExtWebResourceLog extResLog = new ExtWebResourceLog(pid, reqStr.toString());
return extResLog;
}
private ExtWebResourceLog generateExtWebResourceLog(URI doiUri) {
return generateExtWebResourceLog(doiUri, null, null);
}
/**
* Sets up two client objects that will be used for HTTP requests - one without a proxy server specified, and
* another without. As DOI requests can be sent from machines with specific IP addresses for testing purposes, the
* DOI requests will be routed through an authorised server if initiated from a test machine.
*
* @param doiConfig
* Config object containing details of proxy server
*/
private static void setupClients(DoiConfig doiConfig) {
if (noProxyClient == null) {
noProxyClient = Client.create();
noProxyClient.addFilter(new LoggingFilter());
}
if (proxyClient == null) {
final String proxyHost = doiConfig.getProxyServer();
final String proxyPort = doiConfig.getProxyPort();
final DefaultApacheHttpClientConfig config = new DefaultApacheHttpClientConfig();
if (proxyHost != null && proxyHost.length() > 0 && proxyPort != null && proxyPort.length() > 0) {
config.getProperties().put(DefaultApacheHttpClientConfig.PROPERTY_PROXY_URI,
"http://" + proxyHost + ":" + proxyPort);
}
proxyClient = ApacheHttpClient.create(config);
proxyClient.addFilter(new LoggingFilter());
}
if (doiConfig.useProxy())
client = proxyClient;
else
client = noProxyClient;
}
}