/******************************************************************************* * 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.webservice; import static java.text.MessageFormat.*; import java.io.IOException; import java.io.StringReader; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import javax.ws.rs.POST; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import au.edu.anu.datacommons.webservice.bindings.CombinedStatusResponse; import au.edu.anu.datacommons.webservice.bindings.DcRequest; import au.edu.anu.datacommons.webservice.bindings.FedoraItem; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.ClientResponse.Status; import com.sun.jersey.api.client.WebResource.Builder; import com.sun.jersey.api.client.filter.ClientFilter; import com.sun.jersey.api.client.filter.LoggingFilter; import com.sun.jersey.core.util.MultivaluedMapImpl; /** * Provides common functionality for individual Machine 2 Machine implementations. Includes the primary Restful endpoint for XML requests from instruments and * other systems. * * This class is intended to act as the middle layer between the web service gateway that dispatches requests to one of the Service modules that implement this * class. */ public abstract class AbstractResource { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractResource.class); protected static final Client client = Client.create(); protected static final ClientFilter loggingFilter = new LoggingFilter(); protected static DocumentBuilder docBuilder; protected Properties genericWsProps; protected Properties packageLookup; static { try { docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); } catch (ParserConfigurationException e) { throw new RuntimeException(e.getMessage(), e); } } @Context protected UriInfo uriInfo; @Context protected Request request; @Context protected HttpHeaders httpHeaders; /** * Accepts an HTTP POST request with an application/xml documents in its body. Generates individual requests for the Generic DataCommons web service and * submits for processing. Then generates a response to be sent back. * * @param xmlDoc * Request as an XML document. * @return HTTP Response with an XML document with results of requests. */ @POST @Produces(MediaType.APPLICATION_XML) public Response doPostRequest(Document xmlDoc) { Response resp = null; resp = processXmlRequest(xmlDoc); return resp; } abstract protected Element processRespElement(Element statusElementFromGenSvc); /** * Updates logging from the Client object as specified in the configuration file. All HTTP requests and responses are sent and received through the Client * object. If http.logging is set to true then a logging filter is added to the Client object. */ protected void updateClientLogging() { if (genericWsProps.getProperty("http.logging", "false").equalsIgnoreCase("true")) { if (!client.isFilterPreset(loggingFilter)) client.addFilter(loggingFilter); } else { if (client.isFilterPreset(loggingFilter)) client.removeFilter(loggingFilter); } } /** * Reads the <code>version</code> attribute in the document element, finds the package name containing the class files for the version specified and creates * a {@link JAXBContext} using the package name as the context path. * * @param xmlDoc * XML document from which version attribute will be read from the root element * @return Generated JAXBContext based on the version attribute * @throws JAXBException * when unable to generate a JAXBContext object */ protected JAXBContext getJaxbContext(Document xmlDoc) throws JAXBException { String version = xmlDoc.getDocumentElement().getAttribute("version"); if (!packageLookup.containsKey(version)) throw new JAXBException(MessageFormat.format("Unrecognised schema version - {0}", version)); String packageName = packageLookup.getProperty(version); LOGGER.debug("Using package '{}' for version '{}'", packageName, version); JAXBContext context = JAXBContext.newInstance(packageName); return context; } /** * Generates a WebResource.Builder object containing template parameters for an HTTP request to Data Commons Web Service. * * @return Generated Builder object */ protected Builder generateHttpRequestBuilder() { UriBuilder uriBuilder = UriBuilder.fromPath(genericWsProps.getProperty("dc.baseUrl")).path(genericWsProps.getProperty("dc.wsPath")); uriBuilder = addQueryParams(uriBuilder); LOGGER.debug("Generated URI {}", uriBuilder.build().toString()); Builder reqBuilder = client.resource(uriBuilder.build()).accept(MediaType.APPLICATION_XML_TYPE); reqBuilder = addHttpHeaders(reqBuilder); return reqBuilder; } /** * Adds query parameters in the context for the current HTTP request to a UriBuilder. All query parameters present in the original HTTP request are included * in all HTTP requests generated by this class. * * @param uriBuilder * UriBuilder object to which query parameters will be added * @return UriBuilder with the added query parameters */ protected UriBuilder addQueryParams(UriBuilder uriBuilder) { Set<Entry<String, List<String>>> queryParamsSet = uriInfo.getQueryParameters().entrySet(); for (Entry<String, List<String>> queryParam : queryParamsSet) for (String queryParamValue : queryParam.getValue()) uriBuilder = uriBuilder.queryParam(queryParam.getKey(), queryParamValue); return uriBuilder; } /** * Generates a WebResource.Builder object containing template parameters for an HTTP request to Data Commons to add a link to a record. * * @param pid * Pid of the record to which another record will be linked * @return Generated Builder object */ protected Builder generateAddLinkBuilder(String pid) { Builder reqBuilder = client .resource(UriBuilder.fromPath(genericWsProps.getProperty("dc.baseUrl")).path(genericWsProps.getProperty("dc.addLinkPath")).path(pid).build()) .type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).accept(MediaType.TEXT_PLAIN_TYPE); reqBuilder = addHttpHeaders(reqBuilder); return reqBuilder; } /** * Sends an HTTP request to authenticate the credentials provided in the original HTTP request. * * @return ClientResponse with the body containing username and display name. * @throws UnauthorisedException * when the credentials provided are invalid */ protected ClientResponse authenticateCredentials() throws UnauthorisedException { ClientResponse respUserInfo = generateUserInfoBuilder().get(ClientResponse.class); if (respUserInfo.getClientResponseStatus() == Status.UNAUTHORIZED) throw new UnauthorisedException("Invalid username and/or password."); return respUserInfo; } /** * Generates an HTTP Request Builder for getting the details of the user sending the request or the user associated with an M2M request. * * @return Generated Builder object */ protected Builder generateUserInfoBuilder() { UriBuilder authUserUriBuilder = UriBuilder.fromPath(genericWsProps.getProperty("dc.baseUrl")).path(genericWsProps.getProperty("dc.userInfo")); authUserUriBuilder = addQueryParams(authUserUriBuilder); Builder reqBuilder = client.resource(authUserUriBuilder.build()).type(MediaType.TEXT_PLAIN_TYPE).accept(MediaType.TEXT_PLAIN_TYPE); reqBuilder = addHttpHeaders(reqBuilder); return reqBuilder; } /** * Adds the HTTP headers present in the original HTTP request to a Builder object that will subsequently be used to send a request to Data Commons. * * @param b * Builder to which HTTP headers will be added * @return Builder object with HTTP headers added to it */ protected Builder addHttpHeaders(Builder b) { MultivaluedMap<String, String> headersMap = httpHeaders.getRequestHeaders(); String headersToCopyLine = genericWsProps.getProperty("http.headers"); if (headersToCopyLine != null) { String[] headersToCopy = headersToCopyLine.split(";"); for (int i = 0; i < headersToCopy.length; i++) { if (headersMap.containsKey(headersToCopy[i])) { for (String value : headersMap.get(headersToCopy[i])) b = b.header(headersToCopy[i], value); } } } return b; } /** * Creates a relation between records created/updated as a result of sending a DcRequest and a list of PIDs of records that already existed in Data Commons. * * @param dcReqs * Map of records and their type of link to another record as <code>Map<DcRequest, Map<String, FedoraItem>></code> */ protected void createRelations(Map<DcRequest, Map<String, FedoraItem>> dcReqs) { for (Entry<DcRequest, Map<String, FedoraItem>> dcReqEntry : dcReqs.entrySet()) { // If the value is null instead of Map<String, FedoraItem, item doesn't need to be related. if (dcReqEntry.getValue() != null) { for (Entry<String, FedoraItem> relEntry : dcReqEntry.getValue().entrySet()) { String sourcePid = dcReqEntry.getKey().getFedoraItem().getPid(); String targetPid = relEntry.getValue().getPid(); if (sourcePid != null && sourcePid.length() > 0 && targetPid != null && targetPid.length() > 0) { MultivaluedMap<String, String> formData = new MultivaluedMapImpl(); formData.add("linkType", relEntry.getKey()); formData.add("itemId", targetPid); try { ClientResponse respFromGenSvc = generateAddLinkBuilder(sourcePid).post(ClientResponse.class, formData); String respStr = respFromGenSvc.getEntity(String.class); LOGGER.debug("Relation request for {} {} {} - HTTP Status {}, body: {}", sourcePid, relEntry.getKey(), targetPid, respFromGenSvc.getStatus(), respStr); if (respFromGenSvc.getClientResponseStatus() == Status.OK) LOGGER.info("Created relation {} {} {}...", sourcePid, relEntry.getKey(), targetPid); } catch (Exception e) { LOGGER.error(format("Unable to set relationship {0} {1} {2}", sourcePid, relEntry.getKey(), targetPid), e); } } } } } } /** * Executes the collection of DcRequests to create/update records. Then links those records with other records as specified. * * @param dcReqs * Map of DcRequests with the relations to other records * @param statusElements * Collection of status elements returned from the Data Commons Web Service for each DcRequest sent * @return Number of requests successfully processed */ public int executeDcRequests(Map<DcRequest, Map<String, FedoraItem>> dcReqs, List<Element> statusElements) { int countSuccess = 0; for (DcRequest iDcRequest : dcReqs.keySet()) { ClientResponse respFromGenService = generateHttpRequestBuilder().post(ClientResponse.class, iDcRequest); String respStr = respFromGenService.getEntity(String.class); try { Document doc = docBuilder.parse(new InputSource(new StringReader(respStr))); Node n = doc.getDocumentElement().getFirstChild(); while (n != null && n.getNodeType() != Node.ELEMENT_NODE) n = n.getNextSibling(); if (n != null) { Element statusElementFromGenSvc = (Element) n; statusElementFromGenSvc = processRespElement(statusElementFromGenSvc); statusElements.add(statusElementFromGenSvc); String pid = statusElementFromGenSvc.getAttribute("anudcid"); LOGGER.trace("Generic service returned Pid: {}", pid); iDcRequest.getFedoraItem().setPid(pid); } // If it reaches this point without exception then the response was a valid XML if (respFromGenService.getClientResponseStatus() == Status.OK) countSuccess++; } catch (IOException e) { LOGGER.error(e.getMessage()); } catch (SAXException e) { LOGGER.error(e.getMessage()); } } return countSuccess; } /** * Processes a specified XML document by: * * <ol> * <li>Authenticates the sender</li> * <li>Generates a list of DcRequests and sends to the Data Commons Web Service to create/update records</li> * <li>Creates/updates the relations between records as specified constants properties file</li> * <li>Generates an HTTP response to be sent back to the client</li> * </ol> * * @param xmlDoc * Document object to be processed * @return HTTP response as Response */ public Response processXmlRequest(Document xmlDoc) { Response resp; updateClientLogging(); CombinedStatusResponse phenRespRootElement = new CombinedStatusResponse(); List<Element> statusElements = new ArrayList<Element>(); try { authenticateCredentials(); JAXBContext context = getJaxbContext(xmlDoc); Unmarshaller um = context.createUnmarshaller(); Processable proc = (Processable) um.unmarshal(xmlDoc); // Iterate through each DcRequest object, wrap it in an HTTP request and send to generic service. Map<DcRequest, Map<String, FedoraItem>> dcReqs = proc.generateDcRequests(); int countTotalRequests = dcReqs.size(); int countSuccess = executeDcRequests(dcReqs, statusElements); createRelations(dcReqs); if (countSuccess == countTotalRequests) phenRespRootElement.setStatus(CombinedStatusResponse.Status.SUCCESS); else if (countSuccess == 0) phenRespRootElement.setStatus(CombinedStatusResponse.Status.FAILURE); else phenRespRootElement.setStatus(CombinedStatusResponse.Status.PARTIAL); resp = Response.ok(phenRespRootElement, MediaType.APPLICATION_XML_TYPE).build(); } catch (UnauthorisedException e) { LOGGER.error(e.getMessage()); phenRespRootElement.setStatus(CombinedStatusResponse.Status.FAILURE); phenRespRootElement.setMsg(e.getMessage()); resp = Response.status(Status.UNAUTHORIZED).entity(phenRespRootElement).build(); } catch (Exception e) { LOGGER.error(e.getMessage(), e); phenRespRootElement.setStatus(CombinedStatusResponse.Status.FAILURE); phenRespRootElement.setMsg(e.getMessage()); resp = Response.serverError().entity(phenRespRootElement).build(); } finally { phenRespRootElement.setNodes(statusElements); } return resp; } }