com.fasterxml.jackson.module.mrbean.BeanBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jackson-module-mrbean Show documentation
Show all versions of jackson-module-mrbean Show documentation
Functionality for implementing interfaces and abstract types
dynamically ("bean materialization"), integrated with Jackson (although usable externally as well)
package com.fasterxml.jackson.module.mrbean;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import com.fasterxml.jackson.databind.introspect.TypeResolutionContext;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import java.lang.reflect.Modifier;
import static org.objectweb.asm.Opcodes.*;
/**
* Heavy lifter of mr Bean package: class that keeps track of logical POJO properties,
* and figures out how to create an implementation class.
*/
public class BeanBuilder
{
protected Map _beanProperties = new LinkedHashMap();
protected LinkedHashMap _unsupportedMethods = new LinkedHashMap();
/**
* Abstract class or interface that the bean is created to extend or implement.
*/
protected final JavaType _type;
protected final AnnotatedClass _typeDefinition;
protected final TypeFactory _typeFactory;
public BeanBuilder(JavaType type, AnnotatedClass ac, TypeFactory tf)
{
_type = type;
_typeDefinition = ac;
_typeFactory = tf;
}
public static BeanBuilder construct(MapperConfig> config, JavaType type, AnnotatedClass ac)
{
return new BeanBuilder(type, ac, config.getTypeFactory());
}
/*
/**********************************************************
/* Core public API
/**********************************************************
*/
/**
* @param failOnUnrecognized If true, and an unrecognized (non-getter, non-setter)
* method is encountered, will throw {@link IllegalArgumentException}; if false,
* will implement bogus method that will throw {@link UnsupportedOperationException}
* if called.
*/
public BeanBuilder implement(boolean failOnUnrecognized)
{
ArrayList implTypes = new ArrayList();
// First: find all supertypes:
implTypes.add(_type);
BeanUtil.findSuperTypes(_type, Object.class, implTypes);
for (JavaType impl : implTypes) {
TypeResolutionContext ctxt = buildTypeContext(impl);
// and then find all getters, setters, and other non-concrete methods therein:
for (Method m : impl.getRawClass().getDeclaredMethods()) {
// 15-Sep-2015, tatu: As per [module-mrbean#25], make sure to ignore static
// methods.
if (Modifier.isStatic(m.getModifiers())
// 22-Sep-2020: [modules-base#110] Looks like generics can introduce
// hidden bridge and/or synthetic methods; skip same way as core
// jackson-databind does
|| m.isSynthetic() || m.isBridge()) {
continue;
}
String methodName = m.getName();
int argCount = m.getParameterTypes().length;
if (argCount == 0) { // getter?
if (methodName.startsWith("get")) {
if (methodName.length() > 3) { // ignore plain "get()"
addGetter(ctxt, m);
continue;
}
} else if (methodName.startsWith("is")) {
if (methodName.length() > 2) { // ignore plain "is()"
if (returnsBoolean(m)) {
addGetter(ctxt, m);
continue;
}
}
}
} else if ((argCount == 1) && methodName.startsWith("set")) { // ignore "set()"
if (methodName.length() > 3) {
addSetter(ctxt, m);
continue;
}
}
// Otherwise, if concrete, or already handled, skip:
if (BeanUtil.isConcrete(m) || _unsupportedMethods.containsKey(methodName)) {
continue;
}
// [module-mrbean#11]: try to support overloaded methods
if (hasConcreteOverride(m, _type)) {
continue;
}
if (failOnUnrecognized) {
throw new IllegalArgumentException("Unrecognized abstract method '"+methodName
+"' (not a getter or setter) -- to avoid exception, disable AbstractTypeMaterializer.Feature.FAIL_ON_UNMATERIALIZED_METHOD");
}
_unsupportedMethods.put(methodName, m);
}
}
return this;
}
/**
* Method that generates byte code for class that implements abstract
* types requested so far.
*
* @param className Fully-qualified name of the class to generate
* @return Byte code Class instance built by this builder
*/
public byte[] build(String className)
{
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
String internalClass = getInternalClassName(className);
String implName = getInternalClassName(_type.getRawClass().getName());
// muchos important: level at least 1.5 to get generics!!!
// Also: abstract class vs interface...
String superName;
if (_type.isInterface()) {
superName = "java/lang/Object";
cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, internalClass, null,
superName, new String[] { implName });
} else {
superName = implName;
cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, internalClass, null,
implName, null);
}
cw.visitSource(className + ".java", null);
BeanBuilder.generateDefaultConstructor(cw, superName);
for (POJOProperty prop : _beanProperties.values()) {
// First: determine type to use; preferably setter (usually more explicit); otherwise getter
TypeDescription type = prop.selectType();
createField(cw, prop, type);
// since some getters and/or setters may be implemented, check:
if (!prop.hasConcreteGetter()) {
createGetter(cw, internalClass, prop, type);
}
if (!prop.hasConcreteSetter()) {
createSetter(cw, internalClass, prop, type);
}
}
for (Method m : _unsupportedMethods.values()) {
createUnimplementedMethod(cw, internalClass, m);
}
cw.visitEnd();
return cw.toByteArray();
}
/*
/**********************************************************
/* Internal methods, property discovery
/**********************************************************
*/
/**
* Helper method used to detect if an abstract method found in a base class
* may actually be implemented in a (more) concrete sub-class.
*
* @since 2.4
*/
protected boolean hasConcreteOverride(Method m0, JavaType implementedType)
{
final String name = m0.getName();
final Class>[] argTypes = m0.getParameterTypes();
try {
// 22-Sep-2020: [modules-base#109]: getMethod returns the most-specific method
// implementation, for public methods only (which is any method in an interface)
Method effectiveMethod = implementedType.getRawClass().getMethod(name, argTypes);
// we've found the method, so we can simply check whether it is concrete
return BeanUtil.isConcrete(effectiveMethod);
} catch (NoSuchMethodException e) {
// method must be non-public, fallback to using getDeclaredMethod
}
for (JavaType curr = implementedType; (curr != null) && !curr.isJavaLangObject();
curr = curr.getSuperClass()) {
// 29-Nov-2015, tatu: Avoiding exceptions would be good, so would linear scan
// be better here?
try {
Method effectiveMethod = curr.getRawClass().getDeclaredMethod(name, argTypes);
// we've found the method, so we can simply check whether it is concrete
return BeanUtil.isConcrete(effectiveMethod);
} catch (NoSuchMethodException e) {
// method must exist on a superclass, continue searching...
}
}
return false;
}
protected String getPropertyName(String methodName)
{
int prefixLen = methodName.startsWith("is") ? 2 : 3;
return decap(methodName.substring(prefixLen));
}
protected String buildGetterName(String fieldName) {
return cap("get", fieldName);
}
protected String buildSetterName(String fieldName) {
return cap("set", fieldName);
}
protected String getInternalClassName(String className) {
return className.replace(".", "/");
}
protected void addGetter(TypeResolutionContext ctxt, Method m)
{
POJOProperty prop = findProperty(ctxt, getPropertyName(m.getName()));
// only set if not yet set; we start with super class:
if (prop.getGetter() == null) {
prop.setGetter(m);
}
}
protected void addSetter(TypeResolutionContext ctxt, Method m)
{
POJOProperty prop = findProperty(ctxt, getPropertyName(m.getName()));
if (prop.getSetter() == null) {
prop.setSetter(m);
}
}
protected POJOProperty findProperty(TypeResolutionContext ctxt, String propName)
{
POJOProperty prop = _beanProperties.get(propName);
if (prop == null) {
prop = new POJOProperty(ctxt, propName);
_beanProperties.put(propName, prop);
}
return prop;
}
protected final static boolean returnsBoolean(Method m)
{
Class> rt = m.getReturnType();
return (rt == Boolean.class || rt == Boolean.TYPE);
}
/*
/**********************************************************
/* Internal methods, bytecode generation
/**********************************************************
*/
/**
* NOTE: only static because it is needed from TypeDetector
*/
protected static void generateDefaultConstructor(ClassWriter cw, String superName)
{
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
// 26-May-2014, tatu: last 'false' since constructor never owned by interface
mv.visitMethodInsn(INVOKESPECIAL, superName, "", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0); // don't care (real values: 1,1)
mv.visitEnd();
}
protected void createField(ClassWriter cw, POJOProperty prop, TypeDescription type)
{
String sig = type.hasGenerics() ? type.genericSignature() : null;
String desc = type.erasedSignature();
/* 15-Mar-2015, tatu: Should not be created public as that can cause problems
* like [mrbean#20]
*/
FieldVisitor fv = cw.visitField(ACC_PROTECTED, prop.getFieldName(), desc, sig, null);
fv.visitEnd();
}
protected void createSetter(ClassWriter cw, String internalClassName,
POJOProperty prop, TypeDescription propertyType)
{
String methodName;
String desc;
Method setter = prop.getSetter();
if (setter != null) { // easy, copy as is
desc = Type.getMethodDescriptor(setter);
methodName = setter.getName();
} else { // otherwise need to explicitly construct from property type (close enough)
desc = "("+ propertyType.erasedSignature() + ")V";
methodName = buildSetterName(prop.getName());
}
String sig = propertyType.hasGenerics() ? ("("+propertyType.genericSignature()+")V") : null;
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, methodName, desc, sig, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0); // this
mv.visitVarInsn(propertyType.getLoadOpcode(), 1);
mv.visitFieldInsn(PUTFIELD, internalClassName, prop.getFieldName(), propertyType.erasedSignature());
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0); // don't care (real values: 2, 2)
mv.visitEnd();
}
protected void createGetter(ClassWriter cw, String internalClassName,
POJOProperty prop, TypeDescription propertyType)
{
String methodName;
String desc;
Method getter = prop.getGetter();
if (getter != null) { // easy, copy as is
desc = Type.getMethodDescriptor(getter);
methodName = getter.getName();
} else { // otherwise need to explicitly construct from property type (close enough)
desc = "()"+propertyType.erasedSignature();
methodName = buildGetterName(prop.getName());
}
String sig = propertyType.hasGenerics() ? ("()"+propertyType.genericSignature()) : null;
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, methodName, desc, sig, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0); // load 'this'
mv.visitFieldInsn(GETFIELD, internalClassName, prop.getFieldName(), propertyType.erasedSignature());
mv.visitInsn(propertyType.getReturnOpcode());
mv.visitMaxs(0, 0); // don't care (real values: 1,1)
mv.visitEnd();
}
/**
* Builder for methods that just throw an exception, basically "unsupported
* operation" implementation.
*/
protected void createUnimplementedMethod(ClassWriter cw, String internalClassName,
Method method)
{
String exceptionName = getInternalClassName(UnsupportedOperationException.class.getName());
String sig = Type.getMethodDescriptor(method);
String name = method.getName();
// should we try to pass generic information?
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, name, sig, null, null);
mv.visitTypeInsn(NEW, exceptionName);
mv.visitInsn(DUP);
mv.visitLdcInsn("Unimplemented method '"+name+"' (not a setter/getter, could not materialize)");
// 26-May-2014, tatu: last 'false' since constructor never owned by interface
mv.visitMethodInsn(INVOKESPECIAL, exceptionName, "", "(Ljava/lang/String;)V", false);
mv.visitInsn(ATHROW);
mv.visitMaxs(0, 0); // don't care (real values: 3, 1 + method.getParameterTypes().length);
mv.visitEnd();
}
/*
/**********************************************************
/* Internal methods, other
/**********************************************************
*/
protected String decap(String name) {
char c = name.charAt(0);
if (name.length() > 1
&& Character.isUpperCase(name.charAt(1))
&& Character.isUpperCase(c)){
return name;
}
char chars[] = name.toCharArray();
chars[0] = Character.toLowerCase(c);
return new String(chars);
}
protected String cap(String prefix, String name)
{
final int plen = prefix.length();
StringBuilder sb = new StringBuilder(plen + name.length());
sb.append(prefix);
sb.append(name);
sb.setCharAt(plen, Character.toUpperCase(name.charAt(0)));
return sb.toString();
}
protected TypeResolutionContext buildTypeContext(JavaType ctxtType)
{
return new TypeResolutionContext.Basic(_typeFactory,
ctxtType.getBindings());
}
/*
/**********************************************************
/* Helper classes
/**********************************************************
*/
/**
* Helper bean used to encapsulate most details of type handling
*/
static class TypeDescription
{
private final Type _asmType;
private JavaType _jacksonType;
/*
/**********************************************************
/* Construction
/**********************************************************
*/
public TypeDescription(JavaType type)
{
_jacksonType = type;
_asmType = Type.getType(type.getRawClass());
}
/*
/**********************************************************
/* Accessors
/**********************************************************
*/
public Class> getRawClass() { return _jacksonType.getRawClass(); }
public String erasedSignature() {
return _jacksonType.getErasedSignature();
}
public String genericSignature() {
return _jacksonType.getGenericSignature();
}
/**
* @return True if type has direct generic declaration (which may need
* to be copied)
*/
public boolean hasGenerics() {
return _jacksonType.hasGenericTypes();
}
/*
public boolean isPrimitive() {
return _signature.length() == 1;
}
*/
/*
public int getStoreOpcode() {
return _signatureType.getOpcode(ISTORE);
}
*/
public int getLoadOpcode() {
return _asmType.getOpcode(ILOAD);
}
public int getReturnOpcode() {
return _asmType.getOpcode(IRETURN);
}
@Override
public String toString() {
return _jacksonType.toString();
}
/*
/**********************************************************
/* Other methods
/**********************************************************
*/
public static TypeDescription moreSpecificType(TypeDescription desc1, TypeDescription desc2)
{
Class> c1 = desc1.getRawClass();
Class> c2 = desc2.getRawClass();
if (c1.isAssignableFrom(c2)) { // c2 more specific than c1
return desc2;
}
if (c2.isAssignableFrom(c1)) { // c1 more specific than c2
return desc1;
}
// not compatible, so:
return null;
}
}
}