blob: 50a5e68ae4aaffc170048b8d6b3b1b20db2eb0d4 [file] [log] [blame]
/*
* 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.fuseki.servlets.ActionExecLib.incCounter;
import static org.apache.jena.fuseki.servlets.SPARQLProtocol.messageForException;
import static org.apache.jena.riot.WebContent.*;
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.http.HttpServletRequest;
import org.apache.jena.atlas.io.IO;
import org.apache.jena.atlas.lib.Bytes;
import org.apache.jena.atlas.lib.StrUtils;
import org.apache.jena.atlas.web.ContentType;
import org.apache.jena.fuseki.Fuseki;
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 ActionService
{
// 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(); }
@Override
public void execOptions(HttpAction action) {
ServletBase.setCommonHeadersForOptions(action.response);
action.response.setHeader(HttpNames.hAllow, "POST,PATCH,OPTIONS");
ServletOps.success(action);
}
@Override
public void execGet(HttpAction action) {
ServletOps.errorMethodNotAllowed(HttpNames.METHOD_GET, "GET not support for SPARQL Update. Use POST or PATCH");
}
@Override
public void execPost(HttpAction action) {
executeLifecycle(action);
}
@Override
public void execPatch(HttpAction action) {
executeLifecycle(action);
}
@Override
public void execute(HttpAction action) {
ContentType ct = ActionLib.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
public 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 = ActionLib.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 = SPARQLProtocol.countParamOccurences(request, paramUpdate) + SPARQLProtocol.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.getContentTypeStr()+")");
}
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).
byte[] bytes = IO.readWholeFile(input);
input = new ByteArrayInputStream(bytes);
try {
String requestStr = Bytes.bytes2string(bytes);
action.log.info(format("[%d] Update = %s", action.id, ServletOps.formatForLog(requestStr)));
} catch (Exception ex) {
action.log.info(format("[%d] Update = <failed to decode>", action.id));
}
} else {
// Some kind of log message to show its an update.
action.log.info(format("[%d] Update", action.id));
}
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");
}
protected 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 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(messageForException(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(messageForException(ex));
} catch (Throwable ex) {
if ( ! ( ex instanceof ActionErrorException ) ) {
try { action.abort(); } catch (Exception ex2) {}
ServletOps.errorOccurred(ex.getMessage(), ex);
}
} finally { action.end(); }
}
/* [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;
}
}
}