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()); }