This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/CXF-7647 in repository https://gitbox.apache.org/repos/asf/cxf.git
commit a556b6a6d2123c18ba4aee6b2a2d27d94ecec5e2 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Oct 22 06:41:46 2026 +0100 CXF-7647: Don't register duplicate endpoints for compiler-generated bridge methods --- .../org/apache/cxf/jaxrs/utils/ResourceUtils.java | 39 +++++++- .../apache/cxf/jaxrs/utils/ResourceUtilsTest.java | 103 +++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ResourceUtils.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ResourceUtils.java index 5b29d15b132..1b5b71ad3a3 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ResourceUtils.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/utils/ResourceUtils.java @@ -336,8 +336,18 @@ public final class ResourceUtils { } for (Method m : serviceClass.getMethods()) { if (m.isBridge() || m.isSynthetic()) { - //if a bridge/synthetic method isn't already mapped to something, go ahead and do it Method annotatedMethod = AnnotationUtils.getAnnotatedMethod(serviceClass, m); + if (annotatedMethod == m && m.isBridge()) { + //no annotated method was found for the bridge other than itself, i.e. the JAX-RS + //annotations were copied onto the bridge by the compiler but with the erased parameter + //types, so if the method it bridges to is already mapped then reuse its operation + Method bridged = findBridgedMethod(serviceClass, m); + if (bridged != null && md.getOperationResourceInfo(bridged) != null) { + bindResourceMethod(md, m, bridged); + continue; + } + } + //if a bridge/synthetic method isn't already mapped to something, go ahead and do it if (!annotatedMethods.containsKey(annotatedMethod)) { evaluateResourceMethod(cri, enableStatic, md, m, annotatedMethod); annotatedMethods.put(annotatedMethod, m); @@ -352,6 +362,33 @@ public final class ResourceUtils { cri.setMethodDispatcher(md); } + private static Method findBridgedMethod(Class<?> serviceClass, Method bridge) { + Class<?>[] bridgeParamTypes = bridge.getParameterTypes(); + Method bridged = null; + for (Method m : serviceClass.getMethods()) { + if (m.isBridge() || m.isSynthetic() + || !m.getName().equals(bridge.getName()) + || m.getDeclaringClass() != bridge.getDeclaringClass() + || m.getParameterCount() != bridgeParamTypes.length + || !bridge.getReturnType().isAssignableFrom(m.getReturnType())) { + continue; + } + Class<?>[] paramTypes = m.getParameterTypes(); + boolean matches = true; + for (int i = 0; i < paramTypes.length && matches; i++) { + matches = bridgeParamTypes[i].isAssignableFrom(paramTypes[i]); + } + if (matches) { + if (bridged != null) { + // ambiguous, more than one candidate + return null; + } + bridged = m; + } + } + return bridged; + } + private static void bindResourceMethod(MethodDispatcher md, Method m, Method bound) { final OperationResourceInfo ori = md.getOperationResourceInfo(bound); if (ori != null && !ori.getMethodToInvoke().equals(m)) { diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/ResourceUtilsTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/ResourceUtilsTest.java index 7d2d1e75fe1..679be80e0d8 100644 --- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/ResourceUtilsTest.java +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/utils/ResourceUtilsTest.java @@ -30,10 +30,15 @@ import java.util.Set; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.BeanParam; import jakarta.ws.rs.GET; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; import jakarta.xml.bind.annotation.XmlRootElement; import org.apache.cxf.BusFactory; @@ -170,6 +175,48 @@ public class ResourceUtilsTest { assertEquals("GET", ori.getHttpMethod()); } + @Test + public void testInterfaceBridgeMethodsWithBeanParam() throws Exception { + ClassResourceInfo cri = ResourceUtils.createClassResourceInfo(VersionedUserResource.class, + VersionedUserResource.class, true, true); + assertNotNull(cri); + Set<OperationResourceInfo> oris = cri.getMethodDispatcher().getOperationResourceInfos(); + assertEquals(2, oris.size()); + for (OperationResourceInfo ori : oris) { + Method m = ori.getMethodToInvoke(); + assertFalse(m.toString(), m.isBridge()); + assertEquals(UserIdParam.class, m.getParameterTypes()[0]); + } + + for (Method m : VersionedUserResource.class.getMethods()) { + if (m.isBridge()) { + OperationResourceInfo ori = cri.getMethodDispatcher().getOperationResourceInfo(m); + assertNotNull(m.toString(), ori); + assertFalse(ori.getMethodToInvoke().isBridge()); + assertEquals(m.getName(), ori.getMethodToInvoke().getName()); + } + } + } + + @Test + public void testBridgeMethodToAnnotatedSuperInterfaceMethod() throws Exception { + ClassResourceInfo cri = ResourceUtils.createClassResourceInfo(OverridingAnnotatedResource.class, + OverridingAnnotatedResource.class, true, true); + assertNotNull(cri); + Set<OperationResourceInfo> oris = cri.getMethodDispatcher().getOperationResourceInfos(); + assertEquals(2, oris.size()); + + OperationResourceInfo ori = cri.getMethodDispatcher().getOperationResourceInfo( + OverridingAnnotatedResource.class.getMethod("update", String.class)); + assertEquals("PUT", ori.getHttpMethod()); + assertEquals("/own", ori.getURITemplate().getValue()); + + ori = cri.getMethodDispatcher().getOperationResourceInfo( + OverridingAnnotatedResource.class.getMethod("update", Object.class)); + assertEquals("POST", ori.getHttpMethod()); + assertEquals("/base", ori.getURITemplate().getValue()); + } + @Test public void testClassResourceInfoWithBridgeMethod() throws Exception { ClassResourceInfo cri = @@ -413,6 +460,62 @@ public class ResourceUtilsTest { } } + public abstract static class RequestParam<T> { + @PathParam("id") + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } + + public static class UserIdParam extends RequestParam<String> { + + } + + public interface VersionedResource<U, I extends RequestParam<String>> { + Response update(I idParam, U updateDto); + + default Response updateByPost(I idParam, U updateDto) { + return update(idParam, updateDto); + } + } + + @Path("/users") + public interface VersionedUserResource extends VersionedResource<Book, UserIdParam> { + @PATCH + @Path("{id}") + @Override + Response update(@BeanParam UserIdParam idParam, Book updateDto); + + @POST + @Path("{id}") + @Override + default Response updateByPost(@BeanParam UserIdParam idParam, Book updateDto) { + return VersionedResource.super.updateByPost(idParam, updateDto); + } + } + + public interface AnnotatedGenericResource<T> { + @POST + @Path("base") + String update(T dto); + } + + @Path("/overriding") + public static class OverridingAnnotatedResource implements AnnotatedGenericResource<String> { + @PUT + @Path("own") + @Override + public String update(String dto) { + return dto; + } + } + @XmlRootElement public static class OrderItem {
