-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Code Cleanup for BeanPropertyIntrospector to reduce its cognitive com…
…plexity + improved tests + fixing empty meta class bug.
- Loading branch information
Showing
5 changed files
with
145 additions
and
148 deletions.
There are no files selected for viewing
238 changes: 99 additions & 139 deletions
238
jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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). | ||
*<p> | ||
* <p> | ||
* 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<String,PropBuilder> propsByName = new TreeMap<String,PropBuilder>(); | ||
_introspect(beanType, propsByName, features); | ||
private POJODefinition construct(Class<?> beanType, int features) { | ||
Map<String, PropBuilder> 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<String, PropBuilder> 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<String, PropBuilder> 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<String,PropBuilder> 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<String, PropBuilder> 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<String> 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. | ||
* <p>a -> a</p> | ||
* <p>A -> a</p> | ||
* <p>AA -> AA</p> | ||
* <p>Aa -> aa</p> | ||
* <p>aA -> aA</p> | ||
* ... | ||
*/ | ||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters