org.eclipse.rdf4j.http.server.ProtocolTest.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.rdf4j.http.server.ProtocolTest.java

Source

/*******************************************************************************
 * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *******************************************************************************/
package org.eclipse.rdf4j.http.server;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.Charsets;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.eclipse.rdf4j.common.io.IOUtil;
import org.eclipse.rdf4j.http.protocol.Protocol;
import org.eclipse.rdf4j.model.Namespace;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleNamespace;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.query.QueryLanguage;
import org.eclipse.rdf4j.query.TupleQueryResult;
import org.eclipse.rdf4j.query.resultio.QueryResultIO;
import org.eclipse.rdf4j.query.resultio.TupleQueryResultFormat;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.Rio;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.opencsv.CSVReader;

public class ProtocolTest {

    private static TestServer server;

    private static ValueFactory vf = SimpleValueFactory.getInstance();

    @BeforeClass
    public static void startServer() throws Exception {
        server = new TestServer();
        try {
            server.start();
        } catch (Exception e) {
            server.stop();
            throw e;
        }
    }

    @AfterClass
    public static void stopServer() throws Exception {
        server.stop();
    }

    /**
     * Tests the server's methods for updating all data in a repository.
     */
    @Test
    public void testRepository_PUT() throws Exception {
        putFile(Protocol.getStatementsLocation(TestServer.REPOSITORY_URL), "/testcases/default-graph-1.ttl");
    }

    /**
     * Tests the server's methods for deleting all data in a repository.
     */
    @Test
    public void testRepository_DELETE() throws Exception {
        delete(Protocol.getStatementsLocation(TestServer.REPOSITORY_URL));
    }

    /**
     * Tests the server's methods for updating the data in the default context of a repository.
     */
    @Test
    public void testNullContext_PUT() throws Exception {
        String location = Protocol.getStatementsLocation(TestServer.REPOSITORY_URL);
        location += "?" + Protocol.CONTEXT_PARAM_NAME + "=" + Protocol.NULL_PARAM_VALUE;
        putFile(location, "/testcases/default-graph-1.ttl");
    }

    /**
     * Tests the server's methods for deleting the data from the default context of a repository.
     */
    @Test
    public void testNullContext_DELETE() throws Exception {
        String location = Protocol.getStatementsLocation(TestServer.REPOSITORY_URL);
        location += "?" + Protocol.CONTEXT_PARAM_NAME + "=" + Protocol.NULL_PARAM_VALUE;
        delete(location);
    }

    /**
     * Tests the server's methods for updating the data in a named context of a repository.
     */
    @Test
    public void testNamedContext_PUT() throws Exception {
        String location = Protocol.getStatementsLocation(TestServer.REPOSITORY_URL);
        String encContext = Protocol.encodeValue(vf.createIRI("urn:x-local:graph1"));
        location += "?" + Protocol.CONTEXT_PARAM_NAME + "=" + encContext;
        putFile(location, "/testcases/named-graph-1.ttl");
    }

    /**
     * Tests the server's methods for deleting the data from a named context of a repository.
     */
    @Test
    public void testNamedContext_DELETE() throws Exception {
        String location = Protocol.getStatementsLocation(TestServer.REPOSITORY_URL);
        String encContext = Protocol.encodeValue(vf.createIRI("urn:x-local:graph1"));
        location += "?" + Protocol.CONTEXT_PARAM_NAME + "=" + encContext;
        delete(location);
    }

    /**
     * Tests the server's methods for quering a repository using GET requests to send SeRQL-select queries.
     */
    @Test
    public void testSeRQLselect() throws Exception {
        TupleQueryResult queryResult = evaluateTupleQuery(TestServer.REPOSITORY_URL, "select * from {X} P {Y}",
                QueryLanguage.SERQL);
        QueryResultIO.writeTuple(queryResult, TupleQueryResultFormat.SPARQL, System.out);
    }

    /**
     * Checks that the server accepts a direct POST with a content type of "application/sparql-query".
     */
    @Test
    public void testQueryDirect_POST() throws Exception {
        String query = "DESCRIBE <monkey:pod>";
        String location = TestServer.REPOSITORY_URL;

        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpPost post = new HttpPost(location);
        HttpEntity entity = new StringEntity(query, ContentType.create(Protocol.SPARQL_QUERY_MIME_TYPE));
        post.setEntity(entity);

        CloseableHttpResponse response = httpclient.execute(post);

        System.out.println("Query Direct POST Status: " + response.getStatusLine());
        int statusCode = response.getStatusLine().getStatusCode();
        assertEquals(true, statusCode >= 200 && statusCode < 400);
    }

    /**
     * Checks that the server accepts a direct POST with a content type of "application/sparql-update".
     */
    @Test
    public void testUpdateDirect_POST() throws Exception {
        String query = "delete where { <monkey:pod> ?p ?o }";
        String location = Protocol.getStatementsLocation(TestServer.REPOSITORY_URL);

        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpPost post = new HttpPost(location);
        HttpEntity entity = new StringEntity(query, ContentType.create(Protocol.SPARQL_UPDATE_MIME_TYPE));
        post.setEntity(entity);

        CloseableHttpResponse response = httpclient.execute(post);

        System.out.println("Update Direct Post Status: " + response.getStatusLine());
        int statusCode = response.getStatusLine().getStatusCode();
        assertEquals(true, statusCode >= 200 && statusCode < 400);
    }

    /**
     * Checks that the server accepts a formencoded POST with an update and a timeout parameter.
     */
    @Test
    public void testUpdateForm_POST() throws Exception {
        String update = "delete where { <monkey:pod> ?p ?o . }";
        String location = Protocol.getStatementsLocation(TestServer.REPOSITORY_URL);
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpPost post = new HttpPost(location);
        List<NameValuePair> nvps = new ArrayList<NameValuePair>();
        nvps.add(new BasicNameValuePair(Protocol.UPDATE_PARAM_NAME, update));
        nvps.add(new BasicNameValuePair(Protocol.TIMEOUT_PARAM_NAME, "1"));
        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(nvps, Charsets.UTF_8);

        post.setEntity(entity);

        CloseableHttpResponse response = httpclient.execute(post);

        System.out.println("Update Form Post Status: " + response.getStatusLine());
        int statusCode = response.getStatusLine().getStatusCode();
        assertEquals(true, statusCode >= 200 && statusCode < 400);
    }

    /**
     * Checks that the requested content type is returned when accept header explicitly set.
     */
    @Test
    public void testContentTypeForGraphQuery1_GET() throws Exception {
        String query = "DESCRIBE <foo:bar>";
        String location = TestServer.REPOSITORY_URL;
        location += "?query=" + URLEncoder.encode(query, "UTF-8");

        URL url = new URL(location);

        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        // Request RDF/XML formatted results:
        conn.setRequestProperty("Accept", RDFFormat.RDFXML.getDefaultMIMEType());

        conn.connect();

        try {
            int responseCode = conn.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                String contentType = conn.getHeaderField("Content-Type");
                assertNotNull(contentType);

                // snip off optional charset declaration
                int charPos = contentType.indexOf(";");
                if (charPos > -1) {
                    contentType = contentType.substring(0, charPos);
                }

                assertEquals(RDFFormat.RDFXML.getDefaultMIMEType(), contentType);
            } else {
                String response = "location " + location + " responded: " + conn.getResponseMessage() + " ("
                        + responseCode + ")";
                fail(response);
                throw new RuntimeException(response);
            }
        } finally {
            conn.disconnect();
        }
    }

    /**
     * Checks that a proper error (HTTP 406) is returned when accept header is set incorrectly on graph query.
     */
    @Test
    public void testContentTypeForGraphQuery2_GET() throws Exception {
        String query = "DESCRIBE <foo:bar>";
        String location = TestServer.REPOSITORY_URL;
        location += "?query=" + URLEncoder.encode(query, "UTF-8");

        URL url = new URL(location);

        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        // incorrect mime-type for graph query results
        conn.setRequestProperty("Accept", TupleQueryResultFormat.SPARQL.getDefaultMIMEType());

        conn.connect();

        try {
            int responseCode = conn.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_NOT_ACCEPTABLE) {
                // do nothing, expected
            } else {
                String response = "location " + location + " responded: " + conn.getResponseMessage() + " ("
                        + responseCode + ")";
                fail(response);
            }
        } finally {
            conn.disconnect();
        }
    }

    @Test
    public void testQueryResponse_HEAD() throws Exception {
        String query = "DESCRIBE <foo:bar>";
        String location = TestServer.REPOSITORY_URL;
        location += "?query=" + URLEncoder.encode(query, "UTF-8");

        URL url = new URL(location);

        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("HEAD");

        // Request RDF/XML formatted results:
        conn.setRequestProperty("Accept", RDFFormat.RDFXML.getDefaultMIMEType());

        conn.connect();

        try {
            int responseCode = conn.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                String contentType = conn.getHeaderField("Content-Type");
                assertNotNull(contentType);

                // snip off optional charset declaration
                int charPos = contentType.indexOf(";");
                if (charPos > -1) {
                    contentType = contentType.substring(0, charPos);
                }

                assertEquals(RDFFormat.RDFXML.getDefaultMIMEType(), contentType);
                assertEquals(0, conn.getContentLength());
            } else {
                String response = "location " + location + " responded: " + conn.getResponseMessage() + " ("
                        + responseCode + ")";
                fail(response);
                throw new RuntimeException(response);
            }
        } finally {
            conn.disconnect();
        }
    }

    @Test
    public void testUpdateResponse_HEAD() throws Exception {
        String query = "INSERT DATA { <foo:foo> <foo:bar> \"foo\". } ";
        String location = Protocol.getStatementsLocation(TestServer.REPOSITORY_URL);
        location += "?update=" + URLEncoder.encode(query, "UTF-8");

        URL url = new URL(location);

        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("HEAD");

        conn.connect();

        try {
            int responseCode = conn.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                String contentType = conn.getHeaderField("Content-Type");
                assertNotNull(contentType);

                // snip off optional charset declaration
                int charPos = contentType.indexOf(";");
                if (charPos > -1) {
                    contentType = contentType.substring(0, charPos);
                }

                assertEquals(0, conn.getContentLength());
            } else {
                String response = "location " + location + " responded: " + conn.getResponseMessage() + " ("
                        + responseCode + ")";
                fail(response);
                throw new RuntimeException(response);
            }
        } finally {
            conn.disconnect();
        }
    }

    /**
     * Test for SES-1861
     * 
     * @throws Exception
     */
    @Test
    public void testSequentialNamespaceUpdates() throws Exception {
        int limitCount = 1000;
        int limitPrefix = 50;

        Random prng = new Random();
        // String repositoryLocation =
        // Protocol.getRepositoryLocation("http://localhost:8080/openrdf-sesame",
        // "Test-NativeStore");
        String repositoryLocation = TestServer.REPOSITORY_URL;

        for (int count = 0; count < limitCount; count++) {
            int i = prng.nextInt(limitPrefix);
            String prefix = "prefix" + i;
            String ns = "http://example.org/namespace" + i;

            String location = Protocol.getNamespacePrefixLocation(repositoryLocation, prefix);

            if (count % 2 == 0) {
                putNamespace(location, ns);
            } else {
                deleteNamespace(location);
            }
        }
    }

    /**
     * Test for GitHub issue #262
     * 
     * @throws Exception
     */
    @Test
    public void testPutEmptyPrefix() throws Exception {
        String repositoryLocation = TestServer.REPOSITORY_URL;
        String namespacesLocation = Protocol.getNamespacesLocation(repositoryLocation);
        String emptyPrefixLocation = Protocol.getNamespacePrefixLocation(repositoryLocation, "");

        Set<Namespace> namespacesBefore = getNamespaces(namespacesLocation);

        putNamespace(emptyPrefixLocation, "http://example.org/");

        Set<Namespace> namespacesAfter = getNamespaces(namespacesLocation);

        Set<Namespace> namespaceDeletions = Sets.difference(namespacesBefore, namespacesAfter);
        Set<Namespace> namespaceAdditions = Sets.difference(namespacesAfter, namespacesBefore);

        assertTrue("Some namespaces have been deleted", namespaceDeletions.isEmpty());
        assertEquals(Sets.newHashSet(new SimpleNamespace("", "http://example.org/")), namespaceAdditions);
    }

    /**
     * Test for SES-1861
     * 
     * @throws Exception
     */
    @Test
    public void testConcurrentNamespaceUpdates() throws Exception {
        int limitCount = 1000;
        int limitPrefix = 50;

        Random prng = new Random();

        // String repositoryLocation =
        // Protocol.getRepositoryLocation("http://localhost:8080/openrdf-sesame",
        // "Test-NativeStore");
        String repositoryLocation = TestServer.REPOSITORY_URL;

        ExecutorService threadPool = Executors.newFixedThreadPool(20,
                new ThreadFactoryBuilder().setNameFormat("rdf4j-protocoltest-%d").build());

        for (int count = 0; count < limitCount; count++) {
            final int number = count;
            final int i = prng.nextInt(limitPrefix);
            final String prefix = "prefix" + i;
            final String ns = "http://example.org/namespace" + i;

            final String location = Protocol.getNamespacePrefixLocation(repositoryLocation, prefix);

            Runnable runner = new Runnable() {

                public void run() {
                    try {
                        if (number % 2 == 0) {
                            putNamespace(location, ns);
                        } else {
                            deleteNamespace(location);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        fail("Failed in test: " + number);
                    }
                }
            };
            threadPool.execute(runner);
        }
        threadPool.shutdown();
        threadPool.awaitTermination(30000, TimeUnit.MILLISECONDS);
        threadPool.shutdownNow();
    }

    private Set<Namespace> getNamespaces(String location) throws Exception {
        URL url = new URL(location);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        try {
            conn.setRequestProperty("Accept", "text/csv; charset=utf-8");
            conn.connect();

            int responseCode = conn.getResponseCode();
            if (responseCode != HttpURLConnection.HTTP_OK) {
                String response = "location " + location + " responded: " + conn.getResponseMessage() + " ("
                        + responseCode + ")";
                fail(response);
            }

            CSVReader reader = new CSVReader(
                    new InputStreamReader(conn.getInputStream(), Charset.forName("UTF-8")));

            try {
                String[] headerRow = reader.readNext();

                if (headerRow == null) {
                    fail("header not found");
                }

                if (!Arrays.equals(headerRow, new String[] { "prefix", "namespace" })) {
                    fail("illegal header row: " + Arrays.toString(headerRow));
                }

                Set<Namespace> namespaces = new HashSet<Namespace>();

                String[] aRow;
                while ((aRow = reader.readNext()) != null) {
                    String prefix = aRow[0];
                    String namespace = aRow[1];

                    namespaces.add(new SimpleNamespace(prefix, namespace));
                }
                return namespaces;
            } finally {
                reader.close();
            }
        } finally {
            conn.disconnect();
        }
    }

    private void putNamespace(String location, String namespace) throws Exception {
        // System.out.println("Put namespace to " + location);

        URL url = new URL(location);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("PUT");
        conn.setDoOutput(true);

        InputStream dataStream = new ByteArrayInputStream(namespace.getBytes("UTF-8"));
        try {
            OutputStream connOut = conn.getOutputStream();

            try {
                IOUtil.transfer(dataStream, connOut);
            } finally {
                connOut.close();
            }
        } finally {
            dataStream.close();
        }

        conn.connect();

        int responseCode = conn.getResponseCode();

        // HTTP 200 or 204
        if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_NO_CONTENT) {
            String response = "location " + location + " responded: " + conn.getResponseMessage() + " ("
                    + responseCode + ")";
            fail(response);
        }
    }

    private void deleteNamespace(String location) throws Exception {
        // System.out.println("Delete namespace at " + location);

        URL url = new URL(location);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("DELETE");
        conn.setDoOutput(true);

        conn.connect();

        int responseCode = conn.getResponseCode();

        // HTTP 200 or 204
        if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_NO_CONTENT) {
            String response = "location " + location + " responded: " + conn.getResponseMessage() + " ("
                    + responseCode + ")";
            fail(response);
        }
    }

    private void putFile(String location, String file) throws Exception {
        System.out.println("Put file to " + location);

        URL url = new URL(location);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("PUT");
        conn.setDoOutput(true);

        RDFFormat dataFormat = Rio.getParserFormatForFileName(file).orElse(RDFFormat.RDFXML);
        conn.setRequestProperty("Content-Type", dataFormat.getDefaultMIMEType());

        InputStream dataStream = ProtocolTest.class.getResourceAsStream(file);
        try {
            OutputStream connOut = conn.getOutputStream();

            try {
                IOUtil.transfer(dataStream, connOut);
            } finally {
                connOut.close();
            }
        } finally {
            dataStream.close();
        }

        conn.connect();

        int responseCode = conn.getResponseCode();

        // HTTP 200 or 204
        if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_NO_CONTENT) {
            String response = "location " + location + " responded: " + conn.getResponseMessage() + " ("
                    + responseCode + ")";
            fail(response);
        }
    }

    private void delete(String location) throws Exception {
        URL url = new URL(location);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("DELETE");

        conn.connect();

        int responseCode = conn.getResponseCode();

        // HTTP 200 or 204
        if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_NO_CONTENT) {
            String response = "location " + location + " responded: " + conn.getResponseMessage() + " ("
                    + responseCode + ")";
            fail(response);
        }
    }

    private TupleQueryResult evaluateTupleQuery(String location, String query, QueryLanguage queryLn)
            throws Exception {
        location += "?query=" + URLEncoder.encode(query, "UTF-8") + "&queryLn=" + queryLn.getName();

        URL url = new URL(location);

        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        // Request SPARQL-XML formatted results:
        conn.setRequestProperty("Accept", TupleQueryResultFormat.SPARQL.getDefaultMIMEType());

        conn.connect();

        try {
            int responseCode = conn.getResponseCode();
            // HTTP 200
            if (responseCode == HttpURLConnection.HTTP_OK) {
                // Process query results
                return QueryResultIO.parseTuple(conn.getInputStream(), TupleQueryResultFormat.SPARQL);
            } else {
                String response = "location " + location + " responded: " + conn.getResponseMessage() + " ("
                        + responseCode + ")";
                fail(response);
                throw new RuntimeException(response);
            }
        } finally {
            conn.disconnect();
        }
    }

}