package com.occamlab.te.spi.jaxrs.resources;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
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.Response;
import javax.ws.rs.core.UriInfo;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.occamlab.te.spi.jaxrs.ErrorResponseBuilder;
import com.occamlab.te.spi.jaxrs.TestSuiteController;
import com.occamlab.te.spi.jaxrs.TestSuiteRegistry;
import com.sun.jersey.multipart.FormDataParam;
/**
* A controller resource that provides the results of a test run. An XML
* representation of the results is obtained using HTTP/1.1 methods in accord
* with the JAX-RS 1.1 specification (JSR 311).
*
* @see <a href="http://jcp.org/en/jsr/detail?id=311">JSR 311</a>
*/
@Path("suites/{etsCode}/{etsVersion}/run")
@Produces({ "application/xml; charset='utf-8'", "application/rdf+xml; charset='utf-8'" })
public class TestRunResource {
private static final Logger LOGR = Logger.getLogger(TestRunResource.class.getPackage().getName());
@Context
private UriInfo reqUriInfo;
@Context
private HttpHeaders headers;
/**
* Processes a request submitted using the GET method. The test run
* arguments are specified in the query component of the Request-URI as a
* sequence of key-value pairs.
*
* @param etsCode
* A String that identifies the test suite to be run.
* @param etsVersion
* A String specifying the desired test suite version.
* @return An XML representation of the test results.
*/
@GET
public Source handleGet(@PathParam("etsCode") String etsCode, @PathParam("etsVersion") String etsVersion) {
MultivaluedMap<String, String> params = this.reqUriInfo.getQueryParameters();
Source results = executeTestRun(etsCode, etsVersion, params);
return results;
}
/**
* Processes a request submitted using the POST method. The request entity
* represents the test subject or provides metadata about it. The entity
* body is written to a local file, the location of which is set as the
* value of the {@code iut} parameter.
*
* @param etsCode
* A String that identifies the test suite to be run.
* @param etsVersion
* A String specifying the desired test suite version.
* @param entityBody
* A File containing the request entity body.
* @return An XML representation of the test results.
*/
@POST
@Consumes({ MediaType.APPLICATION_XML, MediaType.TEXT_XML })
public Source handlePost(@PathParam("etsCode") String etsCode, @PathParam("etsVersion") String etsVersion,
File entityBody) {
if (!entityBody.exists() || entityBody.length() == 0) {
throw new WebApplicationException(400);
}
Map<String, java.util.List<String>> args = new HashMap<String, List<String>>();
args.put("iut", Arrays.asList(entityBody.toURI().toString()));
Source results = executeTestRun(etsCode, etsVersion, args);
return results;
}
/**
* Processes a request containing a multipart (multipart/form-data) entity.
* The entity is expected to consist of two parts:
* <ol>
* <li>The (required) "iut" part represents the test subject or provides
* metadata about it; the entity body is written to a local file, the
* location of which is set as the value of the {@code iut } argument.</li>
* <li>The "sch" part defines supplementary constraints defined in a
* Schematron schema; it is also written to a local file, the location of
* which is set as the value of the {@code sch} argument.</li>
* </ol>
*
* @param etsCode
* A String that identifies the test suite to be run.
* @param etsVersion
* A String specifying the desired test suite version.
* @param entityBody
* A File containing a representation of the test subject.
* @param schBody
* A File containing supplementary constraints (e.g. a Schematron
* schema).
* @return An XML representation of the test results.
*
* @see <a href="http://tools.ietf.org/html/rfc7578" target="_blank">RFC
* 7578: Returning Values from Forms: multipart/form-data</a>
* @see <a href=
* "http://standards.iso.org/ittf/PubliclyAvailableStandards/c040833_ISO_IEC_19757-3_2006(E).zip"
* target="_blank">ISO 19757-3: Schematron</a>
*/
@POST
@Consumes({ MediaType.MULTIPART_FORM_DATA })
public Source handleMultipartFormData(@PathParam("etsCode") String etsCode,
@PathParam("etsVersion") String etsVersion, @FormDataParam("iut") File entityBody,
@FormDataParam("sch") File schBody) {
Map<String, java.util.List<String>> args = new HashMap<String, List<String>>();
if (!entityBody.exists() || entityBody.length() == 0) {
throw new WebApplicationException(400);
}
args.put("iut", Arrays.asList(entityBody.toURI().toString()));
if (null != schBody) {
if (!schBody.exists() || schBody.length() == 0) {
throw new WebApplicationException(400);
}
if (LOGR.isLoggable(Level.FINE)) {
StringBuilder msg = new StringBuilder("Test run arguments - ");
msg.append(etsCode).append("/").append(etsVersion).append("\n");
msg.append("Entity media type: " + this.headers.getMediaType());
msg.append("File location: " + schBody.getAbsolutePath());
LOGR.fine(msg.toString());
}
args.put("sch", Arrays.asList(schBody.toURI().toString()));
}
Source results = executeTestRun(etsCode, etsVersion, args);
return results;
}
/**
* Executes a test run using the supplied arguments.
*
* @param etsCode
* A String that identifies the test suite to be run.
* @param etsVersion
* A String specifying the desired test suite version.
* @param testRunArgs
* A multi-valued Map containing the test run arguments.
* @return An XML representation of the test run results.
* @throws WebApplicationException
* If an error occurs while executing a test run.
*/
Source executeTestRun(String etsCode, String etsVersion, Map<String, java.util.List<String>> testRunArgs) {
MediaType preferredMediaType = this.headers.getAcceptableMediaTypes().get(0);
testRunArgs.put("acceptMediaType", Arrays.asList(preferredMediaType.toString()));
if (LOGR.isLoggable(Level.FINE)) {
StringBuilder msg = new StringBuilder("Test run arguments - ");
msg.append(etsCode).append("/").append(etsVersion).append("\n");
msg.append(testRunArgs.toString());
if (null != this.headers.getMediaType()) {
msg.append("Entity media type: " + this.headers.getMediaType());
}
LOGR.fine(msg.toString());
}
Document xmlArgs = readTestRunArguments(testRunArgs);
TestSuiteController controller = findController(etsCode, etsVersion);
Source testResults = null;
try {
testResults = controller.doTestRun(xmlArgs);
} catch (IllegalArgumentException iae) {
ErrorResponseBuilder builder = new ErrorResponseBuilder();
Response rsp = builder.buildErrorResponse(400, iae.getMessage());
throw new WebApplicationException(rsp);
} catch (Exception ex) {
LOGR.log(Level.WARNING, ex.getMessage(), ex);
ErrorResponseBuilder builder = new ErrorResponseBuilder();
Response rsp = builder.buildErrorResponse(500,
String.format("Error executing test suite (%s-%s)", etsCode, etsVersion));
throw new WebApplicationException(rsp);
}
LOGR.fine(String.format("Test results for suite %s-%s: %s", etsCode, etsVersion, testResults.getSystemId()));
return testResults;
}
/**
* Obtains a <code>TestSuiteController</code> for a particular executable
* test suite (ETS) identified by code and version.
*
* @param code
* A <code>String</code> identifying the ETS to execute.
* @param version
* A <code>String</code> indicating the version of the ETS.
* @return The <code>TestSuiteController</code> for the requested ETS.
* @throws WebApplicationException
* If a corresponding controller cannot be found.
*/
TestSuiteController findController(String code, String version) throws WebApplicationException {
TestSuiteRegistry registry = TestSuiteRegistry.getInstance();
TestSuiteController controller = registry.getController(code, version);
if (null == controller) {
throw new WebApplicationException(404);
}
return controller;
}
/**
* Extracts test run arguments from the given Map and inserts them into a
* DOM Document representing an XML properties file.
*
* @param requestParams
* A collection of key-value pairs. Each key can have zero or
* more values but only the first value is used.
* @return A DOM Document node.
* @see java.util.Properties
*/
Document readTestRunArguments(Map<String, java.util.List<String>> requestParams) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document propsDoc = null;
try {
DocumentBuilder db = dbf.newDocumentBuilder();
propsDoc = db.newDocument();
} catch (ParserConfigurationException ex) {
Logger.getLogger(TestRunResource.class.getName()).log(Level.SEVERE, null, ex);
}
Element docElem = propsDoc.createElement("properties");
docElem.setAttribute("version", "1.0");
for (Map.Entry<String, List<String>> param : requestParams.entrySet()) {
Element entry = propsDoc.createElement("entry");
entry.setAttribute("key", param.getKey());
StringBuilder values = new StringBuilder();
for (Iterator<String> itr = param.getValue().iterator(); itr.hasNext();) {
values.append(itr.next());
if (itr.hasNext()) {
values.append(",");
}
}
entry.setTextContent(values.toString());
docElem.appendChild(entry);
}
propsDoc.appendChild(docElem);
return propsDoc;
}
}