org.apache.marmotta.platform.sparql.webservices.SparqlWebService.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.marmotta.platform.sparql.webservices.SparqlWebService.java

Source

/*
 * 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 Fernndez
 */
@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 {
            String acceptHeader = StringUtils.defaultString(request.getHeader(ACCEPT), "");
            if (StringUtils.isBlank(query)) { //empty query
                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) {
                    List<ContentType> acceptedTypes = MarmottaHttpUtils
                            .parseAcceptHeader(request.getHeader(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 {
            acceptedTypes = MarmottaHttpUtils.parseAcceptHeader(request.getHeader(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;
    }
}