You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by co...@apache.org on 2026/10/13 06:59:36 UTC

(cxf) branch main updated: CXF-6880 - Resolve generic property types in search condition parsers (#3571)

This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/main by this push:
     new 5e5862019d0 CXF-6880 - Resolve generic property types in search condition parsers (#3571)
5e5862019d0 is described below

commit 5e5862019d01d6e545fe165a62ae42a6e4c8e8b2
Author: Colm O hEigeartaigh <co...@users.noreply.github.com>
AuthorDate: Tue Oct 13 07:59:30 2026 +0100

    CXF-6880 - Resolve generic property types in search condition parsers (#3571)
    
    * CXF-6880 - Resolve generic property types in search condition parsers
    
    * Addressing review comment
---
 .../ext/search/AbstractSearchConditionParser.java  | 12 +++--
 .../apache/cxf/jaxrs/ext/search/Beanspector.java   | 54 ++++++++++++++++++-
 .../cxf/jaxrs/ext/search/BeanspectorTest.java      | 63 ++++++++++++++++++++++
 .../cxf/jaxrs/ext/search/fiql/FiqlParserTest.java  | 58 ++++++++++++++++++++
 .../jaxrs/ext/search/odata/ODataParserTest.java    | 61 ++++++++++++++++++++-
 5 files changed, 241 insertions(+), 7 deletions(-)

diff --git a/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/AbstractSearchConditionParser.java b/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/AbstractSearchConditionParser.java
index defd06bb8a6..f0d75a1115c 100644
--- a/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/AbstractSearchConditionParser.java
+++ b/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/AbstractSearchConditionParser.java
@@ -21,6 +21,7 @@ package org.apache.cxf.jaxrs.ext.search;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
 import java.sql.Time;
 import java.sql.Timestamp;
 import java.text.DateFormat;
@@ -183,10 +184,13 @@ public abstract class AbstractSearchConditionParser<T> implements SearchConditio
         try {
             String nextPart = getMethodNameSuffix(names[1]);
             Method getterM = actualType.getMethod("get" + nextPart, new Class[]{});
-            Class<?> returnType = getterM.getReturnType();
+            // The getter may be declared by a generic superclass, e.g. 'PK getId()',
+            // in which case its type has to be resolved against the actual class
+            Type genericReturnType = Beanspector.resolveType(actualType, getterM.getGenericReturnType());
+            Class<?> returnType = Beanspector.getRawType(genericReturnType, getterM.getReturnType());
             boolean returnCollection = InjectionUtils.isSupportedCollectionOrArray(returnType);
             Class<?> actualReturnType = !returnCollection ? returnType
-                : InjectionUtils.getActualType(getterM.getGenericReturnType());
+                : InjectionUtils.getActualType(genericReturnType);
 
             boolean isPrimitive = !returnCollection
                 && InjectionUtils.isPrimitive(returnType) || returnType.isEnum();
@@ -229,7 +233,7 @@ public abstract class AbstractSearchConditionParser<T> implements SearchConditio
             } else {
                 nextObject = actualReturnType.getDeclaredConstructor().newInstance();
             }
-            Method setterM = actualType.getMethod("set" + nextPart, new Class[]{returnType});
+            Method setterM = Beanspector.findSetter(actualType, "set" + nextPart, returnType);
             final Object valueObjectValue;
             if (lastTry || !returnCollection) {
                 valueObjectValue = nextObject;
@@ -245,7 +249,7 @@ public abstract class AbstractSearchConditionParser<T> implements SearchConditio
             }
             lastCastedValue = valueObject;
 
-            TypeInfo nextTypeInfo = new TypeInfo(valueObjectValue.getClass(), getterM.getGenericReturnType());
+            TypeInfo nextTypeInfo = new TypeInfo(valueObjectValue.getClass(), genericReturnType);
             Object response = parseType(originalPropName,
                              nextObject,
                              lastCastedValue,
diff --git a/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/Beanspector.java b/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/Beanspector.java
index 42e949355ac..92c3e167909 100644
--- a/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/Beanspector.java
+++ b/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/Beanspector.java
@@ -22,6 +22,7 @@ import java.beans.IntrospectionException;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -32,6 +33,7 @@ import java.util.Map;
 import java.util.Set;
 
 import org.apache.cxf.jaxrs.ext.search.collections.CollectionCheckInfo;
+import org.apache.cxf.jaxrs.utils.InjectionUtils;
 
 /**
  * Bean introspection utility.
@@ -140,8 +142,56 @@ public class Beanspector<T> {
                                        setters.keySet(), getters.keySet());
             throw new IntrospectionException(msg);
         }
-        return new TypeInfo(m.getReturnType(), m.getGenericReturnType(),
-            primitiveToWrapper(m.getReturnType()));
+        Type genericType = resolveType(tclass, m.getGenericReturnType());
+        Class<?> cls = getRawType(genericType, m.getReturnType());
+        return new TypeInfo(cls, genericType, primitiveToWrapper(cls));
+    }
+
+    /**
+     * Resolves a type variable (for example, the type of a property declared in a generic
+     * superclass such as {@code AbstractEntity<PK extends Serializable>}) against the given
+     * concrete class. Any other type is returned unchanged.
+     */
+    static Type resolveType(Class<?> concreteClass, Type type) {
+        if (type instanceof TypeVariable) {
+            Type resolved = InjectionUtils.getSuperType(concreteClass, (TypeVariable<?>)type);
+            if (resolved != null) {
+                return resolved;
+            }
+        }
+        return type;
+    }
+
+    static Class<?> getRawType(Type type, Class<?> defaultType) {
+        Class<?> cls = InjectionUtils.getRawType(type);
+        return cls != null ? cls : defaultType;
+    }
+
+    /**
+     * Finds a public single-argument setter which accepts the given parameter type. The setter
+     * may be declared by a generic superclass (for example, {@code setId(PK id)}), in which case
+     * its erased parameter type is a supertype of the resolved one. As in {@link #init()}, the
+     * setter with the most specialized parameter type is preferred.
+     */
+    static Method findSetter(Class<?> cls, String name, Class<?> paramType) throws NoSuchMethodException {
+        try {
+            return cls.getMethod(name, paramType);
+        } catch (NoSuchMethodException ex) {
+            Method setter = null;
+            for (Method m : cls.getMethods()) {
+                if (m.getName().equals(name) && m.getParameterCount() == 1) {
+                    Class<?> type = m.getParameterTypes()[0];
+                    if (type.isAssignableFrom(paramType)
+                        && (setter == null || setter.getParameterTypes()[0].isAssignableFrom(type))) {
+                        setter = m;
+                    }
+                }
+            }
+            if (setter == null) {
+                throw ex;
+            }
+            return setter;
+        }
     }
 
     public Beanspector<T> swap(T newobject) throws Exception {
diff --git a/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/BeanspectorTest.java b/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/BeanspectorTest.java
index 699af0e93d7..831ab0c70fc 100644
--- a/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/BeanspectorTest.java
+++ b/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/BeanspectorTest.java
@@ -18,6 +18,8 @@
  */
 package org.apache.cxf.jaxrs.ext.search;
 
+import java.io.Serializable;
+import java.lang.reflect.Method;
 import java.util.Date;
 import java.util.Set;
 
@@ -25,6 +27,7 @@ import org.junit.Ignore;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 public class BeanspectorTest {
@@ -162,4 +165,64 @@ public class BeanspectorTest {
         
         public void setSimpleBean(String simpleBean) { }
     }
+
+    @Test
+    public void testFindSetterExactMatch() throws Exception {
+        Method m = Beanspector.findSetter(OverloadedSetterEntity.class, "setName", String.class);
+        assertEquals(String.class, m.getParameterTypes()[0]);
+    }
+
+    @Test
+    public void testFindSetterGenericSuperclass() throws Exception {
+        // 'setId(PK)' erases to 'setId(Serializable)', there is no 'setId(Long)'
+        Method m = Beanspector.findSetter(OverloadedSetterEntity.class, "setId", Long.class);
+        assertEquals(Serializable.class, m.getParameterTypes()[0]);
+    }
+
+    @Test
+    public void testFindSetterPrefersMostSpecializedType() throws Exception {
+        // Both 'setName(Object)' and 'setName(CharSequence)' accept a StringBuilder
+        Method m = Beanspector.findSetter(OverloadedSetterEntity.class, "setName", StringBuilder.class);
+        assertEquals(CharSequence.class, m.getParameterTypes()[0]);
+    }
+
+    @Test
+    public void testFindSetterNotFound() {
+        assertThrows(NoSuchMethodException.class,
+            () -> Beanspector.findSetter(OverloadedSetterEntity.class, "setId", Object.class));
+    }
+
+    @Ignore
+    public abstract static class AbstractIdEntity<PK extends Serializable> {
+        private PK id;
+
+        public PK getId() {
+            return id;
+        }
+
+        public void setId(PK id) {
+            this.id = id;
+        }
+    }
+
+    @Ignore
+    public static class OverloadedSetterEntity extends AbstractIdEntity<Long> {
+        private Object name;
+
+        public Object getName() {
+            return name;
+        }
+
+        public void setName(Object name) {
+            this.name = name;
+        }
+
+        public void setName(CharSequence name) {
+            this.name = name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+    }
 }
diff --git a/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/fiql/FiqlParserTest.java b/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/fiql/FiqlParserTest.java
index db406954c95..5c4b6e16915 100644
--- a/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/fiql/FiqlParserTest.java
+++ b/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/fiql/FiqlParserTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.cxf.jaxrs.ext.search.fiql;
 
+import java.io.Serializable;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
@@ -448,6 +449,22 @@ public class FiqlParserTest {
         }
     }
 
+    @Test
+    public void testGenericSuperclassProperty() throws SearchParseException {
+        SearchCondition<CallHeader> filter = new FiqlParser<>(CallHeader.class).parse("id==1");
+        assertEquals(Long.valueOf(1L), filter.getStatement().getValue());
+        assertTrue(filter.isMet(new CallHeader().withId(1L)));
+        assertFalse(filter.isMet(new CallHeader().withId(2L)));
+    }
+
+    @Test
+    public void testNestedGenericSuperclassProperty() throws SearchParseException {
+        SearchCondition<CallHeader> filter = new FiqlParser<>(CallHeader.class).parse("call.id==1");
+        assertEquals(Long.valueOf(1L), ((Call)filter.getStatement().getValue()).getId());
+        assertTrue(filter.isMet(new CallHeader().withCall(new Call().withId(1L))));
+        assertFalse(filter.isMet(new CallHeader().withCall(new Call().withId(2L))));
+    }
+
     @Ignore
     public static class Condition {
         private String name;
@@ -616,6 +633,47 @@ public class FiqlParserTest {
     }
 
 
+    public abstract static class AbstractEntity<PK extends Serializable> {
+        private PK id;
+
+        public PK getId() {
+            return id;
+        }
+
+        public void setId(PK id) {
+            this.id = id;
+        }
+    }
+
+    public static class Call extends AbstractEntity<Long> {
+        Call withId(Long newId) {
+            setId(newId);
+            return this;
+        }
+    }
+
+    public static class CallHeader extends AbstractEntity<Long> {
+        private Call call;
+
+        public Call getCall() {
+            return call;
+        }
+
+        public void setCall(Call call) {
+            this.call = call;
+        }
+
+        CallHeader withId(Long newId) {
+            setId(newId);
+            return this;
+        }
+
+        CallHeader withCall(Call newCall) {
+            setCall(newCall);
+            return this;
+        }
+    }
+
     public static class Job {
         private List<Task> tasks;
         public List<Task> getTasks() {
diff --git a/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/odata/ODataParserTest.java b/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/odata/ODataParserTest.java
index a2bb3ff666d..c4eba5dbd60 100644
--- a/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/odata/ODataParserTest.java
+++ b/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/odata/ODataParserTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.cxf.jaxrs.ext.search.odata;
 
+import java.io.Serializable;
 import java.util.Collections;
 
 import org.apache.cxf.jaxrs.ext.search.SearchCondition;
@@ -26,6 +27,7 @@ import org.apache.cxf.jaxrs.ext.search.SearchParseException;
 import org.junit.Before;
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -117,6 +119,47 @@ public class ODataParserTest {
         }
     }
 
+    public abstract static class AbstractEntity<PK extends Serializable> {
+        private PK id;
+
+        public PK getId() {
+            return id;
+        }
+
+        public void setId(PK id) {
+            this.id = id;
+        }
+    }
+
+    public static class Call extends AbstractEntity<Long> {
+        Call withId(Long newId) {
+            setId(newId);
+            return this;
+        }
+    }
+
+    public static class CallHeader extends AbstractEntity<Long> {
+        private Call call;
+
+        public Call getCall() {
+            return call;
+        }
+
+        public void setCall(Call call) {
+            this.call = call;
+        }
+
+        CallHeader withId(Long newId) {
+            setId(newId);
+            return this;
+        }
+
+        CallHeader withCall(Call newCall) {
+            setCall(newCall);
+            return this;
+        }
+    }
+
     @Before
     public void setUp() {
         parser = new ODataParser<>(Person.class, Collections.<String, String>emptyMap(),
@@ -196,4 +239,20 @@ public class ODataParserTest {
         assertTrue(filter.isMet(new Person("Tom", "Bombadil").withSsn(553232222L)));
         assertFalse(filter.isMet(new Person("Tom", "Bombadil").withHourlyRate(748232221L)));
     }
-}
\ No newline at end of file
+
+    @Test
+    public void testFilterByGenericSuperclassProperty() throws SearchParseException {
+        SearchCondition< CallHeader > filter = new ODataParser<>(CallHeader.class).parse("id eq 1");
+        assertEquals(Long.valueOf(1L), filter.getStatement().getValue());
+        assertTrue(filter.isMet(new CallHeader().withId(1L)));
+        assertFalse(filter.isMet(new CallHeader().withId(2L)));
+    }
+
+    @Test
+    public void testFilterByNestedGenericSuperclassProperty() throws SearchParseException {
+        SearchCondition< CallHeader > filter = new ODataParser<>(CallHeader.class).parse("call.id eq 1");
+        assertEquals(Long.valueOf(1L), ((Call)filter.getStatement().getValue()).getId());
+        assertTrue(filter.isMet(new CallHeader().withCall(new Call().withId(1L))));
+        assertFalse(filter.isMet(new CallHeader().withCall(new Call().withId(2L))));
+    }
+}