new InvocationBuilderListener SPI (#4313)

* new InvocationBuilderListener SPI

Signed-off-by: Jan Supol <jan.supol@oracle.com>
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/InvocationBuilderListenerStage.java b/core-client/src/main/java/org/glassfish/jersey/client/InvocationBuilderListenerStage.java
new file mode 100644
index 0000000..dc947c6
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/InvocationBuilderListenerStage.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2019 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 org.glassfish.jersey.client.spi.InvocationBuilderListener;
+import org.glassfish.jersey.internal.inject.InjectionManager;
+import org.glassfish.jersey.internal.inject.Providers;
+import org.glassfish.jersey.model.internal.RankedComparator;
+
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.core.CacheControl;
+import javax.ws.rs.core.Configuration;
+import javax.ws.rs.core.Cookie;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import java.net.URI;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Client request processing stage. During a request creation, when the {@link Invocation.Builder}
+ * would be created, this class is utilized.
+ */
+/* package */ class InvocationBuilderListenerStage {
+    final Iterator<InvocationBuilderListener> invocationBuilderListenerIterator;
+
+    /* package */ InvocationBuilderListenerStage(InjectionManager injectionManager) {
+        final RankedComparator<InvocationBuilderListener> comparator =
+                new RankedComparator<>(RankedComparator.Order.ASCENDING);
+        invocationBuilderListenerIterator = Providers
+                .getAllProviders(injectionManager, InvocationBuilderListener.class, comparator).iterator();
+    }
+
+    /* package */ void invokeListener(JerseyInvocation.Builder builder) {
+        while (invocationBuilderListenerIterator.hasNext()) {
+            invocationBuilderListenerIterator.next().onNewBuilder(new InvocationBuilderContextImpl(builder));
+        }
+    }
+
+    private static class InvocationBuilderContextImpl implements InvocationBuilderListener.InvocationBuilderContext {
+        private final JerseyInvocation.Builder builder;
+
+        private InvocationBuilderContextImpl(JerseyInvocation.Builder builder) {
+            this.builder = builder;
+        }
+
+        @Override
+        public InvocationBuilderListener.InvocationBuilderContext accept(String... mediaTypes) {
+            builder.accept(mediaTypes);
+            return this;
+        }
+
+        @Override
+        public InvocationBuilderListener.InvocationBuilderContext accept(MediaType... mediaTypes) {
+            builder.accept(mediaTypes);
+            return this;
+        }
+
+        @Override
+        public InvocationBuilderListener.InvocationBuilderContext acceptLanguage(Locale... locales) {
+            builder.acceptLanguage(locales);
+            return this;
+        }
+
+        @Override
+        public InvocationBuilderListener.InvocationBuilderContext acceptLanguage(String... locales) {
+            builder.acceptLanguage(locales);
+            return this;
+        }
+
+        @Override
+        public InvocationBuilderListener.InvocationBuilderContext acceptEncoding(String... encodings) {
+            builder.acceptEncoding(encodings);
+            return this;
+        }
+
+        @Override
+        public InvocationBuilderListener.InvocationBuilderContext cookie(Cookie cookie) {
+            builder.cookie(cookie);
+            return this;
+        }
+
+        @Override
+        public InvocationBuilderListener.InvocationBuilderContext cookie(String name, String value) {
+            builder.cookie(name, value);
+            return this;
+        }
+
+        @Override
+        public InvocationBuilderListener.InvocationBuilderContext cacheControl(CacheControl cacheControl) {
+            builder.cacheControl(cacheControl);
+            return this;
+        }
+
+        @Override
+        public List<String> getAccepted() {
+            return getHeader(HttpHeaders.ACCEPT);
+        }
+
+        @Override
+        public List<String> getAcceptedLanguages() {
+            return getHeader(HttpHeaders.ACCEPT_LANGUAGE);
+        }
+
+        @Override
+        public List<CacheControl> getCacheControls() {
+            return (List<CacheControl>) (List<?>) builder.request().getHeaders().get(HttpHeaders.CACHE_CONTROL);
+        }
+
+        @Override
+        public Configuration getConfiguration() {
+            return builder.request().getConfiguration();
+        }
+
+        @Override
+        public Map<String, Cookie> getCookies() {
+            return builder.request().getCookies();
+        }
+
+        @Override
+        public List<String> getEncodings() {
+            return getHeader(HttpHeaders.ACCEPT_ENCODING);
+        }
+
+        @Override
+        public List<String> getHeader(String name) {
+            return builder.request().getRequestHeader(name);
+        }
+
+        @Override
+        public MultivaluedMap<String, Object> getHeaders() {
+            return builder.request().getHeaders();
+        }
+
+        @Override
+        public Object getProperty(String name) {
+            return builder.request().getProperty(name);
+        }
+
+        @Override
+        public Collection<String> getPropertyNames() {
+            return builder.request().getPropertyNames();
+        }
+
+        @Override
+        public URI getUri() {
+            return builder.request().getUri();
+        }
+
+
+        @Override
+        public InvocationBuilderListener.InvocationBuilderContext header(String name, Object value) {
+            builder.header(name, value);
+            return this;
+        }
+
+        @Override
+        public InvocationBuilderListener.InvocationBuilderContext headers(MultivaluedMap<String, Object> headers) {
+            builder.headers(headers);
+            return this;
+        }
+
+        @Override
+        public InvocationBuilderListener.InvocationBuilderContext property(String name, Object value) {
+            builder.property(name, value);
+            return this;
+        }
+
+        @Override
+        public void removeProperty(String name) {
+            builder.request().removeProperty(name);
+        }
+    }
+}
+
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java
index 6c4f029..ed986d7 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyWebTarget.java
@@ -188,14 +188,15 @@
     @Override
     public JerseyInvocation.Builder request() {
         checkNotClosed();
-        return new JerseyInvocation.Builder(getUri(), config.snapshot());
+        JerseyInvocation.Builder b = new JerseyInvocation.Builder(getUri(), config.snapshot());
+        return onBuilder(b);
     }
 
     @Override
     public JerseyInvocation.Builder request(String... acceptedResponseTypes) {
         checkNotClosed();
         JerseyInvocation.Builder b = new JerseyInvocation.Builder(getUri(), config.snapshot());
-        b.request().accept(acceptedResponseTypes);
+        onBuilder(b).request().accept(acceptedResponseTypes);
         return b;
     }
 
@@ -203,7 +204,7 @@
     public JerseyInvocation.Builder request(MediaType... acceptedResponseTypes) {
         checkNotClosed();
         JerseyInvocation.Builder b = new JerseyInvocation.Builder(getUri(), config.snapshot());
-        b.request().accept(acceptedResponseTypes);
+        onBuilder(b).request().accept(acceptedResponseTypes);
         return b;
     }
 
@@ -358,4 +359,9 @@
     public String toString() {
         return "JerseyWebTarget { " + targetUri.toTemplate() + " }";
     }
+
+    private static JerseyInvocation.Builder onBuilder(JerseyInvocation.Builder builder) {
+        new InvocationBuilderListenerStage(builder.request().getInjectionManager()).invokeListener(builder);
+        return builder;
+    }
 }
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/spi/InvocationBuilderListener.java b/core-client/src/main/java/org/glassfish/jersey/client/spi/InvocationBuilderListener.java
new file mode 100644
index 0000000..d0cd24c
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/spi/InvocationBuilderListener.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (c) 2019 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.spi;
+
+import org.glassfish.jersey.Beta;
+import org.glassfish.jersey.spi.Contract;
+
+import javax.ws.rs.ConstrainedTo;
+import javax.ws.rs.RuntimeType;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.CacheControl;
+import javax.ws.rs.core.Configuration;
+import javax.ws.rs.core.Cookie;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import java.net.URI;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Implementations of this interface will be notified when a new Invocation.Builder
+ * is created. This will allow implementations to access the invocation builders,
+ * and is intended for global providers. For example, the Invocation.Builder properties can be
+ * accessed to set properties that are available on the {@link javax.ws.rs.client.ClientRequestContext}.
+ * <p>
+ * In order for the InvocationBuilderListener to be called, the implementation of the interface needs
+ * to be registered on the {@code Client} the same way the {@code ClientRequestFilter} is registered, for instance.
+ *
+ * If multiple {@code InvocationBuilderListeners} are to be utilized, the order of execution is driven by the {@code Priority},
+ * the lower the priority value, the higher the priority, the sooner the execution.
+ *
+ * @since 2.30
+ */
+@Beta
+@Contract
+@ConstrainedTo(RuntimeType.CLIENT)
+public interface InvocationBuilderListener {
+
+    /**
+     * An {@link javax.ws.rs.client.Invocation.Builder} subset of setter methods.
+     */
+    public interface InvocationBuilderContext {
+        /**
+         * Add the accepted response media types.
+         *
+         * @param mediaTypes accepted response media types.
+         * @return the updated context.
+         */
+        InvocationBuilderContext accept(String... mediaTypes);
+
+        /**
+         * Add the accepted response media types.
+         *
+         * @param mediaTypes accepted response media types.
+         * @return the updated context.
+         */
+        InvocationBuilderContext accept(MediaType... mediaTypes);
+
+        /**
+         * Add acceptable languages.
+         *
+         * @param locales an array of the acceptable languages.
+         * @return the updated context.
+         */
+        InvocationBuilderContext acceptLanguage(Locale... locales);
+
+        /**
+         * Add acceptable languages.
+         *
+         * @param locales an array of the acceptable languages.
+         * @return the updated context.
+         */
+        InvocationBuilderContext acceptLanguage(String... locales);
+
+        /**
+         * Add acceptable encodings.
+         *
+         * @param encodings an array of the acceptable encodings.
+         * @return the updated context.
+         */
+        InvocationBuilderContext acceptEncoding(String... encodings);
+
+        /**
+         * Add a cookie to be set.
+         *
+         * @param cookie to be set.
+         * @return the updated context.
+         */
+        InvocationBuilderContext cookie(Cookie cookie);
+
+        /**
+         * Add a cookie to be set.
+         *
+         * @param name  the name of the cookie.
+         * @param value the value of the cookie.
+         * @return the updated context.
+         */
+        InvocationBuilderContext cookie(String name, String value);
+
+        /**
+         * Set the cache control data of the message.
+         *
+         * @param cacheControl the cache control directives, if {@code null}
+         *                     any existing cache control directives will be removed.
+         * @return the updated context.
+         */
+        InvocationBuilderContext cacheControl(CacheControl cacheControl);
+
+        /**
+         * Get the accepted response media types.
+         *
+         * @return accepted response media types.
+         */
+        List<String> getAccepted();
+
+        /**
+         * Get acceptable languages.
+         *
+         * @return acceptable languages.
+         */
+        List<String> getAcceptedLanguages();
+
+        /**
+         * Get the cache control data of the message.
+         *
+         * @return the cache control data of the message.
+         */
+        List<CacheControl> getCacheControls();
+
+        /**
+         * Get runtime configuration.
+         *
+         * @return runtime configuration.
+         */
+        Configuration getConfiguration();
+
+        /**
+         * Get any cookies that accompanied the request.
+         *
+         * @return a read-only map of cookie name (String) to {@link javax.ws.rs.core.Cookie}.
+         */
+        Map<String, Cookie> getCookies();
+
+        /**
+         * Get acceptable encodings.
+         *
+         * @return acceptable encodings.
+         */
+        List<String> getEncodings();
+
+        /**
+         * Get the values of a HTTP request header. The returned List is read-only.
+         *
+         * @param name the header name, case insensitive.
+         * @return a read-only list of header values.
+         */
+        List<String> getHeader(String name);
+
+        /**
+         * Get the mutable message headers multivalued map.
+         *
+         * @return mutable multivalued map of message headers.
+         */
+        MultivaluedMap<String, Object> getHeaders();
+
+        /**
+         * Returns the property with the given name registered in the current request/response
+         * exchange context, or {@code null} if there is no property by that name.
+         * <p>
+         * A property allows filters and interceptors to exchange
+         * additional custom information not already provided by this interface.
+         * </p>
+         * <p>
+         * A list of supported properties can be retrieved using {@link #getPropertyNames()}.
+         * Custom property names should follow the same convention as package names.
+         * </p>
+         *
+         * @param name a {@code String} specifying the name of the property.
+         * @return an {@code Object} containing the value of the property, or
+         * {@code null} if no property exists matching the given name.
+         * @see #getPropertyNames()
+         */
+        Object getProperty(String name);
+
+        /**
+         * Returns an immutable {@link Collection collection} containing the property names
+         * available within the context of the current request/response exchange context.
+         * <p>
+         * Use the {@link #getProperty} method with a property name to get the value of
+         * a property.
+         * </p>
+         *
+         * @return an immutable {@link Collection collection} of property names.
+         * @see #getProperty
+         */
+        Collection<String> getPropertyNames();
+
+        /**
+         * Get the request URI.
+         *
+         * @return request URI.
+         */
+        URI getUri();
+
+        /**
+         * Add an arbitrary header.
+         *
+         * @param name  the name of the header
+         * @param value the value of the header, the header will be serialized
+         *              using a {@link javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if
+         *              one is available via {@link javax.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)}
+         *              for the class of {@code value} or using its {@code toString} method
+         *              if a header delegate is not available. If {@code value} is {@code null}
+         *              then all current headers of the same name will be removed.
+         * @return the updated context.
+         */
+        InvocationBuilderContext header(String name, Object value);
+
+        /**
+         * Replaces all existing headers with the newly supplied headers.
+         *
+         * @param headers new headers to be set, if {@code null} all existing
+         *                headers will be removed.
+         * @return the updated context.
+         */
+        InvocationBuilderContext headers(MultivaluedMap<String, Object> headers);
+
+        /**
+         * Set a new property in the context of a request represented by this invocation builder.
+         * <p>
+         * The property is available for a later retrieval via {@link ClientRequestContext#getProperty(String)}
+         * or {@link javax.ws.rs.ext.InterceptorContext#getProperty(String)}.
+         * If a property with a given name is already set in the request context,
+         * the existing value of the property will be updated.
+         * Setting a {@code null} value into a property effectively removes the property
+         * from the request property bag.
+         * </p>
+         *
+         * @param name  property name.
+         * @param value (new) property value. {@code null} value removes the property
+         *              with the given name.
+         * @return the updated context.
+         * @see Invocation#property(String, Object)
+         */
+        InvocationBuilderContext property(String name, Object value);
+
+        /**
+         * Removes a property with the given name from the current request/response
+         * exchange context. After removal, subsequent calls to {@link #getProperty}
+         * to retrieve the property value will return {@code null}.
+         *
+         * @param name a {@code String} specifying the name of the property to be removed.
+         */
+        void removeProperty(String name);
+    }
+
+    /**
+     * Whenever an {@link Invocation.Builder} is created, (i.e. when
+     * {@link WebTarget#request()}, {@link WebTarget#request(String...)},
+     * {@link WebTarget#request(MediaType...)} is called), this method would be invoked.
+     *
+     * @param context the updated {@link InvocationBuilderContext}.
+     */
+    void onNewBuilder(InvocationBuilderContext context);
+
+}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/spi/InvocationBuilderListenerTest.java b/core-client/src/test/java/org/glassfish/jersey/client/spi/InvocationBuilderListenerTest.java
new file mode 100644
index 0000000..ddb6e76
--- /dev/null
+++ b/core-client/src/test/java/org/glassfish/jersey/client/spi/InvocationBuilderListenerTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2019 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.spi;
+
+import org.glassfish.jersey.internal.PropertiesDelegate;
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.annotation.Priority;
+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.WebTarget;
+import javax.ws.rs.core.CacheControl;
+import javax.ws.rs.core.Configuration;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.RuntimeDelegate;
+import java.io.IOException;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+
+public class InvocationBuilderListenerTest {
+
+    private static final String PROPERTY_NAME = "test_property";
+    private static final String ONE = "one";
+
+    private WebTarget target;
+
+    @Before
+    public void setUp() {
+        target = ClientBuilder.newClient().target("http://localhost:8080").register(AbortRequestFilter.class)
+                .register(new PropertySetterInvocationBuilderListener(a -> a.property(key(ONE), ONE)));
+    }
+
+    @Test
+    public void testRequest() throws ExecutionException, InterruptedException {
+        try (Response r = target.request().async().get().get()) {
+            assertDefault(r);
+        }
+    }
+
+    @Test
+    public void testRequestString() {
+        try (Response r = target.request(MediaType.TEXT_HTML).build("GET").invoke()) {
+            assertDefault(r);
+        }
+    }
+
+    @Test
+    public void testRequestMediaType() throws ExecutionException, InterruptedException {
+        try (Response r = target.request(MediaType.TEXT_PLAIN_TYPE).rx().get().toCompletableFuture().get()) {
+            assertDefault(r);
+        }
+    }
+
+    @Test
+    public void testConfigurationProperties() {
+        String value = "OTHER_VALUE";
+        try (Response r = target.property(key(ConfigurationInvocationBuilderListener.OTHER_PROPERTY), value)
+                .register(ConfigurationInvocationBuilderListener.class).request().get()) {
+            Assert.assertTrue(
+                    r.readEntity(String.class).contains(key(ConfigurationInvocationBuilderListener.OTHER_PROPERTY) + "=" + value)
+            );
+        }
+    }
+
+    @Test
+    public void testGetters() {
+        try (Response r = target.register(SetterInvocationBuilderListener.class, 100)
+                .register(GetterInvocationBuilderListener.class, 200).request().get()) {
+            assertDefault(r);
+        }
+    }
+
+    private void assertDefault(Response response) {
+        Assert.assertEquals(key(ONE) + "=" + ONE, response.readEntity(String.class));
+    }
+
+    private static String key(String keySuffix) {
+        return new StringBuilder().append(PROPERTY_NAME).append('_').append(keySuffix).toString();
+    }
+
+    public static class PropertySetterInvocationBuilderListener implements InvocationBuilderListener {
+
+        private final Consumer<InvocationBuilderContext> builderConsumer;
+
+        public PropertySetterInvocationBuilderListener(Consumer<InvocationBuilderContext> builderConsumer) {
+            this.builderConsumer = builderConsumer;
+        }
+
+        @Override
+        public void onNewBuilder(InvocationBuilderContext context) {
+            builderConsumer.accept(context);
+        }
+    }
+
+    public static class AbortRequestFilter implements ClientRequestFilter {
+
+        @Override
+        public void filter(ClientRequestContext requestContext) throws IOException {
+            StringBuilder sb = new StringBuilder();
+            for (String propertyName : requestContext.getPropertyNames()) {
+                if (propertyName.startsWith(PROPERTY_NAME)) {
+                    sb.append(propertyName).append("=").append(requestContext.getProperty(propertyName));
+                }
+            }
+            requestContext.abortWith(Response.ok().entity(sb.toString()).build());
+        }
+    }
+
+    public static class ConfigurationInvocationBuilderListener implements InvocationBuilderListener {
+        static final String OTHER_PROPERTY = "OTHER_PROPERTY";
+
+        @Override
+        public void onNewBuilder(InvocationBuilderContext context) {
+            context.property(key(OTHER_PROPERTY), context.getConfiguration().getProperty(key(OTHER_PROPERTY)));
+        }
+    }
+
+    public static class SetterInvocationBuilderListener implements InvocationBuilderListener {
+
+        @Override
+        public void onNewBuilder(InvocationBuilderContext context) {
+            context.accept(MediaType.APPLICATION_JSON)
+                    .accept(MediaType.APPLICATION_JSON_PATCH_JSON_TYPE)
+                    .acceptEncoding("GZIP")
+                    .acceptLanguage(Locale.GERMAN)
+                    .acceptLanguage(new Locale.Builder().setLanguage("sr").setScript("Latn").setRegion("RS").build())
+                    .property(PROPERTY_NAME, PROPERTY_NAME)
+                    .cacheControl(CacheControl.valueOf(PROPERTY_NAME))
+                    .cookie("Cookie", "CookieValue")
+                    .header(HttpHeaders.CONTENT_ID, PROPERTY_NAME);
+        }
+    }
+
+    public static class GetterInvocationBuilderListener implements InvocationBuilderListener {
+
+        @Override
+        public void onNewBuilder(InvocationBuilderContext context) {
+            Date date = new Date();
+            RuntimeDelegate.HeaderDelegate localeDelegate = RuntimeDelegate.getInstance().createHeaderDelegate(Locale.class);
+            Assert.assertThat(context.getAccepted(),
+                    Matchers.containsInAnyOrder(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_PATCH_JSON));
+            Assert.assertThat(context.getEncodings(), Matchers.contains("GZIP"));
+            Assert.assertThat(context.getAcceptedLanguages(),
+                    Matchers.containsInAnyOrder(localeDelegate.toString(Locale.GERMAN),
+                            localeDelegate.toString(
+                                    new Locale.Builder().setLanguage("sr").setScript("Latn").setRegion("RS").build()
+                            )
+                    )
+            );
+
+            Assert.assertThat(context.getHeader(HttpHeaders.CONTENT_ID), Matchers.contains(PROPERTY_NAME));
+            context.getHeaders().add(HttpHeaders.DATE, date);
+            Assert.assertThat(context.getHeader(HttpHeaders.DATE), Matchers.notNullValue());
+            Assert.assertThat(context.getHeaders().getFirst(HttpHeaders.DATE), Matchers.is(date));
+
+            Assert.assertNotNull(context.getUri());
+            Assert.assertTrue(context.getUri().toASCIIString().startsWith("http://"));
+
+            Assert.assertThat(context.getPropertyNames(), Matchers.contains(PROPERTY_NAME));
+            Assert.assertThat(context.getProperty(PROPERTY_NAME), Matchers.is(PROPERTY_NAME));
+            context.removeProperty(PROPERTY_NAME);
+            Assert.assertTrue(context.getPropertyNames().isEmpty());
+
+            Assert.assertThat(context.getCacheControls().get(0).toString(),
+                    Matchers.is(CacheControl.valueOf(PROPERTY_NAME).toString())
+            );
+            Assert.assertThat(context.getCookies().size(), Matchers.is(1));
+            Assert.assertThat(context.getCookies().get("Cookie"), Matchers.notNullValue());
+        }
+    }
+}