/*
* Copyright 2011 Revelytix Inc.
*
* 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.
*/
package spark.protocol.parser;
import static spark.protocol.ProtocolCommand.ResultType.ASK;
import static spark.protocol.ProtocolCommand.ResultType.GRAPH;
import static spark.protocol.ProtocolCommand.ResultType.SELECT;
import java.io.IOException;
import java.io.InputStream;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.api.Command;
import spark.api.Result;
import spark.api.exception.SparqlException;
import spark.protocol.ProtocolCommand.ResultType;
/**
* Factory for creating SPARQL {@link Result} objects from SPARQL HTTP Protocol responses.
* Borrows heavily from Paul Gearon's ResultBuilder.java implementation in Knoodl.
*
* @author Alex Hall
* @author Paul Gearon
* @created Aug 3, 2011
*/
public class ResultFactory {
private static final Logger logger = LoggerFactory.getLogger(ResultFactory.class);
private static final Map<String,ResponseFormat> mimeFormats = new HashMap<String,ResponseFormat>();
/**
* Enumeration of expected response formats. A format is defined by a combination of a parser,
* one or more supported result types, and one or more MIME types.
*/
enum ResponseFormat {
SPARQL_XML(new XMLResultsParser(), EnumSet.of(SELECT, ASK), "application/sparql-results+xml"),
SPARQL_JSON(new UnsupportedFormatParser("SPARQL JSON results"), EnumSet.of(SELECT, ASK), "application/sparql-results+json"),
RDF_XML(new UnsupportedFormatParser("RDF/XML"), EnumSet.of(GRAPH), "application/rdf+xml"),
RDF_TURTLE(new UnsupportedFormatParser("RDF Turtle"), EnumSet.of(GRAPH), "text/turtle", "text/n3", "text/rdf+n3", "application/n3"),
TEXT_HTML(new HTMLParser(), EnumSet.of(ASK), "text/html");
private final ResultParser parser;
private final EnumSet<ResultType> resultTypes;
private final String mimeText;
private ResponseFormat(ResultParser parser, EnumSet<ResultType> resultTypes, String... mimeTexts) {
this.parser = parser;
this.resultTypes = resultTypes;
String mimeText = null;
for (String txt : mimeTexts) {
if (mimeText == null) mimeText = txt; // Take the first MIME type as the "preferred" one.
mimeFormats.put(txt, this);
}
this.mimeText = mimeText;
}
}
/** Define default response formats for each result type. */
private static final Map<ResultType,ResponseFormat> defaultTypeFormats = new HashMap<ResultType,ResponseFormat>();
static {
defaultTypeFormats.put(SELECT, ResponseFormat.SPARQL_XML);
defaultTypeFormats.put(ASK, ResponseFormat.SPARQL_XML);
defaultTypeFormats.put(GRAPH, ResponseFormat.RDF_TURTLE);
}
/** System-wide default format to use if the result type is unknown. */
private static final ResponseFormat DEFAULT_FORMAT = ResponseFormat.SPARQL_XML;
/** Placeholder for un-implemented result formats; throws unsupported operation exceptions. */
private static class UnsupportedFormatParser implements ResultParser {
private final String format;
UnsupportedFormatParser(String format) { this.format = format; }
/** Always throws an exception. */
@Override
public Result parse(Command cmd, InputStream input, ResultType type) {
throw new UnsupportedOperationException("Unsupported SPARQL result format: " + format);
}
}
/**
* Strips the parameters from the end of a mediaType description.
* @param mediaType The text in a Content-Type header.
* @return The content type string without any parameters.
*/
private static final String stripParams(String mediaType) {
int sc = mediaType.indexOf(';');
if (sc >= 0) mediaType = mediaType.substring(0, sc);
return mediaType;
}
/**
* Determine whether the given media type supports the expected result type, i.e. don't request
* RDF/XML if the user is expecting SELECT results.
* @param mediaType The requested media type.
* @param expectedType The expected result type.
* @return <tt>false</tt> if the requested media type is unrecognized or does not support the
* requested result type, <tt>true</tt> otherwise.
*/
public static boolean supports(String mediaType, ResultType expectedType) {
if (mediaType == null) return true; // Assume the server will choose a reasonable media type.
ResponseFormat format = mimeFormats.get(stripParams(mediaType));
return (format != null && (expectedType == null || format.resultTypes.contains(expectedType)));
}
/**
* Gets the default content type to use when sending a query request with the given expected result type.
* @param expectedType The expected result type, or null if none is specified.
* @return The default MIME content type to use for the given result type.
*/
public static String getDefaultMediaType(ResultType expectedType) {
ResponseFormat format = (expectedType != null) ? defaultTypeFormats.get(expectedType) : null;
// If the expected type is unknown, we should let the server decide, otherwise we could
// wind up requesting a response type that doesn't match the actual resuts (e.g. xml with CONSTRUCT).
// TODO: We could include multiple media types in the Accept: field, but that assumes that the
// server has proper support for content negotiation. Many servers only look at the first value.
return (format != null) ? format.mimeText : null;
}
/**
* Find a parser to handle the protocol response body based on the content type found in the response
* and the expected result type specified by the user; if one or both fields is missing then
* attempts to choose a sensible default.
* @param mediaType The content type in the response, or null if none was given.
* @param expectedType The expected response type indicated by the user, or
* @return
*/
private static final ResultParser findParser(String mediaType, ResultType expectedType) {
ResponseFormat format = null;
// Prefer MIME type when choosing result format.
if (mediaType != null) {
mediaType = stripParams(mediaType);
format = mimeFormats.get(mediaType);
if (format == null) {
logger.warn("Unrecognized media type ({}) in SPARQL server response", mediaType);
} else {
logger.debug("Using result format {} for media type {}", format, mediaType);
}
}
// If MIME type was absent or unrecognized, choose default based on expected result type.
if (format == null) {
logger.debug("Unable to determine result format from media type");
if (expectedType != null) {
format = defaultTypeFormats.get(expectedType);
logger.debug("Using default format {} for expected result type {}", format, expectedType);
} else {
format = DEFAULT_FORMAT;
logger.debug("No expected type provided; using default format {}", format);
}
}
assert format != null:"Could not determine result format";
// Validate that the chosen format can produce the expected result type.
if (expectedType != null && !format.resultTypes.contains(expectedType)) {
throw new SparqlException("Result format " + format + " does not support expected result type " + expectedType);
}
return format.parser;
}
/**
* Creates a SPARQL {@link Result} object by parsing the given server response.
* @param cmd The command which originated the request.
* @param response The HTTP response from the SPARQL server.
* @param expectedType The expected result type specified by the caller, or null if none given.
* @return The parsed result object to return to the caller.
* @throws SparqlException If the response from the server could not be parsed, or could not be
* converted to the expected result type.
*/
public static Result getResult(Command cmd, HttpResponse response, ResultType expectedType) throws SparqlException {
HttpEntity entity = response.getEntity();
if (entity == null) throw new SparqlException("No data in response from server");
Header header = entity.getContentType();
String mediaType = (header != null) ? header.getValue() : null;
ResultParser parser = null;
try {
parser = findParser(mediaType, expectedType);
} catch (SparqlException e) {
logger.debug("Couldn't find parser to use for protocol response; cleaning up.");
try {
entity.getContent().close();
} catch (IOException ioe) {
logger.warn("Error cleaning up response for failed protocol command", e);
}
throw e;
}
assert parser != null:"Could not find result parser";
Result result = null;
try {
result = parser.parse(cmd, entity.getContent(), expectedType);
} catch (IOException e) {
throw new SparqlException("Error reading response from server", e);
}
if (result == null) {
// Don't know how we got here, but parser should have returned a result or thrown an exception.
throw new IllegalStateException("Could not parse result from server response.");
}
// Should never happen because the result format should have been validated against the expected
// class when selecting the format to use for parsing, but check anyways.
if (expectedType != null && !expectedType.getResultClass().isInstance(result)) {
try {
result.close();
} catch (IOException e) {
logger.warn("Error closing result of incorrect type", e);
}
throw new IllegalStateException("Result parsed from server response (" +
result.getClass().getName() + ") does not match expected result type (" + expectedType + ")");
}
return result;
}
}