Prevent leaking of annotation in a reused response. Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java b/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java index 52d812f..a1dda0a 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java
@@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 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 @@ -21,7 +21,6 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; @@ -280,13 +279,8 @@ model.getPriority(ContainerResponseFilter.class))); } } - - _readerInterceptors.addAll( - StreamSupport.stream(processingProviders.getGlobalReaderInterceptors().spliterator(), false) - .collect(Collectors.toList())); - _writerInterceptors.addAll( - StreamSupport.stream(processingProviders.getGlobalWriterInterceptors().spliterator(), false) - .collect(Collectors.toList())); + processingProviders.getGlobalReaderInterceptors().forEach(_readerInterceptors::add); + processingProviders.getGlobalWriterInterceptors().forEach(_writerInterceptors::add); if (resourceMethod != null) { addNameBoundFiltersAndInterceptors( @@ -458,9 +452,7 @@ if (entityAnn.length == 0) { response.setEntityAnnotations(methodAnnotations); } else { - final Annotation[] mergedAnn = Arrays.copyOf(methodAnnotations, - methodAnnotations.length + entityAnn.length); - System.arraycopy(entityAnn, 0, mergedAnn, methodAnnotations.length, entityAnn.length); + final Annotation[] mergedAnn = mergeDistinctAnnotations(methodAnnotations, entityAnn); response.setEntityAnnotations(mergedAnn); } } @@ -487,6 +479,22 @@ return jaxrsResponse; } + private static Annotation[] mergeDistinctAnnotations(Annotation[] existing, Annotation[] newOnes) { + List<Annotation> merged = new ArrayList<>(existing.length + newOnes.length); + Collections.addAll(merged, existing); + + newOnesLoop: + for (Annotation n : newOnes) { + for (Annotation ex : existing) { + if (ex == n) { + continue newOnesLoop; + } + } + merged.add(n); + } + return merged.toArray(new Annotation[0]); + } + private Type unwrapInvocableResponseType(ContainerRequest request, Type entityType) { if (isCompletionStageResponseType && request.resolveProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.FALSE)) {
diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/Jersey5939Test.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/Jersey5939Test.java new file mode 100644 index 0000000..f48dbf9 --- /dev/null +++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/Jersey5939Test.java
@@ -0,0 +1,81 @@ +/* + * 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.tests.api; + +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Check the reused response does not leak by additional Annotations (ContainerResponse#setEntityAnnottaions. + */ +public class Jersey5939Test extends JerseyTest { + + private final List<ContainerResponse> capturedResponses = new ArrayList<>(); + + @Override + protected Application configure() { + return new ResourceConfig(Restlet.class).register(new ContainerResponseFilter() { + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + if (responseContext instanceof ContainerResponse) { + capturedResponses.add((ContainerResponse) responseContext); + } + } + }); + } + + @Test + public void testIssue5939() { + for (int i = 0; i < 10; i++) { + Response response = target("foo/bar").request().get(); + assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); + ContainerResponse containerResponse = capturedResponses.get(i); + Annotation[] annotations = containerResponse.getEntityAnnotations(); + // [@javax.ws.rs.GET(), @javax.ws.rs.Path("/bar")] + assertEquals(2, annotations.length, "Found " + annotations.length + " annotations, in iteration " + i); + } + } + + @Path("/foo") + public static class Restlet { + + private static final Response RESPONSE_204 = Response.noContent().build(); + + @GET + @Path("/bar") + public Response fooBar() { + return RESPONSE_204; + } + } +} \ No newline at end of file