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