JerseyInvocationTest.java

/*
 * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.client;

import java.io.IOException;
import java.net.ConnectException;
import java.net.ProtocolException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.AsyncInvoker;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.InvocationCallback;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;

import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

/**
 * @author Martin Matula
 */
public class JerseyInvocationTest {

    /**
     * Regression test for JERSEY-1257
     */
    @Test
    public void testOverrideHeadersWithMap() {
        final MultivaluedMap<String, Object> map = new MultivaluedHashMap<String, Object>();
        map.add("a-header", "b-header");
        final JerseyInvocation invocation = buildInvocationWithHeaders(map);
        assertEquals(1, invocation.request().getHeaders().size());
        assertEquals("b-header", invocation.request().getHeaders().getFirst("a-header"));
    }

    /**
     * Regression test for JERSEY-1257
     */
    @Test
    public void testClearHeaders() {
        final JerseyInvocation invocation = buildInvocationWithHeaders(null);
        assertTrue(invocation.request().getHeaders().isEmpty());
    }

    /**
     * Regression test for JERSEY-2562.
     */
    @Test
    public void testClearHeader() {
        final Client client = ClientBuilder.newClient();
        final Invocation.Builder builder = client.target("http://localhost:8080/mypath").request();
        final JerseyInvocation invocation = (JerseyInvocation) builder
                .header("foo", "bar").header("foo", null).header("bar", "foo")
                .buildGet();
        final MultivaluedMap<String, Object> headers = invocation.request().getHeaders();

        assertThat(headers.size(), is(1));
        assertThat(headers.keySet(), hasItem("bar"));
    }

    private JerseyInvocation buildInvocationWithHeaders(final MultivaluedMap<String, Object> headers) {
        final Client c = ClientBuilder.newClient();
        final Invocation.Builder builder = c.target("http://localhost:8080/mypath").request();
        return (JerseyInvocation) builder.header("unexpected-header", "unexpected-header").headers(headers).buildGet();
    }

    /**
     * Checks that presence of request entity fo HTTP DELETE method does not fail in Jersey.
     * Instead, the request is propagated up to HttpURLConnection, where it fails with
     * {@code ProtocolException}.
     * <p/>
     * See also JERSEY-1711.
     *
     * @see #overrideHttpMethodBasedComplianceCheckNegativeTest()
     */
    @Test
    public void overrideHttpMethodBasedComplianceCheckTest() {
        final Client c1 = ClientBuilder.newClient().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true);
        try {
            c1.target("http://localhost:8080/myPath").request().method("DELETE", Entity.text("body"));
            fail("ProcessingException expected.");
        } catch (final ProcessingException ex) {
            assertThat(ex.getCause().getClass(), anyOf(CoreMatchers.<Class<?>>equalTo(ProtocolException.class),
                    CoreMatchers.<Class<?>>equalTo(ConnectException.class)));
        }

        final Client c2 = ClientBuilder.newClient();
        try {
            c2.target("http://localhost:8080/myPath").request().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION,
                    true).method("DELETE", Entity.text("body"));
            fail("ProcessingException expected.");
        } catch (final ProcessingException ex) {
            assertThat(ex.getCause().getClass(), anyOf(CoreMatchers.<Class<?>>equalTo(ProtocolException.class),
                    CoreMatchers.<Class<?>>equalTo(ConnectException.class)));
        }

        final Client c3 = ClientBuilder.newClient().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, false);
        try {
            c3.target("http://localhost:8080/myPath").request().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION,
                    true).method("DELETE", Entity.text("body"));
            fail("ProcessingException expected.");
        } catch (final ProcessingException ex) {
            assertThat(ex.getCause().getClass(), anyOf(CoreMatchers.<Class<?>>equalTo(ProtocolException.class),
                    CoreMatchers.<Class<?>>equalTo(ConnectException.class)));
        }
    }

    /**
     * Checks that presence of request entity fo HTTP DELETE method fails in Jersey with {@code IllegalStateException}
     * if HTTP spec compliance is not suppressed by {@link ClientProperties#SUPPRESS_HTTP_COMPLIANCE_VALIDATION} property.
     * <p/>
     * See also JERSEY-1711.
     *
     * @see #overrideHttpMethodBasedComplianceCheckTest()
     */
    @Test
    public void overrideHttpMethodBasedComplianceCheckNegativeTest() {
        final Client c1 = ClientBuilder.newClient();
        try {
            c1.target("http://localhost:8080/myPath").request().method("DELETE", Entity.text("body"));
            fail("IllegalStateException expected.");
        } catch (final IllegalStateException expected) {
            // pass
        }

        final Client c2 = ClientBuilder.newClient();
        try {
            c2.target("http://localhost:8080/myPath").request().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION,
                    false).method("DELETE", Entity.text("body"));
            fail("IllegalStateException expected.");
        } catch (final IllegalStateException expected) {
            // pass
        }

        final Client c3 = ClientBuilder.newClient().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true);
        try {
            c3.target("http://localhost:8080/myPath").request().property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION,
                    false).method("DELETE", Entity.text("body"));
            fail("IllegalStateException expected.");
        } catch (final IllegalStateException expected) {
            // pass
        }
    }

    @Test
    public void testNullResponseType() throws Exception {
        final Client client = ClientBuilder.newClient();
        client.register(new ClientRequestFilter() {
            @Override
            public void filter(final ClientRequestContext requestContext) throws IOException {
                requestContext.abortWith(Response.ok().build());
            }
        });

        final WebTarget target = client.target("http://localhost:8080/mypath");

        final Class<Response> responseType = null;
        final String[] methods = new String[] {"GET", "PUT", "POST", "DELETE", "OPTIONS"};

        for (final String method : methods) {
            final Invocation.Builder request = target.request();

            try {
                request.method(method, responseType);
                fail("IllegalArgumentException expected.");
            } catch (final IllegalArgumentException iae) {
                // OK.
            }

            final Invocation build = "PUT".equals(method) ? request.build(method, Entity.entity("",
                    MediaType.TEXT_PLAIN_TYPE)) : request.build(method);

            try {
                build.submit(responseType);
                fail("IllegalArgumentException expected.");
            } catch (final IllegalArgumentException iae) {
                // OK.
            }

            try {
                build.invoke(responseType);
                fail("IllegalArgumentException expected.");
            } catch (final IllegalArgumentException iae) {
                // OK.
            }

            try {
                request.async().method(method, responseType);
                fail("IllegalArgumentException expected.");
            } catch (final IllegalArgumentException iae) {
                // OK.
            }
        }
    }

    @Test
    public void failedCallbackTest() throws InterruptedException {
        final Invocation.Builder builder = ClientBuilder.newClient().target("http://localhost:888/").request();
        for (int i = 0; i < 1; i++) {
            final CountDownLatch latch = new CountDownLatch(1);
            final AtomicInteger ai = new AtomicInteger(0);
            final InvocationCallback<String> callback = new InvocationCallback<String>() {
                @Override
                public void completed(final String arg0) {
                    try {
                        ai.set(ai.get() + 1);
                    } finally {
                        latch.countDown();
                    }
                }

                @Override
                public void failed(final Throwable throwable) {
                    try {

                        int result = 10;
                        if (throwable instanceof ProcessingException) {
                            result += 100;
                        }
                        final Throwable ioe = throwable.getCause();
                        if (ioe instanceof IOException) {
                            result += 1000;
                        }
                        ai.set(ai.get() + result);
                    } finally {
                        latch.countDown();
                    }
                }
            };

            final Invocation invocation = builder.buildGet();
            final Future<String> future = invocation.submit(callback);
            try {
                future.get();
                fail("future.get() should have failed.");
            } catch (final ExecutionException e) {
                final Throwable pe = e.getCause();
                assertTrue(pe instanceof ProcessingException,
                        "Execution exception cause is not a ProcessingException: " + pe.toString());
                final Throwable ioe = pe.getCause();
                assertTrue(ioe instanceof IOException, "Execution exception cause is not an IOException: " + ioe.toString());
            } catch (final InterruptedException e) {
                throw new RuntimeException(e);
            }

            latch.await(1, TimeUnit.SECONDS);

            assertEquals(1110, ai.get());
        }
    }

    public static class MyUnboundCallback<V> implements InvocationCallback<V> {

        private final CountDownLatch latch;
        private volatile Throwable throwable;

        public MyUnboundCallback(final CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void completed(final V v) {
            latch.countDown();
        }

        @Override
        public void failed(final Throwable throwable) {
            this.throwable = throwable;
            latch.countDown();
        }

        public Throwable getThrowable() {
            return throwable;
        }
    }

    @Test
    public void failedUnboundGenericCallback() throws InterruptedException {
        final Invocation invocation = ClientBuilder.newClient().target("http://localhost:888/").request().buildGet();
        final CountDownLatch latch = new CountDownLatch(1);

        final MyUnboundCallback<String> callback = new MyUnboundCallback<String>(latch);
        invocation.submit(callback);

        latch.await(1, TimeUnit.SECONDS);

        assertThat(callback.getThrowable(), CoreMatchers.instanceOf(ProcessingException.class));
        assertThat(callback.getThrowable().getCause(), CoreMatchers.instanceOf(IllegalArgumentException.class));
        assertThat(callback.getThrowable().getCause().getMessage(), CoreMatchers
                .allOf(CoreMatchers.containsString(MyUnboundCallback.class.getName()),
                        CoreMatchers.containsString(InvocationCallback.class.getName())));
    }

    @Test
    public void testSubmitWithGenericType() throws Exception {
        _submitWithGenericType(new GenericType<String>() {});
    }

    @Test
    public void testSubmitWithGenericTypeParam() throws Exception {
        _submitWithGenericType(new GenericType(String.class) {});
    }

    private void _submitWithGenericType(final GenericType type) throws Exception {
        final Invocation.Builder builder = ClientBuilder.newClient()
                .register(TerminatingFilter.class)
                .target("http://localhost/")
                .request();

        final AtomicReference<String> reference = new AtomicReference<String>();
        final CountDownLatch latch = new CountDownLatch(1);

        final InvocationCallback callback = new InvocationCallback<Object>() {
            @Override
            public void completed(final Object obj) {
                reference.set(obj.toString());
                latch.countDown();
            }

            @Override
            public void failed(final Throwable throwable) {
                latch.countDown();
            }
        };

        //noinspection unchecked
        ((JerseyInvocation) builder.buildGet())
                .submit(type, (InvocationCallback<String>) callback);

        latch.await();

        assertThat(reference.get(), is("ENTITY"));
    }

    public static class TerminatingFilter implements ClientRequestFilter {

        @Override
        public void filter(final ClientRequestContext requestContext) throws IOException {
            requestContext.abortWith(Response.ok("ENTITY").build());
        }
    }

    @Test
    public void runtimeExceptionInAsyncInvocation() throws ExecutionException, InterruptedException {
        final AsyncInvoker ai = ClientBuilder.newClient().register(new ExceptionInvokerFilter())
                .target("http://localhost:888/").request().async();

        try {
            ai.get().get();
            fail("ExecutionException should be thrown");
        } catch (ExecutionException ee) {
            assertEquals(ProcessingException.class, ee.getCause().getClass());
            assertEquals(RuntimeException.class, ee.getCause().getCause().getClass());
        }
    }

    public static class ExceptionInvokerFilter implements ClientRequestFilter {

        @Override
        public void filter(final ClientRequestContext requestContext) throws IOException {
            throw new RuntimeException("ExceptionInvokerFilter RuntimeException");
        }
    }
}