VIVO-731 Create SparqlQueryApiExecutor with tests.

This commit is contained in:
Jim Blake 2014-04-13 18:22:39 -04:00
parent 7b849ace9b
commit 0c0915ef65
11 changed files with 1064 additions and 0 deletions

View file

@ -0,0 +1,12 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery;
/**
* Indicates that the API can't process this type of query.
*/
public class InvalidQueryTypeException extends Exception {
public InvalidQueryTypeException(String message) {
super(message);
}
}

View file

@ -0,0 +1,105 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* The supported media types for SPARQL queries that return RDF (i.e., CONSTRUCT
* and DESCRIBE).
*/
public enum RdfResultMediaType {
TEXT("text/plain", true, "NTRIPLE", null),
RDF_XML("application/rdf+xml", true, "RDFXML", null),
N3("text/n3", true, "N3", null),
TTL("text/turtle", false, "N3", "TTL"),
JSON("application/json", false, "N3", "JSON");
// ----------------------------------------------------------------------
// Keep a map of content types, for easy conversion back and forth
// ----------------------------------------------------------------------
private final static Map<String, RdfResultMediaType> contentTypesMap = buildMap();
private static Map<String, RdfResultMediaType> buildMap() {
Map<String, RdfResultMediaType> map = new LinkedHashMap<>();
for (RdfResultMediaType value : values()) {
map.put(value.contentType, value);
}
return Collections.unmodifiableMap(map);
}
public static Collection<String> contentTypes() {
return contentTypesMap.keySet();
}
public static RdfResultMediaType fromContentType(String contentType)
throws IllegalArgumentException {
RdfResultMediaType type = contentTypesMap.get(contentType);
if (type == null) {
throw new IllegalArgumentException(
"No RdfResultMediaType has contentType='" + contentType
+ "'");
} else {
return type;
}
}
// ----------------------------------------------------------------------
// The instance
// ----------------------------------------------------------------------
/**
* The MIME type as it would appear in an HTTP Accept or Content-Type
* header.
*/
private final String contentType;
/**
* Is this a format that is supported directly by the RDFService?
*/
private final boolean nativeFormat;
/**
* What format shall we ask the RDFService to supply?
*/
private final String serializationFormat;
/**
* What format shall we ask the resulting OntModel to write? (Applies only
* to non-native formats)
*/
private final String jenaResponseFormat;
private RdfResultMediaType(String contentType, boolean nativeFormat,
String serializationFormat, String jenaResponseFormat) {
this.contentType = contentType;
this.nativeFormat = nativeFormat;
this.serializationFormat = serializationFormat;
this.jenaResponseFormat = jenaResponseFormat;
}
public String getContentType() {
return contentType;
}
public boolean isNativeFormat() {
return nativeFormat;
}
public String getSerializationFormat() {
return serializationFormat;
}
public String getJenaResponseFormat() {
return jenaResponseFormat;
}
}

View file

@ -0,0 +1,105 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* The supported media types for SPARQL queries that return a Result Set (i.e.,
* SELECT and ASK).
*/
public enum ResultSetMediaType {
TEXT("text/plain", true, "TEXT", null),
CSV("text/csv", true, "CSV", null),
TSV("text/tab-separated-values", false, "CSV", "tsv"),
XML("application/sparql-results+xml", true, "XML", null),
JSON("application/sparql-results+json", true, "JSON", null);
// ----------------------------------------------------------------------
// Keep a map of content types, for easy conversion back and forth
// ----------------------------------------------------------------------
private final static Map<String, ResultSetMediaType> contentTypesMap = buildMap();
private static Map<String, ResultSetMediaType> buildMap() {
Map<String, ResultSetMediaType> map = new LinkedHashMap<>();
for (ResultSetMediaType value : values()) {
map.put(value.contentType, value);
}
return Collections.unmodifiableMap(map);
}
public static Collection<String> contentTypes() {
return contentTypesMap.keySet();
}
public static ResultSetMediaType fromContentType(String contentType)
throws IllegalArgumentException {
ResultSetMediaType type = contentTypesMap.get(contentType);
if (type == null) {
throw new IllegalArgumentException(
"No ResultSetMediaType has contentType='" + contentType
+ "'");
} else {
return type;
}
}
// ----------------------------------------------------------------------
// The instance
// ----------------------------------------------------------------------
/**
* The MIME type as it would appear in an HTTP Accept or Content-Type
* header.
*/
private final String contentType;
/**
* Is this a format that is supported directly by the RDFService?
*/
private final boolean nativeFormat;
/**
* What format shall we ask the RDFService to supply?
*/
private final String rdfServiceFormat;
/**
* What format shall we ask the ResultSetFormatter to output? (Applies only
* to non-native formats)
*/
private final String jenaResponseFormat;
private ResultSetMediaType(String contentType, boolean nativeFormat,
String rdfServiceFormat, String jenaResponseFormat) {
this.contentType = contentType;
this.nativeFormat = nativeFormat;
this.rdfServiceFormat = rdfServiceFormat;
this.jenaResponseFormat = jenaResponseFormat;
}
public String getContentType() {
return contentType;
}
public boolean isNativeFormat() {
return nativeFormat;
}
public String getRdfServiceFormat() {
return rdfServiceFormat;
}
public String getJenaResponseFormat() {
return jenaResponseFormat;
}
}

View file

@ -0,0 +1,52 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
import edu.cornell.mannlib.vitro.webapp.utils.http.AcceptHeaderParsingException;
import edu.cornell.mannlib.vitro.webapp.utils.http.NotAcceptableException;
/**
* Process ASK queries.
*/
public class SparqlQueryApiAskExecutor extends SparqlQueryApiResultSetProducer {
public SparqlQueryApiAskExecutor(RDFService rdfService, String queryString,
String acceptHeader) throws AcceptHeaderParsingException,
NotAcceptableException {
super(rdfService, queryString, acceptHeader);
}
/**
* The RDFService returns a boolean from an ASK query, without regard to a
* requested format.
*
* For TEXT, CSV and TSV, we can simple return the String value of the
* boolean as an InputStream. For XML and JSON, however, the W3C documents
* require something a bit more fancy.
*/
@Override
protected InputStream getRawResultStream() throws RDFServiceException {
boolean queryResult = rdfService.sparqlAskQuery(queryString);
String resultString;
if (mediaType == ResultSetMediaType.XML) {
resultString = String
.format("<?xml version=\"1.0\"?>\n" //
+ "<sparql xmlns=\"http://www.w3.org/2005/sparql-results#\">\n" //
+ " <head></head>\n" //
+ " <boolean>%b</boolean>\n" //
+ "</sparql>", queryResult);
} else if (mediaType == ResultSetMediaType.JSON) {
resultString = String.format(
"{\n \"head\" : { } ,\n \"boolean\" : %b\n}\n",
queryResult);
} else {
resultString = String.valueOf(queryResult);
}
return new ByteArrayInputStream(resultString.getBytes());
}
}

View file

@ -0,0 +1,30 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery;
import java.io.InputStream;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService.ModelSerializationFormat;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
import edu.cornell.mannlib.vitro.webapp.utils.http.AcceptHeaderParsingException;
import edu.cornell.mannlib.vitro.webapp.utils.http.NotAcceptableException;
/**
* Process CONSTRUCT queries
*/
public class SparqlQueryApiConstructExecutor extends SparqlQueryApiRdfProducer {
public SparqlQueryApiConstructExecutor(RDFService rdfService,
String queryString, String acceptHeader)
throws AcceptHeaderParsingException, NotAcceptableException {
super(rdfService, queryString, acceptHeader);
}
@Override
protected InputStream getRawResultStream() throws RDFServiceException {
ModelSerializationFormat format = ModelSerializationFormat
.valueOf(mediaType.getSerializationFormat());
return rdfService.sparqlConstructQuery(queryString, format);
}
}

View file

@ -0,0 +1,31 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery;
import java.io.InputStream;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService.ModelSerializationFormat;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
import edu.cornell.mannlib.vitro.webapp.utils.http.AcceptHeaderParsingException;
import edu.cornell.mannlib.vitro.webapp.utils.http.NotAcceptableException;
/**
* Process DESCRIBE queries.
*/
public class SparqlQueryApiDescribeExecutor extends SparqlQueryApiRdfProducer {
public SparqlQueryApiDescribeExecutor(RDFService rdfService,
String queryString, String acceptHeader)
throws AcceptHeaderParsingException, NotAcceptableException {
super(rdfService, queryString, acceptHeader);
}
@Override
protected InputStream getRawResultStream() throws RDFServiceException {
ModelSerializationFormat format = ModelSerializationFormat
.valueOf(mediaType.getSerializationFormat());
return rdfService.sparqlDescribeQuery(queryString, format);
}
}

View file

@ -0,0 +1,87 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery;
import java.io.IOException;
import java.io.OutputStream;
import com.hp.hpl.jena.query.Query;
import com.hp.hpl.jena.query.QueryParseException;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
import edu.cornell.mannlib.vitro.webapp.utils.SparqlQueryUtils;
import edu.cornell.mannlib.vitro.webapp.utils.http.AcceptHeaderParsingException;
import edu.cornell.mannlib.vitro.webapp.utils.http.NotAcceptableException;
/**
* The base class for the SPARQL query API.
*/
public abstract class SparqlQueryApiExecutor {
/**
* Get an instance that is appropriate to the query and the acceptable
* types.
*
* @throws AcceptHeaderParsingException
* if the accept header was not in a valid format
* @throws NotAcceptableException
* if the accept header did not contain a content type that is
* supported by the query
* @throws QueryParseException
* if the query was not syntactically valid
* @throws InvalidQueryTypeException
* if the query was not SELECT, ASK, CONSTRUCT, or DESCRIBE
*/
public static SparqlQueryApiExecutor instance(RDFService rdfService,
String queryString, String acceptHeader)
throws NotAcceptableException, QueryParseException,
InvalidQueryTypeException, AcceptHeaderParsingException {
if (rdfService == null) {
throw new NullPointerException("rdfService may not be null.");
}
if (queryString == null) {
throw new NullPointerException("queryString may not be null.");
}
Query query = SparqlQueryUtils.create(queryString);
if (query.isSelectType()) {
return new SparqlQueryApiSelectExecutor(rdfService, queryString,
acceptHeader);
} else if (query.isAskType()) {
return new SparqlQueryApiAskExecutor(rdfService, queryString,
acceptHeader);
} else if (query.isConstructType()) {
return new SparqlQueryApiConstructExecutor(rdfService, queryString,
acceptHeader);
} else if (query.isDescribeType()) {
return new SparqlQueryApiDescribeExecutor(rdfService, queryString,
acceptHeader);
} else {
throw new InvalidQueryTypeException("The API only accepts SELECT, "
+ "ASK, CONSTRUCT, or DESCRIBE queries: '" + queryString
+ "'");
}
}
protected final RDFService rdfService;
protected final String queryString;
protected SparqlQueryApiExecutor(RDFService rdfService, String queryString) {
this.rdfService = rdfService;
this.queryString = queryString;
}
/**
* What media type was selected, based on the Accept header?
*/
public abstract String getMediaType();
/**
* Execute the query and write it to the output stream, in the selected
* format.
*/
public abstract void executeAndFormat(OutputStream out)
throws RDFServiceException, IOException;
}

View file

@ -0,0 +1,86 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Collection;
import org.apache.commons.io.IOUtils;
import com.github.jsonldjava.core.JSONLD;
import com.github.jsonldjava.core.JSONLDProcessingError;
import com.github.jsonldjava.impl.JenaRDFParser;
import com.github.jsonldjava.utils.JSONUtils;
import com.hp.hpl.jena.rdf.model.Model;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService.ModelSerializationFormat;
import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils;
import edu.cornell.mannlib.vitro.webapp.utils.http.AcceptHeaderParsingException;
import edu.cornell.mannlib.vitro.webapp.utils.http.ContentTypeUtil;
import edu.cornell.mannlib.vitro.webapp.utils.http.NotAcceptableException;
/**
* Base class for processing SPARQL queries that produce RDF: CONSTRUCT and
* DESCRIBE.
*/
abstract class SparqlQueryApiRdfProducer extends SparqlQueryApiExecutor {
protected final RdfResultMediaType mediaType;
public SparqlQueryApiRdfProducer(RDFService rdfService, String queryString,
String acceptHeader) throws AcceptHeaderParsingException,
NotAcceptableException {
super(rdfService, queryString);
Collection<String> contentTypes = RdfResultMediaType.contentTypes();
String bestType = ContentTypeUtil.bestContentType(acceptHeader,
contentTypes);
this.mediaType = RdfResultMediaType.fromContentType(bestType);
}
@Override
public String getMediaType() {
return mediaType.getContentType();
}
@Override
public void executeAndFormat(OutputStream out) throws RDFServiceException,
IOException {
InputStream rawResult = getRawResultStream();
if (mediaType.isNativeFormat()) {
IOUtils.copy(rawResult, out);
} else if (mediaType == RdfResultMediaType.JSON) {
// JSON-LD is a special case, since jena 2.6.4 doesn't support it.
try {
JenaRDFParser parser = new JenaRDFParser();
Object json = JSONLD.fromRDF(parseToModel(rawResult), parser);
JSONUtils.write(new OutputStreamWriter(out, "UTF-8"), json);
} catch (JSONLDProcessingError e) {
throw new RDFServiceException(
"Could not convert from Jena model to JSON-LD", e);
}
} else {
parseToModel(rawResult).write(out,
mediaType.getJenaResponseFormat());
}
}
private Model parseToModel(InputStream rawResult) {
ModelSerializationFormat format = ModelSerializationFormat
.valueOf(mediaType.getSerializationFormat());
return RDFServiceUtils.parseModel(rawResult, format);
}
/**
* Ask the RDFService to run the query, and get the resulting stream.
*/
protected abstract InputStream getRawResultStream()
throws RDFServiceException;
}

View file

@ -0,0 +1,84 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery;
import static edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery.ResultSetMediaType.TSV;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import org.apache.commons.io.IOUtils;
import com.hp.hpl.jena.query.ResultSet;
import com.hp.hpl.jena.query.ResultSetFactory;
import com.hp.hpl.jena.query.ResultSetFormatter;
import com.hp.hpl.jena.sparql.resultset.ResultSetFormat;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
import edu.cornell.mannlib.vitro.webapp.utils.http.AcceptHeaderParsingException;
import edu.cornell.mannlib.vitro.webapp.utils.http.ContentTypeUtil;
import edu.cornell.mannlib.vitro.webapp.utils.http.NotAcceptableException;
/**
* Base class for processing SPARQL queries that produce Result Sets: SELECT and
* ASK.
*/
abstract class SparqlQueryApiResultSetProducer extends SparqlQueryApiExecutor {
protected final ResultSetMediaType mediaType;
public SparqlQueryApiResultSetProducer(RDFService rdfService,
String queryString, String acceptHeader)
throws AcceptHeaderParsingException, NotAcceptableException {
super(rdfService, queryString);
Collection<String> contentTypes = ResultSetMediaType.contentTypes();
String bestType = ContentTypeUtil.bestContentType(acceptHeader,
contentTypes);
this.mediaType = ResultSetMediaType.fromContentType(bestType);
}
@Override
public String getMediaType() {
return mediaType.getContentType();
}
@Override
public void executeAndFormat(OutputStream out) throws RDFServiceException,
IOException {
InputStream rawResult = getRawResultStream();
if (mediaType.isNativeFormat()) {
IOUtils.copy(rawResult, out);
} else if (mediaType == TSV) {
// ARQ doesn't support TSV, so we will do the translation.
pipeWithReplacement(rawResult, out);
} else {
ResultSet rs = ResultSetFactory.fromJSON(rawResult);
ResultSetFormat format = ResultSetFormat.lookup(mediaType
.getJenaResponseFormat());
ResultSetFormatter.output(out, rs, format);
}
}
private void pipeWithReplacement(InputStream in, OutputStream out)
throws IOException {
int size;
byte[] buffer = new byte[4096];
while ((size = in.read(buffer)) > -1) {
for (int i = 0; i < size; i++) {
if (buffer[i] == ',') {
buffer[i] = '\t';
}
}
out.write(buffer, 0, size);
}
}
/**
* Ask the RDFService to run the query, and get the resulting stream.
*/
protected abstract InputStream getRawResultStream()
throws RDFServiceException;
}

View file

@ -0,0 +1,32 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery;
import java.io.InputStream;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService.ResultFormat;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
import edu.cornell.mannlib.vitro.webapp.utils.http.AcceptHeaderParsingException;
import edu.cornell.mannlib.vitro.webapp.utils.http.NotAcceptableException;
/**
* Process SELECT queries.
*/
public class SparqlQueryApiSelectExecutor extends
SparqlQueryApiResultSetProducer {
public SparqlQueryApiSelectExecutor(RDFService rdfService,
String queryString, String acceptHeader)
throws AcceptHeaderParsingException, NotAcceptableException {
super(rdfService, queryString, acceptHeader);
}
@Override
protected InputStream getRawResultStream() throws RDFServiceException {
ResultFormat format = ResultFormat.valueOf(mediaType
.getRdfServiceFormat());
return rdfService.sparqlSelectQuery(queryString, format);
}
}