http://git-wip-us.apache.org/repos/asf/jena/blob/662cf71d/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Query.java ---------------------------------------------------------------------- diff --git a/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Query.java b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Query.java new file mode 100644 index 0000000..06f8340 --- /dev/null +++ b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_Query.java @@ -0,0 +1,387 @@ +/* + * 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.HttpNames.* ; +import static org.apache.jena.fuseki.server.CounterName.QueryExecErrors ; +import static org.apache.jena.fuseki.server.CounterName.QueryTimeouts ; +import static org.apache.jena.fuseki.server.CounterName.RequestsBad ; + +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.RuntimeIOException ; +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.FusekiException ; +import org.apache.jena.fuseki.FusekiLib ; +import org.apache.jena.fuseki.HttpNames ; +import org.apache.jena.riot.WebContent ; +import org.apache.jena.riot.web.HttpOp ; +import org.apache.jena.web.HttpSC ; + +import com.hp.hpl.jena.query.* ; +import com.hp.hpl.jena.rdf.model.Model ; +import com.hp.hpl.jena.sparql.core.Prologue ; +import com.hp.hpl.jena.sparql.resultset.SPARQLResult ; + +/** + * Handles SPARQL Query requests. + */ +public abstract class SPARQL_Query extends SPARQL_Protocol +{ + public SPARQL_Query() { super() ; } + + // Choose REST verbs to support. + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + { doCommon(request, response) ; } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + { doCommon(request, response) ; } + + // HEAD + + @Override + protected void doOptions(HttpServletRequest request, HttpServletResponse response) + { + setCommonHeadersForOptions(response) ; + response.setHeader(HttpNames.hAllow, "GET,OPTIONS,POST"); + response.setHeader(HttpNames.hContentLengh, "0") ; + } + + @Override + protected final void perform(HttpAction action) + { + // GET + if ( action.request.getMethod().equals(HttpNames.METHOD_GET) ) { + executeWithParameter(action) ; + return ; + } + + ContentType ct = FusekiLib.getContentType(action) ; + if ( ct == null ) { + // Validation check it's POST with ?query= + executeWithParameter(action) ; + return ; + } + + String incoming = ct.getContentType() ; + // POST application/sparql-query + if (WebContent.contentTypeSPARQLQuery.equals(incoming)) { + executeBody(action) ; + return ; + } + // POST application/x-www-form-url + if (WebContent.contentTypeHTMLForm.equals(incoming)) { + executeWithParameter(action) ; + return ; + } + + error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "Bad content type: "+incoming) ; + } + + // All the params we support + + protected static List<String> allParams = Arrays.asList(paramQuery, + paramDefaultGraphURI, paramNamedGraphURI, + paramQueryRef, + paramStyleSheet, + paramAccept, + paramOutput1, paramOutput2, + paramCallback, + paramForceAccept, + paramTimeout) ; + + @Override + protected void validate(HttpAction action) + { + String method = action.request.getMethod().toUpperCase(Locale.ROOT) ; + + if ( ! HttpNames.METHOD_POST.equals(method) && ! HttpNames.METHOD_GET.equals(method) ) + errorMethodNotAllowed("Not a GET or POST request") ; + + if ( HttpNames.METHOD_GET.equals(method) && action.request.getQueryString() == null ) + { + warning("Service Description / SPARQL Query / "+action.request.getRequestURI()) ; + errorNotFound("Service Description: "+action.request.getRequestURI()) ; + } + + // Use of the dataset describing parameters is check later. + try { + validateParams(action.request, 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(HttpServletRequest request, Collection<String> params) + { + ContentType ct = FusekiLib.getContentType(request) ; + boolean mustHaveQueryParam = true ; + if ( ct != null ) + { + String incoming = ct.getContentType() ; + + if ( WebContent.contentTypeSPARQLQuery.equals(incoming) ) + { + mustHaveQueryParam = false ; + //error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "Unofficial "+WebContent.contentTypeSPARQLQuery+" not supported") ; + } + else if ( WebContent.contentTypeHTMLForm.equals(incoming) ) {} + else + 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 ) errorBadRequest("SPARQL Query: No 'query=' parameter") ; + if ( N > 1 ) errorBadRequest("SPARQL Query: Multiple 'query=' parameters") ; + + // application/sparql-query does not use a query param. + String queryStr = request.getParameter(HttpNames.paramQuery) ; + + if ( queryStr == null ) + errorBadRequest("SPARQL Query: No query specified (no 'query=' found)") ; + if ( queryStr.isEmpty() ) + 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) ) + warning("SPARQL Query: Unrecognize request parameter (ignored): "+name) ; + } + } + } + + private void executeWithParameter(HttpAction action) + { + String queryString = action.request.getParameter(paramQuery) ; + execute(queryString, action) ; + } + + private void executeBody(HttpAction action) + { + String queryString = null ; + try { + InputStream input = action.request.getInputStream() ; + queryString = IO.readWholeFileAsUTF8(input) ; + } + catch (IOException ex) { errorOccurred(ex) ; } + execute(queryString, action) ; + } + + private void execute(String queryString, HttpAction action) + { + String queryStringLog = formatForLog(queryString) ; + if ( action.verbose ) + log.info(format("[%d] Query = \n%s", action.id, queryString)); + else + 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, "http://example/query-base", Syntax.syntaxARQ) ; + queryStringLog = formatForLog(query) ; + validateQuery(action, query) ; + } catch (ActionErrorException ex) { + incCounter(action.srvRef, RequestsBad) ; + throw ex ; + } catch (QueryParseException ex) { + incCounter(action.srvRef, RequestsBad) ; + errorBadRequest("Parse error: \n" + queryString + "\n\r" + messageForQPE(ex)) ; + } catch (RuntimeIOException ex) { + errorBadRequest("Runtime IO Exception: \n" + queryString + "\n\r" + ex.getMessage()) ; + } + // Should not happen. + catch (QueryException ex) { + errorBadRequest("Error: \n" + queryString + "\n\r" + ex.getMessage()) ; + } + + // Assumes finished whole thing by end of sendResult. + action.beginRead() ; + try { + 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 (QueryCancelledException ex) { + // Additional counter information. + incCounter(action.srvRef, QueryTimeouts) ; + throw ex ; + } catch (RuntimeIOException ex) { + incCounter(action.srvRef, QueryExecErrors) ; + throw ex ; + } catch (QueryExecException ex) { + // Additional counter information. + incCounter(action.srvRef, QueryExecErrors) ; + throw ex ; + } finally { + action.endRead() ; + } + } + + /** + * Check the query, throwing ActionErrorException when not valid, or calling super#error. + * @param action HTTP Action + * @param query the Query + */ + protected abstract void validateQuery(HttpAction action, Query query) ; + + protected QueryExecution createQueryExecution(Query query, Dataset dataset) + { + return QueryExecutionFactory.create(query, dataset) ; + } + + protected SPARQLResult executeQuery(HttpAction action, QueryExecution qExec, Query query, String queryStringLog) + { + setAnyTimeouts(qExec, action); + + if ( query.isSelectType() ) + { + ResultSet rs = qExec.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) ; + + log.info(format("[%d] exec/select", action.id)) ; + return new SPARQLResult(rs) ; + } + + if ( query.isConstructType() ) + { + Model model = qExec.execConstruct() ; + log.info(format("[%d] exec/construct", action.id)) ; + return new SPARQLResult(model) ; + } + + if ( query.isDescribeType() ) + { + Model model = qExec.execDescribe() ; + log.info(format("[%d] exec/describe",action.id)) ; + return new SPARQLResult(model) ; + } + + if ( query.isAskType() ) + { + boolean b = qExec.execAsk() ; + log.info(format("[%d] exec/ask",action.id)) ; + return new SPARQLResult(b) ; + } + + errorBadRequest("Unknown query type - "+queryStringLog) ; + return null ; + } + + private void setAnyTimeouts(QueryExecution qexec, HttpAction action) { + if (!(action.getDatasetRef().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.getDatasetRef().maximumTimeoutOverride, desiredTimeout); + if (desiredTimeout != Long.MAX_VALUE) + qexec.setTimeout(desiredTimeout); + } + + protected abstract Dataset decideDataset(HttpAction action, Query query, String queryStringLog) ; + + protected void sendResults(HttpAction action, SPARQLResult result, Prologue qPrologue) + { + if ( result.isResultSet() ) + ResponseResultSet.doResponseResultSet(action, result.getResultSet(), qPrologue) ; + else if ( result.isGraph() ) + ResponseModel.doResponseModel(action, result.getModel()) ; + else if ( result.isBoolean() ) + ResponseResultSet.doResponseResultSet(action, result.getBooleanResult()) ; + else + 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) ; + } + +}
http://git-wip-us.apache.org/repos/asf/jena/blob/662cf71d/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryDataset.java ---------------------------------------------------------------------- diff --git a/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryDataset.java b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryDataset.java new file mode 100644 index 0000000..9e9df36 --- /dev/null +++ b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryDataset.java @@ -0,0 +1,60 @@ +/* + * 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 com.hp.hpl.jena.query.Dataset ; +import com.hp.hpl.jena.query.DatasetFactory ; +import com.hp.hpl.jena.query.Query ; +import com.hp.hpl.jena.sparql.core.DatasetDescription ; +import com.hp.hpl.jena.sparql.core.DatasetGraph ; +import com.hp.hpl.jena.sparql.core.DynamicDatasets ; + +public class SPARQL_QueryDataset extends SPARQL_Query +{ + public SPARQL_QueryDataset(boolean verbose) { super() ; } + + public SPARQL_QueryDataset() + { this(false) ; } + + @Override + protected void validateRequest(HttpAction action) + { } + + @Override + protected void validateQuery(HttpAction action, Query query) + { } + + @Override + protected Dataset decideDataset(HttpAction action, Query query, String queryStringLog) + { + DatasetGraph dsg = action.getActiveDSG() ; + + // query.getDatasetDescription() ; + + // Protocol. + DatasetDescription dsDesc = getDatasetDescription(action) ; + if (dsDesc != null ) + { + //errorBadRequest("SPARQL Query: Dataset description in the protocol request") ; + dsg = DynamicDatasets.dynamicDataset(dsDesc, dsg, false) ; + } + + return DatasetFactory.create(dsg) ; + } +} http://git-wip-us.apache.org/repos/asf/jena/blob/662cf71d/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryGeneral.java ---------------------------------------------------------------------- diff --git a/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryGeneral.java b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryGeneral.java new file mode 100644 index 0000000..a022e96 --- /dev/null +++ b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_QueryGeneral.java @@ -0,0 +1,143 @@ +/* + * 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 java.util.List ; + +import org.apache.jena.atlas.lib.InternalErrorException ; +import org.apache.jena.fuseki.migrate.GraphLoadUtils ; +import org.apache.jena.riot.RiotException ; + +import com.hp.hpl.jena.query.Dataset ; +import com.hp.hpl.jena.query.DatasetFactory ; +import com.hp.hpl.jena.query.Query ; +import com.hp.hpl.jena.rdf.model.Model ; +import com.hp.hpl.jena.rdf.model.ModelFactory ; +import com.hp.hpl.jena.sparql.core.DatasetDescription ; + +public class SPARQL_QueryGeneral extends SPARQL_Query +{ + final static int MaxTriples = 100*1000 ; + + public SPARQL_QueryGeneral() { super() ; } + + @Override + protected void validateRequest(HttpAction action) {} + + @Override + protected void validateQuery(HttpAction action, Query query) {} + + @Override + protected String mapRequestToDataset(String uri) + { return null ; } + + @Override + protected Dataset decideDataset(HttpAction action, Query query, String queryStringLog) + { + DatasetDescription datasetDesc = getDatasetDescription(action) ; + if ( datasetDesc == null ) + datasetDesc = getDatasetDescription(query) ; + if ( datasetDesc == null ) + errorBadRequest("No dataset description in protocol request or in the query string") ; + + return datasetFromDescription(action, datasetDesc) ; + } + + /** + * Construct a Dataset based on a dataset description. + */ + + protected static Dataset datasetFromDescription(HttpAction action, DatasetDescription datasetDesc) + { + try { + if ( datasetDesc == null ) + return null ; + if ( datasetDesc.isEmpty() ) + return null ; + + List<String> graphURLs = datasetDesc.getDefaultGraphURIs() ; + List<String> namedGraphs = datasetDesc.getNamedGraphURIs() ; + + if ( graphURLs.size() == 0 && namedGraphs.size() == 0 ) + return null ; + + Dataset dataset = DatasetFactory.createMem() ; + // Look in cache for loaded graphs!! + + // ---- Default graph + { + Model model = ModelFactory.createDefaultModel() ; + for ( String uri : graphURLs ) + { + if ( uri == null || uri.equals("") ) + throw new InternalErrorException("Default graph URI is null or the empty string") ; + + try { + //TODO Clearup - RIOT integration. + GraphLoadUtils.loadModel(model, uri, MaxTriples) ; + log.info(format("[%d] Load (default graph) %s", action.id, uri)) ; + } catch (RiotException ex) { + log.info(format("[%d] Parsing error loading %s: %s", action.id, uri, ex.getMessage())) ; + errorBadRequest("Failed to load URL (parse error) "+uri+" : "+ex.getMessage()) ; + } catch (Exception ex) + { + log.info(format("[%d] Failed to load (default) %s: %s", action.id, uri, ex.getMessage())) ; + errorBadRequest("Failed to load URL "+uri) ; + } + } + dataset.setDefaultModel(model) ; + } + // ---- Named graphs + if ( namedGraphs != null ) + { + for ( String uri : namedGraphs ) + { + if ( uri == null || uri.equals("") ) + throw new InternalErrorException("Named graph URI is null or the empty string") ; + + try { + Model model = ModelFactory.createDefaultModel() ; + GraphLoadUtils.loadModel(model, uri, MaxTriples) ; + log.info(format("[%d] Load (named graph) %s", action.id, uri)) ; + dataset.addNamedModel(uri, model) ; + } catch (RiotException ex) { + log.info(format("[%d] Parsing error loading %s: %s", action.id, uri, ex.getMessage())) ; + errorBadRequest("Failed to load URL (parse error) "+uri+" : "+ex.getMessage()) ; + } catch (Exception ex) + { + log.info(format("[%d] Failed to load (named graph) %s: %s", action.id, uri, ex.getMessage())) ; + errorBadRequest("Failed to load URL "+uri) ; + } + } + } + + return dataset ; + + } + catch (ActionErrorException ex) { throw ex ; } + catch (Exception ex) + { + log.info(format("[%d] SPARQL parameter error: "+ex.getMessage(),action.id, ex)) ; + errorBadRequest("Parameter error: "+ex.getMessage()); + return null ; + } + } +} http://git-wip-us.apache.org/repos/asf/jena/blob/662cf71d/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST.java ---------------------------------------------------------------------- diff --git a/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST.java b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST.java new file mode 100644 index 0000000..4ab386b --- /dev/null +++ b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST.java @@ -0,0 +1,354 @@ +/* + * 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 org.apache.jena.fuseki.HttpNames.* ; + +import java.io.IOException ; +import java.io.InputStream ; +import java.util.Enumeration ; +import java.util.Locale ; + +import javax.servlet.ServletException ; +import javax.servlet.http.HttpServletRequest ; +import javax.servlet.http.HttpServletResponse ; + +import org.apache.jena.fuseki.HttpNames ; +import org.apache.jena.fuseki.server.CounterName ; +import org.apache.jena.riot.Lang ; +import org.apache.jena.riot.RDFDataMgr ; +import org.apache.jena.riot.ReaderRIOT ; +import org.apache.jena.riot.RiotException ; +import org.apache.jena.riot.system.ErrorHandler ; +import org.apache.jena.riot.system.ErrorHandlerFactory ; +import org.apache.jena.riot.system.IRIResolver ; +import org.apache.jena.riot.system.StreamRDF ; +import org.slf4j.Logger ; +import org.slf4j.LoggerFactory ; + +import com.hp.hpl.jena.graph.Graph ; +import com.hp.hpl.jena.graph.Node ; +import com.hp.hpl.jena.graph.NodeFactory ; +import com.hp.hpl.jena.sparql.core.DatasetGraph ; + +public abstract class SPARQL_REST extends SPARQL_ServletBase +{ + protected static Logger classLog = LoggerFactory.getLogger(SPARQL_REST.class) ; + + protected static ErrorHandler errorHandler = ErrorHandlerFactory.errorHandlerStd(log) ; + + protected final static Target determineTarget(HttpAction action) { + // Delayed until inside a transaction. + if ( action.getActiveDSG() == null ) + errorOccurred("Internal error : No action graph (not in a transaction?)") ; + + boolean dftGraph = getOneOnly(action.request, HttpNames.paramGraphDefault) != null ; + String uri = getOneOnly(action.request, HttpNames.paramGraph) ; + + if ( !dftGraph && uri == null ) { + // Direct naming or error. + uri = action.request.getRequestURL().toString() ; + if ( action.request.getRequestURI().equals(action.getDatasetRef().name) ) + // No name + errorBadRequest("Neither default graph nor named graph specified; no direct name") ; + } + + if ( dftGraph ) + return Target.createDefault(action.getActiveDSG()) ; + + // Named graph + if ( uri.equals(HttpNames.valueDefault ) ) + // But "named" default + return Target.createDefault(action.getActiveDSG()) ; + + // Strictly, a bit naughty on the URI resolution. But more sensible. + // Base is dataset. + String base = action.request.getRequestURL().toString() ; //wholeRequestURL(request) ; + // Make sure it ends in "/", ie. dataset as container. + if ( action.request.getQueryString() != null && ! base.endsWith("/") ) + base = base + "/" ; + + String absUri = IRIResolver.resolveString(uri, base) ; + Node gn = NodeFactory.createURI(absUri) ; + return Target.createNamed(action.getActiveDSG(), absUri, gn) ; + } + + + // struct for target + protected static final class Target + { + final boolean isDefault ; + final DatasetGraph dsg ; + private Graph _graph ; + final String name ; + final Node graphName ; + + static Target createNamed(DatasetGraph dsg, String name, Node graphName) { + return new Target(false, dsg, name, graphName) ; + } + + static Target createDefault(DatasetGraph dsg) { + return new Target(true, dsg, null, null) ; + } + + private Target(boolean isDefault, DatasetGraph dsg, String name, Node graphName) { + this.isDefault = isDefault ; + this.dsg = dsg ; + this._graph = null ; + this.name = name ; + this.graphName = graphName ; + + if ( isDefault ) + { + if ( name != null || graphName != null ) + throw new IllegalArgumentException("Inconsistent: default and a graph name/node") ; + } + else + { + if ( name == null || graphName == null ) + throw new IllegalArgumentException("Inconsistent: not default and/or no graph name/node") ; + } + } + + /** Get a graph for the action - this may create a graph in the dataset - this is not a test for graph existence */ + public Graph graph() { + if ( ! isGraphSet() ) + { + if ( isDefault ) + _graph = dsg.getDefaultGraph() ; + else + _graph = dsg.getGraph(graphName) ; + } + return _graph ; + } + + public boolean exists() + { + if ( isDefault ) return true ; + return dsg.containsGraph(graphName) ; + } + + public boolean isGraphSet() + { + return _graph != null ; + } + + @Override + public String toString() + { + if ( isDefault ) return "default" ; + return name ; + } + } + + public SPARQL_REST() + { super() ; } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + // Direct all verbs to our common framework. + doCommon(request, response) ; + } + + private void maybeSetLastModified(HttpServletResponse resp, long lastModified) { + if (resp.containsHeader(HEADER_LASTMOD)) return ; + if (lastModified >= 0) resp.setDateHeader(HEADER_LASTMOD, lastModified); + } + + @Override + protected void perform(HttpAction action) { + dispatch(action) ; + } + + private void dispatch(HttpAction action) { + HttpServletRequest req = action.request ; + HttpServletResponse resp = action.response ; + String method = req.getMethod().toUpperCase(Locale.ROOT) ; + + if (method.equals(METHOD_GET)) + doGet$(action); + else if (method.equals(METHOD_HEAD)) + doHead$(action); + else if (method.equals(METHOD_POST)) + doPost$(action); + else if (method.equals(METHOD_PATCH)) + doPatch$(action) ; + else if (method.equals(METHOD_OPTIONS)) + doOptions$(action) ; + else if (method.equals(METHOD_TRACE)) + //doTrace(action) ; + errorMethodNotAllowed("TRACE") ; + else if (method.equals(METHOD_PUT)) + doPut$(action) ; + else if (method.equals(METHOD_DELETE)) + doDelete$(action) ; + else + errorNotImplemented("Unknown method: "+method) ; + } + + // Counter wrappers + + protected void doGet$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPget) ; + try { + doGet(action) ; + incCounter(action.srvRef, CounterName.GSPgetGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPgetBad) ; + throw ex ; + } + } + + protected void doHead$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPhead) ; + try { + doHead(action) ; + incCounter(action.srvRef, CounterName.GSPheadGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPheadBad) ; + throw ex ; + } + } + + protected void doPost$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPpost) ; + try { + doPost(action) ; + incCounter(action.srvRef, CounterName.GSPpostGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPpostBad) ; + throw ex ; + } + } + + protected void doPatch$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPpatch) ; + try { + doPatch(action) ; + incCounter(action.srvRef, CounterName.GSPpatchGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPpatchBad) ; + throw ex ; + } + } + + protected void doDelete$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPdelete) ; + try { + doDelete(action) ; + incCounter(action.srvRef, CounterName.GSPdeleteGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPdeleteBad) ; + throw ex ; + } + } + + protected void doPut$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPput) ; + try { + doPut(action) ; + incCounter(action.srvRef, CounterName.GSPputGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPputBad) ; + throw ex ; + } + } + + protected void doOptions$(HttpAction action) { + incCounter(action.srvRef, CounterName.GSPoptions) ; + try { + doOptions(action) ; + incCounter(action.srvRef, CounterName.GSPoptionsGood) ; + } catch ( ActionErrorException ex) { + incCounter(action.srvRef, CounterName.GSPoptionsBad) ; + throw ex ; + } + } + + protected abstract void doGet(HttpAction action) ; + protected abstract void doHead(HttpAction action) ; + protected abstract void doPost(HttpAction action) ; + protected abstract void doPatch(HttpAction action) ; + protected abstract void doDelete(HttpAction action) ; + protected abstract void doPut(HttpAction action) ; + protected abstract void doOptions(HttpAction action) ; + + // @@ Move to SPARQL_ServletBase + // Check for all RiotReader + public static void parse(HttpAction action, StreamRDF dest, InputStream input, Lang lang, String base) { + try { + ReaderRIOT r = RDFDataMgr.createReader(lang) ; + if ( r == null ) + errorBadRequest("No parser for language '"+lang.getName()+"'") ; + r.setErrorHandler(errorHandler); + r.read(input, base, null, dest, null) ; + } + catch (RiotException ex) { errorBadRequest("Parse error: "+ex.getMessage()) ; } + } + + @Override + protected void validate(HttpAction action) + { + HttpServletRequest request = action.request ; + // Direct naming. + if ( request.getQueryString() == null ) + //errorBadRequest("No query string") ; + return ; + + String g = request.getParameter(HttpNames.paramGraph) ; + String d = request.getParameter(HttpNames.paramGraphDefault) ; + + if ( g != null && d !=null ) + errorBadRequest("Both ?default and ?graph in the query string of the request") ; + + if ( g == null && d == null ) + errorBadRequest("Neither ?default nor ?graph in the query string of the request") ; + + int x1 = SPARQL_Protocol.countParamOccurences(request, HttpNames.paramGraph) ; + int x2 = SPARQL_Protocol.countParamOccurences(request, HttpNames.paramGraphDefault) ; + + if ( x1 > 1 ) + errorBadRequest("Multiple ?default in the query string of the request") ; + if ( x2 > 1 ) + errorBadRequest("Multiple ?graph in the query string of the request") ; + + Enumeration<String> en = request.getParameterNames() ; + for ( ; en.hasMoreElements() ; ) + { + String h = en.nextElement() ; + if ( ! HttpNames.paramGraph.equals(h) && ! HttpNames.paramGraphDefault.equals(h) ) + errorBadRequest("Unknown parameter '"+h+"'") ; + // one of ?default and &graph + if ( request.getParameterValues(h).length != 1 ) + errorBadRequest("Multiple parameters '"+h+"'") ; + } + } + + protected static String getOneOnly(HttpServletRequest request, String name) + { + String[] values = request.getParameterValues(name) ; + if ( values == null ) + return null ; + if ( values.length == 0 ) + return null ; + if ( values.length > 1 ) + errorBadRequest("Multiple occurrences of '"+name+"'") ; + return values[0] ; + } +} http://git-wip-us.apache.org/repos/asf/jena/blob/662cf71d/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_R.java ---------------------------------------------------------------------- diff --git a/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_R.java b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_R.java new file mode 100644 index 0000000..0c02b51 --- /dev/null +++ b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_R.java @@ -0,0 +1,128 @@ +/* + * 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 java.io.IOException ; + +import javax.servlet.ServletOutputStream ; + +import org.apache.jena.atlas.web.MediaType ; +import org.apache.jena.atlas.web.TypedOutputStream ; +import org.apache.jena.fuseki.HttpNames ; +import org.apache.jena.riot.* ; + +import com.hp.hpl.jena.graph.Graph ; + +/** Only the READ operations */ +public class SPARQL_REST_R extends SPARQL_REST +{ + public SPARQL_REST_R() + { super() ; } + + + @Override + protected String mapRequestToDataset(String uri) { return mapRequestToDatasetLongest$(uri) ; } + + @Override + protected void doGet(HttpAction action) + { + // Assume success - do the set up before grabbing the lock. + // Sets content type. + MediaType mediaType = HttpAction.contentNegotationRDF(action) ; + + ServletOutputStream output ; + try { output = action.response.getOutputStream() ; } + catch (IOException ex) { errorOccurred(ex) ; output = null ; } + + TypedOutputStream out = new TypedOutputStream(output, mediaType) ; + Lang lang = RDFLanguages.contentTypeToLang(mediaType.getContentType()) ; + + if ( action.verbose ) + log.info(format("[%d] Get: Content-Type=%s, Charset=%s => %s", + action.id, mediaType.getContentType(), mediaType.getCharset(), lang.getName())) ; + + action.beginRead() ; + setCommonHeaders(action.response) ; + + try { + Target target = determineTarget(action) ; + if ( log.isDebugEnabled() ) + log.debug("GET->"+target) ; + boolean exists = target.exists() ; + if ( ! exists ) + errorNotFound("No such graph: <"+target.name+">") ; + // If we want to set the Content-Length, we need to buffer. + //response.setContentLength(??) ; + String ct = lang.getContentType().toHeaderString() ; + action.response.setContentType(ct) ; + Graph g = target.graph() ; + //Special case RDF/XML to be the plain (faster, less readable) form + RDFFormat fmt = + ( lang == Lang.RDFXML ) ? RDFFormat.RDFXML_PLAIN : RDFWriterRegistry.defaultSerialization(lang) ; + RDFDataMgr.write(out, g, fmt) ; + success(action) ; + } finally { action.endRead() ; } + } + + @Override + protected void doOptions(HttpAction action) + { + setCommonHeadersForOptions(action.response) ; + action.response.setHeader(HttpNames.hAllow, "GET,HEAD,OPTIONS") ; + action.response.setHeader(HttpNames.hContentLengh, "0") ; + success(action) ; + } + + @Override + protected void doHead(HttpAction action) + { + setCommonHeaders(action.response) ; + action.beginRead() ; + try { + Target target = determineTarget(action) ; + if ( log.isDebugEnabled() ) + log.debug("HEAD->"+target) ; + if ( ! target.exists() ) + { + successNotFound(action) ; + return ; + } + MediaType mediaType = HttpAction.contentNegotationRDF(action) ; + success(action) ; + } finally { action.endRead() ; } + } + + @Override + protected void doPost(HttpAction action) + { errorMethodNotAllowed("POST") ; } + + @Override + protected void doDelete(HttpAction action) + { errorMethodNotAllowed("DELETE") ; } + + @Override + protected void doPut(HttpAction action) + { errorMethodNotAllowed("PUT") ; } + + @Override + protected void doPatch(HttpAction action) + { errorMethodNotAllowed("PATCH") ; } +} http://git-wip-us.apache.org/repos/asf/jena/blob/662cf71d/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_RW.java ---------------------------------------------------------------------- diff --git a/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_RW.java b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_RW.java new file mode 100644 index 0000000..712d543 --- /dev/null +++ b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_REST_RW.java @@ -0,0 +1,232 @@ +/* + * 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 java.io.IOException ; +import java.io.InputStream ; +import java.util.Map ; +import java.util.Map.Entry ; + +import org.apache.jena.atlas.io.IO ; +import org.apache.jena.atlas.web.ContentType ; +import org.apache.jena.fuseki.FusekiLib ; +import org.apache.jena.fuseki.HttpNames ; +import org.apache.jena.riot.Lang ; +import org.apache.jena.riot.RDFLanguages ; +import org.apache.jena.riot.RiotException ; +import org.apache.jena.riot.WebContent ; +import org.apache.jena.riot.system.StreamRDF ; +import org.apache.jena.riot.system.StreamRDFLib ; +import org.apache.jena.web.HttpSC ; + +import com.hp.hpl.jena.graph.Graph ; +import com.hp.hpl.jena.sparql.graph.GraphFactory ; + +/** The WRITE operations added to the READ operations */ +public class SPARQL_REST_RW extends SPARQL_REST_R +{ + public SPARQL_REST_RW() + { super() ; } + + @Override + protected void doOptions(HttpAction action) + { + setCommonHeadersForOptions(action.response) ; + action.response.setHeader(HttpNames.hAllow, "GET,HEAD,OPTIONS,PUT,DELETE,POST"); + action.response.setHeader(HttpNames.hContentLengh, "0") ; + success(action) ; + } + + @Override + protected void doDelete(HttpAction action) + { + action.beginWrite() ; + try { + Target target = determineTarget(action) ; + if ( log.isDebugEnabled() ) + log.debug("DELETE->"+target) ; + boolean existedBefore = target.exists() ; + if ( ! existedBefore) + { + // commit, not abort, because locking "transactions" don't support abort. + action.commit() ; + errorNotFound("No such graph: "+target.name) ; + } + deleteGraph(action) ; + action.commit() ; + } + finally { action.endWrite() ; } + ServletBase.successNoContent(action) ; + } + + @Override + protected void doPut(HttpAction action) { doPutPost(action, true) ; } + + @Override + protected void doPost(HttpAction action) { doPutPost(action, false) ; } + + private void doPutPost(HttpAction action, boolean overwrite) { + ContentType ct = FusekiLib.getContentType(action) ; + if ( ct == null ) + errorBadRequest("No Content-Type:") ; + + // Helper case - if it's a possible HTTP file upload, pretend that's the action. + if ( WebContent.contentTypeMultipartFormData.equalsIgnoreCase(ct.getContentType()) ) { + String base = wholeRequestURL(action.request) ; + SPARQL_Upload.upload(action, base) ; + return ; + } + + if ( WebContent.matchContentType(WebContent.ctMultipartMixed, ct) ) + error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "multipart/mixed not supported") ; + + boolean existedBefore = false ; + if ( action.isTransactional() ) + existedBefore = addDataIntoTxn(action, overwrite) ; + else + existedBefore = addDataIntoNonTxn(action, overwrite) ; + + if ( existedBefore ) + ServletBase.successNoContent(action) ; + else + ServletBase.successCreated(action) ; + } + + /** Directly add data in a transaction. + * Assumes recovery from parse errors by transaction abort. + * Return whether the target existed before. + * @param action + * @param cleanDest Whether to remove data first (true = PUT, false = POST) + * @return whether the target existed beforehand + */ + protected static boolean addDataIntoTxn(HttpAction action, boolean overwrite) { + action.beginWrite(); + Target target = determineTarget(action) ; + boolean existedBefore = false ; + try { + if ( log.isDebugEnabled() ) + log.debug(" ->"+target) ; + existedBefore = target.exists() ; + Graph g = target.graph() ; + if ( overwrite && existedBefore ) + clearGraph(target) ; + StreamRDF sink = StreamRDFLib.graph(g) ; + incomingData(action, sink); + action.commit() ; + return existedBefore ; + } catch (RiotException ex) { + // Parse error + action.abort() ; + errorBadRequest(ex.getMessage()) ; + return existedBefore ; + } catch (Exception ex) { + // Something else went wrong. Backout. + action.abort() ; + errorOccurred(ex.getMessage()) ; + return existedBefore ; + } finally { + action.endWrite() ; + } + } + + /** Add data where the destination does not support full transactions. + * In particular, with no abort, and actions probably going to the real storage + * parse errors can lead to partial updates. Instead, parse to a temporary + * graph, then insert that data. + * @param action + * @param cleanDest Whether to remove data first (true = PUT, false = POST) + * @return whether the target existed beforehand. + */ + + protected static boolean addDataIntoNonTxn(HttpAction action, boolean overwrite) { + Graph graphTmp = GraphFactory.createGraphMem() ; + StreamRDF dest = StreamRDFLib.graph(graphTmp) ; + + try { incomingData(action, dest); } + catch (RiotException ex) { + errorBadRequest(ex.getMessage()) ; + return false ; + } + // Now insert into dataset + action.beginWrite() ; + Target target = determineTarget(action) ; + boolean existedBefore = false ; + try { + if ( log.isDebugEnabled() ) + log.debug(" ->"+target) ; + existedBefore = target.exists() ; + if ( overwrite && existedBefore ) + clearGraph(target) ; + FusekiLib.addDataInto(graphTmp, target.dsg, target.graphName) ; + action.commit() ; + return existedBefore ; + } catch (Exception ex) { + // We parsed into a temporary graph so an exception at this point + // is not because of a parse error. + // We're in the non-transactional branch, this probably will not work + // but it might and there is no harm safely trying. + try { action.abort() ; } catch (Exception ex2) {} + errorOccurred(ex.getMessage()) ; + return existedBefore ; + } finally { action.endWrite() ; } + } + + private static void incomingData(HttpAction action, StreamRDF dest) { + String base = wholeRequestURL(action.request) ; + ContentType ct = FusekiLib.getContentType(action) ; + Lang lang = RDFLanguages.contentTypeToLang(ct.getContentType()) ; + if ( lang == null ) { + errorBadRequest("Unknown content type for triples: " + ct) ; + return ; + } + InputStream input = null ; + try { input = action.request.getInputStream() ; } + catch (IOException ex) { IO.exception(ex) ; } + + int len = action.request.getContentLength() ; + if ( action.verbose ) { + if ( len >= 0 ) + log.info(format("[%d] Body: Content-Length=%d, Content-Type=%s, Charset=%s => %s", action.id, len, + ct.getContentType(), ct.getCharset(), lang.getName())) ; + else + log.info(format("[%d] Body: Content-Type=%s, Charset=%s => %s", action.id, ct.getContentType(), + ct.getCharset(), lang.getName())) ; + } + + parse(action, dest, input, lang, base) ; + } + + protected static void deleteGraph(HttpAction action) { + Target target = determineTarget(action) ; + if ( target.isDefault ) + target.graph().clear() ; + else + action.getActiveDSG().removeGraph(target.graphName) ; + } + + protected static void clearGraph(Target target) { + Graph g = target.graph() ; + g.clear() ; + Map<String, String> pm = g.getPrefixMapping().getNsPrefixMap() ; + for ( Entry<String, String> e : pm.entrySet() ) + g.getPrefixMapping().removeNsPrefix(e.getKey()) ; + } +} http://git-wip-us.apache.org/repos/asf/jena/blob/662cf71d/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_ServletBase.java ---------------------------------------------------------------------- diff --git a/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_ServletBase.java b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_ServletBase.java new file mode 100644 index 0000000..a3d5271 --- /dev/null +++ b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_ServletBase.java @@ -0,0 +1,458 @@ +/* + * 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.Requests ; +import static org.apache.jena.fuseki.server.CounterName.RequestsBad ; +import static org.apache.jena.fuseki.server.CounterName.RequestsGood ; + +import java.io.IOException ; +import java.util.Enumeration ; +import java.util.Map ; +import java.util.concurrent.atomic.AtomicLong ; + +import javax.servlet.ServletException ; +import javax.servlet.http.HttpServletRequest ; +import javax.servlet.http.HttpServletResponse ; + +import org.apache.jena.atlas.RuntimeIOException ; +import org.apache.jena.fuseki.Fuseki ; +import org.apache.jena.fuseki.HttpNames ; +import org.apache.jena.fuseki.server.* ; +import org.apache.jena.web.HttpSC ; + +import com.hp.hpl.jena.query.ARQ ; +import com.hp.hpl.jena.query.QueryCancelledException ; +import com.hp.hpl.jena.sparql.util.Context ; + +/** + * Base servlet for SPARQL requests. + */ +public abstract class SPARQL_ServletBase extends ServletBase +{ + /** + * Creates a new SPARQL base Servlet. + */ + protected SPARQL_ServletBase() { super() ; } + + // Common framework for handling HTTP requests + /** + * Handles GET and POST requests. + * @param request HTTP request + * @param response HTTP response + */ + protected void doCommon(HttpServletRequest request, HttpServletResponse response) + //throws ServletException, IOException + { + try { + long id = allocRequestId(request, response); + + // Lifecycle + HttpAction action = allocHttpAction(id, request, response) ; + // then add to doCommonWorker + // work with HttpServletResponseTracker + + printRequest(action) ; + action.setStartTime() ; + + response = action.response ; + initResponse(request, response) ; + Context cxt = ARQ.getContext() ; + + try { + execCommonWorker(action) ; + } catch (QueryCancelledException ex) { + // Also need the per query info ... + String message = String.format("The query timed out (restricted to %s ms)", cxt.get(ARQ.queryTimeout)); + // Possibility :: response.setHeader("Retry-after", "600") ; // 5 minutes + responseSendError(response, HttpSC.SERVICE_UNAVAILABLE_503, message); + } catch (ActionErrorException ex) { + if ( ex.exception != null ) + ex.exception.printStackTrace(System.err) ; + // Log message done by printResponse in a moment. + if ( ex.message != null ) + responseSendError(response, ex.rc, ex.message) ; + else + responseSendError(response, ex.rc) ; + } catch (RuntimeIOException ex) { + log.warn(format("[%d] Runtime IO Exception (client left?) RC = %d", id, HttpSC.INTERNAL_SERVER_ERROR_500)) ; + responseSendError(response, HttpSC.INTERNAL_SERVER_ERROR_500, ex.getMessage()) ; + } catch (Throwable ex) { + // This should not happen. + //ex.printStackTrace(System.err) ; + log.warn(format("[%d] RC = %d : %s", id, HttpSC.INTERNAL_SERVER_ERROR_500, ex.getMessage()), ex) ; + responseSendError(response, HttpSC.INTERNAL_SERVER_ERROR_500, ex.getMessage()) ; + } + + action.setFinishTime() ; + printResponse(action) ; + archiveHttpAction(action) ; + } catch (Throwable th) { + log.error("Internal error", th) ; + } + } + + // ---- Operation lifecycle + + /** + * Returns a fresh HTTP Action for this request. + * @param id the Request ID + * @param request HTTP request + * @param response HTTP response + * @return a new HTTP Action + */ + protected HttpAction allocHttpAction(long id, HttpServletRequest request, HttpServletResponse response) { + // Need a way to set verbose logging on a per servlet and per request basis. + return new HttpAction(id, request, response, verboseLogging) ; + } + + /** + * Validates a HTTP Action. + * @param action HTTP Action + */ + protected abstract void validate(HttpAction action) ; + + /** + * Performs the HTTP Action. + * @param action HTTP Action + */ + protected abstract void perform(HttpAction action) ; + + /** + * Default start step. + * @param action HTTP Action + */ + protected void startRequest(HttpAction action) { + } + + /** + * Default finish step. + * @param action HTTP Action + */ + protected void finishRequest(HttpAction action) { } + + /** + * Archives the HTTP Action. + * @param action HTTP Action + * @see HttpAction#minimize() + */ + private void archiveHttpAction(HttpAction action) + { + action.minimize() ; + } + + /** + * Executes common tasks, including mapping the request to the right dataset, setting the dataset into the HTTP + * action, and retrieving the service for the dataset requested. Finally, it calls the + * {@link #executeAction(HttpAction)} method, which executes the HTTP Action life cycle. + * @param action HTTP Action + */ + private void execCommonWorker(HttpAction action) + { + DatasetRef dsRef = null ; + String uri = action.request.getRequestURI() ; + + String datasetUri = mapRequestToDataset(uri) ; + + if ( datasetUri != null ) { + dsRef = DatasetRegistry.get().get(datasetUri) ; + if ( dsRef == null ) { + errorNotFound("No dataset for URI: "+datasetUri) ; + return ; + } + } else + dsRef = FusekiConfig.serviceOnlyDatasetRef() ; + + action.setDataset(dsRef) ; + String serviceName = mapRequestToService(dsRef, uri, datasetUri) ; + ServiceRef srvRef = dsRef.getServiceRef(serviceName) ; + action.setService(srvRef) ; + executeAction(action) ; + } + + /** + * Utility method, that increments and returns the AtomicLong value. + * @param x AtomicLong + */ + protected void inc(AtomicLong x) + { + x.incrementAndGet() ; + } + + /** + * Executes the HTTP Action. Serves as intercept point for the UberServlet. + * @param action HTTP Action + */ + protected void executeAction(HttpAction action) + { + executeLifecycle(action) ; + } + + /** + * Handles the service request lifecycle. Called directly by the UberServlet, + * which has not done any stats by this point. + * @param action {@link HttpAction} + * @see HttpAction + */ + protected void executeLifecycle(HttpAction action) + { + incCounter(action.dsRef, Requests) ; + incCounter(action.srvRef, Requests) ; + + startRequest(action) ; + try { + validate(action) ; + } catch (ActionErrorException ex) { + incCounter(action.dsRef,RequestsBad) ; + throw ex ; + } + + try { + perform(action) ; + // Success + incCounter(action.srvRef, RequestsGood) ; + incCounter(action.dsRef, RequestsGood) ; + } catch (ActionErrorException ex) { + incCounter(action.srvRef, RequestsBad) ; + incCounter(action.dsRef, RequestsBad) ; + throw ex ; + } catch (QueryCancelledException ex) { + incCounter(action.srvRef, RequestsBad) ; + incCounter(action.dsRef, RequestsBad) ; + throw ex ; + } finally { + finishRequest(action) ; + } + } + + /** + * Increments a counter. + * @param counters a {@link Counter} + * @param name a {@link CounterName} + */ + protected static void incCounter(Counters counters, CounterName name) { + try { + if ( counters.getCounters().contains(name) ) + counters.getCounters().inc(name) ; + } catch (Exception ex) { + Fuseki.serverLog.warn("Exception on counter inc", ex) ; + } + } + + /** + * Decrements a counter. + * @param counters a {@link Counter} + * @param name a {@link CounterName} + */ + protected static void decCounter(Counters counters, CounterName name) { + try { + if ( counters.getCounters().contains(name) ) + counters.getCounters().dec(name) ; + } catch (Exception ex) { + Fuseki.serverLog.warn("Exception on counter dec", ex) ; + } + } + + /** + * <p>Sends an <strong>error</strong> when the PATCH method is called.</p> + * <p>Throws ServletException or IOException as per overloaded method signature.</p> + * @param request HTTP request + * @param response HTTP response + * @throws ServletException from overloaded method signature + * @throws IOException from overloaded method signature + */ + protected void doPatch(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException + { + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "HTTP PATCH not supported"); + } + + /** + * Prints the HTTP Action request to the program log, using the INFO level. + * @param action {@link HttpAction} + */ + private void printRequest(HttpAction action) + { + String url = wholeRequestURL(action.request) ; + String method = action.request.getMethod() ; + + log.info(format("[%d] %s %s", action.id, method, url)) ; + if ( action.verbose ) { + Enumeration<String> en = action.request.getHeaderNames() ; + for (; en.hasMoreElements();) { + String h = en.nextElement() ; + Enumeration<String> vals = action.request.getHeaders(h) ; + if (!vals.hasMoreElements()) + log.info(format("[%d] ", action.id, h)) ; + else { + for (; vals.hasMoreElements();) + log.info(format("[%d] %-20s %s", action.id, h, vals.nextElement())) ; + } + } + } + } + + /** + * Initiates the response, by setting common headers such as Access-Control-Allow-Origin and Server, and + * the Vary header if the request method used was a GET. + * @param request HTTP request + * @param response HTTP response + */ + private void initResponse(HttpServletRequest request, HttpServletResponse response) + { + setCommonHeaders(response) ; + String method = request.getMethod() ; + // All GET and HEAD operations are sensitive to conneg so ... + if ( HttpNames.METHOD_GET.equalsIgnoreCase(method) || HttpNames.METHOD_HEAD.equalsIgnoreCase(method) ) + setVaryHeader(response) ; + } + + /** + * Prints the HTTP Action response to the program log, using the INFO level. + * @param action {@link HttpAction} + */ + private void printResponse(HttpAction action) + { + long time = action.getTime() ; + + HttpServletResponseTracker response = action.response ; + if ( action.verbose ) + { + if ( action.contentType != null ) + log.info(format("[%d] %-20s %s", action.id, HttpNames.hContentType, action.contentType)) ; + if ( action.contentLength != -1 ) + log.info(format("[%d] %-20s %d", action.id, HttpNames.hContentLengh, action.contentLength)) ; + for ( Map.Entry<String, String> e: action.headers.entrySet() ) + log.info(format("[%d] %-20s %s", action.id, e.getKey(), e.getValue())) ; + } + + String timeStr = fmtMillis(time) ; + + if ( action.message == null ) + log.info(String.format("[%d] %d %s (%s) ", action.id, action.statusCode, HttpSC.getMessage(action.statusCode), timeStr)) ; + else + log.info(String.format("[%d] %d %s (%s) ", action.id, action.statusCode, action.message, timeStr)) ; + } + + /** + * <p>Given a time epoch, it will return the time in milli seconds if it is less than 1000, + * otherwise it will normalize it to display as second.</p> + * <p>It appends a 'ms' suffix when using milli seconds, and ditto <i>s</i> for seconds.</p> + * <p>For instance: </p> + * <ul> + * <li>10 emits 10 ms</li> + * <li>999 emits 999 ms</li> + * <li>1000 emits 1.000000 s</li> + * <li>10000 emits 10.000000 s</li> + * </ul> + * @param time the time epoch + * @return the time in milli seconds or in seconds + */ + private static String fmtMillis(long time) + { + // Millis only? seconds only? + if ( time < 1000 ) + return String.format("%,d ms", time) ; + return String.format("%,.3f s", time/1000.0) ; + } + + /** + * Map request to uri in the registry. null means no mapping done (passthrough). + * @param uri the URI + * @return the dataset + */ + protected String mapRequestToDataset(String uri) + { + return mapRequestToDataset$(uri) ; + } + + /** + * A possible implementation for mapRequestToDataset(String) that assumes the form /dataset/service. + * @param uri the URI + * @return the dataset + */ + protected static String mapRequestToDataset$(String uri) + { + // Chop off trailing part - the service selector + // e.g. /dataset/sparql => /dataset + int i = uri.lastIndexOf('/') ; + if ( i == -1 ) + return null ; + if ( i == 0 ) + { + // started with '/' - leave. + return uri ; + } + + return uri.substring(0, i) ; + } + + /** + * Maps a request to a service (e.g. Query, Update). + * @param dsRef a {@link DatasetRef} + * @param uri the URI + * @param datasetURI the dataset URI + * @return an empty String (i.e. "") if the DatasetRef is null, or if its name is longer than the URI's name. + * Otherwise will return the service name. + */ + protected String mapRequestToService(DatasetRef dsRef, String uri, String datasetURI) + { + if ( dsRef == null ) + return "" ; + if ( dsRef.name.length() >= uri.length() ) + return "" ; + return uri.substring(dsRef.name.length()+1) ; // Skip the separating "/" + + } + + /** + * Implementation of mapRequestToDataset(String) that looks for the longest match in the registry. + * This includes use in direct naming GSP. + * @param uri the URI + * @return <code>null</code> if the URI is null, otherwise will return the longest match in the registry. + */ + protected static String mapRequestToDatasetLongest$(String uri) + { + if ( uri == null ) + return null ; + + // This covers local, using the URI as a direct name for + // a graph, not just using the indirect ?graph= or ?default + // forms. + + String ds = null ; + for ( String ds2 : DatasetRegistry.get().keys() ) { + if ( ! uri.startsWith(ds2) ) + continue ; + + if ( ds == null ) + { + ds = ds2 ; + continue ; + } + if ( ds.length() < ds2.length() ) + { + ds = ds2 ; + continue ; + } + } + return ds ; + } +} http://git-wip-us.apache.org/repos/asf/jena/blob/662cf71d/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_UberServlet.java ---------------------------------------------------------------------- diff --git a/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_UberServlet.java b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_UberServlet.java new file mode 100644 index 0000000..0c10cee --- /dev/null +++ b/jena-fuseki1/src/main/java/org/apache/jena/fuseki/servlets/SPARQL_UberServlet.java @@ -0,0 +1,338 @@ +/** + * 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 java.util.List ; + +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.FusekiException ; +import org.apache.jena.fuseki.HttpNames ; +import org.apache.jena.fuseki.conneg.ConNeg ; +import org.apache.jena.fuseki.server.DatasetRef ; +import org.apache.jena.fuseki.server.ServiceRef ; +import org.apache.jena.riot.WebContent ; + +/** 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). + */ +public abstract class SPARQL_UberServlet extends SPARQL_ServletBase +{ + 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 + { + 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 + { + 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 + { + public AccessByConfig() { super() ; } + @Override protected boolean allowQuery(HttpAction action) { return isEnabled(action.dsRef.query) ; } + @Override protected boolean allowUpdate(HttpAction action) { return isEnabled(action.dsRef.update) ; } + @Override protected boolean allowREST_R(HttpAction action) { return isEnabled(action.dsRef.readGraphStore) || allowREST_W(action); } + @Override protected boolean allowREST_W(HttpAction action) { return isEnabled(action.dsRef.readWriteGraphStore) ; } + // Quad operations tied to presence/absence of GSP. + @Override protected boolean allowQuadsR(HttpAction action) { return isEnabled(action.dsRef.readGraphStore) ; } + @Override protected boolean allowQuadsW(HttpAction action) { return isEnabled(action.dsRef.readWriteGraphStore) ; } + + private boolean isEnabled(ServiceRef service) { return service.isActive() ; } + } + + /* 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. + */ + + // Refactor? Extract the direct naming handling. + // To test: enable in SPARQLServer.configureOneDataset + + private final SPARQL_ServletBase queryServlet = new SPARQL_QueryDataset() ; + private final SPARQL_ServletBase updateServlet = new SPARQL_Update() ; + private final SPARQL_ServletBase uploadServlet = new SPARQL_Upload() ; + private final SPARQL_REST restServlet_RW = new SPARQL_REST_RW() ; + private final SPARQL_REST restServlet_R = new SPARQL_REST_R() ; + private final SPARQL_ServletBase restQuads = new REST_Quads() ; + + 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(String uri) + { + return mapRequestToDatasetLongest$(uri) ; + } + + + /** 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) + { + long id = action.id ; + HttpServletRequest request = action.request ; + HttpServletResponse response = action.response ; + String uri = request.getRequestURI() ; + String method = request.getMethod() ; + DatasetRef desc = action.dsRef ; + + String trailing = findTrailing(uri, desc.name) ; + String qs = request.getQueryString() ; + + boolean hasParams = request.getParameterMap().size() > 0 ; + + // Test for parameters - includes HTML forms. + boolean hasParamQuery = request.getParameter(HttpNames.paramQuery) != null ; + // Include old name "request=" + boolean hasParamUpdate = 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 isForm = WebContent.contentTypeHTMLForm.equalsIgnoreCase(request.getContentType()) ; + + String ct = request.getContentType() ; + String charset = request.getCharacterEncoding() ; + + MediaType mt = null ; + if ( ct != null ) + mt = MediaType.create(ct, charset) ; + + log.info(format("[%d] All: %s %s :: '%s' :: %s ? %s", id, method, desc.name, trailing, (mt==null?"<none>":mt), (qs==null?"":qs))) ; + + boolean hasTrailing = ( trailing.length() != 0 ) ; + + if ( ! hasTrailing && ! hasParams ) + { + restQuads.executeLifecycle(action) ; + return ; + } + + if ( ! hasTrailing ) + { + // Has params of some kind. + if ( hasParamQuery || WebContent.contentTypeSPARQLQuery.equalsIgnoreCase(ct) ) + { + // SPARQL Query + if ( ! allowQuery(action)) + errorForbidden("Forbidden: SPARQL query") ; + executeRequest(action, queryServlet, desc.query) ; + return ; + } + + if ( hasParamUpdate || WebContent.contentTypeSPARQLUpdate.equalsIgnoreCase(ct) ) + { + // SPARQL Update + if ( ! allowQuery(action)) + errorForbidden("Forbidden: SPARQL query") ; + executeRequest(action, updateServlet, desc.update) ; + return ; + } + + if ( hasParamGraph || hasParamGraphDefault ) + { + doGraphStoreProtocol(action) ; + return ; + } + + errorBadRequest("Malformed request") ; + errorForbidden("Forbidden: SPARQL Graph Store Protocol : Read operation : "+method) ; + } + + final boolean checkForPossibleService = true ; + if ( checkForPossibleService ) + { + // 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, desc.query, trailing, queryServlet) ) return ; + if ( serviceDispatch(action, desc.update, trailing, updateServlet) ) return ; + if ( serviceDispatch(action, desc.upload, trailing, uploadServlet) ) return ; + if ( serviceDispatch(action, desc.readGraphStore, trailing, restServlet_R) ) return ; + if ( serviceDispatch(action, desc.readWriteGraphStore, trailing, restServlet_RW) ) 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") ; + errorNotFound("Not found: dataset='"+printName(desc.name)+"' service='"+printName(trailing)+"'"); + + // There is a trailing part - not a service, no params ==> GSP direct naming. + doGraphStoreProtocol(action) ; + } + + 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. + DatasetRef desc = action.dsRef ; + String method = action.request.getMethod() ; + + if ( HttpNames.METHOD_GET.equalsIgnoreCase(method) || + HttpNames.METHOD_HEAD.equalsIgnoreCase(method) ) + { + if ( ! allowREST_R(action)) + // Graphs Store Protocol, indirect naming, read + // Indirect naming. Prefer the R service if available. + if ( desc.readGraphStore.isActive() ) + executeRequest(action, restServlet_R, desc.readGraphStore) ; + else if ( desc.readWriteGraphStore.isActive() ) + executeRequest(action, restServlet_RW, desc.readWriteGraphStore) ; + else + errorMethodNotAllowed(method) ; + return ; + } + + // Graphs Store Protocol, indirect naming, write + if ( ! allowREST_W(action)) + errorForbidden("Forbidden: SPARQL Graph Store Protocol : Write operation : "+method) ; + executeRequest(action, restServlet_RW, desc.readWriteGraphStore) ; + return ; + } + + private void executeRequest(HttpAction action, SPARQL_ServletBase servlet, ServiceRef service) + { + if ( service.endpoints.size() == 0 ) + errorMethodNotAllowed(action.request.getMethod()) ; + servlet.executeLifecycle(action) ; + } + + private void executeRequest(HttpAction action,SPARQL_ServletBase servlet) + { + servlet.executeLifecycle(action) ; +// // Forwarded dispatch. +// try +// { +// String target = getEPName(desc.name, endpointList) ; +// if ( target == null ) +// errorMethodNotAllowed(request.getMethod()) ; +// // ** relative servlet forward +// request.getRequestDispatcher(target).forward(request, response) ; + + +// // ** absolute srvlet forward +// // getServletContext().getRequestDispatcher(target) ; +// } catch (Exception e) { 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 ; + } + + /** return true if dispatched */ + private boolean serviceDispatch(HttpAction action, ServiceRef service, String srvName , SPARQL_ServletBase servlet) + { + if ( ! service.endpoints.contains(srvName) ) + return false ; + servlet.executeLifecycle(action) ; + return true ; + } + + /** Find the graph (direct naming) or service name */ + protected String findTrailing(String uri, String dsname) + { + if ( dsname.length() >= uri.length() ) + return "" ; + return uri.substring(dsname.length()+1) ; // Skip the separating "/" + } + + @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) ; } +} +
