com.bigdata.rdf.sail.webapp.UpdateServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.bigdata.rdf.sail.webapp.UpdateServlet.java

Source

/**
Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016.  All rights reserved.
    
Contact:
 SYSTAP, LLC DBA Blazegraph
 2501 Calvert ST NW #106
 Washington, DC 20008
 licenses@blazegraph.com
    
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
    
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
package com.bigdata.rdf.sail.webapp;

import java.io.IOException;
import java.io.InputStream;
import java.io.PipedOutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicLong;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.log4j.Logger;
import org.openrdf.model.Resource;
import org.openrdf.model.Value;
import org.openrdf.query.MalformedQueryException;
import org.openrdf.rio.RDFFormat;
import org.openrdf.rio.RDFHandler;
import org.openrdf.rio.RDFParser;
import org.openrdf.rio.RDFParserFactory;
import org.openrdf.rio.RDFParserRegistry;

import com.bigdata.journal.ITx;
import com.bigdata.rdf.sail.BigdataSail.BigdataSailConnection;
import com.bigdata.rdf.sail.sparql.Bigdata2ASTSPARQLParser;
import com.bigdata.rdf.sail.BigdataSailRepositoryConnection;
import com.bigdata.rdf.sail.webapp.BigdataRDFContext.AbstractQueryTask;
import com.bigdata.rdf.sail.webapp.DeleteServlet.BufferStatementHandler;
import com.bigdata.rdf.sail.webapp.DeleteServlet.RemoveStatementHandler;
import com.bigdata.rdf.sail.webapp.InsertServlet.AddStatementHandler;
import com.bigdata.rdf.sail.webapp.client.MiniMime;
import com.bigdata.rdf.sparql.ast.ASTContainer;

/**
 * Handler for NanoSparqlServer REST API UPDATE operations (PUT, not SPARQL
 * UPDATE).
 * 
 * @author martyncutcher
 */
public class UpdateServlet extends BigdataRDFServlet {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    static private final transient Logger log = Logger.getLogger(UpdateServlet.class);

    /*
     * Note: includedInferred is false because inferences can
     * not be deleted (they are retracted by truth maintenance
     * when they can no longer be proven).
     */
    private static final boolean includeInferred = false;

    public UpdateServlet() {

    }

    @Override
    protected void doPut(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {

        if (!isWritable(getServletContext(), req, resp)) {
            // Service must be writable.
            return;
        }

        final String queryStr = req.getParameter(QueryServlet.ATTR_QUERY);

        final String contentType = req.getContentType();

        if (contentType == null) {

            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);

        }

        if (queryStr == null) {

            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);

        }

        doUpdateWithQuery(req, resp);

    }

    /**
    * Delete all statements materialized by a DESCRIBE or CONSTRUCT query and
    * then insert all statements in the request body.
    * <p>
    * Note: To avoid materializing the statements, this runs the query against
    * the last commit time and uses a pipe to connect the query directly to the
    * process deleting the statements. This is done while it is holding the
    * unisolated connection which prevents concurrent modifications. Therefore
    * the entire <code>SELECT + DELETE</code> operation is ACID.
    */
    private void doUpdateWithQuery(final HttpServletRequest req, final HttpServletResponse resp)
            throws IOException {

        final String baseURI = req.getRequestURL().toString();

        final String namespace = getNamespace(req);

        final String queryStr = req.getParameter(QueryServlet.ATTR_QUERY);

        final boolean suppressTruthMaintenance = getBooleanValue(req, QueryServlet.ATTR_TRUTH_MAINTENANCE, false);

        if (queryStr == null)
            buildAndCommitResponse(resp, HTTP_BADREQUEST, MIME_TEXT_PLAIN,
                    "Required parameter not found: " + QueryServlet.ATTR_QUERY);

        final Map<String, Value> bindings = parseBindings(req, resp);
        if (bindings == null) { // invalid bindings definition generated error response 400 while parsing
            return;
        }

        final String contentType = req.getContentType();

        if (log.isInfoEnabled())
            log.info("Request body: " + contentType);

        /**
         * <a href="https://sourceforge.net/apps/trac/bigdata/ticket/620">
         * UpdateServlet fails to parse MIMEType when doing conneg. </a>
         */

        final RDFFormat requestBodyFormat = RDFFormat.forMIMEType(new MiniMime(contentType).getMimeType());

        if (requestBodyFormat == null) {

            buildAndCommitResponse(resp, HTTP_BADREQUEST, MIME_TEXT_PLAIN,
                    "Content-Type not recognized as RDF: " + contentType);

            return;

        }

        final RDFParserFactory rdfParserFactory = RDFParserRegistry.getInstance().get(requestBodyFormat);

        if (rdfParserFactory == null) {

            buildAndCommitResponse(resp, HTTP_INTERNALERROR, MIME_TEXT_PLAIN,
                    "Parser factory not found: Content-Type=" + contentType + ", format=" + requestBodyFormat);

            return;

        }

        /*
         * Allow the caller to specify the default context for insert.
         */
        final Resource[] defaultContextInsert;
        {
            final String[] s = req.getParameterValues("context-uri-insert");
            if (s != null && s.length > 0) {
                try {
                    defaultContextInsert = toURIs(s);
                } catch (IllegalArgumentException ex) {
                    buildAndCommitResponse(resp, HTTP_INTERNALERROR, MIME_TEXT_PLAIN, ex.getLocalizedMessage());
                    return;
                }
            } else {
                defaultContextInsert = null;
            }
        }

        /*
         * Allow the caller to specify the default context for delete.
         */
        final Resource[] defaultContextDelete;
        {
            final String[] s = req.getParameterValues("context-uri-delete");
            if (s != null && s.length > 0) {
                try {
                    defaultContextDelete = toURIs(s);
                } catch (IllegalArgumentException ex) {
                    buildAndCommitResponse(resp, HTTP_INTERNALERROR, MIME_TEXT_PLAIN, ex.getLocalizedMessage());
                    return;
                }
            } else {
                defaultContextDelete = null;
            }
        }

        try {

            if (getIndexManager().isGroupCommit()) {

                // compatible with group commit serializability semantics.
                submitApiTask(new UpdateWithQueryMaterializedTask(req, resp, namespace, ITx.UNISOLATED, //
                        queryStr, //
                        baseURI, //
                        suppressTruthMaintenance, //
                        bindings, //
                        rdfParserFactory, //
                        defaultContextDelete, //
                        defaultContextInsert//
                )).get();

            } else {

                // streaming implementation. not compatible with group commit.
                submitApiTask(new UpdateWithQueryStreamingTask(req, resp, namespace, ITx.UNISOLATED, //
                        queryStr, //
                        baseURI, //
                        suppressTruthMaintenance, //
                        bindings, //
                        rdfParserFactory, //
                        defaultContextDelete, //
                        defaultContextInsert//
                )).get();

            }

        } catch (Throwable t) {

            launderThrowable(t, resp,
                    "UPDATE-WITH-QUERY" + ": queryStr=" + queryStr + ", baseURI=" + baseURI
                            + (defaultContextInsert == null ? ""
                                    : ",context-uri-insert=" + Arrays.toString(defaultContextInsert))
                            + (defaultContextDelete == null ? ""
                                    : ",context-uri-delete=" + Arrays.toString(defaultContextDelete)));

        }

    }

    /**
    * Streaming version is more scalable but is not compatible with group
    * commit. The underlying issue is the serializability of the mutation
    * operations. This task reads from the last commit time and writes on the
    * unisolated indices. It holds the lock on the unisolated indices while
    * doing this. However, if group commit is enabled, then it might not observe
    * mutations which may have been applied since the last commit which breaks
    * the serializability guarantee.
    * 
    * @author bryan
    * 
    */
    private static class UpdateWithQueryStreamingTask extends AbstractRestApiTask<Void> {

        private final String queryStr;
        private final String baseURI;
        private final boolean suppressTruthMaintenance;
        private final RDFParserFactory parserFactory;
        private final Resource[] defaultContextDelete;
        private final Resource[] defaultContextInsert;
        private final Map<String, Value> bindings;

        /**
         * 
         * @param namespace
         *            The namespace of the target KB instance.
         * @param timestamp
         *            The timestamp used to obtain a mutable connection.
         * @param baseURI
         *            The base URI for the operation.
         * @param defaultContextDelete
         *            When removing statements, the context(s) for triples
         *            without an explicit named graph when the KB instance is
         *            operating in a quads mode.
         * @param defaultContextInsert
         *            When inserting statements, the context(s) for triples
         *            without an explicit named graph when the KB instance is
         *            operating in a quads mode.
         */
        public UpdateWithQueryStreamingTask(final HttpServletRequest req, final HttpServletResponse resp,
                final String namespace, final long timestamp, final String queryStr, //
                final String baseURI, final boolean suppressTruthMaintenance, //
                final Map<String, Value> bindings, final RDFParserFactory parserFactory,
                final Resource[] defaultContextDelete, //
                final Resource[] defaultContextInsert//
        ) {
            super(req, resp, namespace, timestamp);
            this.queryStr = queryStr;
            this.baseURI = baseURI;
            this.suppressTruthMaintenance = suppressTruthMaintenance;
            this.bindings = bindings;
            this.parserFactory = parserFactory;
            this.defaultContextDelete = defaultContextDelete;
            this.defaultContextInsert = defaultContextInsert;
        }

        @Override
        public boolean isReadOnly() {
            return false;
        }

        @Override
        public Void call() throws Exception {

            final long begin = System.currentTimeMillis();

            final AtomicLong nmodified = new AtomicLong(0L);

            /*
             * Parse the query before obtaining the connection object.
             * 
             * @see BLZG-2039 SPARQL QUERY and SPARQL UPDATE should be parsed
             * before obtaining the connection
             */

            // Setup the baseURI for this request. 
            final String baseURI = BigdataRDFContext.getBaseURI(req, resp);

            // Parse the query.
            final ASTContainer astContainer = new Bigdata2ASTSPARQLParser().parseQuery2(queryStr, baseURI);

            BigdataSailRepositoryConnection repoConn = null;
            BigdataSailConnection conn = null;
            boolean success = false;
            try {

                repoConn = getConnection();

                conn = repoConn.getSailConnection();

                boolean truthMaintenance = conn.getTruthMaintenance();

                if (truthMaintenance && suppressTruthMaintenance) {

                    conn.setTruthMaintenance(false);

                }

                {

                    if (log.isInfoEnabled())
                        log.info("update with query: " + queryStr);

                    final BigdataRDFContext context = BigdataServlet.getBigdataRDFContext(req.getServletContext());

                    /*
                     * Note: pipe is drained by this thread to consume the query
                     * results, which are the statements to be deleted.
                     */
                    final PipedOutputStream os = new PipedOutputStream();

                    // The read-only connection for the query.
                    BigdataSailRepositoryConnection roconn = null;
                    try {

                        final long readOnlyTimestamp = ITx.READ_COMMITTED;

                        roconn = getQueryConnection(namespace, readOnlyTimestamp);

                        // Use this format for the query results.
                        final RDFFormat deleteQueryFormat = RDFFormat.NTRIPLES;

                        final AbstractQueryTask queryTask = context.getQueryTask(roconn, namespace,
                                readOnlyTimestamp, queryStr, baseURI, astContainer, includeInferred, bindings,
                                deleteQueryFormat.getDefaultMIMEType(), req, resp, os);

                        switch (queryTask.queryType) {
                        case DESCRIBE:
                        case CONSTRUCT:
                            break;
                        default:
                            throw new MalformedQueryException("Must be DESCRIBE or CONSTRUCT query");
                        }

                        // Run DELETE
                        {

                            final RDFParserFactory factory = RDFParserRegistry.getInstance().get(deleteQueryFormat);

                            final RDFParser rdfParser = factory.getParser();

                            rdfParser.setValueFactory(conn.getTripleStore().getValueFactory());

                            rdfParser.setVerifyData(false);

                            rdfParser.setStopAtFirstError(true);

                            rdfParser.setDatatypeHandling(RDFParser.DatatypeHandling.IGNORE);

                            rdfParser.setRDFHandler(
                                    new RemoveStatementHandler(conn, nmodified, defaultContextDelete));

                            // Wrap as Future.
                            final FutureTask<Void> ft = new FutureTask<Void>(queryTask);

                            // Submit query for evaluation.
                            context.queryService.execute(ft);

                            // Reads on the statements produced by the query.
                            final InputStream is = newPipedInputStream(os);

                            // Run parser : visited statements will be deleted.
                            rdfParser.parse(is, baseURI);

                            // Await the Future (of the Query)
                            ft.get();

                        }

                        // Run INSERT
                        {

                            /*
                             * There is a request body, so let's try and parse
                             * it.
                             */

                            final RDFParser rdfParser = parserFactory.getParser();

                            rdfParser.setValueFactory(conn.getTripleStore().getValueFactory());

                            rdfParser.setVerifyData(true);

                            rdfParser.setStopAtFirstError(true);

                            rdfParser.setDatatypeHandling(RDFParser.DatatypeHandling.IGNORE);

                            rdfParser.setRDFHandler(new AddStatementHandler(conn, nmodified, defaultContextInsert));

                            /*
                             * Run the parser, which will cause statements to be
                             * inserted.
                             */
                            rdfParser.parse(req.getInputStream(), baseURI);

                        }

                    } finally {

                        if (roconn != null) {
                            // close the read-only connection for the query.
                            roconn.rollback();
                        }

                    }

                }

                if (truthMaintenance && suppressTruthMaintenance) {

                    conn.setTruthMaintenance(true);

                }

                conn.commit();

                success = true;

                final long elapsed = System.currentTimeMillis() - begin;

                reportModifiedCount(nmodified.get(), elapsed);

                return null;

            } finally {

                if (conn != null) {

                    if (!success)
                        conn.rollback();

                    conn.close();

                }

                if (repoConn != null) {

                    repoConn.close();

                }

            }

        }

    } // class UpdateWithQueryStreamingTask

    /**
    * This version runs the query against the unisolated connection, fully
    * buffers the statements to be removed, and then removes them. Since both
    * the read and the write are on the unisolated connection, it see all
    * mutations that have been applied since the last group commit and preserves
    * the serializability guarantee.
    * 
    * @author bryan
    * 
    */
    private static class UpdateWithQueryMaterializedTask extends AbstractRestApiTask<Void> {

        private final String queryStr;
        private final String baseURI;
        private final boolean suppressTruthMaintenance;
        private final RDFParserFactory parserFactory;
        private final Resource[] defaultContextDelete;
        private final Resource[] defaultContextInsert;
        private final Map<String, Value> bindings;

        /**
         * 
         * @param namespace
         *           The namespace of the target KB instance.
         * @param timestamp
         *           The timestamp used to obtain a mutable connection.
         * @param baseURI
         *           The base URI for the operation.
         * @param bindings 
         * @param defaultContextDelete
         *           When removing statements, the context(s) for triples without
         *           an explicit named graph when the KB instance is operating in
         *           a quads mode.
         * @param defaultContextInsert
         *           When inserting statements, the context(s) for triples without
         *           an explicit named graph when the KB instance is operating in
         *           a quads mode.
         */
        public UpdateWithQueryMaterializedTask(final HttpServletRequest req, final HttpServletResponse resp,
                final String namespace, final long timestamp, final String queryStr, //
                final String baseURI, final boolean suppressTruthMaintenance, //
                final Map<String, Value> bindings, final RDFParserFactory parserFactory,
                final Resource[] defaultContextDelete, //
                final Resource[] defaultContextInsert//
        ) {
            super(req, resp, namespace, timestamp);
            this.queryStr = queryStr;
            this.baseURI = baseURI;
            this.suppressTruthMaintenance = suppressTruthMaintenance;
            this.bindings = bindings;
            this.parserFactory = parserFactory;
            this.defaultContextDelete = defaultContextDelete;
            this.defaultContextInsert = defaultContextInsert;
        }

        @Override
        public boolean isReadOnly() {
            return false;
        }

        @Override
        public Void call() throws Exception {

            final long begin = System.currentTimeMillis();

            final AtomicLong nmodified = new AtomicLong(0L);

            /*
             * Parse the query before obtaining the connection object.
             * 
             * @see BLZG-2039 SPARQL QUERY and SPARQL UPDATE should be parsed
             * before obtaining the connection
             */

            // Setup the baseURI for this request. 
            final String baseURI = BigdataRDFContext.getBaseURI(req, resp);

            // Parse the query.
            final ASTContainer astContainer = new Bigdata2ASTSPARQLParser().parseQuery2(queryStr, baseURI);

            BigdataSailRepositoryConnection repoConn = null;
            BigdataSailConnection conn = null;
            boolean success = false;
            try {

                repoConn = getConnection();

                conn = repoConn.getSailConnection();

                boolean truthMaintenance = conn.getTruthMaintenance();

                if (truthMaintenance && suppressTruthMaintenance) {

                    conn.setTruthMaintenance(false);

                }

                BigdataSailRepositoryConnection roconn = null;
                try {

                    final long readOnlyTimestamp = ITx.READ_COMMITTED;

                    roconn = getQueryConnection(namespace, readOnlyTimestamp);

                    {

                        if (log.isInfoEnabled())
                            log.info("update with query: " + queryStr);

                        final BigdataRDFContext context = BigdataServlet
                                .getBigdataRDFContext(req.getServletContext());

                        /*
                         * Note: pipe is drained by this thread to consume the query
                         * results, which are the statements to be deleted.
                         */
                        final PipedOutputStream os = new PipedOutputStream();

                        // Use this format for the query results.
                        final RDFFormat deleteQueryFormat = RDFFormat.NTRIPLES;

                        final AbstractQueryTask queryTask = context.getQueryTask(roconn, namespace, ITx.UNISOLATED,
                                queryStr, baseURI, astContainer, includeInferred, bindings,
                                deleteQueryFormat.getDefaultMIMEType(), req, resp, os);

                        switch (queryTask.queryType) {
                        case DESCRIBE:
                        case CONSTRUCT:
                            break;
                        default:
                            throw new MalformedQueryException("Must be DESCRIBE or CONSTRUCT query");
                        }

                        // Run DELETE
                        {

                            final RDFParserFactory factory = RDFParserRegistry.getInstance().get(deleteQueryFormat);

                            final RDFParser rdfParser = factory.getParser();

                            rdfParser.setValueFactory(conn.getTripleStore().getValueFactory());

                            rdfParser.setVerifyData(false);

                            rdfParser.setStopAtFirstError(true);

                            rdfParser.setDatatypeHandling(RDFParser.DatatypeHandling.IGNORE);

                            //                  rdfParser.setRDFHandler(new BufferStatementHandler(conn
                            //                        .getSailConnection(), nmodified, defaultContextDelete));

                            final BufferStatementHandler buffer = new BufferStatementHandler(conn, nmodified,
                                    defaultContextDelete);

                            rdfParser.setRDFHandler(buffer);

                            // Wrap as Future.
                            final FutureTask<Void> ft = new FutureTask<Void>(queryTask);

                            // Submit query for evaluation.
                            context.queryService.execute(ft);

                            // Reads on the statements produced by the query.
                            final InputStream is = newPipedInputStream(os);

                            // Run parser : visited statements will be buffered.
                            rdfParser.parse(is, baseURI);

                            // Await the Future (of the Query)
                            ft.get();

                            // Delete the buffered statements.
                            buffer.removeAll();

                        }

                        // Run INSERT
                        {

                            /*
                             * There is a request body, so let's try and parse it.
                             */

                            final RDFParser rdfParser = parserFactory.getParser();

                            rdfParser.setValueFactory(conn.getTripleStore().getValueFactory());

                            rdfParser.setVerifyData(true);

                            rdfParser.setStopAtFirstError(true);

                            rdfParser.setDatatypeHandling(RDFParser.DatatypeHandling.IGNORE);

                            rdfParser.setRDFHandler(new AddStatementHandler(conn, nmodified, defaultContextInsert));

                            /*
                             * Run the parser, which will cause statements to be inserted.
                             */
                            rdfParser.parse(req.getInputStream(), baseURI);

                        }

                    }

                } finally {

                    if (roconn != null) {
                        // close the read-only connection for the query.
                        roconn.rollback();
                    }

                }

                if (truthMaintenance && suppressTruthMaintenance) {

                    conn.setTruthMaintenance(true);

                }

                conn.commit();

                success = true;

                final long elapsed = System.currentTimeMillis() - begin;

                reportModifiedCount(nmodified.get(), elapsed);

                return null;

            } finally {

                if (conn != null) {

                    if (!success)
                        conn.rollback();

                    conn.close();

                }

                if (repoConn != null) {

                    repoConn.close();

                }

            }

        }

    } // class UpdateWithQueryMaterializedTask

    @Override
    protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {

        if (!isWritable(getServletContext(), req, resp)) {
            // Service must be writable.
            return;
        }

        if (ServletFileUpload.isMultipartContent(req)) {

            doUpdateWithBody(req, resp);

        } else {

            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);

        }

    }

    /**
     * UPDATE request with a request body containing the statements to be
     * removed and added as a multi-part mime request.
     */
    private void doUpdateWithBody(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {

        final DiskFileItemFactory factory = new DiskFileItemFactory();

        final ServletFileUpload upload = new ServletFileUpload(factory);

        FileItem add = null, remove = null;

        try {

            final List<FileItem> items = upload.parseRequest(req);

            for (FileItem item : items) {

                if (item.getFieldName().equals("add")) {

                    if (!validateItem(resp, add = item)) {
                        return;
                    }

                } else if (item.getFieldName().equals("remove")) {

                    if (!validateItem(resp, remove = item)) {
                        return;
                    }

                }

            }

        } catch (FileUploadException ex) {

            throw new IOException(ex);

        }

        final String baseURI = req.getRequestURL().toString();

        final boolean suppressTruthMaintenance = getBooleanValue(req, QueryServlet.ATTR_TRUTH_MAINTENANCE, false);

        /*
         * Allow the caller to specify the default context for insert.
         */
        final Resource[] defaultContextInsert;
        {
            final String[] s = req.getParameterValues("context-uri-insert");
            if (s != null && s.length > 0) {
                try {
                    defaultContextInsert = toURIs(s);
                } catch (IllegalArgumentException ex) {
                    buildAndCommitResponse(resp, HTTP_INTERNALERROR, MIME_TEXT_PLAIN, ex.getLocalizedMessage());
                    return;
                }
            } else {
                defaultContextInsert = null;
            }
        }

        /*
         * Allow the caller to specify the default context for delete.
         */
        final Resource[] defaultContextDelete;
        {
            final String[] s = req.getParameterValues("context-uri-delete");
            if (s != null && s.length > 0) {
                try {
                    defaultContextDelete = toURIs(s);
                } catch (IllegalArgumentException ex) {
                    buildAndCommitResponse(resp, HTTP_INTERNALERROR, MIME_TEXT_PLAIN, ex.getLocalizedMessage());
                    return;
                }
            } else {
                defaultContextDelete = null;
            }
        }

        final String namespace = getNamespace(req);

        try {

            submitApiTask(new UpdateWithBodyTask(req, resp, namespace, ITx.UNISOLATED, //
                    baseURI, //
                    suppressTruthMaintenance, //
                    remove, //
                    defaultContextDelete, //
                    add, //
                    defaultContextInsert//
            )).get();

        } catch (Throwable t) {

            launderThrowable(t, resp,
                    "UPDATE-WITH-BODY: baseURI=" + baseURI + (add == null ? null
                            : ", add=" + add
                                    + (defaultContextInsert == null ? ""
                                            : ",context-uri-insert=" + Arrays.toString(defaultContextInsert)))
                            + (remove == null ? null
                                    : ", remove=" + remove + (defaultContextDelete == null ? ""
                                            : ",context-uri-delete=" + Arrays.toString(defaultContextDelete))));

        }

    }

    private static class UpdateWithBodyTask extends AbstractRestApiTask<Void> {

        private final String baseURI;
        private final boolean suppressTruthMaintenance;
        private final FileItem remove;
        private final FileItem add;
        private final Resource[] defaultContextDelete;
        private final Resource[] defaultContextInsert;

        /**
         * 
         * @param namespace
         *            The namespace of the target KB instance.
         * @param timestamp
         *            The timestamp used to obtain a mutable connection.
         * @param baseURI
         *            The base URI for the operation.
         * @param defaultContextDelete
         *            When removing statements, the context(s) for triples
         *            without an explicit named graph when the KB instance is
         *            operating in a quads mode.
         * @param defaultContextInsert
         *            When inserting statements, the context(s) for triples
         *            without an explicit named graph when the KB instance is
         *            operating in a quads mode.
         */
        public UpdateWithBodyTask(final HttpServletRequest req, final HttpServletResponse resp,
                final String namespace, final long timestamp, final String baseURI,
                final boolean suppressTruthMaintenance, //
                final FileItem remove, final Resource[] defaultContextDelete, //
                final FileItem add, final Resource[] defaultContextInsert//
        ) {
            super(req, resp, namespace, timestamp);
            this.baseURI = baseURI;
            this.suppressTruthMaintenance = suppressTruthMaintenance;
            this.remove = remove;
            this.defaultContextDelete = defaultContextDelete;
            this.add = add;
            this.defaultContextInsert = defaultContextInsert;
        }

        @Override
        public boolean isReadOnly() {
            return false;
        }

        @Override
        public Void call() throws Exception {

            final long begin = System.currentTimeMillis();

            final AtomicLong nmodified = new AtomicLong(0L);

            BigdataSailRepositoryConnection repoConn = null;
            BigdataSailConnection conn = null;
            boolean success = false;
            try {

                repoConn = getConnection();

                conn = repoConn.getSailConnection();

                boolean truthMaintenance = conn.getTruthMaintenance();

                if (truthMaintenance && suppressTruthMaintenance) {

                    conn.setTruthMaintenance(false);

                }

                if (remove != null) {

                    final String contentType = remove.getContentType();

                    final InputStream is = remove.getInputStream();

                    final RemoveStatementHandler handler = new RemoveStatementHandler(conn, nmodified,
                            defaultContextDelete);

                    processData(conn, contentType, is, handler, baseURI);

                }

                if (add != null) {

                    final String contentType = add.getContentType();

                    final InputStream is = add.getInputStream();

                    final RDFHandler handler = new AddStatementHandler(conn, nmodified, defaultContextInsert);

                    processData(conn, contentType, is, handler, baseURI);

                }

                if (truthMaintenance && suppressTruthMaintenance) {

                    conn.setTruthMaintenance(true);

                }

                conn.commit();

                success = true;

                final long elapsed = System.currentTimeMillis() - begin;

                reportModifiedCount(nmodified.get(), elapsed);

                return null;

            } finally {

                if (conn != null) {

                    if (!success)
                        conn.rollback();

                    conn.close();

                }

                if (repoConn != null) {

                    repoConn.close();

                }

            }

        }

        private void processData(final BigdataSailConnection conn, final String contentType, final InputStream is,
                final RDFHandler handler, final String baseURI) throws Exception {

            /**
             * Note: The request was already validated.
             * 
             * <a href="https://sourceforge.net/apps/trac/bigdata/ticket/620">
             * UpdateServlet fails to parse MIMEType when doing conneg. </a>
             */

            final RDFFormat format = RDFFormat.forMIMEType(new MiniMime(contentType).getMimeType());

            final RDFParserFactory rdfParserFactory = RDFParserRegistry.getInstance().get(format);

            final RDFParser rdfParser = rdfParserFactory.getParser();

            rdfParser.setValueFactory(conn.getTripleStore().getValueFactory());

            rdfParser.setVerifyData(true);

            rdfParser.setStopAtFirstError(true);

            rdfParser.setDatatypeHandling(RDFParser.DatatypeHandling.IGNORE);

            rdfParser.setRDFHandler(handler);

            /*
             * Run the parser, which will cause statements to be deleted.
             */
            rdfParser.parse(is, baseURI);

        }

    } // class UpdateWithBodyTask

    private boolean validateItem(final HttpServletResponse resp, final FileItem item) throws IOException {

        final String contentType = item.getContentType();

        if (contentType == null) {

            buildAndCommitResponse(resp, HTTP_BADREQUEST, MIME_TEXT_PLAIN, "Content-Type not specified");

            return false;

        }

        final RDFFormat format = RDFFormat.forMIMEType(new MiniMime(contentType).getMimeType());

        if (format == null) {

            buildAndCommitResponse(resp, HTTP_BADREQUEST, MIME_TEXT_PLAIN,
                    "Content-Type not recognized as RDF: " + contentType);

            return false;

        }

        final RDFParserFactory rdfParserFactory = RDFParserRegistry.getInstance().get(format);

        if (rdfParserFactory == null) {

            buildAndCommitResponse(resp, HTTP_INTERNALERROR, MIME_TEXT_PLAIN,
                    "Parser factory not found: Content-Type=" + contentType + ", format=" + format);

            return false;

        }

        if (item.getInputStream() == null) {

            buildAndCommitResponse(resp, HTTP_BADREQUEST, MIME_TEXT_PLAIN, "No content");

            return false;

        }

        return true;

    }

}