package cz.habarta.typescript.generator.parser;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.ser.*;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
import cz.habarta.typescript.generator.*;
import cz.habarta.typescript.generator.compiler.EnumKind;
import cz.habarta.typescript.generator.compiler.EnumMemberModel;
import cz.habarta.typescript.generator.util.Predicate;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.*;
public class Jackson2Parser extends ModelParser {
private final ObjectMapper objectMapper = new ObjectMapper();
public Jackson2Parser(Settings settings, TypeProcessor typeProcessor) {
this(settings, typeProcessor, false);
}
public Jackson2Parser(Settings settings, TypeProcessor typeProcessor, boolean useJaxbAnnotations) {
super(settings, typeProcessor);
if (!settings.disableJackson2ModuleDiscovery) {
objectMapper.registerModules(ObjectMapper.findModules(settings.classLoader));
}
if (useJaxbAnnotations) {
AnnotationIntrospector introspector = new JaxbAnnotationIntrospector(objectMapper.getTypeFactory());
objectMapper.setAnnotationIntrospector(introspector);
}
}
@Override
protected DeclarationModel parseClass(SourceType<Class<?>> sourceClass) {
if (sourceClass.type.isEnum()) {
return parseEnumOrObjectEnum(sourceClass);
} else {
return parseBean(sourceClass);
}
}
private BeanModel parseBean(SourceType<Class<?>> sourceClass) {
final List<PropertyModel> properties = new ArrayList<>();
final BeanHelper beanHelper = getBeanHelper(sourceClass.type);
if (beanHelper != null) {
for (BeanPropertyWriter beanPropertyWriter : beanHelper.getProperties()) {
final Member propertyMember = beanPropertyWriter.getMember().getMember();
Type propertyType = getGenericType(propertyMember);
if (propertyType == JsonNode.class) {
propertyType = Object.class;
}
boolean isInAnnotationFilter = settings.includePropertyAnnotations.isEmpty();
if (!isInAnnotationFilter) {
for (Class<? extends Annotation> optionalAnnotation : settings.includePropertyAnnotations) {
if (beanPropertyWriter.getAnnotation(optionalAnnotation) != null) {
isInAnnotationFilter = true;
break;
}
}
if (!isInAnnotationFilter) {
System.out.println("Skipping " + sourceClass.type + "." + beanPropertyWriter.getName() + " because it is missing an annotation from includePropertyAnnotations!");
continue;
}
}
boolean optional = false;
for (Class<? extends Annotation> optionalAnnotation : settings.optionalAnnotations) {
if (beanPropertyWriter.getAnnotation(optionalAnnotation) != null) {
optional = true;
break;
}
}
// @JsonUnwrapped
PropertyModel.PullProperties pullProperties = null;
final Member originalMember = beanPropertyWriter.getMember().getMember();
if (originalMember instanceof AccessibleObject) {
final AccessibleObject accessibleObject = (AccessibleObject) originalMember;
final JsonUnwrapped annotation = accessibleObject.getAnnotation(JsonUnwrapped.class);
if (annotation != null && annotation.enabled()) {
pullProperties = new PropertyModel.PullProperties(annotation.prefix(), annotation.suffix());
}
}
properties.add(processTypeAndCreateProperty(beanPropertyWriter.getName(), propertyType, optional, sourceClass.type, originalMember, pullProperties));
}
}
if (sourceClass.type.isEnum()) {
return new BeanModel(sourceClass.type, null, null, null, null, null, properties, null);
}
final String discriminantProperty;
final String discriminantLiteral;
final JsonTypeInfo jsonTypeInfo = sourceClass.type.getAnnotation(JsonTypeInfo.class);
final JsonTypeInfo parentJsonTypeInfo;
if (isSupported(jsonTypeInfo)) {
// this is parent
discriminantProperty = getDiscriminantPropertyName(jsonTypeInfo);
discriminantLiteral = null;
} else if (!sourceClass.type.isInterface() && !Modifier.isAbstract(sourceClass.type.getModifiers()) && isSupported(parentJsonTypeInfo = getAnnotationRecursive(sourceClass.type, JsonTypeInfo.class))) {
// this is child class
discriminantProperty = getDiscriminantPropertyName(parentJsonTypeInfo);
discriminantLiteral = getTypeName(sourceClass.type);
} else {
// not part of explicit hierarchy
discriminantProperty = null;
discriminantLiteral = null;
}
final List<Class<?>> taggedUnionClasses;
final JsonSubTypes jsonSubTypes = sourceClass.type.getAnnotation(JsonSubTypes.class);
if (jsonSubTypes != null) {
taggedUnionClasses = new ArrayList<>();
for (JsonSubTypes.Type type : jsonSubTypes.value()) {
addBeanToQueue(new SourceType<>(type.value(), sourceClass.type, "<subClass>"));
taggedUnionClasses.add(type.value());
}
} else {
taggedUnionClasses = null;
}
final Type superclass = sourceClass.type.getGenericSuperclass() == Object.class ? null : sourceClass.type.getGenericSuperclass();
if (superclass != null) {
addBeanToQueue(new SourceType<>(superclass, sourceClass.type, "<superClass>"));
}
final List<Type> interfaces = Arrays.asList(sourceClass.type.getGenericInterfaces());
for (Type aInterface : interfaces) {
addBeanToQueue(new SourceType<>(aInterface, sourceClass.type, "<interface>"));
}
return new BeanModel(sourceClass.type, superclass, taggedUnionClasses, discriminantProperty, discriminantLiteral, interfaces, properties, null);
}
private static Type getGenericType(Member member) {
if (member instanceof Method) {
return ((Method) member).getGenericReturnType();
}
if (member instanceof Field) {
return ((Field) member).getGenericType();
}
return null;
}
private static boolean isSupported(JsonTypeInfo jsonTypeInfo) {
return jsonTypeInfo != null &&
jsonTypeInfo.include() == JsonTypeInfo.As.PROPERTY &&
(jsonTypeInfo.use() == JsonTypeInfo.Id.NAME || jsonTypeInfo.use() == JsonTypeInfo.Id.CLASS);
}
private String getDiscriminantPropertyName(JsonTypeInfo jsonTypeInfo) {
return jsonTypeInfo.property().isEmpty()
? jsonTypeInfo.use().getDefaultPropertyName()
: jsonTypeInfo.property();
}
private String getTypeName(final Class<?> cls) {
// find @JsonTypeName recursively
final JsonTypeName jsonTypeName = getAnnotationRecursive(cls, JsonTypeName.class);
if (jsonTypeName != null) {
return jsonTypeName.value();
}
// find @JsonSubTypes.Type recursively
final JsonSubTypes jsonSubTypes = getAnnotationRecursive(cls, JsonSubTypes.class, new Predicate<JsonSubTypes>() {
@Override
public boolean test(JsonSubTypes types) {
return getJsonSubTypeForClass(types, cls) != null;
}
});
if (jsonSubTypes != null) {
final JsonSubTypes.Type jsonSubType = getJsonSubTypeForClass(jsonSubTypes, cls);
if (!jsonSubType.name().isEmpty()) {
return jsonSubType.name();
}
}
// use simplified class name
return cls.getName().substring(cls.getName().lastIndexOf(".") + 1);
}
private static JsonSubTypes.Type getJsonSubTypeForClass(JsonSubTypes types, Class<?> cls) {
for (JsonSubTypes.Type type : types.value()) {
if (type.value().equals(cls)) {
return type;
}
}
return null;
}
private static <T extends Annotation> T getAnnotationRecursive(Class<?> cls, Class<T> annotationClass) {
return getAnnotationRecursive(cls, annotationClass, null);
}
private static <T extends Annotation> T getAnnotationRecursive(Class<?> cls, Class<T> annotationClass, Predicate<T> annotationFilter) {
if (cls == null) {
return null;
}
final T annotation = cls.getAnnotation(annotationClass);
if (annotation != null && (annotationFilter == null || annotationFilter.test(annotation))) {
return annotation;
}
for (Class<?> aInterface : cls.getInterfaces()) {
final T interfaceAnnotation = getAnnotationRecursive(aInterface, annotationClass, annotationFilter);
if (interfaceAnnotation != null) {
return interfaceAnnotation;
}
}
final T superclassAnnotation = getAnnotationRecursive(cls.getSuperclass(), annotationClass, annotationFilter);
if (superclassAnnotation != null) {
return superclassAnnotation;
}
return null;
}
private BeanHelper getBeanHelper(Class<?> beanClass) {
if (beanClass == null) {
return null;
}
try {
final DefaultSerializerProvider.Impl serializerProvider1 = (DefaultSerializerProvider.Impl) objectMapper.getSerializerProvider();
final DefaultSerializerProvider.Impl serializerProvider2 = serializerProvider1.createInstance(objectMapper.getSerializationConfig(), objectMapper.getSerializerFactory());
final JavaType simpleType = objectMapper.constructType(beanClass);
final JsonSerializer<?> jsonSerializer = BeanSerializerFactory.instance.createSerializer(serializerProvider2, simpleType);
if (jsonSerializer == null) {
return null;
}
if (jsonSerializer instanceof BeanSerializer) {
return new BeanHelper((BeanSerializer) jsonSerializer);
} else {
final String jsonSerializerName = jsonSerializer.getClass().getName();
if (settings.displaySerializerWarning) {
System.out.println(String.format("Warning: Unknown serializer '%s' for class '%s'", jsonSerializerName, beanClass));
}
return null;
}
} catch (JsonMappingException e) {
throw new RuntimeException(e);
}
}
private static class BeanHelper extends BeanSerializer {
private static final long serialVersionUID = 1;
public BeanHelper(BeanSerializer src) {
super(src);
}
public BeanPropertyWriter[] getProperties() {
return _props;
}
}
private DeclarationModel parseEnumOrObjectEnum(SourceType<Class<?>> sourceClass) {
final JsonFormat jsonFormat = sourceClass.type.getAnnotation(JsonFormat.class);
if (jsonFormat != null && jsonFormat.shape() == JsonFormat.Shape.OBJECT) {
return parseBean(sourceClass);
}
final boolean isNumberBased = jsonFormat != null && (
jsonFormat.shape() == JsonFormat.Shape.NUMBER ||
jsonFormat.shape() == JsonFormat.Shape.NUMBER_FLOAT ||
jsonFormat.shape() == JsonFormat.Shape.NUMBER_INT);
final List<EnumMemberModel<String>> stringMembers = new ArrayList<>();
final List<EnumMemberModel<Number>> numberMembers = new ArrayList<>();
if (sourceClass.type.isEnum()) {
final Class<?> enumClass = (Class<?>) sourceClass.type;
try {
Method valueMethod = null;
final BeanInfo beanInfo = Introspector.getBeanInfo(enumClass);
for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
final Method readMethod = propertyDescriptor.getReadMethod();
if (readMethod.isAnnotationPresent(JsonValue.class)) {
valueMethod = readMethod;
}
}
int index = 0;
for (Field field : enumClass.getFields()) {
if (field.isEnumConstant()) {
if (isNumberBased) {
final Number value = getNumberEnumValue(field, valueMethod, index++);
numberMembers.add(new EnumMemberModel<>(field.getName(), value, null));
} else {
final String value = getStringEnumValue(field, valueMethod);
stringMembers.add(new EnumMemberModel<>(field.getName(), value, null));
}
}
}
} catch (Exception e) {
System.out.println(String.format("Cannot get enum values for '%s' enum", enumClass.getName()));
e.printStackTrace(System.out);
}
}
if (isNumberBased) {
return new EnumModel<>(sourceClass.type, EnumKind.NumberBased, numberMembers, null);
} else {
return new EnumModel<>(sourceClass.type, EnumKind.StringBased, stringMembers, null);
}
}
private Number getNumberEnumValue(Field field, Method valueMethod, int index) throws Exception {
if (valueMethod != null) {
final Object valueObject = invokeJsonValueMethod(field, valueMethod);
if (valueObject instanceof Number) {
return (Number) valueObject;
}
}
return index;
}
private String getStringEnumValue(Field field, Method valueMethod) throws Exception {
if (valueMethod != null) {
final Object valueObject = invokeJsonValueMethod(field, valueMethod);
if (valueObject instanceof String) {
return (String) valueObject;
}
}
if (field.isAnnotationPresent(JsonProperty.class)) {
final JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class);
if (!jsonProperty.value().equals(JsonProperty.USE_DEFAULT_NAME)) {
return jsonProperty.value();
}
}
return field.getName();
}
private Object invokeJsonValueMethod(Field field, Method valueMethod) throws ReflectiveOperationException {
field.setAccessible(true);
final Object constant = field.get(null);
valueMethod.setAccessible(true);
final Object valueObject = valueMethod.invoke(constant);
return valueObject;
}
}