Guard list of headers for modifications

Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/GuardianStringKeyMultivaluedMap.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/GuardianStringKeyMultivaluedMap.java
index 8931111..5a2425e 100644
--- a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/GuardianStringKeyMultivaluedMap.java
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/GuardianStringKeyMultivaluedMap.java
@@ -16,13 +16,17 @@
 
 package org.glassfish.jersey.internal.util.collection;
 
+import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * The {@link MultivaluedMap} wrapper that is able to set guards observing changes of values represented by a key.
@@ -35,6 +39,10 @@
     private final MultivaluedMap<String, V> inner;
     private final Map<String, Boolean> guards = new HashMap<>();
 
+    private static boolean isMutable(Object mutable) {
+        return !String.class.isInstance(mutable) && !MediaType.class.isInstance(mutable);
+    }
+
     public GuardianStringKeyMultivaluedMap(MultivaluedMap<String, V> inner) {
         this.inner = inner;
     }
@@ -53,7 +61,11 @@
 
     @Override
     public V getFirst(String key) {
-        return inner.getFirst(key);
+        V first = inner.getFirst(key);
+        if (isMutable(key)) {
+            observe(key);
+        }
+        return first;
     }
 
     @Override
@@ -101,7 +113,15 @@
 
     @Override
     public List<V> get(Object key) {
-        return inner.get(key);
+        final List<V> innerList = inner.get(key);
+        if (innerList != null) {
+            for (Map.Entry<String, Boolean> guard : guards.entrySet()) {
+                if (guard.getKey().equals(key)) {
+                    return new GuardianList(innerList, guard);
+                }
+            }
+        }
+        return innerList;
     }
 
     @Override
@@ -139,11 +159,13 @@
 
     @Override
     public Collection<List<V>> values() {
+        observeAll();
         return inner.values();
     }
 
     @Override
     public Set<Entry<String, List<V>>> entrySet() {
+        observeAll();
         return inner.entrySet();
     }
 
@@ -208,4 +230,225 @@
     public int hashCode() {
         return Objects.hash(inner, guards);
     }
+
+    private static class MutableGuardian<V> {
+        protected final Map.Entry<String, Boolean> guard;
+
+        private MutableGuardian(Entry<String, Boolean> guard) {
+            this.guard = guard;
+        }
+
+        protected V guardMutable(V mutable) {
+            if (isMutable(mutable)) {
+                guard.setValue(true);
+            }
+            return mutable;
+        }
+    }
+
+    private static class GuardianList<V> extends MutableGuardian<V> implements List<V>  {
+        private final List<V> guarded;
+
+        public GuardianList(List<V> guarded, Map.Entry<String, Boolean> guard) {
+            super(guard);
+            this.guarded = guarded;
+        }
+
+        @Override
+        public int size() {
+            return guarded.size();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return guarded.isEmpty();
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            return guarded.contains(o);
+        }
+
+        @Override
+        public Iterator<V> iterator() {
+            return new GuardianIterator<>(guarded.iterator(), guard);
+        }
+
+        @Override
+        public Object[] toArray() {
+            guard.setValue(true);
+            return guarded.toArray();
+        }
+
+        @Override
+        public <T> T[] toArray(T[] a) {
+            guard.setValue(true);
+            return guarded.toArray(a);
+        }
+
+        @Override
+        public boolean add(V e) {
+            guard.setValue(true);
+            return guarded.add(e);
+        }
+
+        @Override
+        public boolean remove(Object o) {
+            guard.setValue(true);
+            return guarded.remove(o);
+        }
+
+        @Override
+        public boolean containsAll(Collection<?> c) {
+            return guarded.containsAll(c);
+        }
+
+        @Override
+        public boolean addAll(Collection<? extends V> c) {
+            guard.setValue(true);
+            return guarded.addAll(c);
+        }
+
+        @Override
+        public boolean addAll(int index, Collection<? extends V> c) {
+            guard.setValue(true);
+            return guarded.addAll(index, c);
+        }
+
+        @Override
+        public boolean removeAll(Collection<?> c) {
+            guard.setValue(true);
+            return guarded.removeAll(c);
+        }
+
+        @Override
+        public boolean retainAll(Collection<?> c) {
+            guard.setValue(true);
+            return guarded.retainAll(c);
+        }
+
+        @Override
+        public void clear() {
+            guard.setValue(true);
+            guarded.clear();
+        }
+
+        @Override
+        public V get(int index) {
+            return guardMutable(guarded.get(index));
+        }
+
+        @Override
+        public V set(int index, V element) {
+            guard.setValue(true);
+            return guarded.set(index, element);
+        }
+
+        @Override
+        public void add(int index, V element) {
+            guard.setValue(true);
+            guarded.add(index, element);
+        }
+
+        @Override
+        public V remove(int index) {
+            guard.setValue(true);
+            return guarded.remove(index);
+        }
+
+        @Override
+        public int indexOf(Object o) {
+            return guarded.indexOf(o);
+        }
+
+        @Override
+        public int lastIndexOf(Object o) {
+            return guarded.lastIndexOf(o);
+        }
+
+        @Override
+        public ListIterator<V> listIterator() {
+            return new GuardianListIterator<>(guarded.listIterator(), guard);
+        }
+
+        @Override
+        public ListIterator<V> listIterator(int index) {
+            return new GuardianListIterator<>(guarded.listIterator(index), guard);
+        }
+
+        @Override
+        public List<V> subList(int fromIndex, int toIndex) {
+            final List<V> sublist = guarded.subList(fromIndex, toIndex);
+            return sublist != null ? new GuardianList<>(sublist, guard) : sublist;
+        }
+    }
+
+    private static class GuardianIterator<V> extends MutableGuardian<V> implements Iterator<V> {
+        protected final Iterator<V> guarded;
+
+        public GuardianIterator(Iterator<V> guarded, Map.Entry<String, Boolean> guard) {
+            super(guard);
+            this.guarded = guarded;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return guarded.hasNext();
+        }
+
+        @Override
+        public V next() {
+            return guardMutable(guarded.next());
+        }
+
+        @Override
+        public void remove() {
+            guard.setValue(true);
+            guarded.remove();
+        }
+
+        @Override
+        public void forEachRemaining(Consumer<? super V> action) {
+            guarded.forEachRemaining(action);
+        }
+    }
+
+    private static class GuardianListIterator<V> extends GuardianIterator<V> implements ListIterator<V> {
+
+        public GuardianListIterator(Iterator<V> guarded, Entry<String, Boolean> guard) {
+            super(guarded, guard);
+        }
+
+        @Override
+        public boolean hasPrevious() {
+            return ((ListIterator<V>) guarded).hasPrevious();
+        }
+
+        @Override
+        public V previous() {
+            return guardMutable(((ListIterator<V>) guarded).previous());
+        }
+
+        @Override
+        public int nextIndex() {
+            return ((ListIterator<V>) guarded).nextIndex();
+        }
+
+        @Override
+        public int previousIndex() {
+            return ((ListIterator<V>) guarded).previousIndex();
+        }
+
+        @Override
+        public void set(V v) {
+            ((ListIterator<V>) guarded).set(v);
+            guard.setValue(true);
+        }
+
+        @Override
+        public void add(V v) {
+            ((ListIterator<V>) guarded).add(v);
+            guard.setValue(true);
+        }
+    }
 }
diff --git a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/OutboundMessageContextTest.java b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/OutboundMessageContextTest.java
index 380a72f..1327edd 100644
--- a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/OutboundMessageContextTest.java
+++ b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/OutboundMessageContextTest.java
@@ -20,6 +20,7 @@
 import java.net.URISyntaxException;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
@@ -237,6 +238,33 @@
     }
 
     @Test
+    public void testChangedContentTypeOnList() {
+        OutboundMessageContext ctx = new OutboundMessageContext((Configuration) null);
+        ctx.setMediaType(MediaType.APPLICATION_XML_TYPE);
+        Assertions.assertEquals(MediaType.APPLICATION_XML_TYPE, ctx.getMediaType());
+        ctx.getHeaders().get(HttpHeaders.CONTENT_TYPE).set(0, MediaType.APPLICATION_JSON);
+        Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, ctx.getMediaType());
+    }
+
+    @Test
+    public void testChangedContentTypeOnValues() {
+        OutboundMessageContext ctx = new OutboundMessageContext((Configuration) null);
+        ctx.setMediaType(MediaType.APPLICATION_XML_TYPE);
+        Assertions.assertEquals(MediaType.APPLICATION_XML_TYPE, ctx.getMediaType());
+        ctx.getHeaders().values().clear();
+        Assertions.assertEquals(null, ctx.getMediaType());
+    }
+
+    @Test
+    public void testChangedContentTypeOnEntrySet() {
+        OutboundMessageContext ctx = new OutboundMessageContext((Configuration) null);
+        ctx.setMediaType(MediaType.APPLICATION_XML_TYPE);
+        Assertions.assertEquals(MediaType.APPLICATION_XML_TYPE, ctx.getMediaType());
+        ctx.getHeaders().entrySet().clear();
+        Assertions.assertEquals(null, ctx.getMediaType());
+    }
+
+    @Test
     public void testCopyConstructor() {
         OutboundMessageContext ctx = new OutboundMessageContext((Configuration) null);
         OutboundMessageContext newCtx = new OutboundMessageContext(ctx);