diff --git a/fluss-client/src/main/java/org/apache/fluss/client/converter/PojoType.java b/fluss-client/src/main/java/org/apache/fluss/client/converter/PojoType.java index 78af497000..7790b91438 100644 --- a/fluss-client/src/main/java/org/apache/fluss/client/converter/PojoType.java +++ b/fluss-client/src/main/java/org/apache/fluss/client/converter/PojoType.java @@ -66,8 +66,8 @@ static PojoType of(Class pojoClass) { Constructor ctor = requirePublicDefaultConstructor(pojoClass); Map allFields = discoverAllInstanceFields(pojoClass); - Map getters = discoverGetters(pojoClass); - Map setters = discoverSetters(pojoClass); + Map getters = discoverGetters(pojoClass, allFields); + Map setters = discoverSetters(pojoClass, allFields); Map props = new LinkedHashMap<>(); for (Map.Entry e : allFields.entrySet()) { @@ -85,10 +85,11 @@ static PojoType of(Class pojoClass) { if (!publicField) { // When not a public field, require both getter and setter if (getter == null || setter == null) { + final String capitalizedName = capitalize(name); throw new IllegalArgumentException( String.format( "POJO class %s field '%s' must be public or have both getter and setter (get%s/set%s).", - pojoClass.getName(), name, capitalize(name), capitalize(name))); + pojoClass.getName(), name, capitalizedName, capitalizedName)); } } props.put( @@ -108,22 +109,30 @@ private static void validatePublicClass(Class pojoClass) { } private static Constructor requirePublicDefaultConstructor(Class pojoClass) { - try { - Constructor ctor = pojoClass.getDeclaredConstructor(); - if (!Modifier.isPublic(ctor.getModifiers())) { - throw new IllegalArgumentException( - String.format( - "POJO class %s must have a public default constructor.", - pojoClass.getName())); + Constructor[] ctors = (Constructor[]) pojoClass.getConstructors(); + for (Constructor c : ctors) { + if (!Modifier.isPublic(c.getModifiers())) { + continue; + } + if (c.getParameterCount() == 0) { + + return c; + } + if (c.getParameterCount() == 1 + && pojoClass + .getName() + .equals( + c.getParameterTypes()[0].getName() + + "$" + + pojoClass.getSimpleName())) { + return c; } - return ctor; - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException( - String.format( - "POJO class %s must have a public default constructor.", - pojoClass.getName()), - e); } + + throw new IllegalArgumentException( + String.format( + "POJO class %s must have a public default constructor.", + pojoClass.getName())); } private static Map discoverAllInstanceFields(Class clazz) { @@ -135,6 +144,13 @@ private static Map discoverAllInstanceFields(Class clazz) { if (Modifier.isStatic(mod) || Modifier.isTransient(mod)) { continue; } + // Skip references to enclosing class + if (f.getName().startsWith("this$")) { + final Class type = f.getType(); + if ((type.getName() + "$" + clazz.getSimpleName()).equals(clazz.getName())) { + continue; + } + } f.setAccessible(true); fields.putIfAbsent(f.getName(), f); } @@ -143,32 +159,68 @@ private static Map discoverAllInstanceFields(Class clazz) { return fields; } - private static Map discoverGetters(Class clazz) { - Map getters = new HashMap<>(); + private static Map discoverGetters( + Class clazz, Map fieldMap) { + final Map getters = new HashMap<>(); for (Method m : clazz.getMethods()) { // public methods incl. inherited - if (m.getParameterCount() == 0 - && m.getName().startsWith("get") - && !m.getReturnType().equals(void.class)) { - String prop = decapitalize(m.getName().substring(3)); + final String prop = getGetterProp(m); + if (fieldMap.containsKey(prop)) { getters.put(prop, m); } } return getters; } - private static Map discoverSetters(Class clazz) { - Map setters = new HashMap<>(); + private static String getGetterProp(Method m) { + if (m.getParameterCount() != 0) { + return null; + } + final Class returnType = m.getReturnType(); + if (void.class.equals(returnType) || Void.class.equals(returnType)) { + return null; + } + final String name = m.getName(); + if (name.startsWith("get")) { + return decapitalize(name.substring(3)); + } + if (returnType.equals(boolean.class) || returnType.equals(Boolean.class)) { + if (name.startsWith("is")) { + return decapitalize(name.substring(2)); + } + if (name.startsWith("has")) { + return decapitalize(name.substring(3)); + } + } + return null; + } + + private static Map discoverSetters( + Class clazz, Map fieldMap) { + final Map setters = new HashMap<>(); for (Method m : clazz.getMethods()) { // public methods incl. inherited - if (m.getParameterCount() == 1 - && m.getName().startsWith("set") - && m.getReturnType().equals(void.class)) { - String prop = decapitalize(m.getName().substring(3)); + final String prop = getSetterProp(m); + if (fieldMap.containsKey(prop)) { setters.put(prop, m); } } return setters; } + private static String getSetterProp(Method m) { + if (m.getParameterCount() != 1) { + return null; + } + final Class returnType = m.getReturnType(); + if (!void.class.equals(returnType) && !Void.class.equals(returnType)) { + return null; + } + final String name = m.getName(); + if (name.startsWith("set")) { + return decapitalize(name.substring(3)); + } + return null; + } + private static String capitalize(String s) { if (s == null || s.isEmpty()) { return s; diff --git a/fluss-client/src/test/java/org/apache/fluss/client/converter/PojoTypeTest.java b/fluss-client/src/test/java/org/apache/fluss/client/converter/PojoTypeTest.java new file mode 100644 index 0000000000..e2d44f5814 --- /dev/null +++ b/fluss-client/src/test/java/org/apache/fluss/client/converter/PojoTypeTest.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.fluss.client.converter; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** Basic tests for {@link PojoType}. */ +class PojoTypeTest { + @Test + void test() { + assertThatThrownBy(() -> PojoType.of(ClassWithNoPublicConstructor.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must have a public default constructor."); + + assertThatThrownBy(() -> PojoType.of(ClassWithNonWithNonPublicField.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(" must be public."); + + assertThatThrownBy(() -> PojoType.of(PublicClass.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "Primitive types are not allowed; all fields must be nullable (use wrapper types)."); + + assertThatThrownBy(() -> PojoType.of(PublicClass.InnerClass.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "Primitive types are not allowed; all fields must be nullable (use wrapper types)."); + + assertThatThrownBy(() -> PojoType.of(PublicWithNonPrimitive.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must be public or have both getter and setter"); + + assertThatThrownBy(() -> PojoType.of(PublicWithPublicWithBoolean.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must be public or have both getter and setter"); + + assertThatThrownBy(() -> PojoType.of(PublicWithPublicWithBooleanWithGetterOnly.class)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must be public or have both getter and setter"); + + PojoType.of(PublicWithPublicWithBooleanWithGetterAndSetter.class); + PojoType.of(PublicWithPublicWithBooleanWithIsAndSetter.class); + PojoType.of(PublicWithPublicWithBooleanWithHasAndSetter.class); + PojoType.of(PublicWithPublicNonPrimitive.class); + } + + public class ClassWithNoPublicConstructor { + int f; + int j; + + private ClassWithNoPublicConstructor() {} + } + + class ClassWithNonWithNonPublicField { + int f; + int j; + } + + public class PublicClass { + public class InnerClass { + final int e; + + public InnerClass() { + e = 2; + } + } + + final int f; + final int j; + + public PublicClass() { + f = 1; + j = 1; + } + } + + public class PublicWithNonPrimitive { + String s; + + public PublicWithNonPrimitive() {} + } + + public class PublicWithPublicNonPrimitive { + public String s; + + public PublicWithPublicNonPrimitive() {} + } + + public class PublicWithPublicWithBoolean { + private Boolean b; + + public PublicWithPublicWithBoolean() {} + } + + public class PublicWithPublicWithBooleanWithGetterOnly { + private Boolean b; + + public PublicWithPublicWithBooleanWithGetterOnly() {} + + public Boolean getB() { + return b; + } + } + + public class PublicWithPublicWithBooleanWithGetterAndSetter { + private Boolean b; + + public PublicWithPublicWithBooleanWithGetterAndSetter() {} + + public Boolean getB() { + return b; + } + + public void setB(boolean b) { + this.b = b; + } + } + + public class PublicWithPublicWithBooleanWithIsAndSetter { + private Boolean b; + + public PublicWithPublicWithBooleanWithIsAndSetter() {} + + public Boolean isB() { + return b; + } + + public void setB(boolean b) { + this.b = b; + } + } + + public class PublicWithPublicWithBooleanWithHasAndSetter { + private Boolean b; + + public PublicWithPublicWithBooleanWithHasAndSetter() {} + + public Boolean hasB() { + return b; + } + + public void setB(boolean b) { + this.b = b; + } + } +}