Make aborted response HttpHeaders appendable

Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java b/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
index 572adf0..7618109 100644
--- a/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
+++ b/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
@@ -26,11 +26,20 @@
 import javax.ws.rs.client.ClientBuilder;
 import javax.ws.rs.client.ClientRequestContext;
 import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.ClientResponseContext;
+import javax.ws.rs.client.ClientResponseFilter;
+import javax.ws.rs.core.Application;
 import javax.ws.rs.core.GenericEntity;
+import javax.ws.rs.core.Link;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.Variant;
 import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.ReaderInterceptor;
+import javax.ws.rs.ext.ReaderInterceptorContext;
+import javax.ws.rs.ext.RuntimeDelegate;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.lang.annotation.Annotation;
@@ -39,9 +48,12 @@
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 
-//import static java.nio.charset.StandardCharsets;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class AbortTest {
@@ -52,6 +64,9 @@
             Arrays.asList("hello", "goodbye"),
             Arrays.asList("salutations", "farewell")
     );
+    private final String entity = "HI";
+    private final String header = "CUSTOM_HEADER";
+
 
     @Test
     void testAbortWithGenericEntity() {
@@ -103,8 +118,6 @@
 
     @Test
     void testAbortWithMBWWritingHeaders() {
-        final String entity = "HI";
-        final String header = "CUSTOM_HEADER";
         try (Response response = ClientBuilder.newClient().register(new ClientRequestFilter() {
             @Override
             public void filter(ClientRequestContext requestContext) throws IOException {
@@ -130,4 +143,145 @@
         }
     }
 
+    @Test
+    void testInterceptorHeaderAdd() {
+        final String header2 = "CUSTOM_HEADER_2";
+
+        try (Response response = ClientBuilder.newClient().register(new ClientRequestFilter() {
+            @Override
+            public void filter(ClientRequestContext requestContext) throws IOException {
+                requestContext.abortWith(Response.ok().entity(entity).build());
+            }
+        }).register(new ReaderInterceptor() {
+                    @Override
+                    public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException {
+                        MultivaluedMap<String, String> headers = context.getHeaders();
+                        headers.put(header, Collections.singletonList(entity));
+                        headers.add(header2, entity);
+                        return context.proceed();
+                    }
+                })
+                .target("http://localhost:8080").request().get()) {
+            Assertions.assertEquals(entity, response.readEntity(String.class));
+            Assertions.assertEquals(entity, response.getHeaderString(header));
+            Assertions.assertEquals(entity, response.getHeaderString(header2));
+        }
+    }
+
+    @Test
+    void testInterceptorHeaderIterate() {
+        final AtomicReference<String> originalHeader = new AtomicReference<>();
+
+        try (Response response = ClientBuilder.newClient().register(new ClientRequestFilter() {
+                    @Override
+                    public void filter(ClientRequestContext requestContext) throws IOException {
+                        requestContext.abortWith(Response.ok().header(header, header).entity(entity).build());
+                    }
+                }).register(new ReaderInterceptor() {
+                    @Override
+                    public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException {
+                        MultivaluedMap<String, String> headers = context.getHeaders();
+                        Iterator<Map.Entry<String, List<String>>> it = headers.entrySet().iterator();
+                        while (it.hasNext()) {
+                            Map.Entry<String, List<String>> next = it.next();
+                            if (header.equals(next.getKey())) {
+                                originalHeader.set(next.setValue(Collections.singletonList(entity)).get(0));
+                            }
+                        }
+                        return context.proceed();
+                    }
+                })
+                .target("http://localhost:8080").request().get()) {
+            Assertions.assertEquals(entity, response.readEntity(String.class));
+            Assertions.assertEquals(entity, response.getHeaderString(header));
+            Assertions.assertEquals(header, originalHeader.get());
+        }
+    }
+
+    @Test
+    void testNullHeader() {
+        final AtomicReference<String> originalHeader = new AtomicReference<>();
+        RuntimeDelegate.setInstance(new StringHeaderRuntimeDelegate(RuntimeDelegate.getInstance()));
+        try (Response response = ClientBuilder.newClient().register(new ClientRequestFilter() {
+                    @Override
+                    public void filter(ClientRequestContext requestContext) throws IOException {
+                        requestContext.abortWith(Response.ok()
+                                .header(header, new StringHeader())
+                                .entity(entity).build());
+                    }
+                }).register(new ClientResponseFilter() {
+                    @Override
+                    public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext)
+                            throws IOException {
+                        originalHeader.set(responseContext.getHeaderString(header));
+                    }
+                })
+                .target("http://localhost:8080").request().get()) {
+            Assertions.assertEquals(entity, response.readEntity(String.class));
+            Assertions.assertEquals("", originalHeader.get());
+        }
+    }
+
+    private static class StringHeader extends AtomicReference<String> {
+
+    }
+
+    private static class StringHeaderDelegate implements RuntimeDelegate.HeaderDelegate<StringHeader> {
+        @Override
+        public StringHeader fromString(String value) {
+            StringHeader stringHeader = new StringHeader();
+            stringHeader.set(value);
+            return stringHeader;
+        }
+
+        @Override
+        public String toString(StringHeader value) {
+            //on purpose
+            return null;
+        }
+    }
+
+    private static class StringHeaderRuntimeDelegate extends RuntimeDelegate {
+        private final RuntimeDelegate original;
+
+        private StringHeaderRuntimeDelegate(RuntimeDelegate original) {
+            this.original = original;
+        }
+
+        @Override
+        public UriBuilder createUriBuilder() {
+            return original.createUriBuilder();
+        }
+
+        @Override
+        public Response.ResponseBuilder createResponseBuilder() {
+            return original.createResponseBuilder();
+        }
+
+        @Override
+        public Variant.VariantListBuilder createVariantListBuilder() {
+            return original.createVariantListBuilder();
+        }
+
+        @Override
+        public <T> T createEndpoint(Application application, Class<T> endpointType)
+                throws IllegalArgumentException, UnsupportedOperationException {
+            return original.createEndpoint(application, endpointType);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> HeaderDelegate<T> createHeaderDelegate(Class<T> type) throws IllegalArgumentException {
+            if (StringHeader.class.equals(type)) {
+                return (HeaderDelegate<T>) new StringHeaderDelegate();
+            }
+            return original.createHeaderDelegate(type);
+        }
+
+        @Override
+        public Link.Builder createLinkBuilder() {
+            return original.createLinkBuilder();
+        }
+    }
+
 }
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java
index 4765056..75b1987 100644
--- a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2025 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
@@ -129,60 +129,107 @@
      * @return transformed map view.
      */
     public static <K, V, O> Map<K, V> mapView(Map<K, O> originalMap, Function<O, V> valuesTransformer) {
-        return new AbstractMap<K, V>() {
-            @Override
-            public Set<Entry<K, V>> entrySet() {
-                return new AbstractSet<Entry<K, V>>() {
+        return new KVOMap<K, V, O>(originalMap, valuesTransformer);
+    }
 
+    private static class KVOMap<K, V, O> extends AbstractMap<K, V> {
+            protected final Map<K, O> originalMap;
+            protected final Function<O, V> valuesTransformer;
 
-                    Set<Entry<K, O>> originalSet = originalMap.entrySet();
-                    Iterator<Entry<K, O>> original = originalSet.iterator();
+        private KVOMap(Map<K, O> originalMap, Function<O, V> valuesTransformer) {
+            this.originalMap = originalMap;
+            this.valuesTransformer = valuesTransformer;
+        }
 
-                    @Override
-                    public Iterator<Entry<K, V>> iterator() {
-                        return new Iterator<Entry<K, V>>() {
-                            @Override
-                            public boolean hasNext() {
-                                return original.hasNext();
-                            }
+        @Override
+        public Set<Entry<K, V>> entrySet() {
+            return new AbstractSet<Entry<K, V>>() {
 
-                            @Override
-                            public Entry<K, V> next() {
+                Set<Entry<K, O>> originalSet = originalMap.entrySet();
+                Iterator<Entry<K, O>> original = originalSet.iterator();
 
-                                Entry<K, O> next = original.next();
+                @Override
+                public Iterator<Entry<K, V>> iterator() {
+                    return new Iterator<Entry<K, V>>() {
+                        @Override
+                        public boolean hasNext() {
+                            return original.hasNext();
+                        }
 
-                                return new Entry<K, V>() {
-                                    @Override
-                                    public K getKey() {
-                                        return next.getKey();
-                                    }
+                        @Override
+                        public Entry<K, V> next() {
 
-                                    @Override
-                                    public V getValue() {
-                                        return valuesTransformer.apply(next.getValue());
-                                    }
+                            Entry<K, O> next = original.next();
 
-                                    @Override
-                                    public V setValue(V value) {
-                                        throw new UnsupportedOperationException("Not supported.");
-                                    }
-                                };
-                            }
+                            return new Entry<K, V>() {
+                                @Override
+                                public K getKey() {
+                                    return next.getKey();
+                                }
 
-                            @Override
-                            public void remove() {
-                                original.remove();
-                            }
-                        };
-                    }
+                                @Override
+                                public V getValue() {
+                                    return valuesTransformer.apply(next.getValue());
+                                }
 
-                    @Override
-                    public int size() {
-                        return originalSet.size();
-                    }
-                };
-            }
-        };
+                                @Override
+                                public V setValue(V value) {
+                                    return KVOMap.this.setValue(next, value);
+                                }
+                            };
+                        }
+
+                        @Override
+                        public void remove() {
+                            original.remove();
+                        }
+                    };
+                }
+
+                @Override
+                public int size() {
+                    return originalSet.size();
+                }
+            };
+        }
+
+        protected V setValue(Map.Entry<K, O> entry, V value) {
+            throw new UnsupportedOperationException("Not supported.");
+        }
+    }
+
+    /**
+     * Create a {@link Map} view, which transforms the values of provided original map.
+     * <p>
+     *
+     * @param originalMap       provided map.
+     * @param valuesTransformer values transformer.
+     * @return transformed map view.
+     */
+    public static Map<String, List<String>> mapObjectToStringView(Map<String, List<Object>> originalMap,
+                                                            Function<List<Object>, List<String>> valuesTransformer) {
+        return new ObjectToStringMap(originalMap, valuesTransformer);
+    }
+
+    private static class ObjectToStringMap extends KVOMap<String, List<String>, List<Object>> {
+
+        private ObjectToStringMap(Map<String, List<Object>> originalMap, Function<List<Object>, List<String>> valuesTransformer) {
+            super(originalMap, valuesTransformer);
+        }
+
+        @Override
+        protected List<String> setValue(Entry<String, List<Object>> entry, List<String> value) {
+            @SuppressWarnings("unchecked")
+            final List<Object> old = entry.setValue((List<Object>) (List<?>) value);
+            return valuesTransformer.apply(old);
+        }
+
+        @Override
+        public List<String> put(String key, List<String> value) {
+            @SuppressWarnings("unchecked")
+            final List<Object> old = originalMap.put(key, (List<Object>) (List<?>) value);
+            return valuesTransformer.apply(old);
+        }
     }
 
     /**
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java
index 5b47d6b..07070cb 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 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
@@ -210,7 +210,7 @@
         }
 
         return new AbstractMultivaluedMap<String, String>(
-                Views.mapView(headers, input -> HeaderUtils.asStringList(input, rd))
+                Views.mapObjectToStringView(headers, input -> HeaderUtils.asStringList(input, rd))
         ) {
         };
     }
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
index 6218d8b..f16b629 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
@@ -316,7 +316,11 @@
         }
 
         final Iterator<String> valuesIterator = values.iterator();
-        StringBuilder buffer = new StringBuilder(valuesIterator.next());
+        String next = valuesIterator.next();
+        if (next == null) {
+            next = "";
+        }
+        StringBuilder buffer = new StringBuilder(next);
         while (valuesIterator.hasNext()) {
             buffer.append(',').append(valuesIterator.next());
         }