diff --git a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java index b5e602cb..ef90f508 100644 --- a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java +++ b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java @@ -1,189 +1,149 @@ package com.fasterxml.jackson.jr.ob.impl; +import com.fasterxml.jackson.jr.ob.impl.POJODefinition.Prop; +import com.fasterxml.jackson.jr.ob.impl.POJODefinition.PropBuilder; + import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.Map; import java.util.TreeMap; +import java.util.function.Consumer; -import com.fasterxml.jackson.jr.ob.JSON; -import com.fasterxml.jackson.jr.ob.impl.POJODefinition.Prop; -import com.fasterxml.jackson.jr.ob.impl.POJODefinition.PropBuilder; +import static com.fasterxml.jackson.jr.ob.JSON.Feature.INCLUDE_STATIC_FIELDS; /** * Helper class that jackson-jr uses by default to introspect POJO properties * (represented as {@link POJODefinition}) to build general POJO readers * (deserializers) and writers (serializers). - *

+ *

* Note that most of the usage is via {@link ValueReaderLocator} and * {@link ValueWriterLocator} * * @since 2.11 */ -public class BeanPropertyIntrospector -{ - protected final static Prop[] NO_PROPS = new Prop[0]; - - private final static BeanPropertyIntrospector INSTANCE = new BeanPropertyIntrospector(); - - public BeanPropertyIntrospector() { } +public class BeanPropertyIntrospector { + public static final BeanPropertyIntrospector INSTANCE = new BeanPropertyIntrospector(); - public static BeanPropertyIntrospector instance() { return INSTANCE; } + private BeanPropertyIntrospector() { + } public POJODefinition pojoDefinitionForDeserialization(JSONReader r, Class pojoType) { - return _construct(pojoType, r.features()); + return construct(pojoType, r.features()); } public POJODefinition pojoDefinitionForSerialization(JSONWriter w, Class pojoType) { - return _construct(pojoType, w.features()); + return construct(pojoType, w.features()); } - /* - /********************************************************************** - /* Internal methods - /********************************************************************** - */ - - private POJODefinition _construct(Class beanType, int features) - { - Map propsByName = new TreeMap(); - _introspect(beanType, propsByName, features); + private POJODefinition construct(Class beanType, int features) { + Map propsByName = new TreeMap<>(); + introspect(beanType, propsByName, features); - Constructor defaultCtor = null; - Constructor stringCtor = null; - Constructor longCtor = null; + Constructor defaultConstructor = null; + Constructor stringConstructor = null; + Constructor longConstructor = null; - for (Constructor ctor : beanType.getDeclaredConstructors()) { - Class[] argTypes = ctor.getParameterTypes(); + for (Constructor constructor : beanType.getDeclaredConstructors()) { + Class[] argTypes = constructor.getParameterTypes(); if (argTypes.length == 0) { - defaultCtor = ctor; - } else if (argTypes.length == 1) { - Class argType = argTypes[0]; - if (argType == String.class) { - stringCtor = ctor; - } else if (argType == Long.class || argType == Long.TYPE) { - longCtor = ctor; - } else { - continue; - } - } else { - continue; + defaultConstructor = constructor; + } else if (argTypes.length == 1 && argTypes[0] == String.class) { + stringConstructor = constructor; + } else if (argTypes.length == 1 && argTypes[0] == Long.class || argTypes[0] == Long.TYPE) { + longConstructor = constructor; } } - final int len = propsByName.size(); - Prop[] props; - if (len == 0) { - props = NO_PROPS; - } else { - props = new Prop[len]; - int i = 0; - for (PropBuilder builder : propsByName.values()) { - props[i++] = builder.build(); - } - } - return new POJODefinition(beanType, props, defaultCtor, stringCtor, longCtor); + + Prop[] props = propsByName.isEmpty() + ? new Prop[0] + : propsByName.values().stream().map(PropBuilder::build).toArray(Prop[]::new); + + return new POJODefinition(beanType, props, defaultConstructor, stringConstructor, longConstructor); } - private static void _introspect(Class currType, Map props, - int features) - { + /** + * Recursively goes parses through object and populates props with serializable objects. + * First checking the base type + */ + private static void introspect(Class currType, Map props, int features) { if (currType == null || currType == Object.class || isGroovyMetaClass(currType)) { return; } // First, check base type - _introspect(currType.getSuperclass(), props, features); - - final boolean noStatics = JSON.Feature.INCLUDE_STATIC_FIELDS.isDisabled(features); - // then public fields (since 2.8); may or may not be ultimately included - // but at this point still possible - for (Field f : currType.getDeclaredFields()) { - if (!Modifier.isPublic(f.getModifiers()) - || f.isEnumConstant() || f.isSynthetic()) { - continue; - } - // Only include static members if (a) inclusion feature enabled and - // (b) not final (cannot deserialize final fields) - if (Modifier.isStatic(f.getModifiers()) - && (noStatics || Modifier.isFinal(f.getModifiers()))) { - continue; - } - _propFrom(props, f.getName()).withField(f); - } - - // then get methods from within this class - for (Method m : currType.getDeclaredMethods()) { - final int flags = m.getModifiers(); - // 13-Jun-2015, tatu: Skip synthetic, bridge methods altogether, for now - // at least (add more complex handling only if absolutely necessary) - if (Modifier.isStatic(flags) - || m.isSynthetic() || m.isBridge()) { - continue; - } - Class argTypes[] = m.getParameterTypes(); - if (argTypes.length == 0) { // getter? - // getters must be public to be used - if (!Modifier.isPublic(flags)) { - continue; - } - - Class resultType = m.getReturnType(); - if (resultType == Void.class) { - continue; - } - String name = m.getName(); - if (name.startsWith("get")) { - if (name.length() > 3) { - name = decap(name.substring(3)); - _propFrom(props, name).withGetter(m); - } - } else if (name.startsWith("is")) { - if (name.length() > 2) { - // May or may not be used, but collect for now all the same: - name = decap(name.substring(2)); - _propFrom(props, name).withIsGetter(m); + introspect(currType.getSuperclass(), props, features); + + Arrays.stream(currType.getDeclaredFields()) + .filter(f -> !isInvalidField(f, features)) + .forEach(f -> storeProps(props, f.getName()).withField(f)); + + Arrays.stream(currType.getDeclaredMethods()) + .filter(m -> !Modifier.isStatic(m.getModifiers()) && !m.isSynthetic() && !m.isBridge() && !isGroovyMetaClass(m.getReturnType())) + .forEach(m -> { + Class[] argTypes = m.getParameterTypes(); + final String name = m.getName(); + if (argTypes.length == 0 && Modifier.isPublic(m.getModifiers()) && !m.getReturnType().equals(Void.class)) { + storePropsIfNameStartsWith(name, "get", newName -> storeProps(props, newName).withGetter(m)); + storePropsIfNameStartsWith(name, "is", newName -> storeProps(props, newName).withIsGetter(m)); + } else if (argTypes.length == 1) { // setter? + // Non-public setters are fine if we can force access, don't yet check + // let's also not bother about return type; setters that return value are fine + storePropsIfNameStartsWith(name, "set", newName -> storeProps(props, newName).withSetter(m)); } - } - } else if (argTypes.length == 1) { // setter? - // Non-public setters are fine if we can force access, don't yet check - // let's also not bother about return type; setters that return value are fine - String name = m.getName(); - if (!name.startsWith("set") || name.length() == 3) { - continue; - } - name = decap(name.substring(3)); - _propFrom(props, name).withSetter(m); - } - } + }); } - private static PropBuilder _propFrom(Map props, String name) { + /** + * Another helper method to deal with Groovy's problematic metadata accessors + * + * @implNote Groovy MetaClass have cyclic reference, and hence the class containing it should not be serialised without + * either removing that reference, or skipping over such references. + */ + private static boolean isGroovyMetaClass(Class clazz) { + return clazz.getName().startsWith("groovy.lang"); + } + + private static PropBuilder storeProps(Map props, String name) { return props.computeIfAbsent(name, Prop::builder); } - private static String decap(String name) { - char c = name.charAt(0); - char lowerC = Character.toLowerCase(c); - - if (c != lowerC) { - // First: do NOT lower case if more than one leading upper case letters: - if ((name.length() == 1) - || !Character.isUpperCase(name.charAt(1))) { - char chars[] = name.toCharArray(); - chars[0] = lowerC; - return new String(chars); - } + /** + * then public fields (since 2.8); may or may not be ultimately included, but at this point still possible + */ + private static boolean isInvalidField(Field f, int features) { + return !Modifier.isPublic(f.getModifiers()) || f.isEnumConstant() || f.isSynthetic() || isStaticField(f, features); + } + + /** + * Only include static members if (a) inclusion feature enabled and (b) not final (cannot deserialize final fields) + */ + private static boolean isStaticField(Field f, int features) { + return Modifier.isStatic(f.getModifiers()) && (INCLUDE_STATIC_FIELDS.isDisabled(features) || Modifier.isFinal(f.getModifiers())); + } + + + private static void storePropsIfNameStartsWith(String name, String startString, Consumer f) { + if (name.startsWith(startString) && name.length() > startString.length()) { + f.accept(deCapitalizeFirstCharacter(name.substring(startString.length()))); } - return name; } /** - * Another helper method to deal with Groovy's problematic metadata accessors - * - * @implNote Groovy MetaClass have cyclic reference, and hence the class containing it should not be serialised without - * either removing that reference, or skipping over such references. + * Only changes the capitalization of first character when it is the only character or the second character is lowercase. + *

a -> a

+ *

A -> a

+ *

AA -> AA

+ *

Aa -> aa

+ *

aA -> aA

+ * ... */ - protected static boolean isGroovyMetaClass(Class clazz) { - return clazz.getName().startsWith("groovy.lang"); + private static String deCapitalizeFirstCharacter(String name) { + if (Character.isUpperCase(name.charAt(0)) && ((name.length() == 1) || Character.isLowerCase(name.charAt(1)))) { + char[] chars = name.toCharArray(); + chars[0] = (char) (name.charAt(0) + 32); + return new String(chars); + } + return name; } } diff --git a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/ValueReaderLocator.java b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/ValueReaderLocator.java index c0bbb10c..e8747569 100644 --- a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/ValueReaderLocator.java +++ b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/ValueReaderLocator.java @@ -430,7 +430,7 @@ protected POJODefinition _resolveBeanDef(Class raw) { return def; } } - return BeanPropertyIntrospector.instance().pojoDefinitionForDeserialization(_readContext, raw); + return BeanPropertyIntrospector.INSTANCE.pojoDefinitionForDeserialization(_readContext, raw); } catch (Exception e) { throw new IllegalArgumentException(String.format ("Failed to introspect ClassDefinition for type '%s': %s", diff --git a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/ValueWriterLocator.java b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/ValueWriterLocator.java index aa3decc9..9e897ce5 100644 --- a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/ValueWriterLocator.java +++ b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/ValueWriterLocator.java @@ -193,7 +193,7 @@ protected POJODefinition _resolveBeanDef(Class raw) { return def; } } - return BeanPropertyIntrospector.instance().pojoDefinitionForSerialization(_writeContext, raw); + return BeanPropertyIntrospector.INSTANCE.pojoDefinitionForSerialization(_writeContext, raw); } catch (Exception e) { throw new IllegalArgumentException(String.format ("Failed to introspect ClassDefinition for type '%s': %s", diff --git a/jr-objects/src/test/java/com/fasterxml/jackson/jr/GroovyTest.groovy b/jr-objects/src/test/java/com/fasterxml/jackson/jr/GroovyTest.groovy index 16e2e63e..70a6e816 100644 --- a/jr-objects/src/test/java/com/fasterxml/jackson/jr/GroovyTest.groovy +++ b/jr-objects/src/test/java/com/fasterxml/jackson/jr/GroovyTest.groovy @@ -7,15 +7,52 @@ class GroovyTest extends TestBase { void testSimpleObject() throws Exception { var data = JSON.std.asString(new MyClass()) - var expected = "{\"aDouble\":0.0,\"aStr\":\"stringData\",\"anInt\":0,\"metaClass\":{}}"; + var expected = """{"AAAAA_A_Field_Starting_With_Two_Capital_Letters":"XYZ","aDouble":0.0,"aPublicInitializedInteger":56,"aPublicInitializedIntegerObject":1516,"aPublicUninitializedInteger":0,"anInitializedIntegerObject":1112,"anInitializedPublicString":"stringData","anInitializedString":"ABC","anInteger":0,"anIntegerWithValue":12}""" assertEquals(data, expected) } private class MyClass { - public int anInt; //testing groovy primitive - public String aStr = "stringData"; //testing allocated object + int anInteger + int anIntegerWithValue = 12 - public double aDouble; // - public Double aDoublesd; //testing boxing object + static int anStaticInteger = 34 + static int anStaticIntegerWithValue = 34 + + public int aPublicUninitializedInteger + public int aPublicInitializedInteger = 56 + + private int aPrivateUninitializedInteger + private int aPrivateInitializedInteger = 78 + + public static int aPublicStaticUninitializedInteger + public static int aPublicStaticInitializedInteger = 910 + + Integer anIntegerObject + Integer anInitializedIntegerObject = 1112 + + static Integer aStaticIntegerObject + static Integer aStaticInitializedIntegerObject = 1314 + + public Integer aPublicUninitializedIntegerObject + public Integer aPublicInitializedIntegerObject = 1516 + + public static Integer aPublicStaticUninitializedIntegerObject + public static Integer aPublicStaticInitializedIntegerObject = 1718 + + String aString + String anInitializedString = "ABC" + + static String aStaticString = "jacksonJR" + + public String aPublicString + public String anInitializedPublicString = "stringData" + + public String AAAAA_A_Field_Starting_With_Two_Capital_Letters = "XYZ" + //Other Items + public static String staticStr = "jacksonJR" // Public Static Object + static int anStaticInt // Uninitialized Static Object + public double aDouble // uninitialized primitive + public Double aDoubleObject // testing boxing object + private int hiddenvalue = 123 // private value } } diff --git a/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/impl/POJODefinitionOverrideTest.java b/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/impl/POJODefinitionOverrideTest.java index b44ee05c..009e46da 100644 --- a/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/impl/POJODefinitionOverrideTest.java +++ b/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/impl/POJODefinitionOverrideTest.java @@ -19,7 +19,7 @@ public MyPropertyModifier(String toDrop) { public POJODefinition pojoDefinitionForDeserialization(JSONReader readContext, Class pojoType) { - POJODefinition def = BeanPropertyIntrospector.instance().pojoDefinitionForDeserialization(readContext, pojoType); + POJODefinition def = BeanPropertyIntrospector.INSTANCE.pojoDefinitionForDeserialization(readContext, pojoType); List newProps = new ArrayList(); for (POJODefinition.Prop prop : def.getProperties()) { if (!_toDrop.equals(prop.name)) { @@ -33,7 +33,7 @@ public POJODefinition pojoDefinitionForDeserialization(JSONReader readContext, public POJODefinition pojoDefinitionForSerialization(JSONWriter writeContext, Class pojoType) { - POJODefinition def = BeanPropertyIntrospector.instance().pojoDefinitionForSerialization(writeContext, pojoType); + POJODefinition def = BeanPropertyIntrospector.INSTANCE.pojoDefinitionForSerialization(writeContext, pojoType); // and then reverse-order Map newProps = new TreeMap(Collections.reverseOrder()); for (POJODefinition.Prop prop : def.getProperties()) {