/* * 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.Fuseki.requestLog ; import static org.apache.jena.fuseki.HttpNames.paramRequest ; import static org.apache.jena.fuseki.HttpNames.paramUpdate ; import static org.apache.jena.fuseki.HttpNames.paramUsingGraphURI ; import static org.apache.jena.fuseki.HttpNames.paramUsingNamedGraphURI ; import static org.apache.jena.fuseki.server.CounterName.UpdateExecErrors ; import java.io.ByteArrayInputStream ; import java.io.IOException ; import java.io.InputStream ; import java.util.Arrays ; import java.util.Collection ; import java.util.Enumeration ; import java.util.List ; import javax.servlet.ServletException ; import javax.servlet.http.HttpServletRequest ; import javax.servlet.http.HttpServletResponse ; import org.apache.jena.atlas.io.IO ; import org.apache.jena.atlas.lib.StrUtils ; import org.apache.jena.atlas.web.ContentType ; import org.apache.jena.fuseki.FusekiLib ; import org.apache.jena.fuseki.HttpNames ; import org.apache.jena.graph.Node ; import org.apache.jena.graph.NodeFactory ; import org.apache.jena.iri.IRI ; import org.apache.jena.query.QueryParseException ; import org.apache.jena.query.Syntax ; import org.apache.jena.riot.WebContent ; import org.apache.jena.riot.system.IRIResolver ; import org.apache.jena.sparql.modify.UsingList ; import org.apache.jena.update.UpdateAction ; import org.apache.jena.update.UpdateException ; import org.apache.jena.update.UpdateFactory ; import org.apache.jena.update.UpdateRequest ; import org.apache.jena.web.HttpSC ; public class SPARQL_Update extends SPARQL_Protocol { // Base URI used to isolate parsing from the current directory of the server. private static final String UpdateParseBase = "http://example/update-base/" ; private static final IRIResolver resolver = IRIResolver.create(UpdateParseBase) ; public SPARQL_Update() { super() ; } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.sendError(HttpSC.BAD_REQUEST_400, "Attempt to perform SPARQL update by GET. Use POST") ; } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doCommon(request, response) ; } @Override protected void doOptions(HttpServletRequest request, HttpServletResponse response) { setCommonHeadersForOptions(response) ; response.setHeader(HttpNames.hAllow, "OPTIONS,POST"); response.setHeader(HttpNames.hContentLengh, "0") ; } @Override protected void perform(HttpAction action) { // WebContent needs to migrate to using ContentType. String ctStr ; { ContentType ct = FusekiLib.getContentType(action) ; if ( ct == null ) ctStr = WebContent.contentTypeSPARQLUpdate ; else ctStr = ct.getContentType() ; } if (WebContent.contentTypeSPARQLUpdate.equals(ctStr)) { executeBody(action) ; return ; } if (WebContent.contentTypeHTMLForm.equals(ctStr)) { executeForm(action) ; return ; } error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "Bad content type: " + action.request.getContentType()) ; } protected static List<String> paramsForm = Arrays.asList(paramRequest, paramUpdate, paramUsingGraphURI, paramUsingNamedGraphURI) ; protected static List<String> paramsPOST = Arrays.asList(paramUsingGraphURI, paramUsingNamedGraphURI) ; @Override protected void validate(HttpAction action) { HttpServletRequest request = action.request ; if ( ! HttpNames.METHOD_POST.equalsIgnoreCase(request.getMethod()) ) errorMethodNotAllowed("SPARQL Update : use POST") ; ContentType incoming = FusekiLib.getContentType(action) ; String ctStr = ( incoming == null ) ? WebContent.contentTypeSPARQLUpdate : incoming.getContentType() ; // ---- if ( WebContent.contentTypeSPARQLUpdate.equals(ctStr) ) { String charset = request.getCharacterEncoding() ; if ( charset != null && ! charset.equalsIgnoreCase(WebContent.charsetUTF8) ) errorBadRequest("Bad charset: "+charset) ; validate(request, paramsPOST) ; return ; } if ( WebContent.contentTypeHTMLForm.equals(ctStr) ) { int x = countParamOccurences(request, paramUpdate) + countParamOccurences(request, paramRequest) ; if ( x == 0 ) errorBadRequest("SPARQL Update: No 'update=' parameter") ; if ( x != 1 ) errorBadRequest("SPARQL Update: Multiple 'update=' parameters") ; String requestStr = request.getParameter(paramUpdate) ; if ( requestStr == null ) requestStr = request.getParameter(paramRequest) ; if ( requestStr == null ) errorBadRequest("SPARQL Update: No update= in HTML form") ; validate(request, paramsForm) ; return ; } error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "Must be "+WebContent.contentTypeSPARQLUpdate+" or "+WebContent.contentTypeHTMLForm+" (got "+ctStr+")") ; } protected void validate(HttpServletRequest request, Collection<String> params) { if ( params != null ) { Enumeration<String> en = request.getParameterNames() ; for ( ; en.hasMoreElements() ; ) { String name = en.nextElement() ; if ( ! params.contains(name) ) warning("SPARQL Update: Unrecognize request parameter (ignored): "+name) ; } } } private void executeBody(HttpAction action) { InputStream input = null ; try { input = action.request.getInputStream() ; } catch (IOException ex) { errorOccurred(ex) ; } if ( action.verbose ) { // Verbose mode only .... capture request for logging (does not scale). String requestStr = null ; try { requestStr = IO.readWholeFileAsUTF8(input) ; } catch (IOException ex) { IO.exception(ex) ; } requestLog.info(format("[%d] Update = %s", action.id, formatForLog(requestStr))) ; input = new ByteArrayInputStream(requestStr.getBytes()); requestStr = null; } execute(action, input) ; successNoContent(action) ; } private void executeForm(HttpAction action) { String requestStr = action.request.getParameter(paramUpdate) ; if ( requestStr == null ) requestStr = action.request.getParameter(paramRequest) ; if ( action.verbose ) //requestLog.info(format("[%d] Form update = %s", action.id, formatForLog(requestStr))) ; requestLog.info(format("[%d] Form update = \n%s", action.id, requestStr)) ; // A little ugly because we are taking a copy of the string, but hopefully shouldn't be too big if we are in this code-path // If we didn't want this additional copy, we could make the parser take a Reader in addition to an InputStream byte[] b = StrUtils.asUTF8bytes(requestStr) ; ByteArrayInputStream input = new ByteArrayInputStream(b); requestStr = null; // free it early at least execute(action, input); successPage(action,"Update succeeded") ; } private void execute(HttpAction action, InputStream input) { UsingList usingList = processProtocol(action.request) ; // If the dsg is transactional, then we can parse and execute the update in a streaming fashion. // If it isn't, we need to read the entire update request before performing any updates, because // we have to attempt to make the request atomic in the face of malformed queries UpdateRequest req = null ; if (!action.isTransactional()) { try { // TODO implement a spill-to-disk version of this req = UpdateFactory.read(usingList, input, UpdateParseBase, Syntax.syntaxARQ); } catch (UpdateException ex) { errorBadRequest(ex.getMessage()) ; return ; } catch (QueryParseException ex) { errorBadRequest(messageForQPE(ex)) ; return ; } } action.beginWrite() ; try { if (req == null ) UpdateAction.parseExecute(usingList, action.getActiveDSG(), input, UpdateParseBase, Syntax.syntaxARQ); else UpdateAction.execute(req, action.getActiveDSG()) ; action.commit() ; } catch (UpdateException ex) { action.abort() ; incCounter(action.srvRef, UpdateExecErrors) ; errorOccurred(ex.getMessage()) ; } catch (QueryParseException ex) { action.abort() ; // Counter inc'ed further out. errorBadRequest(messageForQPE(ex)) ; } catch (Throwable ex) { if ( ! ( ex instanceof ActionErrorException ) ) { try { action.abort() ; } catch (Exception ex2) {} errorOccurred(ex.getMessage(), ex) ; } } finally { action.endWrite(); } } /* [It is an error to supply the using-graph-uri or using-named-graph-uri parameters * when using this protocol to convey a SPARQL 1.1 Update request that contains an * operation that uses the USING, USING NAMED, or WITH clause.] * * We will simply capture any using parameters here and pass them to the parser, which will be * responsible for throwing an UpdateException if the query violates the above requirement, * and will also be responsible for adding the using parameters to update queries that can * accept them. */ private UsingList processProtocol(HttpServletRequest request) { UsingList toReturn = new UsingList(); String[] usingArgs = request.getParameterValues(paramUsingGraphURI) ; String[] usingNamedArgs = request.getParameterValues(paramUsingNamedGraphURI) ; if ( usingArgs == null && usingNamedArgs == null ) return toReturn; if ( usingArgs == null ) usingArgs = new String[0] ; if ( usingNamedArgs == null ) usingNamedArgs = new String[0] ; // Impossible. // if ( usingArgs.length == 0 && usingNamedArgs.length == 0 ) // return ; for (String nodeUri : usingArgs) { toReturn.addUsing(createNode(nodeUri)); } for (String nodeUri : usingNamedArgs) { toReturn.addUsingNamed(createNode(nodeUri)); } return toReturn; } private static Node createNode(String x) { try { IRI iri = resolver.resolve(x) ; return NodeFactory.createURI(iri.toString()) ; } catch (Exception ex) { errorBadRequest("SPARQL Update: bad IRI: "+x) ; return null ; } } }