com.bigdata.rdf.sail.webapp.client.RemoteRepository.java Source code

Java tutorial

Introduction

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

Source

/**
Copyright (C) SYSTAP, LLC DBA Blazegraph 2014.  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.client;

import info.aduna.io.IOUtil;

import java.io.Closeable;
import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;

import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.mime.FormBodyPart;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.ByteArrayBody;
import org.apache.log4j.Logger;
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.query.GraphQueryResult;
import org.openrdf.query.TupleQueryResult;
import org.openrdf.rio.RDFFormat;

import com.bigdata.rdf.sail.remote.BigdataSailRemoteRepository;

/**
 * Java API to the Nano Sparql Server.
 * <p>
 * Note: The {@link RemoteRepository} object SHOULD be reused for multiple
 * operations against the same end point.
 * 
 * @see <a href=
 *      "https://sourceforge.net/apps/mediawiki/bigdata/index.php?title=NanoSparqlServer"
 *      > NanoSparqlServer REST API </a>
 * 
 * @see <a href="https://sourceforge.net/apps/trac/bigdata/ticket/628" > Create
 *      a bigdata-client jar for the NSS REST API </a>
 */
public class RemoteRepository extends RemoteRepositoryBase {

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

    /**
     * The {@link RemoteRepositoryManager} object use to manage all access to the
     * service backing the {@link #sparqlEndpointURL}.
     */
    private final RemoteRepositoryManager mgr;

    /**
     * The service end point for the default data set.
     */
    private final String sparqlEndpointURL;

    /**
    * When non-<code>null</code> the operations against the
    * {@link #sparqlEndpointURL} will be isolated by the transaction.
    */
    private final IRemoteTx tx;

    /**
     * Return the SPARQL end point.
     */
    public String getSparqlEndPoint() {

        return sparqlEndpointURL;

    }

    @Override
    public String toString() {

        return super.toString() + "{sparqlEndpoint=" + sparqlEndpointURL + ", mgr=" + getRemoteRepositoryManager()
                + (tx == null ? "" : ", tx=" + tx) + "}";

    }

    /**
     * The {@link RemoteRepositoryManager} object use to manage all access to the
     * service backing the {@link #sparqlEndpointURL}.
     */
    public RemoteRepositoryManager getRemoteRepositoryManager() {

        return mgr;

    }

    /**
     * Create a connection to a remote repository. This can be used with any
     * SPARQL end point as long as you restrict yourself to SPARQL QUERY or
     * SPARQL UPDATE.  The other methods can only be used with a blazegraph
     * backend.
     * 
     * @param mgr
     * @param sparqlEndpointURL
     *            The SPARQL end point.
     * 
     * @see RemoteRepositoryManager
     * @see HttpClientConfigurator
     * @see <a href="http://wiki.blazegraph.com/wiki/index.php/HALoadBalancer">
     *      HALoadBalancer </a>
     */
    RemoteRepository(final RemoteRepositoryManager mgr, final String sparqlEndpointURL, final IRemoteTx tx) {

        if (mgr == null)
            throw new IllegalArgumentException();

        if (sparqlEndpointURL == null)
            throw new IllegalArgumentException();

        this.mgr = mgr;

        this.sparqlEndpointURL = sparqlEndpointURL;

        this.tx = tx;

    }

    /**
     * Flyweight method returns a wrapper for the sparql end point associated
     * with this instance.
     */
    public BigdataSailRemoteRepository getBigdataSailRemoteRepository() {

        return new BigdataSailRemoteRepository(this);

    }

    /**
     * Post a GraphML file to the blueprints layer of the remote bigdata instance.
     */
    public long postGraphML(final String path) throws Exception {

        // TODO Allow client to specify UUID for postGraphML. See #1254.
        final UUID uuid = UUID.randomUUID();

        final ConnectOptions opts = mgr.newConnectOptions(sparqlEndpointURL, uuid, tx);

        opts.addRequestParam("blueprints");

        JettyResponseListener response = null;
        try {

            final File file = new File(path);

            if (!file.exists()) {
                throw new RuntimeException("cannot locate file: " + file.getAbsolutePath());
            }

            final byte[] data = IOUtil.readBytes(file);

            final ByteArrayEntity entity = new ByteArrayEntity(data);

            entity.setContentType(ConnectOptions.MIME_GRAPH_ML);

            opts.entity = entity;

            opts.setAcceptHeader(ConnectOptions.MIME_APPLICATION_XML);

            checkResponseCode(response = doConnect(opts));

            final MutationResult result = mutationResults(response);

            return result.mutationCount;

        } finally {

            if (response != null)
                response.abort();

        }

    }

    /**
     * Return the SPARQL 1.1 Service Description for the end point.
     */
    public GraphQueryResult getServiceDescription() throws Exception {

        return getServiceDescription(UUID.randomUUID());

    }

    public GraphQueryResult getServiceDescription(final UUID uuid) throws Exception {

        // TODO Unit test when isolated by a transaction. The server is already
        // creating a tx for this so it might hit a fence post.

        final ConnectOptions opts = mgr.newConnectOptions(sparqlEndpointURL, uuid, tx);

        opts.method = "GET";

        opts.setAcceptHeader(ConnectOptions.DEFAULT_GRAPH_ACCEPT_HEADER);

        return mgr.graphResults(opts, uuid, null/* preparedListener */);

    }

    /**
     * Prepare a tuple (select) query.
     * 
     * @param query
     *            the query string
     * 
     * @return The {@link IPreparedTupleQuery}.
     */
    public IPreparedTupleQuery prepareTupleQuery(final String query) throws Exception {

        return prepareTupleQuery(query, UUID.randomUUID());

    }

    /**
     * Prepare a tuple (select) query.
     * 
     * @param query
     *            the query string
     * 
     * @param uuid
     *            The {@link UUID} used to identify this query.
     * 
     * @return The {@link IPreparedTupleQuery}.
     */
    public IPreparedTupleQuery prepareTupleQuery(final String query, final UUID uuid) throws Exception {

        return new TupleQuery(mgr.newQueryConnectOptions(sparqlEndpointURL, uuid, tx), uuid, query);

    }

    /**
     * Prepare a graph query.
     * 
     * @param query
     *            the query string
     * 
     * @return The {@link IPreparedGraphQuery}
     */
    public IPreparedGraphQuery prepareGraphQuery(final String query) throws Exception {

        return prepareGraphQuery(query, UUID.randomUUID());

    }

    /**
     * Prepare a graph query.
     * 
     * @param query
     *            the query string
     * @param uuid
     *            The {@link UUID} used to identify this query.
     * 
     * @return The {@link IPreparedGraphQuery}
     */
    public IPreparedGraphQuery prepareGraphQuery(final String query, final UUID uuid) throws Exception {

        return new GraphQuery(mgr.newQueryConnectOptions(sparqlEndpointURL, uuid, tx), uuid, query);

    }

    /**
     * Prepare a boolean (ask) query.
     * 
     * @param query
     *            the query string
     * 
     * @return the {@link IPreparedBooleanQuery}
     */
    public IPreparedBooleanQuery prepareBooleanQuery(final String query) throws Exception {

        return prepareBooleanQuery(query, UUID.randomUUID());

    }

    /**
     * Prepare a boolean (ask) query.
     * 
     * @param query
     *            the query string
     * 
     * @param uuid
     *            The {@link UUID} used to identify this query.
     *            
     * @return the {@link IPreparedBooleanQuery}
     */
    public IPreparedBooleanQuery prepareBooleanQuery(final String query, final UUID uuid) throws Exception {

        return new BooleanQuery(mgr.newQueryConnectOptions(sparqlEndpointURL, uuid, tx), uuid, query);

    }

    /**
     * Prepare a SPARQL UPDATE request.
     * 
     * @param updateStr
     *            The SPARQL UPDATE request.
     * 
     * @return The {@link SparqlUpdate} operation.
     * 
     * @throws Exception
     */
    public IPreparedSparqlUpdate prepareUpdate(final String updateStr) throws Exception {

        return prepareUpdate(updateStr, UUID.randomUUID());

    }

    /**
     * Prepare a SPARQL UPDATE request.
     * 
     * @param updateStr
     *            The SPARQL UPDATE request.
     * @param uuid
     *            The {@link UUID} used to identify this query.
     * 
     * @return The {@link SparqlUpdate} operation.
     * 
     * @throws Exception
     */
    public IPreparedSparqlUpdate prepareUpdate(final String updateStr, final UUID uuid) throws Exception {

        return new SparqlUpdate(mgr.newUpdateConnectOptions(sparqlEndpointURL, uuid, tx), uuid, updateStr);

    }

    /**
     * Return all matching statements.
     * 
     * @param subj
     * @param pred
     * @param obj
     * @param includeInferred
     * @param contexts
     * @return
     * @throws Exception
     */
    public GraphQueryResult getStatements(final Resource subj, final URI pred, final Value obj,
            final boolean includeInferred, final Resource... contexts) throws Exception {

        if (contexts == null) {
            // Note: May not be a null Resource[] reference.
            // MAY be Resource[null], which is the openrdf nullGraph.
            // See #1177
            throw new IllegalArgumentException();
        }

        final UUID uuid = UUID.randomUUID();
        final ConnectOptions opts = mgr.newQueryConnectOptions(sparqlEndpointURL, uuid, tx);

        opts.addRequestParam("GETSTMTS");
        opts.addRequestParam(RemoteRepositoryDecls.INCLUDE_INFERRED, Boolean.toString(includeInferred));
        if (subj != null) {
            opts.addRequestParam("s", EncodeDecodeValue.encodeValue(subj));
        }
        if (pred != null) {
            opts.addRequestParam("p", EncodeDecodeValue.encodeValue(pred));
        }
        if (obj != null) {
            opts.addRequestParam("o", EncodeDecodeValue.encodeValue(obj));
        }
        opts.addRequestParam("c", EncodeDecodeValue.encodeContexts(contexts));

        opts.setAcceptHeader(ConnectOptions.DEFAULT_GRAPH_ACCEPT_HEADER);

        JettyResponseListener resp = null;
        try {

            checkResponseCode(resp = doConnect(opts));

            GraphQueryResult result = mgr.graphResults(opts, null, null);

            return result;

        } finally {

            if (resp != null)
                resp.abort();

        }

    }

    /**
     * Method to line up with the Sesame interface.
     * 
     * @param s
     *           The subject (optional).
     * @param p
     *           The predicate (optional).
     * @param o
     *           The value (optional).
     * @param includeInferred
     *           when <code>true</code> inferred statements will also be
     *           considered.
     * @param c
     *           The contexts (optional, BUT may not be a null Resource[]).
     *           
     * @return <code>true</code> iff a statement exists that matches the request.
     * 
     * @throws Exception
     * 
     * @see <a href="http://trac.bigdata.com/ticket/1109" >hasStatements can
     *      overestimate and ignores includeInferred (REST API) </a>
     * @see <a href="http://trac.bigdata.com/ticket/1177"> Resource... contexts
     *      not encoded/decoded according to openrdf semantics (REST API) </a>
     */
    public boolean hasStatement(final Resource s, final URI p, final Value o, final boolean includeInferred,
            final Resource... c) throws Exception {
        if (c == null) {
            // Note: May not be a null Resource[] reference.
            // MAY be Resource[null], which is the openrdf nullGraph.
            // See #1177
            throw new IllegalArgumentException();
        }
        final UUID uuid = UUID.randomUUID();
        final ConnectOptions opts = mgr.newQueryConnectOptions(sparqlEndpointURL, uuid, tx);

        opts.addRequestParam("HASSTMT");
        opts.addRequestParam(RemoteRepositoryDecls.INCLUDE_INFERRED, Boolean.toString(includeInferred));
        if (s != null) {
            opts.addRequestParam("s", EncodeDecodeValue.encodeValue(s));
        }
        if (p != null) {
            opts.addRequestParam("p", EncodeDecodeValue.encodeValue(p));
        }
        if (o != null) {
            opts.addRequestParam("o", EncodeDecodeValue.encodeValue(o));
        }
        opts.addRequestParam("c", EncodeDecodeValue.encodeContexts(c));

        JettyResponseListener resp = null;
        try {

            opts.setAcceptHeader(ConnectOptions.MIME_APPLICATION_XML);

            checkResponseCode(resp = doConnect(opts));

            final BooleanResult result = RemoteRepositoryManager.booleanResults(resp);

            return result.result;

        } finally {

            if (resp != null)
                resp.abort();

        }

    }

    private String asConstOrVar(final AST2SPARQLUtil util, final String var, final Value val) {

        if (val == null)
            return var;

        return util.toExternal(val);

    }

    /**
     * Cancel a query running remotely on the server.
     * 
     * @param queryID
     *             the UUID of the query to cancel
     */
    public void cancel(final UUID queryId) throws Exception {

        mgr.cancel(queryId);

    }

    /**
    * Perform a fast range count on the statement indices for a given triple
    * (quad) pattern.
    * 
    * @param s
    *           the subject (can be null)
    * @param p
    *           the predicate (can be null)
    * @param o
    *           the object (can be null)
    * @param c
    *           the context (can be null)
    *           
    * @return the range count
    * 
    * @see <a href="http://trac.bigdata.com/ticket/1127"> Add REST API method
    *      for exact range counts </a>
    */
    public long rangeCount(final Resource s, final URI p, final Value o, final Resource... c) throws Exception {

        return rangeCount(false/* exact */, s, p, o, c);

    }

    /**
     * Perform a range count on the statement indices for a given triple (quad)
     * pattern.
     * <p>
     * Note: fast range counts are *fast*. They require two key probes into the
     * indices. Exact range counts are only fast when the indices do not support
     * isolation or fused views. Isolation is used if the namespace supports full
     * read/write transactions. Fused views are used in scale-out to model shards
     * and are also used in full read/write transaction support.
     * 
     * @param exact
     *           if <code>true</code> then an exact range count is requested,
     *           otherwise a fast range count is requested.
     * @param s
     *           the subject (can be null)
     * @param p
     *           the predicate (can be null)
     * @param o
     *           the object (can be null)
     * @param c
     *           the context (can be null)
     * 
     * @return the range count
     * 
     * @see <a href="http://trac.bigdata.com/ticket/1127"> Add REST API method
     *      for exact range counts </a>
     * @see <a href="http://trac.bigdata.com/ticket/1177"> Resource... contexts
     *      not encoded/decoded according to openrdf semantics (REST API) </a>
     */
    public long rangeCount(final boolean exact, final Resource s, final URI p, final Value o, final Resource... c)
            throws Exception {

        if (c == null) {
            // Note: May not be a null Resource[] reference.
            // MAY be Resource[null], which is the openrdf nullGraph.
            // See #1177
            throw new IllegalArgumentException();
        }

        // TODO Allow client to specify UUID for ESTCARD. See #1254.
        final UUID uuid = UUID.randomUUID();
        final ConnectOptions opts = mgr.newQueryConnectOptions(sparqlEndpointURL, uuid, tx);

        opts.addRequestParam("ESTCARD");
        if (exact) {
            opts.addRequestParam("exact", "true");
        }
        if (s != null) {
            opts.addRequestParam("s", EncodeDecodeValue.encodeValue(s));
        }
        if (p != null) {
            opts.addRequestParam("p", EncodeDecodeValue.encodeValue(p));
        }
        if (o != null) {
            opts.addRequestParam("o", EncodeDecodeValue.encodeValue(o));
        }
        opts.addRequestParam("c", EncodeDecodeValue.encodeContexts(c));

        JettyResponseListener resp = null;
        try {

            opts.setAcceptHeader(ConnectOptions.MIME_APPLICATION_XML);

            checkResponseCode(resp = doConnect(opts));

            final RangeCountResult result = rangeCountResults(resp);

            return result.rangeCount;

        } finally {

            if (resp != null)
                resp.abort();

        }

    }

    /**
     * Perform a fast range count on the statement indices. This reports an
     * estimate of the number of statements in the namespace. That estimate is
     * exact unless the namespace is provisioned for full read/write transactions
     * or the endpoint is scale-out.
     * 
     * @return the range count (#of statements in the database).
     */
    public long size() throws Exception {

        return rangeCount(/* s */null, /* p */null, /* o */null);

    }

    /**
    * Return a list of contexts in use in a remote quads database.
    * 
    * FIXME This should be a streaming response for scalability. That will
    * require us to change the return type for the method. E.g., to something
    * that implements {@link Closeable}. Callers will then have to invoke
    * {@link Closeable#close()} to avoid leaking resources. (This change could
    * be made when making the CONTEXTS an operation that can be given a UUID
    * for cancellation by the client.)
    */
    public Collection<Resource> getContexts() throws Exception {

        // TODO Allow client to specify UUID for CONTEXTS. See #1254.
        final UUID uuid = UUID.randomUUID();

        final ConnectOptions opts = mgr.newQueryConnectOptions(sparqlEndpointURL, uuid, tx);

        opts.addRequestParam("CONTEXTS");

        JettyResponseListener resp = null;
        try {

            opts.setAcceptHeader(ConnectOptions.MIME_APPLICATION_XML);

            checkResponseCode(resp = doConnect(opts));

            final ContextsResult result = contextsResults(resp);

            return result.contexts;

        } finally {

            if (resp != null)
                resp.abort();

        }

    }

    /**
     * Adds RDF data to the remote repository.
     * 
     * @param add
     *            The RDF data to be added.
     * 
     * @return The mutation count.
     */
    public long add(final AddOp add) throws Exception {

        return add(add, UUID.randomUUID()/*queryId*/);

    }

    /**
     * Adds RDF data to the remote repository.
     * 
     * @param add
     *            The RDF data to be added.
     * @param uuid
     *            The {@link UUID} used to identify this query.
     *            
     * @return The mutation count.
     * 
     * @see See #1254 / BLZG-1259
     */
    public long add(final AddOp add, final UUID uuid) throws Exception {

        if (add == null)
            throw new IllegalArgumentException();

        final ConnectOptions opts = mgr.newUpdateConnectOptions(sparqlEndpointURL, uuid, tx);

        add.prepareForWire();

        if (add.format != null) {

            final ByteArrayEntity entity = new ByteArrayEntity(add.data);

            entity.setContentType(add.format.getDefaultMIMEType());

            opts.entity = entity;

        }

        if (add.uris != null) {
            // set the resource(s) to load
            opts.addRequestParam("uri", add.uris.toArray(new String[0]));
        }

        if (add.context != null && add.context.length > 0) {
            // set the default context.
            opts.addRequestParam("context-uri", toStrings(add.context));
        }

        opts.setAcceptHeader(ConnectOptions.MIME_APPLICATION_XML);

        JettyResponseListener response = null;
        boolean ok = false;
        try {

            checkResponseCode(response = doConnect(opts));

            final MutationResult result = mutationResults(response);

            ok = true;

            return result.mutationCount;

        } finally {

            if (response != null) {
                // Abort the http response handling.
                response.abort();
                if (!ok) {
                    try {
                        /*
                         * POST back to the server to cancel the request in case
                         * it is still running on the server.
                         */
                        cancel(uuid);
                    } catch (Exception ex) {
                        log.warn(ex);
                    }
                }
            }
        }

    }

    /**
    * Removes RDF data from the remote repository.
    * 
    * @param remove
    *           The RDF data to be removed.
    * 
    * @return The mutation count.
    * 
    * @see <a href="http://trac.bigdata.com/ticket/1177"> Resource... contexts
    *      not encoded/decoded according to openrdf semantics (REST API) </a>
    */
    public long remove(final RemoveOp remove) throws Exception {

        return remove(remove, UUID.randomUUID());

    }

    /**
     * Removes RDF data from the remote repository.
     * 
     * @param remove
     *            The RDF data to be removed.
     * 
     * @return The mutation count.
     * 
     * @see <a href="http://trac.bigdata.com/ticket/1177"> Resource... contexts
     *      not encoded/decoded according to openrdf semantics (REST API) </a>
     * @see #1254 / BLZG-1259
     */
    public long remove(final RemoveOp remove, final UUID uuid) throws Exception {

        if (remove == null)
            throw new IllegalArgumentException();

        final ConnectOptions opts = mgr.newUpdateConnectOptions(sparqlEndpointURL, uuid, tx);

        remove.prepareForWire();

        if (remove.format != null) {

            opts.method = "POST";
            opts.addRequestParam("delete");

            final ByteArrayEntity entity = new ByteArrayEntity(remove.data);

            entity.setContentType(remove.format.getDefaultMIMEType());

            opts.entity = entity;

            if (remove.context != null && remove.context.length > 0) {
                // set the default context.
                opts.addRequestParam("context-uri", toStrings(remove.context));
            }

        } else {

            opts.method = "DELETE";

            if (remove.query != null) {
                opts.addRequestParam("query", remove.query);
            }

            if (remove.s != null) {
                opts.addRequestParam("s", EncodeDecodeValue.encodeValue(remove.s));
            }

            if (remove.p != null) {
                opts.addRequestParam("p", EncodeDecodeValue.encodeValue(remove.p));
            }

            if (remove.o != null) {
                opts.addRequestParam("o", EncodeDecodeValue.encodeValue(remove.o));
            }

            if (remove.c != null) {
                /*
                 * Note: Due to the way in which the RemoveOp declares [c] even when
                 * it is not a "delete-by-accesspath" request, we have to check for
                 * [c!=null] here.
                 * 
                 * TODO This could be fixed if we had a factory for RemoveOp such
                 * that the concrete instance only declared [c] when it was a
                 * delete-by-access-path request. See #1177
                 */
                opts.addRequestParam("c", EncodeDecodeValue.encodeContexts(remove.c));
            }

        }

        opts.setAcceptHeader(ConnectOptions.MIME_APPLICATION_XML);

        JettyResponseListener response = null;
        boolean ok = false;
        try {

            checkResponseCode(response = doConnect(opts));

            final MutationResult result = mutationResults(response);

            ok = true;

            return result.mutationCount;

        } finally {

            if (response != null) {
                // Abort the http response handling.
                response.abort();
                if (!ok) {
                    try {
                        /*
                         * POST back to the server to cancel the request in case
                         * it is still running on the server.
                         */
                        cancel(uuid);
                    } catch (Exception ex) {
                        log.warn(ex);
                    }
                }
            }

        }

    }

    /**
     * Perform an ACID update (delete+insert) per the semantics of <a href=
     * "https://sourceforge.net/apps/mediawiki/bigdata/index.php?title=NanoSparqlServer#UPDATE_.28DELETE_.2B_INSERT.29"
     * > the NanoSparqlServer. </a>
     * <p>
     * Currently, the only combination supported is delete by query with add by
     * post (Iterable<Statement> and File). You can embed statements you want to
     * delete inside a construct query without a where clause.
     * 
     * @param remove
     *            The RDF data to be removed.
     * @param add
     *            The RDF data to be added.
     *            
     * @return The mutation count.
     */
    public long update(final RemoveOp remove, final AddOp add) throws Exception {

        return update(remove, add, UUID.randomUUID());

    }

    /**
     * Perform an ACID update
     * <p>
     * There are two different patterns which are supported:
     * <dl>
     * <dt>UPDATE (DELETE statements selected by a QUERY plus INSERT statements
     * from Request Body using PUT)</dt>
     * <dd>Where query is a CONSTRUCT or DESCRIBE query. <br>
     * Note: The QUERY + DELETE operation is ACID. <br>
     * Note: You MAY specify a CONSTRUCT query with an empty WHERE clause in
     * order to specify a set of statements to be removed without reference to
     * statements already existing in the database.</dd>
     * <dt>UPDATE (POST with Multi-Part Request Body)</dt>
     * <dd>You can specify two sets of serialized statements - one to be removed
     * and one to be added..</dd>
     * </dl>
     * 
     * @param remove
     *            The RDF data to be removed (either a collection of statements
     *            or a CONSTRUCT or DESCRIBE QUERY identifying the data to be
     *            deleted).
     * @param add
     *            The RDF data to be added (must be a collection of statements).
     * 
     * @return The mutation count.
     * @see http
     *      ://wiki.blazegraph.com/wiki/index.php/NanoSparqlServer#UPDATE_.28D
     *      ELETE_.2B_INSERT.29
     */
    public long update(final RemoveOp remove, final AddOp add, final UUID uuid) throws Exception {

        if (remove == null)
            throw new IllegalArgumentException();

        if (add == null)
            throw new IllegalArgumentException();

        remove.prepareForWire();

        add.prepareForWire();

        final ConnectOptions opts = mgr.newUpdateConnectOptions(sparqlEndpointURL, uuid, tx);

        if (remove.format != null) {

            // Code path when caller specifies data to be removed.
            opts.method = "POST";
            opts.addRequestParam("update");

            // Note: Multi-part MIME request entity.
            final MultipartEntity entity = new MultipartEntity();

            // The data to be removed.
            entity.addPart(new FormBodyPart("remove",
                    new ByteArrayBody(remove.data, remove.format.getDefaultMIMEType(), "remove")));

            // The data to be added.
            entity.addPart(
                    new FormBodyPart("add", new ByteArrayBody(add.data, add.format.getDefaultMIMEType(), "add")));

            // The multi-part request entity.
            opts.entity = entity;

        } else {

            // Code path when caller specifies CONSTRUCT or DESCRIBE query
            // identifying the data to be removed.
            opts.method = "PUT";

            // QUERY specifying the data to be removed.
            opts.addRequestParam("query", remove.query);

            // The data to be added.
            final ByteArrayEntity entity = new ByteArrayEntity(add.data);
            entity.setContentType(add.format.getDefaultMIMEType());
            opts.entity = entity;

        }

        if (add.context != null) {
            // set the default context for insert.
            opts.addRequestParam("context-uri-insert", toStrings(add.context));
        }

        if (remove.context != null) {
            // set the default context for delete.
            opts.addRequestParam("context-uri-delete", toStrings(remove.context));
        }

        opts.setAcceptHeader(ConnectOptions.MIME_APPLICATION_XML);

        JettyResponseListener response = null;
        boolean ok = false;
        try {

            checkResponseCode(response = doConnect(opts));

            final MutationResult result = mutationResults(response);

            ok = true;

            return result.mutationCount;

        } finally {

            if (response != null) {
                // Abort the http response handling.
                response.abort();
                if (!ok) {
                    try {
                        /*
                         * POST back to the server to cancel the request in case
                         * it is still running on the server.
                         */
                        cancel(uuid);
                    } catch (Exception ex) {
                        log.warn(ex);
                    }
                }
            }

        }

    }

    /**
     * A prepared query will hold metadata for a particular query instance.
     * <p>
     * Right now, the only metadata is the query ID.
     */
    protected abstract class QueryOrUpdate implements IPreparedOperation, IPreparedQuery {

        protected final ConnectOptions opts;

        private final UUID uuid;

        protected final String query;

        private final boolean update;

        public QueryOrUpdate(final ConnectOptions opts, final UUID id, final String query) {

            this(opts, id, query, false/* update */);

        }

        /**
         * 
         * @param id
         *            The query id.
         * @param query
         *            The SPARQL query or update string.
         * @param update
         *            <code>true</code> iff this is a SPARQL update.
         */
        public QueryOrUpdate(final ConnectOptions opts, final UUID uuid, final String query, final boolean update) {

            if (opts == null)
                throw new IllegalArgumentException();

            if (query == null)
                throw new IllegalArgumentException();

            if (uuid == null)
                throw new IllegalArgumentException();

            this.opts = opts;
            this.uuid = uuid;
            this.query = query;
            this.update = update;

        }

        @Override
        final public UUID getQueryId() {

            return uuid;

        }

        @Override
        public final boolean isUpdate() {

            return update;

        }

        public void addRequestParam(String name, String... val) {
            opts.addRequestParam(name, val);
        }

        /**
         * Setup the connection options.
         */
        protected void setupConnectOptions() {

            opts.method = mgr.getQueryMethod();

            if (update) {

                opts.addRequestParam("update", query);

            } else {

                opts.addRequestParam("query", query);

            }

            final UUID queryId = getQueryId();

            if (queryId != null)
                opts.addRequestParam(QUERYID, queryId.toString());

        }

        @Override
        public void setAcceptHeader(final String value) {

            opts.setAcceptHeader(value);

        }

        @Override
        public void setHeader(final String name, final String value) {

            opts.setHeader(name, value);

        }

        @Override
        public void setMaxQueryMillis(final long timeout) {

            opts.setHeader(HTTP_HEADER_BIGDATA_MAX_QUERY_MILLIS, Long.toString(timeout));

        }

        /**
         * {@inheritDoc}
         * <p>
         * Note: <code>-1L</code> is returned if the http header is not
         * specified.
         */
        @Override
        public long getMaxQueryMillis() {

            final String s = opts.getHeader(HTTP_HEADER_BIGDATA_MAX_QUERY_MILLIS);

            if (s == null) {

                return -1L;

            }

            return StringUtil.toLong(s);

        }

        @Override
        public String getHeader(final String name) {

            return opts.getHeader(name);

        }

    }

    private final class TupleQuery extends QueryOrUpdate implements IPreparedTupleQuery {

        public TupleQuery(final ConnectOptions opts, final UUID id, final String query) {

            super(opts, id, query);

        }

        @Override
        protected void setupConnectOptions() {

            super.setupConnectOptions();

            if (opts.getAcceptHeader() == null)
                opts.setAcceptHeader(ConnectOptions.DEFAULT_SOLUTIONS_ACCEPT_HEADER);

        }

        @Override
        public TupleQueryResult evaluate() throws Exception {

            return evaluate(null);

        }

        @Override
        public TupleQueryResult evaluate(final IPreparedQueryListener listener) throws Exception {

            setupConnectOptions();

            return mgr.tupleResults(opts, getQueryId(), listener);

        }

    }

    private final class GraphQuery extends QueryOrUpdate implements IPreparedGraphQuery {

        public GraphQuery(final ConnectOptions opts, final UUID id, final String query) {

            super(opts, id, query);

        }

        @Override
        protected void setupConnectOptions() {

            super.setupConnectOptions();

            if (opts.getAcceptHeader() == null)
                opts.setAcceptHeader(ConnectOptions.DEFAULT_GRAPH_ACCEPT_HEADER);

        }

        @Override
        public GraphQueryResult evaluate() throws Exception {

            return evaluate(null);

        }

        @Override
        public GraphQueryResult evaluate(final IPreparedQueryListener listener) throws Exception {

            setupConnectOptions();

            return mgr.graphResults(opts, getQueryId(), listener);

        }

    }

    private final class BooleanQuery extends QueryOrUpdate implements IPreparedBooleanQuery {

        public BooleanQuery(final ConnectOptions opts, final UUID id, final String query) {

            super(opts, id, query);

        }

        @Override
        protected void setupConnectOptions() {

            super.setupConnectOptions();

            if (opts.getAcceptHeader() == null)
                opts.setAcceptHeader(ConnectOptions.DEFAULT_BOOLEAN_ACCEPT_HEADER);

        }

        @Override
        public boolean evaluate() throws Exception {

            return evaluate(null);

        }

        @Override
        public boolean evaluate(final IPreparedQueryListener listener) throws Exception {

            setupConnectOptions();

            return mgr.booleanResults(opts, getQueryId(), listener);

        }

    }

    private final class SparqlUpdate extends QueryOrUpdate implements IPreparedSparqlUpdate {

        public SparqlUpdate(final ConnectOptions opts, final UUID uuid, final String updateStr) {

            super(opts, uuid, updateStr, true/*update*/);

        }

        @Override
        public void evaluate() throws Exception {

            evaluate(null);

        }

        @Override
        public void evaluate(final IPreparedQueryListener listener) throws Exception {

            setupConnectOptions();

            mgr.sparqlUpdateResults(opts, getQueryId(), listener);

        }

    }

    /**
     * Add by URI, statements, or file.
     */
    public static class AddOp {

        private List<String> uris;
        private Iterable<? extends Statement> stmts;
        private byte[] data;
        private File file;
        private InputStream is;
        private Reader reader;
        private RDFFormat format;
        private Resource[] context;

        public AddOp(final String uri) {
            if (uri == null)
                throw new IllegalArgumentException();
            this.uris = Collections.singletonList(uri);
        }

        public AddOp(final Collection<String> uris) {
            if (uris == null)
                throw new IllegalArgumentException();
            if (uris.isEmpty())
                throw new IllegalArgumentException();
            this.uris = new LinkedList<String>(uris);
        }

        public AddOp(final Iterable<? extends Statement> stmts) {
            this.stmts = stmts;
        }

        public AddOp(final File file, final RDFFormat format) {
            this.file = file;
            this.format = format;
        }

        public AddOp(final InputStream is, final RDFFormat format) {
            this.is = is;
            this.format = format;
        }

        public AddOp(final Reader reader, final RDFFormat format) {
            this.reader = reader;
            this.format = format;
        }

        /**
         * This ctor is for the test cases.
         */
        public AddOp(final byte[] data, final RDFFormat format) {
            this.data = data;
            this.format = format;
        }

        public void setContext(final Resource... context) {
            this.context = context;
        }

        private void prepareForWire() throws Exception {

            if (file != null) {

                // set the data
                data = IOUtil.readBytes(file);

            } else if (is != null) {

                // set the data
                data = IOUtil.readBytes(is);

            } else if (reader != null) {

                // set the data
                data = IOUtil.readString(reader).getBytes();

            } else if (stmts != null) {

                // set the data and content type (TRIG by default)
                format = RDFFormat.TRIG;
                data = serialize(stmts, format);

            }

        }

    }

    /**
     * Remove by query, access path, statements, or file.
     */
    public static class RemoveOp {

        private String query;

        private Iterable<? extends Statement> stmts;

        private Value s, p, o;

        private Resource[] c;

        private byte[] data;

        private File file;

        private RDFFormat format;

        private Resource[] context;

        public RemoveOp(final String query) {
            this.query = query;
        }

        public RemoveOp(final Iterable<? extends Statement> stmts) {
            this.stmts = stmts;
        }

        /**
         * 
         * @param s
         *           The subject (optional).
         * @param p
         *           The predicate (optional).
         * @param o
         *           The value (optional).
         * @param c
         *           The contexts (optional, BUT may not be a null Resource[]).
         */
        public RemoveOp(final Resource s, final URI p, final Value o, final Resource... c) {
            if (c == null)
                throw new IllegalArgumentException();
            this.s = s;
            this.p = p;
            this.o = o;
            this.c = c;
        }

        public RemoveOp(final File file, final RDFFormat format) {
            this.file = file;
            this.format = format;
        }

        /**
         * This ctor is for the test cases.
         */
        public RemoveOp(final byte[] data, final RDFFormat format) {
            this.data = data;
            this.format = format;
        }

        public void setContext(final Resource... context) {
            this.context = context;
        }

        private void prepareForWire() throws Exception {

            if (file != null) {

                // set the data
                data = IOUtil.readBytes(file);

            } else if (stmts != null) {

                // set the data and content type (TRIG by default)
                format = RDFFormat.TRIG;
                data = serialize(stmts, format);

            }

        }

    }

    /**
     * Connect to a SPARQL end point (GET or POST query only).
     * 
     * @param opts
     *            The connection options.
     * 
     * @return The connection.
     */
    private JettyResponseListener doConnect(final ConnectOptions opts) throws Exception {

        return mgr.doConnect(opts);

    }

}