AbortTest.java

/*
 * Copyright (c) 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
 * 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.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import javax.ws.rs.Priorities;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
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.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;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
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.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class AbortTest {
    private static final String TEXT_CSV = "text/csv";
    private static final String TEXT_HEADER = "text/header";
    private static final String EXPECTED_CSV = "hello;goodbye\nsalutations;farewell";
    private static final List<List<String>> CSV_LIST = Arrays.asList(
            Arrays.asList("hello", "goodbye"),
            Arrays.asList("salutations", "farewell")
    );
    private final String entity = "HI";
    private final String header = "CUSTOM_HEADER";


    @Test
    void testAbortWithGenericEntity() {
        Client client = ClientBuilder.newBuilder()
                .register(AbortRequestFilter.class)
                .register(CsvWriter.class)
                .build();
        String csvString = client.target("http://localhost:8080")
                .request(TEXT_CSV)
                .get(String.class);
        assertEquals(EXPECTED_CSV, csvString);
        client.close();
    }

    public static class AbortRequestFilter implements ClientRequestFilter {

        @Override
        public void filter(ClientRequestContext requestContext) {
            requestContext.abortWith(Response.ok(new GenericEntity<List<List<String>>>(CSV_LIST) {
            }).type(TEXT_CSV).build());
        }
    }

    @Produces(TEXT_CSV)
    public static class CsvWriter implements MessageBodyWriter<List<List<String>>> {

        @Override
        public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
            return List.class.isAssignableFrom(type) && genericType instanceof ParameterizedType
                    && ((ParameterizedType) genericType).getActualTypeArguments()[0] instanceof ParameterizedType
                    && String.class.equals(((ParameterizedType) ((ParameterizedType) genericType).getActualTypeArguments()[0])
                        .getActualTypeArguments()[0]);
        }

        @Override
        public void writeTo(List<List<String>> csvList, Class<?> type, Type genericType, Annotation[] annotations,
                            MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
                throws IOException, WebApplicationException {
            List<String> rows = new ArrayList<>();
            for (List<String> row : csvList) {
                rows.add(String.join(";", row));
            }
            String csv = String.join("\n", rows);

            entityStream.write(csv.getBytes(StandardCharsets.UTF_8));
            entityStream.flush();
        }
    }

    @Test
    void testAbortWithMBWWritingHeaders() {
        try (Response response = ClientBuilder.newClient().register(new ClientRequestFilter() {
            @Override
            public void filter(ClientRequestContext requestContext) throws IOException {
                requestContext.abortWith(Response.ok(entity, TEXT_HEADER).build());
            }
        }).register(new MessageBodyWriter<String>() {

            @Override
            public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
                return mediaType.toString().equals(TEXT_HEADER);
            }

            @Override
            public void writeTo(String s, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
                                MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException,
                    WebApplicationException {
                httpHeaders.add(header, entity);
                entityStream.write(s.getBytes());
            }
        }, Priorities.USER - 1).target("http://localhost:8080").request().get()) {
            Assertions.assertEquals(entity, response.readEntity(String.class));
            Assertions.assertEquals(entity, response.getHeaderString(header));
        }
    }

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

    @Test
    public void testResponseContextCaseInsensitiveKeys() {
        try (Response response = ClientBuilder.newClient()
                .register(new ClientRequestFilter() {
                    @Override
                    public void filter(ClientRequestContext requestContext) throws IOException {
                        requestContext.abortWith(Response.ok()
                                .header("header1", "value")
                                .header("header1", "value1 , value2")
                                .header("header1", "Value3,white space ")
                                .header("header2", "Value4;;Value5")
                                .build());
                    }
                })
                .register(new ClientResponseFilter() {
                    @Override
                    public void filter(ClientRequestContext requestContext, ClientResponseContext context) {
                        Assertions.assertTrue(context.getHeaderString("header1").contains("value"));
                        Assertions.assertTrue(context.getHeaderString("HEADER1").contains("value2"));
                        //White space in value not trimmed
                        Assertions.assertFalse(context.getHeaderString("header1").contains("whitespace"));
                        //Multiple character separator
                        Assertions.assertTrue(context.getHeaderString("header2").contains("Value5"));

                        Assertions.assertTrue(context.getHeaders().containsKey("HEADer1"));
                        Assertions.assertFalse(context.getHeaders().get("HEADer1").isEmpty());
                        Assertions.assertFalse(context.getHeaders().remove("HeAdEr1").isEmpty());
                        Assertions.assertFalse(context.getHeaders().containsKey("header1"));
                    }
                })
                .target("http://localhost:8080")
                .request()
                .get()) {
            Assertions.assertEquals(200, response.getStatus());
        }
    }

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

}