org.fcrepo.client.ConnectionManagementTest.java Source code

Java tutorial

Introduction

Here is the source code for org.fcrepo.client.ConnectionManagementTest.java

Source

/**
 * Copyright 2015 DuraSpace, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.fcrepo.client;

import static org.fcrepo.client.MockHttpExpectations.host;
import static org.fcrepo.client.MockHttpExpectations.port;
import static org.fcrepo.client.TestUtils.TEXT_TURTLE;
import static org.fcrepo.client.TestUtils.setField;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Stream;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.NullInputStream;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.http.HttpClientConnection;
import org.apache.http.HttpStatus;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockserver.client.server.MockServerClient;
import org.mockserver.junit.MockServerRule;

/**
 * Integration test used to demonstrate connection management issues with the FcrepoClient.
 *
 * @author esm
 */
@RunWith(MockitoJUnitRunner.class)
public class ConnectionManagementTest {

    /**
     * Starts a mock HTTP server on a free port
     */
    @Rule
    public MockServerRule mockServerRule = new MockServerRule(this);

    // Set by the above @Rule, initialized on @Before via MockHttpExpectations
    private MockServerClient mockServerClient;

    /**
     * URIs that our Mock HTTP server responds to.
     */
    private MockHttpExpectations.SupportedUris uris;

    /**
     * Verifies that the expected number of connections have been requested and then closed.
     *
     * @param connectionCount   the number of connections that have been requested and closed.
     * @param connectionManager the HttpClientConnectionManager
     * @return a Consumer that verifies the supplied HttpClientConnectionManager has opened and closed the expected
     * number of connections.
     */
    private static void verifyConnectionRequestedAndClosed(final int connectionCount,
            final HttpClientConnectionManager connectionManager) {
        // A new connection was requested by the Http client ...
        verify(connectionManager, times(connectionCount)).requestConnection(any(HttpRoute.class), any());

        // Verify it was released.
        verify(connectionManager, times(connectionCount)).releaseConnection(any(HttpClientConnection.class), any(),
                anyLong(), any(TimeUnit.class));
    }

    /**
     * Verifies that the expected number of connections have been requested and <em>have not been</em> closed.
     *
     * @param connectionCount   the number of connections that have been requested.
     * @param connectionManager the HttpClientConnectionManager
     */
    private static void verifyConnectionRequestedButNotClosed(final int connectionCount,
            final HttpClientConnectionManager connectionManager) {
        // A new connection was requested by the Http client ...
        verify(connectionManager, times(connectionCount)).requestConnection(any(HttpRoute.class), any());

        // Verify it was NOT released.
        verify(connectionManager, times(0)).releaseConnection(any(HttpClientConnection.class), any(), anyLong(),
                any(TimeUnit.class));
    }

    /**
     * FcrepoResponse handlers.
     */
    private static class FcrepoResponseHandler {

        /**
         * Closes the InputStream that constitutes the response body.
         */
        private static Consumer<FcrepoResponse> closeEntityBody = response -> {
            try {
                response.getBody().close();
            } catch (IOException e) {
                // ignore
            }
        };

        /**
         * Reads the InputStream that constitutes the response body.
         */
        private static Consumer<FcrepoResponse> readEntityBody = response -> {
            assertNotNull("Expected a non-null InputStream.", response.getBody());
            try {
                IOUtils.copy(response.getBody(), NullOutputStream.NULL_OUTPUT_STREAM);
            } catch (IOException e) {
                // ignore
            }
        };

    }

    /**
     * The Fedora Repository client.
     */
    private FcrepoClient client;

    /**
     * The Apache HttpClient under test.
     */
    private CloseableHttpClient underTest;

    /**
     * The {@link org.apache.http.conn.HttpClientConnectionManager} implementation that the {@link #underTest
     * HttpClient} is configured to used.
     */
    @Spy
    private PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();

    @Before
    public void setUp() {

        // Required because we have a test that doesn't close connections, so we have to insure that the
        // connection manager doesn't block during that test.
        connectionManager.setDefaultMaxPerRoute(HttpMethods.values().length);

        // Set up the expectations on the Mock http server
        new MockHttpExpectations().initializeExpectations(this.mockServerClient, this.mockServerRule.getPort());

        // Uris that we connect to, and answered by the Mock http server
        uris = new MockHttpExpectations.SupportedUris();

        // A FcrepoClient configured to throw exceptions when an error is encountered.
        client = new FcrepoClient(null, null, host + ":" + port, true);

        // We're testing the behavior of a default HttpClient with a pooling connection manager.
        underTest = HttpClientBuilder.create().setConnectionManager(connectionManager).build();

        // Put our testable HttpClient instance on the FcrepoClient
        setField(client, "httpclient", underTest);

    }

    @After
    public void tearDown() throws IOException {
        underTest.close();
    }

    /**
     * Demonstrates that HTTP connections are released when the FcrepoClient throws an exception.  Each method of the
     * FcrepoClient (get, put, post, etc.) is tested.
     */
    @Test
    public void connectionReleasedOnException() {
        // Removing MOVE and COPY operations as the mock server does not handle them
        final int expectedCount = HttpMethods.values().length - 2;
        final AtomicInteger actualCount = new AtomicInteger(0);
        final MockHttpExpectations.Uris uri = uris.uri500;

        Stream.of(HttpMethods.values())
                // MOVE and COPY do not appear to be supported in the mock server
                .filter(method -> HttpMethods.MOVE != method && HttpMethods.COPY != method).forEach(method -> {
                    connect(client, uri, method, null);
                    actualCount.getAndIncrement();
                });

        assertEquals("Expected to make " + expectedCount + " connections; made " + actualCount.get(), expectedCount,
                actualCount.get());

        verifyConnectionRequestedAndClosed(actualCount.get(), connectionManager);
    }

    /**
     * Demonstrates that HTTP connections are released when the user of the FcrepoClient closes the HTTP entity body.
     * Each method of the FcrepoClient (get, put, post, etc.) is tested.
     */
    @Test
    public void connectionReleasedOnEntityBodyClose() {
        final int expectedCount = (int) Stream.of(HttpMethods.values()).filter(m -> m.entity).count();
        final AtomicInteger actualCount = new AtomicInteger(0);
        final MockHttpExpectations.Uris uri = uris.uri200;

        Stream.of(HttpMethods.values()).filter(method -> method.entity).forEach(method -> {
            connect(client, uri, method, FcrepoResponseHandler.closeEntityBody);
            actualCount.getAndIncrement();
        });

        assertEquals("Expected to make " + expectedCount + " connections; made " + actualCount.get(), expectedCount,
                actualCount.get());
        verifyConnectionRequestedAndClosed(actualCount.get(), connectionManager);
    }

    /**
     * Demonstrates that are connections are released when the user of the FcrepoClient reads the HTTP entity body.
     */
    @Test
    public void connectionReleasedOnEntityBodyRead() {
        final int expectedCount = (int) Stream.of(HttpMethods.values()).filter(m -> m.entity).count();
        final AtomicInteger actualCount = new AtomicInteger(0);
        final MockHttpExpectations.Uris uri = uris.uri200;

        Stream.of(HttpMethods.values()).filter(method -> method.entity).forEach(method -> {
            connect(client, uri, method, FcrepoResponseHandler.readEntityBody);
            actualCount.getAndIncrement();
        });

        assertEquals("Expected to make " + expectedCount + " connections; made " + actualCount.get(), expectedCount,
                actualCount.get());
        verifyConnectionRequestedAndClosed(actualCount.get(), connectionManager);
    }

    /**
     * Demonstrates that are connections are NOT released if the user of the FcrepoClient does not handle the response
     * body at all.
     */
    @Test
    public void connectionNotReleasedWhenEntityBodyIgnored() {
        final int expectedCount = (int) Stream.of(HttpMethods.values()).filter(m -> m.entity).count();
        final AtomicInteger actualCount = new AtomicInteger(0);
        final MockHttpExpectations.Uris uri = uris.uri200;

        Stream.of(HttpMethods.values()).filter(method -> method.entity).forEach(method -> {
            connect(client, uri, method, null);
            actualCount.getAndIncrement();
        });

        assertEquals("Expected to make " + expectedCount + " connections; made " + actualCount.get(), expectedCount,
                actualCount.get());
        verifyConnectionRequestedButNotClosed(actualCount.get(), connectionManager);
    }

    /**
     * Uses the FcrepoClient to connect to supplied {@code uri} using the supplied {@code method}.
     * This method invokes the supplied {@code responseHandler} on the {@code FcrepoResponse}.
     *
     * @param client the FcrepoClient used to invoke the request
     * @param uri the request URI to connect to
     * @param method the HTTP method corresponding to the FcrepoClient method invoked
     * @param responseHandler invoked on the {@code FcrepoResponse}, may be {@code null}
     */
    private void connect(final FcrepoClient client, final MockHttpExpectations.Uris uri, final HttpMethods method,
            final Consumer<FcrepoResponse> responseHandler) {

        final NullInputStream nullIn = new NullInputStream(1, true, false);
        FcrepoResponse response = null;

        try {

            switch (method) {

            case OPTIONS:
                response = client.options(uri.asUri()).perform();
                break;

            case DELETE:
                response = client.delete(uri.asUri()).perform();
                break;

            case GET:
                response = client.get(uri.asUri()).accept(TEXT_TURTLE).perform();
                break;

            case HEAD:
                response = client.head(uri.asUri()).perform();
                break;

            case PATCH:
                response = client.patch(uri.asUri()).perform();
                break;

            case POST:
                response = client.post(uri.asUri()).body(nullIn, TEXT_TURTLE).perform();
                break;

            case PUT:
                response = client.put(uri.asUri()).body(nullIn, TEXT_TURTLE).perform();
                break;

            case MOVE:
                response = client.move(uri.asUri(), uri.asUri()).perform();
                break;

            case COPY:
                response = client.copy(uri.asUri(), uri.asUri()).perform();
                break;

            default:
                fail("Unknown HTTP method: " + method.name());
            }

            if (uri.statusCode >= HttpStatus.SC_INTERNAL_SERVER_ERROR) {
                fail("Expected a FcrepoOperationFailedException to be thrown for HTTP method " + method.name());
            }
        } catch (FcrepoOperationFailedException e) {
            assertEquals("Expected request for " + uri.asUri() + " to return a " + uri.statusCode + ".  " + "Was: "
                    + e.getStatusCode() + " Method:" + method, uri.statusCode, e.getStatusCode());
        } finally {
            if (responseHandler != null) {
                responseHandler.accept(response);
            }
        }
    }

}