/*
* 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.UpdateExecErrors ;
import static org.apache.jena.riot.WebContent.charsetUTF8 ;
import static org.apache.jena.riot.WebContent.contentTypeHTMLForm ;
import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate ;
import static org.apache.jena.riot.WebContent.ctSPARQLUpdate ;
import static org.apache.jena.riot.WebContent.isHtmlForm ;
import static org.apache.jena.riot.WebContent.matchContentType ;
import static org.apache.jena.riot.web.HttpNames.paramRequest ;
import static org.apache.jena.riot.web.HttpNames.paramUpdate ;
import static org.apache.jena.riot.web.HttpNames.paramUsingGraphURI ;
import static org.apache.jena.riot.web.HttpNames.paramUsingNamedGraphURI ;
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.Fuseki ;
import org.apache.jena.fuseki.FusekiLib ;
import org.apache.jena.graph.Node ;
import org.apache.jena.graph.NodeFactory ;
import org.apache.jena.iri.IRI ;
import org.apache.jena.query.QueryBuildException ;
import org.apache.jena.query.QueryParseException ;
import org.apache.jena.query.Syntax ;
import org.apache.jena.riot.system.IRIResolver ;
import org.apache.jena.riot.web.HttpNames ;
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
{
private static final long serialVersionUID = 6136544994836781248L;
// Base URI used to isolate parsing from the current directory of the server.
private static final String UpdateParseBase = Fuseki.BaseParserSPARQL ;
private static final IRIResolver resolver = IRIResolver.create(UpdateParseBase) ;
public SPARQL_Update()
{ super() ; }
// doMethod : Not used with UberServlet dispatch.
@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") ;
}
protected void doOptions(HttpAction action) {
doOptions(action.request, action.response) ;
}
@Override
protected void perform(HttpAction action) {
ContentType ct = FusekiLib.getContentType(action) ;
if ( ct == null )
ct = ctSPARQLUpdate ;
if ( matchContentType(ctSPARQLUpdate, ct) ) {
executeBody(action) ;
return ;
}
if ( isHtmlForm(ct) ) {
executeForm(action) ;
return ;
}
ServletOps.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_OPTIONS.equals(request.getMethod()) )
return ;
if ( ! HttpNames.METHOD_POST.equalsIgnoreCase(request.getMethod()) )
ServletOps.errorMethodNotAllowed("SPARQL Update : use POST") ;
ContentType ct = FusekiLib.getContentType(action) ;
if ( ct == null )
ct = ctSPARQLUpdate ;
if ( matchContentType(ctSPARQLUpdate, ct) ) {
String charset = request.getCharacterEncoding() ;
if ( charset != null && !charset.equalsIgnoreCase(charsetUTF8) )
ServletOps.errorBadRequest("Bad charset: " + charset) ;
validate(action, paramsPOST) ;
return ;
}
if ( isHtmlForm(ct) ) {
int x = countParamOccurences(request, paramUpdate) + countParamOccurences(request, paramRequest) ;
if ( x == 0 )
ServletOps.errorBadRequest("SPARQL Update: No 'update=' parameter") ;
if ( x != 1 )
ServletOps.errorBadRequest("SPARQL Update: Multiple 'update=' parameters") ;
String requestStr = request.getParameter(paramUpdate) ;
if ( requestStr == null )
requestStr = request.getParameter(paramRequest) ;
if ( requestStr == null )
ServletOps.errorBadRequest("SPARQL Update: No update= in HTML form") ;
validate(action, paramsForm) ;
return ;
}
ServletOps.error(HttpSC.UNSUPPORTED_MEDIA_TYPE_415, "Must be "+contentTypeSPARQLUpdate+" or "+contentTypeHTMLForm+" (got "+ct.getContentType()+")") ;
}
protected void validate(HttpAction action, Collection<String> params) {
if ( params != null ) {
Enumeration<String> en = action.request.getParameterNames() ;
for ( ; en.hasMoreElements() ; ) {
String name = en.nextElement() ;
if ( !params.contains(name) )
ServletOps.warning(action, "SPARQL Update: Unrecognized request parameter (ignored): "+name) ;
}
}
}
private void executeBody(HttpAction action) {
InputStream input = null ;
try { input = action.request.getInputStream() ; }
catch (IOException ex) { ServletOps.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) ; }
action.log.info(format("[%d] Update = %s", action.id, ServletOps.formatForLog(requestStr))) ;
input = new ByteArrayInputStream(requestStr.getBytes());
requestStr = null;
}
execute(action, input) ;
ServletOps.successNoContent(action) ;
}
private void executeForm(HttpAction action) {
String requestStr = action.request.getParameter(paramUpdate) ;
if ( requestStr == null )
requestStr = action.request.getParameter(paramRequest) ;
if ( action.verbose )
action.log.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);
ServletOps.successPage(action,"Update succeeded") ;
}
private void execute(HttpAction action, InputStream input) {
// OPTIONS
if ( action.request.getMethod().equals(HttpNames.METHOD_OPTIONS) ) {
// Share with update via SPARQL_Protocol.
doOptions(action) ;
return ;
}
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 updates
UpdateRequest req = null ;
if (!action.isTransactional()) {
try {
req = UpdateFactory.read(usingList, input, UpdateParseBase, Syntax.syntaxARQ);
}
catch (UpdateException ex) { ServletOps.errorBadRequest(ex.getMessage()) ; return ; }
catch (QueryParseException ex) { ServletOps.errorBadRequest(messageForQueryException(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.getEndpoint().getCounters(), UpdateExecErrors) ;
ServletOps.errorOccurred(ex.getMessage()) ;
} catch (QueryParseException|QueryBuildException ex) {
action.abort() ;
// Counter inc'ed further out.
ServletOps.errorBadRequest(messageForQueryException(ex)) ;
} catch (Throwable ex) {
if ( ! ( ex instanceof ActionErrorException ) )
{
try { action.abort() ; } catch (Exception ex2) {}
ServletOps.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)
{
ServletOps.errorBadRequest("SPARQL Update: bad IRI: "+x) ;
return null ;
}
}
}