| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.marmotta.platform.sparql.webservices; |
| |
| import com.google.common.collect.Lists; |
| import com.google.common.io.CharStreams; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.marmotta.commons.http.ContentType; |
| import org.apache.marmotta.commons.http.MarmottaHttpUtils; |
| import org.apache.marmotta.platform.core.api.config.ConfigurationService; |
| import org.apache.marmotta.platform.core.api.exporter.ExportService; |
| import org.apache.marmotta.platform.core.api.templating.TemplatingService; |
| import org.apache.marmotta.platform.core.exception.InvalidArgumentException; |
| import org.apache.marmotta.platform.core.exception.MarmottaException; |
| import org.apache.marmotta.platform.core.util.WebServiceUtil; |
| import org.apache.marmotta.platform.sparql.api.sparql.QueryType; |
| import org.apache.marmotta.platform.sparql.api.sparql.SparqlService; |
| import org.jboss.resteasy.spi.NoLogWebApplicationException; |
| import org.openrdf.query.MalformedQueryException; |
| import org.openrdf.query.QueryLanguage; |
| import org.openrdf.query.UpdateExecutionException; |
| import org.openrdf.query.resultio.BooleanQueryResultWriterRegistry; |
| import org.openrdf.query.resultio.QueryResultIO; |
| import org.openrdf.query.resultio.TupleQueryResultFormat; |
| import org.openrdf.query.resultio.TupleQueryResultWriterRegistry; |
| import org.openrdf.rio.RDFFormat; |
| import org.openrdf.rio.RDFHandlerException; |
| import org.openrdf.rio.RDFWriter; |
| import org.openrdf.rio.Rio; |
| import org.slf4j.Logger; |
| |
| import javax.enterprise.context.ApplicationScoped; |
| import javax.inject.Inject; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.ws.rs.*; |
| import javax.ws.rs.core.*; |
| import javax.ws.rs.core.Response.ResponseBuilder; |
| import javax.ws.rs.core.Response.Status; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URLDecoder; |
| import java.util.*; |
| import java.util.concurrent.TimeoutException; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import static javax.ws.rs.core.HttpHeaders.ACCEPT; |
| import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; |
| import static org.openrdf.rio.RDFFormat.RDFXML; |
| |
| /** |
| * Execute SPARQL query (both query and update) on the LMF triple store |
| * according the SPARQL 1.1 Protocol |
| * |
| * @see <a href="http://www.w3.org/TR/sparql11-protocol/">http://www.w3.org/TR/sparql11-protocol/</a> |
| * @see <a href="http://www.w3.org/TR/sparql11-service-description/">http://www.w3.org/TR/sparql11-service-description/</a> |
| * |
| * @author Sebastian Schaffert |
| * @author Sergio Fernández |
| */ |
| @ApplicationScoped |
| @Path("/" + SparqlWebService.PATH) |
| public class SparqlWebService { |
| |
| public static final String PATH = "sparql"; |
| public static final String SELECT = "/select"; |
| public static final String UPDATE = "/update"; |
| |
| private static final Map<String,String> outputMapper = new HashMap<String,String>(){ |
| private static final long serialVersionUID = 1L; |
| { |
| put("json","application/sparql-results+json"); |
| put("xml","application/sparql-results+xml"); |
| put("tabs","text/tab-separated-values"); |
| put("csv","text/csv"); |
| put("html","text/html"); |
| }}; |
| |
| @Inject |
| private Logger log; |
| |
| @Inject |
| private SparqlService sparqlService; |
| |
| @Inject |
| private ConfigurationService configurationService; |
| |
| @Inject |
| private ExportService exportService; |
| |
| @Inject |
| private TemplatingService templatingService; |
| |
| /** |
| * Single SPARQL endpoint, redirecting to the actual select endpoint |
| * when possible |
| * |
| * @param query |
| * @param update |
| * @param request |
| * @return |
| * @throws URISyntaxException |
| */ |
| @GET |
| public Response get(@QueryParam("query") String query, @QueryParam("update") String update, @Context HttpServletRequest request) throws URISyntaxException { |
| if (StringUtils.isNotBlank(update)) { |
| String msg = "update operations are not supported through get"; //or yes? |
| log.error(msg); |
| return Response.status(Response.Status.BAD_REQUEST).entity(msg).build(); |
| } else { |
| UriBuilder builder = UriBuilder.fromPath(PATH + SELECT); |
| if (StringUtils.isNotBlank(query)) { |
| builder.replaceQuery(request.getQueryString()); |
| } |
| return Response.seeOther(builder.build()).build(); |
| } |
| } |
| |
| /** |
| * Single endpoint for direct post queries (not yet implemented) |
| * |
| * @param request |
| * @return |
| */ |
| @POST |
| public Response post(@Context HttpServletRequest request) { |
| //String query = CharStreams.toString(request.getReader()); |
| //TODO: introspect the query to determine the operation type |
| String msg = "impossible to determine which type of operation (query/update) the request contains"; |
| log.error(msg); |
| return Response.status(Response.Status.CONFLICT).entity(msg).build(); |
| } |
| |
| /** |
| * Execute a SPARQL 1.1 tuple query on the LMF triple store using the query passed as query parameter to the |
| * GET request. Result will be formatted using the result type passed as argument (either "html", "json" or "xml"). |
| * <p/> |
| * see SPARQL 1.1 Query syntax at http://www.w3.org/TR/sparql11-query/ |
| * |
| * @param query the SPARQL 1.1 Query as a string parameter |
| * @param resultType the format for serializing the query results ("html", "json", or "xml") |
| * @HTTP 200 in case the query was executed successfully |
| * @HTTP 500 in case there was an error during the query evaluation |
| * @return the query result in the format passed as argument |
| */ |
| @GET |
| @Path(SELECT) |
| public Response selectGet(@QueryParam("query") String query, @QueryParam("output") String resultType, @Context HttpServletRequest request) { |
| if (StringUtils.isBlank(query)) { |
| return createServiceDescriptionResponse(request, false); |
| } |
| //get real return type: even it is not in the standard, this is useful |
| if(resultType != null && outputMapper.containsKey(resultType)) resultType = outputMapper.get(resultType); |
| return select(query, resultType, request); |
| } |
| |
| /** |
| * Execute a SPARQL 1.1 tuple query on the LMF triple store using the query passed as form parameter to the |
| * POST request. Result will be formatted using the result type passed as argument (either "html", "json" or "xml"). |
| * <p/> |
| * see SPARQL 1.1 Query syntax at http://www.w3.org/TR/sparql11-query/ |
| * |
| * @param query the SPARQL 1.1 Query as a string parameter |
| * @param resultType the format for serializing the query results ("html", "json", or "xml") |
| * @HTTP 200 in case the query was executed successfully |
| * @HTTP 500 in case there was an error during the query evaluation |
| * @return the query result in the format passed as argument |
| */ |
| @POST |
| @Consumes({"application/x-www-url-form-urlencoded", "application/x-www-form-urlencoded"}) |
| @Path(SELECT) |
| public Response selectPostForm(@FormParam("query") String query, @QueryParam("output") String resultType, @Context HttpServletRequest request) { |
| if(resultType != null && outputMapper.containsKey(resultType)) resultType = outputMapper.get(resultType); |
| return select(query, resultType, request); |
| } |
| |
| /** |
| * Execute a SPARQL 1.1 tuple query on the LMF triple store using the query passed in the body of the |
| * POST request. Result will be formatted using the result type passed as argument (either "html", "json" or "xml"). |
| * <p/> |
| * see SPARQL 1.1 Query syntax at http://www.w3.org/TR/sparql11-query/ |
| * |
| * @param request the servlet request (to retrieve the SPARQL 1.1 Query passed in the body of the POST request) |
| * @param resultType the format for serializing the query results ("html", "json", or "xml") |
| * @HTTP 200 in case the query was executed successfully |
| * @HTTP 500 in case there was an error during the query evaluation |
| * @return the query result in the format passed as argument |
| */ |
| @POST |
| @Path(SELECT) |
| public Response selectPost(@QueryParam("output") String resultType, @Context HttpServletRequest request) { |
| try { |
| if(resultType != null && outputMapper.containsKey(resultType)) resultType = outputMapper.get(resultType); |
| if(request.getCharacterEncoding() == null) { |
| request.setCharacterEncoding("utf-8"); |
| } |
| String query = CharStreams.toString(request.getReader()); |
| //String query = IOUtils.toString(request.getInputStream(),"utf-8"); |
| log.debug("Query: {}",query); |
| return select(query, resultType, request); |
| } catch (IOException e) { |
| log.error("body not found", e); |
| return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build(); |
| } |
| } |
| |
| /** |
| * Actual SELECT implementation |
| * |
| * @param query |
| * @param resultType |
| * @param request |
| * @return |
| */ |
| private Response select(String query, String resultType, HttpServletRequest request) { |
| try { |
| // MARMOTTA-606 - check all "Accept" Headers, not only the first one |
| List<ContentType> acceptedTypes = MarmottaHttpUtils.parseAcceptHeader(request.getHeaders(ACCEPT)); |
| String acceptHeader = StringUtils.defaultString(request.getHeader(ACCEPT), ""); |
| if (StringUtils.isBlank(query)) { //empty query |
| // combine the list of accepted types to search for HTML header |
| acceptHeader = StringUtils.join(acceptedTypes, ","); |
| if (acceptHeader.contains("html")) { |
| return Response.seeOther(new URI(configurationService.getServerUri() + "sparql/admin/squebi.html")).build(); |
| } else { |
| return Response.status(Status.ACCEPTED).entity("no SPARQL query specified").build(); |
| } |
| } else { |
| //query duck typing |
| QueryType queryType = sparqlService.getQueryType(QueryLanguage.SPARQL, query); |
| // List<ContentType> acceptedTypes; |
| List<ContentType> offeredTypes; |
| if (resultType != null) { |
| acceptedTypes = MarmottaHttpUtils.parseAcceptHeader(resultType); |
| } |
| // else { |
| // acceptedTypes = MarmottaHttpUtils.parseAcceptHeader(acceptHeader); |
| // } |
| if (QueryType.TUPLE.equals(queryType)) { |
| offeredTypes = MarmottaHttpUtils.parseQueryResultFormatList(TupleQueryResultWriterRegistry.getInstance().getKeys()); |
| } else if (QueryType.BOOL.equals(queryType)) { |
| offeredTypes = MarmottaHttpUtils.parseQueryResultFormatList(BooleanQueryResultWriterRegistry.getInstance().getKeys()); |
| } else if (QueryType.GRAPH.equals(queryType)) { |
| Set<String> producedTypes = new HashSet<String>(exportService.getProducedTypes()); |
| producedTypes.remove("application/xml"); |
| producedTypes.remove("text/plain"); |
| producedTypes.remove("text/html"); |
| producedTypes.remove("application/xhtml+xml"); |
| offeredTypes = MarmottaHttpUtils.parseStringList(producedTypes); |
| } else { |
| return Response.status(Response.Status.BAD_REQUEST).entity("no result format specified or unsupported result format").build(); |
| } |
| ContentType bestType = MarmottaHttpUtils.bestContentType(offeredTypes, acceptedTypes); |
| if (bestType == null) { |
| return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE).entity("no result format specified or unsupported result format").build(); |
| } else { |
| return buildQueryResponse(bestType, query, queryType); |
| } |
| } |
| } catch (InvalidArgumentException e) { |
| log.error("query parsing threw an exception", e); |
| return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build(); |
| } catch(Exception e) { |
| log.error("query execution threw an exception", e); |
| return Response.serverError().entity("query not supported").build(); |
| } |
| } |
| |
| /** |
| * For CORS operations TODO: make it more fine grained (maybe user dependent) |
| * + TODO filter chain do not work properly |
| * |
| * @param reqHeaders |
| * @return responde |
| |
| @OPTIONS |
| @Path(UPDATE) |
| public Response optionsResourceRemote(@HeaderParam("Access-Control-Request-Headers") String reqHeaders) { |
| if(reqHeaders == null) { |
| reqHeaders = "Accept, Content-Type"; |
| } |
| return Response.ok() |
| .header("Allow", "POST") |
| .header("Access-Control-Allow-Methods", "POST") |
| .header("Access-Control-Allow-Headers", reqHeaders) |
| .header("Access-Control-Allow-Origin", configurationService.getStringConfiguration("sparql.allow_origin","*")) |
| .build(); |
| |
| } |
| */ |
| |
| /** |
| * Execute a SPARQL 1.1 Update request passed in the query parameter of the GET. The update will |
| * be carried out |
| * on the LMF triple store. |
| * <p/> |
| * see SPARQL 1.1 Update syntax at http://www.w3.org/TR/sparql11-update/ |
| * |
| * @param update the update query in SPARQL 1.1 syntax |
| * @param query the update query in SPARUL syntax |
| * @HTTP 200 in case the update was carried out successfully |
| * @HTTP 500 in case the update was not successful |
| * @return empty content in case the update was successful, the error message in case an error occurred |
| */ |
| @GET |
| @Path(UPDATE) |
| public Response updateGet(@QueryParam("update") String update, @QueryParam("query") String query, @QueryParam("output") String resultType, @Context HttpServletRequest request) { |
| String q = getUpdateQuery(update, query); |
| if (StringUtils.isBlank(q)) { |
| return createServiceDescriptionResponse(request, true); |
| } |
| return update(q, resultType, request); |
| } |
| |
| /** |
| * Execute a SPARQL 1.1 Update request using update via POST directly; |
| * see details at http://www.w3.org/TR/sparql11-protocol/\#update-operation |
| * |
| * @param request the servlet request (to retrieve the SPARQL 1.1 Update query passed in the |
| * body of the POST request) |
| * @HTTP 200 in case the update was carried out successfully |
| * @HTTP 400 in case the update query is missing or invalid |
| * @HTTP 500 in case the update was not successful |
| * @return empty content in case the update was successful, the error message in case an error |
| * occurred |
| */ |
| @POST |
| @Path(UPDATE) |
| @Consumes("application/sparql-update") |
| public Response updatePostDirectly(@Context HttpServletRequest request, @QueryParam("output") String resultType) { |
| try { |
| if(request.getCharacterEncoding() == null) { |
| request.setCharacterEncoding("utf-8"); |
| } |
| String q = CharStreams.toString(request.getReader()); |
| return update(q, resultType, request); |
| } catch (IOException e) { |
| return Response.serverError().entity(WebServiceUtil.jsonErrorResponse(e)).build(); |
| } |
| } |
| |
| /** |
| * Execute a SPARQL 1.1 Update request using update via URL-encoded POST; |
| * see details at http://www.w3.org/TR/sparql11-protocol/\#update-operation |
| * |
| * @param request the servlet request (to retrieve the SPARQL 1.1 Update query passed in the |
| * body of the POST request) |
| * @HTTP 200 in case the update was carried out successfully |
| * @HTTP 400 in case the update query is missing or invalid |
| * @HTTP 500 in case the update was not successful |
| * @return empty content in case the update was successful, the error message in case an error |
| * occurred |
| */ |
| @POST |
| @Path(UPDATE) |
| @Consumes({"application/x-www-url-form-urlencoded", "application/x-www-form-urlencoded"}) |
| public Response updatePostUrlEncoded(@Context HttpServletRequest request) { |
| try { |
| Map<String,String> params = parseEncodedQueryParameters(CharStreams.toString(request.getReader())); |
| String q = StringUtils.defaultString(params.get("update")); |
| String resultType = StringUtils.defaultString(params.get("output")); |
| return update(q, resultType, request); |
| } catch (IOException e) { |
| return Response.serverError().entity(WebServiceUtil.jsonErrorResponse(e)).build(); |
| } |
| } |
| |
| /** |
| * Actual update implementation |
| * |
| */ |
| private Response update(String update, String resultType, HttpServletRequest request) { |
| try { |
| if (StringUtils.isNotBlank(update)) { |
| sparqlService.update(QueryLanguage.SPARQL, update); |
| return Response.ok().build(); |
| } else { |
| if (resultType == null) { |
| // MARMOTTA-606: Check all provdes accept headers, not only the first one |
| List<ContentType> acceptedTypes = MarmottaHttpUtils.parseAcceptHeader(request.getHeaders(ACCEPT)); |
| List<ContentType> offeredTypes = MarmottaHttpUtils.parseStringList(Lists.newArrayList("*/*", "text/html")); |
| ContentType bestType = MarmottaHttpUtils.bestContentType(offeredTypes, acceptedTypes); |
| if (bestType != null) { |
| resultType = bestType.getMime(); |
| } |
| } |
| if (parseSubType(resultType).equals("html")) |
| return Response.seeOther(new URI(configurationService.getServerUri() + "sparql/admin/update.html")).build(); |
| else |
| return Response.status(Status.ACCEPTED).entity("no SPARQL query specified").build(); |
| } |
| } catch (MalformedQueryException ex) { |
| return Response.status(Response.Status.BAD_REQUEST).entity(WebServiceUtil.jsonErrorResponse(ex)).build(); |
| } catch(UpdateExecutionException e) { |
| log.error("update execution threw an exception",e); |
| return Response.serverError().entity(WebServiceUtil.jsonErrorResponse(e)).build(); |
| } catch (MarmottaException e) { |
| return Response.serverError().entity(WebServiceUtil.jsonErrorResponse(e)).build(); |
| } catch (URISyntaxException e) { |
| return Response.serverError().entity(WebServiceUtil.jsonErrorResponse(e)).build(); |
| } |
| } |
| |
| /** |
| * Get right update query from both possible parameters, for keeping |
| * backward compatibility with the old parameter |
| * |
| * @param update update parameter |
| * @param query query parameter |
| * @return |
| */ |
| private String getUpdateQuery(String update, String query) { |
| if (StringUtils.isNotBlank(update)) |
| return update; |
| else if (StringUtils.isNotBlank(query)) { |
| log.warn("Update query still uses the old 'query' parameter"); |
| return query; |
| } else |
| return null; |
| } |
| |
| /** |
| * Parse the encoded query parameters |
| * |
| * @todo this should be somewhere already implemented |
| * @param body |
| * @return parameters |
| */ |
| private Map<String,String> parseEncodedQueryParameters(String body) { |
| Map<String,String> params = new HashMap<String,String>(); |
| for (String pair : body.split("&")) { |
| int eq = pair.indexOf("="); |
| try { |
| if (eq < 0) { |
| // key with no value |
| params.put(URLDecoder.decode(pair, "UTF-8"), ""); |
| } else { |
| // key=value |
| String key = URLDecoder.decode(pair.substring(0, eq), "UTF-8"); |
| String value = URLDecoder.decode(pair.substring(eq + 1), "UTF-8"); |
| params.put(key, value); |
| } |
| } catch (UnsupportedEncodingException e) { |
| log.error("Query parameter cannot be decoded: {}", e.getMessage(), e); |
| } |
| } |
| return params; |
| } |
| |
| private Response createServiceDescriptionResponse(final HttpServletRequest request, final boolean isUpdate) { |
| final List<ContentType> acceptedTypes; |
| if (StringUtils.isBlank(request.getHeader(ACCEPT))) { |
| acceptedTypes = Collections.singletonList(MarmottaHttpUtils.parseContentType(RDFXML.getDefaultMIMEType())); |
| } else { |
| // MARMOTTA-606 - retrieve all headers instead of the first one |
| acceptedTypes = MarmottaHttpUtils.parseAcceptHeader(request.getHeaders(ACCEPT)); |
| } |
| |
| ContentType _bestType = null; |
| RDFFormat _format = null; |
| for (ContentType ct : acceptedTypes) { |
| final RDFFormat f = Rio.getWriterFormatForMIMEType(ct.getMime()); |
| if (f != null) { |
| _bestType = ct; |
| _format = f; |
| break; |
| } |
| } |
| if (_bestType == null || _format == null) { |
| // FIXME: todo |
| return Response.status(Status.BAD_REQUEST).entity("Could not determine Format").build(); |
| } |
| |
| final RDFFormat format = _format; |
| final ContentType returnType = _bestType; |
| |
| final StreamingOutput entity = new StreamingOutput() { |
| @Override |
| public void write(OutputStream outputStream) throws IOException, |
| WebApplicationException { |
| try { |
| final RDFWriter writer = Rio.createWriter(format, outputStream); |
| sparqlService.createServiceDescription(writer, request.getRequestURL().toString(), isUpdate); |
| } catch (RDFHandlerException e) { |
| log.warn("Could not send SpaqlServiceDescription: {}", e); |
| throw new NoLogWebApplicationException(e, Response.serverError().entity(e).build()); |
| } |
| } |
| }; |
| |
| return Response.ok(entity, new MediaType(returnType.getType(), returnType.getSubtype(), returnType.getCharset().name())).build(); |
| } |
| |
| private Response buildQueryResponse(final ContentType format, final String query, final QueryType queryType) throws Exception { |
| StreamingOutput entity = new StreamingOutput() { |
| @Override |
| public void write(OutputStream output) throws IOException, WebApplicationException { |
| try { |
| sparqlService.query(QueryLanguage.SPARQL, query, output, format.getMime(), configurationService.getIntConfiguration("sparql.timeout", 60)); |
| } catch (MarmottaException ex) { |
| throw new WebApplicationException(ex.getCause(), Response.status(Response.Status.BAD_REQUEST).entity(WebServiceUtil.jsonErrorResponse(ex)).build()); |
| } catch (MalformedQueryException e) { |
| throw new WebApplicationException(e.getCause(), Response.status(Response.Status.BAD_REQUEST).entity(WebServiceUtil.jsonErrorResponse(e)).build()); |
| } catch (TimeoutException e) { |
| throw new WebApplicationException(e.getCause(), Response.status(Response.Status.GATEWAY_TIMEOUT).entity(WebServiceUtil.jsonErrorResponse(e)).build()); |
| } |
| } |
| }; |
| |
| final ResponseBuilder responseBuilder = Response.ok().entity(entity).header(CONTENT_TYPE, format.getMime()); |
| final TupleQueryResultFormat fmt = QueryResultIO.getWriterFormatForMIMEType(format.getMime()); |
| if (fmt != null) { |
| responseBuilder.header("Content-Disposition", String.format("attachment; filename=\"%s.%s\"", queryType.toString().toLowerCase(), fmt.getDefaultFileExtension())); |
| } |
| return responseBuilder.build(); |
| } |
| |
| private static Pattern subTypePattern = Pattern.compile("[a-z]+/([a-z0-9-._]+\\+)?([a-z0-9-._]+)(;.*)?"); |
| private String parseSubType(String mimeType) { |
| Matcher matcher = subTypePattern.matcher(mimeType); |
| if (matcher.matches()) |
| return matcher.group(2); |
| else |
| return mimeType; |
| } |
| } |