/* * 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.jena.fuseki.servlets ; import static java.lang.String.format ; import static org.apache.jena.fuseki.server.CounterName.QueryTimeouts ; import static org.apache.jena.riot.WebContent.ctHTMLForm ; import static org.apache.jena.riot.WebContent.ctSPARQLQuery ; import static org.apache.jena.riot.WebContent.isHtmlForm ; import static org.apache.jena.riot.WebContent.matchContentType ; import static org.apache.jena.riot.web.HttpNames.paramAccept ; import static org.apache.jena.riot.web.HttpNames.paramCallback ; import static org.apache.jena.riot.web.HttpNames.paramDefaultGraphURI ; import static org.apache.jena.riot.web.HttpNames.paramForceAccept ; import static org.apache.jena.riot.web.HttpNames.paramNamedGraphURI ; import static org.apache.jena.riot.web.HttpNames.paramOutput1 ; import static org.apache.jena.riot.web.HttpNames.paramOutput2 ; import static org.apache.jena.riot.web.HttpNames.paramQuery ; import static org.apache.jena.riot.web.HttpNames.paramQueryRef ; import static org.apache.jena.riot.web.HttpNames.paramStyleSheet ; import static org.apache.jena.riot.web.HttpNames.paramTimeout ; import java.io.IOException ; import java.io.InputStream ; import java.util.* ; import javax.servlet.http.HttpServletRequest ; import javax.servlet.http.HttpServletResponse ; import org.apache.jena.atlas.io.IO ; import org.apache.jena.atlas.io.IndentedLineBuffer ; import org.apache.jena.atlas.web.ContentType ; import org.apache.jena.fuseki.Fuseki ; import org.apache.jena.fuseki.FusekiException ; import org.apache.jena.fuseki.FusekiLib ; import org.apache.jena.query.* ; import org.apache.jena.rdf.model.Model ; import org.apache.jena.riot.web.HttpNames ; import org.apache.jena.riot.web.HttpOp ; import org.apache.jena.sparql.core.Prologue ; import org.apache.jena.sparql.resultset.SPARQLResult ; import org.apache.jena.web.HttpSC ; /** Handle SPARQL Query requests over the SPARQL Protocol. * Subclasses provide this algorithm with the actual dataset to query, whether * a dataset hosted by this server ({@link SPARQL_QueryDataset}) or * specified in the protocol request ({@link SPARQL_QueryGeneral}). */ public abstract class SPARQL_Query extends SPARQL_Protocol { private static final long serialVersionUID = 6670547318463759949L; private static final String QueryParseBase = Fuseki.BaseParserSPARQL ; public SPARQL_Query() { super() ; } // Choose REST verbs to support. // doMethod : Not used with UberServlet dispatch. @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) { doCommon(request, response) ; } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) { doCommon(request, response) ; } @Override protected void doOptions(HttpServletRequest request, HttpServletResponse response) { setCommonHeadersForOptions(response) ; response.setHeader(HttpNames.hAllow, "GET,OPTIONS,POST") ; response.setHeader(HttpNames.hContentLengh, "0") ; } protected void doOptions(HttpAction action) { doOptions(action.request, action.response) ; } // All the params we support protected static List<String> allParams = Arrays.asList(paramQuery, paramDefaultGraphURI, paramNamedGraphURI, paramQueryRef, paramStyleSheet, paramAccept, paramOutput1, paramOutput2, paramCallback, paramForceAccept, paramTimeout) ; /** * Validate the request, checking HTTP method and HTTP Parameters. * @param action HTTP Action */ @Override protected void validate(HttpAction action) { String method = action.request.getMethod().toUpperCase(Locale.ROOT) ; if ( HttpNames.METHOD_OPTIONS.equals(method) ) return ; if ( !HttpNames.METHOD_POST.equals(method) && !HttpNames.METHOD_GET.equals(method) ) ServletOps.errorMethodNotAllowed("Not a GET or POST request") ; if ( HttpNames.METHOD_GET.equals(method) && action.request.getQueryString() == null ) { ServletOps.warning(action, "Service Description / SPARQL Query / " + action.request.getRequestURI()) ; ServletOps.errorNotFound("Service Description: " + action.request.getRequestURI()) ; } // Use of the dataset describing parameters is check later. try { validateParams(action, allParams) ; validateRequest(action) ; } catch (ActionErrorException ex) { throw ex ; } // Query not yet parsed. } /** * Validate the request after checking HTTP method and HTTP Parameters. * @param action HTTP Action */ protected abstract void validateRequest(HttpAction action) ; /** * Helper method for validating request. * @param request HTTP request * @param params parameters in a collection of Strings */ protected void validateParams(HttpAction action, Collection<String> params) { HttpServletRequest request = action.request ; ContentType ct = FusekiLib.getContentType(request) ; boolean mustHaveQueryParam = true ; if ( ct != null ) { String incoming = ct.getContentType() ; if ( matchContentType(ctSPARQLQuery, ct) ) { mustHaveQueryParam = false ; // Drop through. } else if ( matchContentType(ctHTMLForm, ct)) { // Nothing specific to do } else ServletOps.error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "Unsupported: " + incoming) ; } // GET/POST of a form at this point. if ( mustHaveQueryParam ) { int N = countParamOccurences(request, paramQuery) ; if ( N == 0 ) ServletOps.errorBadRequest("SPARQL Query: No 'query=' parameter") ; if ( N > 1 ) ServletOps.errorBadRequest("SPARQL Query: Multiple 'query=' parameters") ; // application/sparql-query does not use a query param. String queryStr = request.getParameter(HttpNames.paramQuery) ; if ( queryStr == null ) ServletOps.errorBadRequest("SPARQL Query: No query specified (no 'query=' found)") ; if ( queryStr.isEmpty() ) ServletOps.errorBadRequest("SPARQL Query: Empty query string") ; } if ( params != null ) { Enumeration<String> en = request.getParameterNames() ; for (; en.hasMoreElements();) { String name = en.nextElement() ; if ( !params.contains(name) ) ServletOps.warning(action, "SPARQL Query: Unrecognize request parameter (ignored): " + name) ; } } } @Override protected final void perform(HttpAction action) { // OPTIONS if ( action.request.getMethod().equals(HttpNames.METHOD_OPTIONS) ) { // Share with update via SPARQL_Protocol. doOptions(action) ; return ; } // GET if ( action.request.getMethod().equals(HttpNames.METHOD_GET) ) { executeWithParameter(action) ; return ; } ContentType ct = FusekiLib.getContentType(action) ; // POST application/x-www-form-url // POST ?query= and no Content-Type if ( ct == null || isHtmlForm(ct) ) { // validation checked that if no Content-type, then its a POST with ?query= executeWithParameter(action) ; return ; } // POST application/sparql-query if ( matchContentType(ct, ctSPARQLQuery) ) { executeBody(action) ; return ; } ServletOps.error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "Bad content type: " + ct.getContentType()) ; } protected void executeWithParameter(HttpAction action) { String queryString = action.request.getParameter(paramQuery) ; execute(queryString, action) ; } protected void executeBody(HttpAction action) { String queryString = null ; try { InputStream input = action.request.getInputStream() ; queryString = IO.readWholeFileAsUTF8(input) ; } catch (IOException ex) { ServletOps.errorOccurred(ex) ; } execute(queryString, action) ; } protected void execute(String queryString, HttpAction action) { String queryStringLog = ServletOps.formatForLog(queryString) ; if ( action.verbose ) action.log.info(format("[%d] Query = \n%s", action.id, queryString)) ; else action.log.info(format("[%d] Query = %s", action.id, queryStringLog)) ; Query query = null ; try { // NB syntax is ARQ (a superset of SPARQL) query = QueryFactory.create(queryString, QueryParseBase, Syntax.syntaxARQ) ; queryStringLog = formatForLog(query) ; validateQuery(action, query) ; } catch (ActionErrorException ex) { throw ex ; } catch (QueryParseException ex) { ServletOps.errorBadRequest("Parse error: \n" + queryString + "\n\r" + messageForQueryException(ex)) ; } // Should not happen. catch (QueryException ex) { ServletOps.errorBadRequest("Error: \n" + queryString + "\n\r" + ex.getMessage()) ; } // Assumes finished whole thing by end of sendResult. try { action.beginRead() ; Dataset dataset = decideDataset(action, query, queryStringLog) ; try ( QueryExecution qExec = createQueryExecution(query, dataset) ; ) { SPARQLResult result = executeQuery(action, qExec, query, queryStringLog) ; // Deals with exceptions itself. sendResults(action, result, query.getPrologue()) ; } } catch (QueryParseException ex) { // Late stage static error (e.g. bad fixed Lucene query string). ServletOps.errorBadRequest("Query parse error: \n" + queryString + "\n\r" + messageForQueryException(ex)) ; } catch (QueryCancelledException ex) { // Additional counter information. incCounter(action.getEndpoint().getCounters(), QueryTimeouts) ; throw ex ; } finally { action.endRead() ; } } /** * Check the query - if unacceptable, throw ActionErrorException or call * super.error * @param action HTTP Action * @param query SPARQL Query */ protected abstract void validateQuery(HttpAction action, Query query) ; /** Create the {@link QueryExecution} for this operation. * @param query * @param dataset * @return QueryExecution */ protected QueryExecution createQueryExecution(Query query, Dataset dataset) { return QueryExecutionFactory.create(query, dataset) ; } /** Perform the {@link QueryExecution} once. * @param action * @param queryExecution * @param query * @param queryStringLog Informational string created from the initial query. * @return */ protected SPARQLResult executeQuery(HttpAction action, QueryExecution queryExecution, Query query, String queryStringLog) { setAnyTimeouts(queryExecution, action) ; if ( query.isSelectType() ) { ResultSet rs = queryExecution.execSelect() ; // Force some query execution now. // If the timeout-first-row goes off, the output stream has not // been started so the HTTP error code is sent. rs.hasNext() ; // If we wanted perfect query time cancellation, we could consume // the result now to see if the timeout-end-of-query goes off. // rs = ResultSetFactory.copyResults(rs) ; //action.log.info(format("[%d] exec/select", action.id)) ; return new SPARQLResult(rs) ; } if ( query.isConstructType() ) { Dataset dataset = queryExecution.execConstructDataset(); //action.log.info(format("[%d] exec/construct", action.id)); return new SPARQLResult(dataset); } if ( query.isDescribeType() ) { Model model = queryExecution.execDescribe() ; //action.log.info(format("[%d] exec/describe", action.id)) ; return new SPARQLResult(model) ; } if ( query.isAskType() ) { boolean b = queryExecution.execAsk() ; //action.log.info(format("[%d] exec/ask", action.id)) ; return new SPARQLResult(b) ; } ServletOps.errorBadRequest("Unknown query type - " + queryStringLog) ; return null ; } private void setAnyTimeouts(QueryExecution qexec, HttpAction action) { // if ( !(action.getDataService().allowTimeoutOverride) ) // return ; long desiredTimeout = Long.MAX_VALUE ; String timeoutHeader = action.request.getHeader("Timeout") ; String timeoutParameter = action.request.getParameter("timeout") ; if ( timeoutHeader != null ) { try { desiredTimeout = (int)(Float.parseFloat(timeoutHeader) * 1000) ; } catch (NumberFormatException e) { throw new FusekiException("Timeout header must be a number", e) ; } } else if ( timeoutParameter != null ) { try { desiredTimeout = (int)(Float.parseFloat(timeoutParameter) * 1000) ; } catch (NumberFormatException e) { throw new FusekiException("timeout parameter must be a number", e) ; } } // desiredTimeout = Math.min(action.getDataService().maximumTimeoutOverride, desiredTimeout) ; if ( desiredTimeout != Long.MAX_VALUE ) qexec.setTimeout(desiredTimeout) ; } /** Choose the dataset for this SPARQL Query request. * @param action * @param query Query - this may be modified to remove a DatasetDescription. * @param queryStringLog * @return {@link Dataset} */ protected abstract Dataset decideDataset(HttpAction action, Query query, String queryStringLog) ; /** Ship the results to the remote caller. * @param action * @param result * @param qPrologue */ protected void sendResults(HttpAction action, SPARQLResult result, Prologue qPrologue) { if ( result.isResultSet() ) ResponseResultSet.doResponseResultSet(action, result.getResultSet(), qPrologue) ; else if ( result.isDataset() ) // CONSTRUCT is processed as a extended CONSTRUCT - result is a dataset. ResponseDataset.doResponseDataset(action, result.getDataset()); else if ( result.isModel() ) // DESCRIBE results are models ResponseDataset.doResponseModel(action, result.getModel()); else if ( result.isBoolean() ) ResponseResultSet.doResponseResultSet(action, result.getBooleanResult()) ; else ServletOps.errorOccurred("Unknown or invalid result type") ; } private String formatForLog(Query query) { IndentedLineBuffer out = new IndentedLineBuffer() ; out.setFlatMode(true) ; query.serialize(out) ; return out.asString() ; } private String getRemoteString(String queryURI) { return HttpOp.execHttpGetString(queryURI) ; } }