/** * 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.riot.WebContent.contentTypeSPARQLQuery ; import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate ; import java.io.IOException ; import java.util.List ; import javax.servlet.ServletException ; import javax.servlet.http.HttpServletRequest ; import javax.servlet.http.HttpServletResponse ; import org.apache.jena.atlas.web.MediaType ; import org.apache.jena.fuseki.DEF ; import org.apache.jena.fuseki.Fuseki ; import org.apache.jena.fuseki.FusekiException ; import org.apache.jena.fuseki.conneg.ConNeg ; import org.apache.jena.fuseki.server.* ; import org.apache.jena.riot.web.HttpNames ; /** This servlet can be attached to a dataset location * and acts as a router for all SPARQL operations * (query, update, graph store, both direct and * indirect naming, quads operations on a dataset and * ?query and ?update directly on a dataset.) */ public abstract class SPARQL_UberServlet extends ActionSPARQL { private static final long serialVersionUID = -491895535163680509L; protected abstract boolean allowQuery(HttpAction action) ; protected abstract boolean allowUpdate(HttpAction action) ; protected abstract boolean allowREST_R(HttpAction action) ; protected abstract boolean allowREST_W(HttpAction action) ; protected abstract boolean allowQuadsR(HttpAction action) ; protected abstract boolean allowQuadsW(HttpAction action) ; public static class ReadOnly extends SPARQL_UberServlet { private static final long serialVersionUID = -3486969173228213955L; public ReadOnly() { super() ; } @Override protected boolean allowQuery(HttpAction action) { return true ; } @Override protected boolean allowUpdate(HttpAction action) { return false ; } @Override protected boolean allowREST_R(HttpAction action) { return true ; } @Override protected boolean allowREST_W(HttpAction action) { return false ; } @Override protected boolean allowQuadsR(HttpAction action) { return true ; } @Override protected boolean allowQuadsW(HttpAction action) { return false ; } } public static class ReadWrite extends SPARQL_UberServlet { private static final long serialVersionUID = 1383389566691599382L; public ReadWrite() { super() ; } @Override protected boolean allowQuery(HttpAction action) { return true ; } @Override protected boolean allowUpdate(HttpAction action) { return true ; } @Override protected boolean allowREST_R(HttpAction action) { return true ; } @Override protected boolean allowREST_W(HttpAction action) { return true ; } @Override protected boolean allowQuadsR(HttpAction action) { return true ; } @Override protected boolean allowQuadsW(HttpAction action) { return true ; } } public static class AccessByConfig extends SPARQL_UberServlet { private static final long serialVersionUID = 5078964040391977778L; public AccessByConfig() { super() ; } @Override protected boolean allowQuery(HttpAction action) { return isEnabled(action, OperationName.Query) ; } @Override protected boolean allowUpdate(HttpAction action) { return isEnabled(action, OperationName.Update) ; } @Override protected boolean allowREST_R(HttpAction action) { return isEnabled(action, OperationName.GSP_R) || isEnabled(action, OperationName.GSP_RW) ; } @Override protected boolean allowREST_W(HttpAction action) { return isEnabled(action, OperationName.GSP_RW) ; } @Override protected boolean allowQuadsR(HttpAction action) { return isEnabled(action, OperationName.Quads_R) || isEnabled(action, OperationName.Quads_RW) ; } @Override protected boolean allowQuadsW(HttpAction action) { return isEnabled(action, OperationName.Quads_RW) ; } // Test whether there is a configuration that allows this action as the operation given. // Ignores the operation in the action (set due to parsing - it might be "quads" // which is the generic operation when just the dataset is specificed. private boolean isEnabled(HttpAction action, OperationName opName) { // Disregard the operation name of the action DataService dSrv = action.getDataService() ; if ( dSrv == null ) return false; return ! dSrv.getOperation(opName).isEmpty() ; } } /* This can be used for a single servlet for everything (über-servlet) * * It can check for a request that looks like a service request and passes it on. * This takes precedence over direct naming. */ private final ActionSPARQL queryServlet = new SPARQL_QueryDataset() ; private final ActionSPARQL updateServlet = new SPARQL_Update() ; private final ActionSPARQL uploadServlet = new SPARQL_Upload() ; private final ActionSPARQL gspServlet_R = new SPARQL_GSP_R() ; private final ActionSPARQL gspServlet_RW = new SPARQL_GSP_RW() ; private final ActionSPARQL restQuads_R = new REST_Quads_R() ; private final ActionSPARQL restQuads_RW = new REST_Quads_RW() ; public SPARQL_UberServlet() { super(); } private String getEPName(String dsname, List<String> endpoints) { if (endpoints == null || endpoints.size() == 0) return null ; String x = endpoints.get(0) ; if ( ! dsname.endsWith("/") ) x = dsname+"/"+x ; else x = dsname+x ; return x ; } // These calls should not happen because we hook in at executeAction @Override protected void validate(HttpAction action) { throw new FusekiException("Call to SPARQL_UberServlet.validate") ; } @Override protected void perform(HttpAction action) { throw new FusekiException("Call to SPARQL_UberServlet.perform") ; } /** Map request to uri in the registry. * null means no mapping done */ @Override protected String mapRequestToDataset(HttpAction action) { String uri = ActionLib.removeContextPath(action) ; return ActionLib.mapRequestToDatasetLongest$(uri, action.getDataAccessPointRegistry()) ; } /** Intercept the processing cycle at the point where the action has been set up, * the dataset target decided but no validation or execution has been done, * nor any stats have been done. */ @Override protected void executeAction(HttpAction action) { // DEBUG: DataAccessPointRegistry.print("UberServlet "); long id = action.id ; HttpServletRequest request = action.request ; HttpServletResponse response = action.response ; String actionURI = action.getActionURI() ; // No context path String method = request.getMethod() ; DataAccessPoint desc = action.getDataAccessPoint() ; DataService dSrv = action.getDataService() ; // if ( ! dSrv.isActive() ) // ServletOps.error(HttpSC.SERVICE_UNAVAILABLE_503, "Dataset not currently active"); // Part after the DataAccessPoint (dataset) name. String trailing = findTrailing(actionURI, desc.getName()) ; String qs = request.getQueryString() ; boolean hasParams = request.getParameterMap().size() > 0 ; // Is it a query or update because of a ?query= , ?request= parameter? // Test for parameters - includes HTML forms. boolean isQuery = request.getParameter(HttpNames.paramQuery) != null ; // Include old name "request=" boolean isUpdate = request.getParameter(HttpNames.paramUpdate) != null || request.getParameter(HttpNames.paramRequest) != null ; boolean hasParamGraph = request.getParameter(HttpNames.paramGraph) != null ; boolean hasParamGraphDefault = request.getParameter(HttpNames.paramGraphDefault) != null ; boolean hasTrailing = ( trailing.length() != 0 ) ; String ct = request.getContentType() ; String charset = request.getCharacterEncoding() ; MediaType mt = null ; if ( ct != null ) { // Parse it. mt = MediaType.create(ct, charset) ; // Another way to send queries and updates is with the content-type. if ( contentTypeSPARQLQuery.equalsIgnoreCase(ct) ) isQuery = true ; else if ( contentTypeSPARQLUpdate.equalsIgnoreCase(ct) ) isUpdate = true ; } if (action.log.isInfoEnabled() ) { //String cxt = action.getContextPath() ; action.log.info(format("[%d] %s %s :: '%s' :: %s ? %s", id, method, desc.getName(), trailing, (mt==null?"<none>":mt), (qs==null?"":qs))) ; } if ( !hasTrailing ) { // Nothing after the DataAccessPoint i.e. Dataset by name. // Action on the dataset itself. This can be: // // http://localhost:3030/ds?query= // http://localhost:3030/ds and a content type of "applicatiopn/sparql-query" // // http://localhost:3030/ds?update= // http://localhost:3030/ds and a content type of "applicatiopn/sparql-update" // // http://localhost:3030/ds?default ?graph= GSP // // http://localhost:3030/ds REST quads action on the dataset itself. if ( isQuery ) { if ( !allowQuery(action) ) ServletOps.errorMethodNotAllowed("SPARQL query : "+method) ; executeRequest(action, queryServlet) ; return ; } if ( isUpdate ) { // SPARQL Update if ( !allowUpdate(action) ) ServletOps.errorMethodNotAllowed("SPARQL update : "+method) ; // This will deal with using GET. executeRequest(action, updateServlet) ; return ; } // ?graph=, ?default if ( hasParamGraph || hasParamGraphDefault ) { doGraphStoreProtocol(action) ; return ; } if ( hasParams ) { // Unrecognized ?key=value ServletOps.errorBadRequest("Malformed request") ; } // REST dataset. boolean isGET = method.equals(HttpNames.METHOD_GET) ; boolean isHEAD = method.equals(HttpNames.METHOD_HEAD) ; // Check enabled. if ( isGET || isHEAD ) { if ( allowQuadsR(action) ) restQuads_R.executeLifecycle(action) ; else ServletOps.errorMethodNotAllowed(method) ; return ; } if ( allowQuadsW(action) ) restQuads_RW.executeLifecycle(action) ; else ServletOps.errorMethodNotAllowed("Read-only dataset : "+method) ; return ; } // Has trailing path name => service or direct naming GSP. final boolean checkForPossibleService = true ; if ( checkForPossibleService && action.getEndpoint() != null ) { // There is a trailing part. // Check it's not the same name as a registered service. // If so, dispatch to that service. if ( serviceDispatch(action, OperationName.Query, queryServlet) ) return ; if ( serviceDispatch(action, OperationName.Update, updateServlet) ) return ; if ( serviceDispatch(action, OperationName.Upload, uploadServlet) ) return ; if ( hasParams ) { if ( serviceDispatch(action, OperationName.GSP_R, gspServlet_R) ) return ; if ( serviceDispatch(action, OperationName.GSP_RW, gspServlet_RW) ) return ; } else { // No parameters - do as a quads operation on the dataset. if ( serviceDispatch(action, OperationName.GSP_R, restQuads_R) ) return ; if ( serviceDispatch(action, OperationName.GSP_RW, restQuads_RW) ) return ; } if ( serviceDispatch(action, OperationName.Quads_RW, restQuads_RW) ) return ; if ( serviceDispatch(action, OperationName.Quads_R, restQuads_R) ) return ; } // There is a trailing part - params are illegal by this point. if ( hasParams ) // ?? Revisit to include query-on-one-graph //errorBadRequest("Can't invoke a query-string service on a direct named graph") ; ServletOps.errorNotFound("Not found: dataset='"+printName(desc.getName())+ "' service='"+printName(trailing)+ "' query string=?"+qs); // There is a trailing part - not a service, no params ==> GSP direct naming. if ( ! Fuseki.GSP_DIRECT_NAMING ) ServletOps.errorNotFound("Not found: dataset='"+printName(desc.getName())+"' service='"+printName(trailing)+"'"); doGraphStoreProtocol(action); } /** See if the operation is enabled for this setup. * Return true if dispatched */ private boolean serviceDispatch(HttpAction action, OperationName opName, ActionSPARQL servlet) { Endpoint operation = action.getEndpoint() ; if ( operation == null ) return false ; if ( ! operation.isType(opName) ) return false ; // Handle OPTIONS specially. // if ( action.getRequest().getMethod().equals(HttpNames.METHOD_OPTIONS) ) { // // See also ServletBase.CORS_ENABLED // //action.log.info(format("[%d] %s", action.id, action.getMethod())) ; // setCommonHeadersForOptions(action.getResponse()) ; // ServletOps.success(action); // return true ; // } executeRequest(action, servlet) ; return true ; } private String printName(String x) { if ( x.startsWith("/") ) return x.substring(1) ; return x ; } private void doGraphStoreProtocol(HttpAction action) { // The GSP servlets handle direct and indirect naming. Endpoint operation = action.getEndpoint() ; String method = action.request.getMethod() ; // Try to route to read service. if ( HttpNames.METHOD_GET.equalsIgnoreCase(method) || HttpNames.METHOD_HEAD.equalsIgnoreCase(method) ) { // Graphs Store Protocol, indirect naming, read operations // Try to send to the R service, else drop through to RW service dispatch. if ( ! allowREST_R(action)) ServletOps.errorForbidden("Forbidden: SPARQL Graph Store Protocol : Read operation : "+method) ; executeRequest(action, gspServlet_R) ; return ; } // Graphs Store Protocol, indirect naming, write (or read, though actually handled above) // operations on the RW service. if ( ! allowREST_W(action)) ServletOps.errorForbidden("Forbidden: SPARQL Graph Store Protocol : "+method) ; executeRequest(action, gspServlet_RW) ; return ; } private void executeRequest(HttpAction action, ActionSPARQL servlet) { if ( true ) { // Execute an ActionSPARQL. // Bypasses HttpServlet.service to doMethod dispatch. servlet.executeLifecycle(action) ; return ; } if ( false ) { // Execute by calling the whole servlet mechanism. // This causes HttpServlet.service to call the appropriate doMethod. // but the action, and the id, are not passed on and a ne one is created. try { servlet.service(action.request, action.response) ; } catch (ServletException | IOException e) { ServletOps.errorOccurred(e); } } } protected static MediaType contentNegotationQuads(HttpAction action) { MediaType mt = ConNeg.chooseContentType(action.request, DEF.quadsOffer, DEF.acceptNQuads) ; if ( mt == null ) return null ; if ( mt.getContentType() != null ) action.response.setContentType(mt.getContentType()); if ( mt.getCharset() != null ) action.response.setCharacterEncoding(mt.getCharset()) ; return mt ; } /** Find part after the dataset name: service name or the graph (direct naming) */ protected String findTrailing(String uri, String dsname) { if ( dsname.length() >= uri.length() ) return "" ; return uri.substring(dsname.length()+1) ; // Skip the separating "/" } // Route everything to "doCommon" @Override protected void doHead(HttpServletRequest request, HttpServletResponse response) { doCommon(request, response) ; } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) { doCommon(request, response) ; } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) { doCommon(request, response) ; } @Override protected void doOptions(HttpServletRequest request, HttpServletResponse response) { doCommon(request, response) ; } @Override protected void doPut(HttpServletRequest request, HttpServletResponse response) { doCommon(request, response) ; } @Override protected void doDelete(HttpServletRequest request, HttpServletResponse response) { doCommon(request, response) ; } }