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 4a46b302..b2354d1f 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,40 +1,42 @@ package com.fasterxml.jackson.jr.ob.impl; -import java.lang.reflect.*; +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.HashMap; import java.util.Map; import java.util.TreeMap; -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; +import static com.fasterxml.jackson.jr.ob.JSON.Feature.USE_FIELD_MATCHING_GETTERS; +import static java.lang.Character.isLowerCase; +import static java.lang.Character.toLowerCase; +import static java.lang.reflect.Modifier.*; /** * 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 static BeanPropertyIntrospector instance() { return INSTANCE; } +public final class BeanPropertyIntrospector { + private BeanPropertyIntrospector() { + } - public POJODefinition pojoDefinitionForDeserialization(JSONReader r, Class pojoType) { - return _introspectDefinition(pojoType, false, r.features()); + public static POJODefinition pojoDefinitionForDeserialization(JSONReader r, Class pojoType) { + return introspectDefinition(pojoType, false, r.features()); } - public POJODefinition pojoDefinitionForSerialization(JSONWriter w, Class pojoType) { - return _introspectDefinition(pojoType, true, w.features()); + public static POJODefinition pojoDefinitionForSerialization(JSONWriter w, Class pojoType) { + return introspectDefinition(pojoType, true, w.features()); } /* @@ -43,152 +45,73 @@ public POJODefinition pojoDefinitionForSerialization(JSONWriter w, Class pojo /********************************************************************** */ - private POJODefinition _introspectDefinition(Class beanType, - boolean forSerialization, int features) - { - Map propsByName = new TreeMap<>(); - _introspect(beanType, propsByName, features); - - final BeanConstructors constructors; - - if (forSerialization) { - constructors = null; - } else { - constructors = new BeanConstructors(beanType); - for (Constructor ctor : beanType.getDeclaredConstructors()) { - Class[] argTypes = ctor.getParameterTypes(); - if (argTypes.length == 0) { - constructors.addNoArgsConstructor(ctor); - } else if (argTypes.length == 1) { - Class argType = argTypes[0]; - if (argType == String.class) { - constructors.addStringConstructor(ctor); - } else if (argType == Integer.class || argType == Integer.TYPE) { - constructors.addIntConstructor(ctor); - } else if (argType == Long.class || argType == Long.TYPE) { - constructors.addLongConstructor(ctor); - } - } - } - } - - 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, constructors); - } - - private static void _introspect(Class currType, Map props, - int features) - { + /** + * Brain Method of {@link BeanPropertyIntrospector}, used to get the list of props + */ + private static void _introspect(Class currType, Map props, int features) { if (currType == null || currType == Object.class) { return; } // First, check base type _introspect(currType.getSuperclass(), props, features); - - final boolean noStatics = JSON.Feature.INCLUDE_STATIC_FIELDS.isDisabled(features); - final boolean isFieldNameGettersEnabled = JSON.Feature.USE_FIELD_MATCHING_GETTERS.isEnabled(features); - + final boolean isFieldNameGettersEnabled = USE_FIELD_MATCHING_GETTERS.isEnabled(features); final Map fieldNameMap = isFieldNameGettersEnabled ? new HashMap<>() : null; + _populatePropsWithField(fieldNameMap, currType, props, features); + _populatePropWithGettersAndSetters(isFieldNameGettersEnabled, fieldNameMap, currType, props); + } - // 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 (fieldNameMap != null) { - fieldNameMap.put(f.getName(), f); - } - 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); - } - + private static void _populatePropWithGettersAndSetters(boolean isFieldNameGettersEnabled, Map fieldMap, Class currType, Map props) { // 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() || isGroovyMetaClass(m.getReturnType())) { + final Class returnType = m.getReturnType(); + + // 13-Jun-2015, tatu: + // Skip synthetic, bridge methods altogether, for now + // at least (add more complex handling only if absolutely necessary) + if (isStatic(flags) || m.isSynthetic() || m.isBridge() || isGroovyMetaClass(returnType)) { 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); - } - } else if (isFieldNameGettersEnabled) { - // 10-Mar-2024: [jackson-jr#94]: - // This will allow getters with field name as their getters, - // like the ones generated by Groovy (or JDK 17 for Records). - // If method name matches with field name, & method return - // type matches the field type only then it can be considered a getter. - Field field = fieldNameMap.get(name); - if (field != null && Modifier.isPublic(m.getModifiers()) && m.getReturnType().equals(field.getType())) { - // NOTE: do NOT decap, field name should be used as-is - _propFrom(props, name).withGetter(m); - } - } + final Class[] argTypes = m.getParameterTypes(); + if (argTypes.length == 0 && returnType != Void.class) { // getter? + if(!isPublic(flags)) continue; + generatePropsWithGetter(m, props); + generatePropsWithIsGetter(m, props); + generatePropsWithFieldMatchingGetter(isFieldNameGettersEnabled, fieldMap, m, props); } 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); + generatePropsWithSetter(m, props); + } + } + } + + private static void _populatePropsWithField(Map fieldNameMap, Class currType, Map props, int features) { + // then public fields (since 2.8); may or may not be ultimately included but at this point still possible + // Also, only include static members if + // (a) inclusion feature enabled and + // (b) not final (cannot deserialize final fields) + for (Field f : currType.getDeclaredFields()) { + if (fieldNameMap != null) { + fieldNameMap.put(f.getName(), f); } + if (!isPublic(f.getModifiers()) || f.isEnumConstant() || f.isSynthetic() || (isStatic(f.getModifiers()) && (INCLUDE_STATIC_FIELDS.isDisabled(features) || isFinal(f.getModifiers())))) { + continue; + } + propFrom(props, f.getName()).withField(f); } } - private static PropBuilder _propFrom(Map props, String name) { + private static PropBuilder propFrom(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); - } + private static String _decap(String name) { + if (!isLowerCase(name.charAt(0)) && ((name.length() == 1) || !Character.isUpperCase(name.charAt(1)))) { + final char[] chars = name.toCharArray(); + chars[0] = toLowerCase(name.charAt(0)); + return new String(chars); } return name; } @@ -199,7 +122,72 @@ private static String decap(String name) { * @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. */ - protected static boolean isGroovyMetaClass(Class clazz) { + private static boolean isGroovyMetaClass(Class clazz) { return "groovy.lang.MetaClass".equals(clazz.getName()); } -} + + private static void generatePropsWithFieldMatchingGetter(boolean isFieldNameGettersEnabled,Map fieldNameMap, final Method m, final Map props) { + final String name = m.getName(); + if (isFieldNameGettersEnabled) { + // 10-Mar-2024: [jackson-jr#94]: + // This will allow getters with field name as their getters, + // like the ones generated by Groovy (or JDK 17 for Records). + // If method name matches with field name, & method return + // type matches the field type only then it can be considered a getter. + Field field = fieldNameMap.get(name); + if (field != null && Modifier.isPublic(m.getModifiers()) && m.getReturnType().equals(field.getType())) { + // NOTE: do NOT decap, field name should be used as-is + propFrom(props, name).withGetter(m); + } + } + } + + private static void generatePropsWithGetter(final Method method, final Map props) { + final String getterPrefix = "get"; + final String name = method.getName(); + if (name.startsWith(getterPrefix) && name.length() > getterPrefix.length()) { + propFrom(props, _decap(name.substring(getterPrefix.length()))).withGetter(method); + } + } + + private static void generatePropsWithIsGetter(final Method method, final Map props) { + final String isGetterPrefix = "is"; + final String name = method.getName(); + if (name.startsWith(isGetterPrefix) && name.length() > isGetterPrefix.length()) { + propFrom(props, _decap(name.substring(isGetterPrefix.length()))).withIsGetter(method); + } + } + + private static void generatePropsWithSetter(final Method method, final Map props) { + final String setterPrefix = "set"; + final String name = method.getName(); + if (name.startsWith(setterPrefix) && name.length() > setterPrefix.length()) { + propFrom(props, _decap(name.substring(setterPrefix.length()))).withSetter(method); + } + } + + private static POJODefinition introspectDefinition(Class beanType, boolean forSerialization, int features) { + final Map propsByName = new TreeMap<>(); + _introspect(beanType, propsByName, features); + + final BeanConstructors constructors = new BeanConstructors(beanType); + for (Constructor ctor : beanType.getDeclaredConstructors()) { + final Class[] argTypes = ctor.getParameterTypes(); + if (argTypes.length == 0) { + constructors.addNoArgsConstructor(ctor); + } else if (argTypes.length == 1) { + final Class argType = argTypes[0]; + if (argType == String.class) { + constructors.addStringConstructor(ctor); + } else if (argType == Integer.class || argType == Integer.TYPE) { + constructors.addIntConstructor(ctor); + } else if (argType == Long.class || argType == Long.TYPE) { + constructors.addLongConstructor(ctor); + } + } + } + + final Prop[] props = propsByName.values().stream().map(PropBuilder::build).toArray(Prop[]::new); + return new POJODefinition(beanType, props, forSerialization ? null : constructors); + } +} \ No newline at end of file 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 35638b5c..398a7cfb 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 @@ -1,9 +1,5 @@ package com.fasterxml.jackson.jr.ob.impl; -import java.lang.reflect.*; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - import com.fasterxml.jackson.jr.ob.JSON; import com.fasterxml.jackson.jr.ob.api.ReaderWriterModifier; import com.fasterxml.jackson.jr.ob.api.ReaderWriterProvider; @@ -12,6 +8,15 @@ import com.fasterxml.jackson.jr.type.TypeBindings; import com.fasterxml.jackson.jr.type.TypeResolver; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import static com.fasterxml.jackson.jr.ob.impl.BeanPropertyIntrospector.pojoDefinitionForDeserialization; + /** * Helper object used for efficient detection of type information relevant * to our conversion needs when writing out Java Objects as JSON. @@ -430,7 +435,7 @@ protected POJODefinition _resolveBeanDef(Class raw) { return def; } } - return BeanPropertyIntrospector.instance().pojoDefinitionForDeserialization(_readContext, raw); + return 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 dc30fcdf..c6533960 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 @@ -1,15 +1,19 @@ package com.fasterxml.jackson.jr.ob.impl; -import java.lang.reflect.*; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; - import com.fasterxml.jackson.jr.ob.JSON; import com.fasterxml.jackson.jr.ob.api.ReaderWriterModifier; import com.fasterxml.jackson.jr.ob.api.ReaderWriterProvider; import com.fasterxml.jackson.jr.ob.api.ValueWriter; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import static com.fasterxml.jackson.jr.ob.impl.BeanPropertyIntrospector.pojoDefinitionForSerialization; + /** * Helper object used for efficient detection of type information * relevant to our conversion needs when writing out Java Objects @@ -193,7 +197,7 @@ protected POJODefinition _resolveBeanDef(Class raw) { return def; } } - return BeanPropertyIntrospector.instance().pojoDefinitionForSerialization(_writeContext, raw); + return 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/ob/IndentationTest.java b/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/IndentationTest.java index 4850e110..af7ab780 100644 --- a/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/IndentationTest.java +++ b/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/IndentationTest.java @@ -6,7 +6,7 @@ public class IndentationTest extends TestBase { public void testSimpleList() throws Exception { - Map map = new LinkedHashMap(); + Map map = new LinkedHashMap<>(); map.put("a", 1); map.put("b", 2); diff --git a/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/SimpleFieldTest.java b/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/SimpleFieldTest.java index 70b40fcd..6d8a788c 100644 --- a/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/SimpleFieldTest.java +++ b/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/SimpleFieldTest.java @@ -37,8 +37,7 @@ public void testSerializeWithField() throws Exception public void testDeserializeWithField() throws Exception { - XY result = JSON.std.with(JSON.Feature.USE_FIELDS) - .beanFrom(XY.class, a2q("{'x':3,'y':4}")); + XY result = JSON.std.with(JSON.Feature.USE_FIELDS).beanFrom(XY.class, a2q("{'x':3,'y':4}")); assertEquals(4, result.getY()); assertEquals(3, result.x); } 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 75688000..198f7213 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 @@ -1,10 +1,11 @@ package com.fasterxml.jackson.jr.ob.impl; -import java.util.*; - -import com.fasterxml.jackson.jr.ob.*; +import com.fasterxml.jackson.jr.ob.JSON; +import com.fasterxml.jackson.jr.ob.TestBase; import com.fasterxml.jackson.jr.ob.api.ReaderWriterModifier; +import java.util.*; + public class POJODefinitionOverrideTest extends TestBase { static class MyPropertyModifier extends ReaderWriterModifier @@ -16,11 +17,9 @@ public MyPropertyModifier(String toDrop) { } @Override - public POJODefinition pojoDefinitionForDeserialization(JSONReader readContext, - Class pojoType) - { - POJODefinition def = BeanPropertyIntrospector.instance().pojoDefinitionForDeserialization(readContext, pojoType); - List newProps = new ArrayList(); + public POJODefinition pojoDefinitionForDeserialization(JSONReader readContext, Class pojoType) { + POJODefinition def = BeanPropertyIntrospector.pojoDefinitionForDeserialization(readContext, pojoType); + List newProps = new ArrayList<>(); for (POJODefinition.Prop prop : def.getProperties()) { if (!_toDrop.equals(prop.name)) { newProps.add(prop); @@ -30,12 +29,10 @@ public POJODefinition pojoDefinitionForDeserialization(JSONReader readContext, } @Override - public POJODefinition pojoDefinitionForSerialization(JSONWriter writeContext, - Class pojoType) - { - POJODefinition def = BeanPropertyIntrospector.instance().pojoDefinitionForSerialization(writeContext, pojoType); + public POJODefinition pojoDefinitionForSerialization(JSONWriter writeContext, Class pojoType) { + POJODefinition def = BeanPropertyIntrospector.pojoDefinitionForSerialization(writeContext, pojoType); // and then reverse-order - Map newProps = new TreeMap(Collections.reverseOrder()); + Map newProps = new TreeMap<>(Collections.reverseOrder()); for (POJODefinition.Prop prop : def.getProperties()) { newProps.put(prop.name, prop); }