Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce the ability to configure Jsonb and generate serializers for JAX-RS return types #3553

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ stages:
timeoutInMinutes: 25
modules:
- resteasy-jackson
- resteasy-jsonb
- vertx
- vertx-http
- virtual-http
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkus.resteasy.jsonb.deployment;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.jboss.jandex.DotName;
import org.jboss.jandex.ParameterizedType;
import org.jboss.jandex.Type;

public final class CollectionUtil {

private static final Set<DotName> SUPPORTED_TYPES = new HashSet<>(
Arrays.asList(DotNames.COLLECTION, DotNames.LIST, DotNames.SET));

private CollectionUtil() {
}

// TODO come up with a better way of determining if the type is supported
public static boolean isCollection(DotName dotName) {
return SUPPORTED_TYPES.contains(dotName);
}

/**
* @return the generic type of a collection
*/
public static Type getGenericType(Type type) {
if (!isCollection(type.name())) {
return null;
}

if (!(type instanceof ParameterizedType)) {
return null;
}

ParameterizedType parameterizedType = type.asParameterizedType();

if (parameterizedType.arguments().size() != 1) {
return null;
}

return parameterizedType.arguments().get(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.quarkus.resteasy.jsonb.deployment;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import javax.json.bind.Jsonb;
import javax.json.bind.annotation.JsonbDateFormat;
import javax.json.bind.annotation.JsonbNillable;
import javax.json.bind.annotation.JsonbNumberFormat;
import javax.json.bind.annotation.JsonbProperty;
import javax.json.bind.annotation.JsonbPropertyOrder;
import javax.json.bind.annotation.JsonbTransient;
import javax.json.bind.annotation.JsonbTypeAdapter;
import javax.json.bind.annotation.JsonbTypeSerializer;
import javax.json.bind.annotation.JsonbVisibility;
import javax.ws.rs.ext.ContextResolver;

import org.jboss.jandex.DotName;

public final class DotNames {

private DotNames() {
}

public static final DotName OBJECT = DotName.createSimple(Object.class.getName());
public static final DotName STRING = DotName.createSimple(String.class.getName());
public static final DotName PRIMITIVE_BOOLEAN = DotName.createSimple(boolean.class.getName());
public static final DotName PRIMITIVE_INT = DotName.createSimple(int.class.getName());
public static final DotName PRIMITIVE_LONG = DotName.createSimple(long.class.getName());
public static final DotName BOOLEAN = DotName.createSimple(Boolean.class.getName());
public static final DotName INTEGER = DotName.createSimple(Integer.class.getName());
public static final DotName LONG = DotName.createSimple(Long.class.getName());
public static final DotName BIG_DECIMAL = DotName.createSimple(BigDecimal.class.getName());
public static final DotName LOCAL_DATE_TIME = DotName.createSimple(LocalDateTime.class.getName());

public static final DotName COLLECTION = DotName.createSimple(Collection.class.getName());
public static final DotName LIST = DotName.createSimple(List.class.getName());
public static final DotName SET = DotName.createSimple(Set.class.getName());

public static final DotName OPTIONAL = DotName.createSimple(Optional.class.getName());

public static final DotName MAP = DotName.createSimple(Map.class.getName());
public static final DotName HASHMAP = DotName.createSimple(HashMap.class.getName());

public static final DotName CONTEXT_RESOLVER = DotName.createSimple(ContextResolver.class.getName());

public static final DotName JSONB = DotName.createSimple(Jsonb.class.getName());
public static final DotName JSONB_TRANSIENT = DotName.createSimple(JsonbTransient.class.getName());
public static final DotName JSONB_PROPERTY = DotName.createSimple(JsonbProperty.class.getName());
public static final DotName JSONB_TYPE_SERIALIZER = DotName.createSimple(JsonbTypeSerializer.class.getName());
public static final DotName JSONB_TYPE_ADAPTER = DotName.createSimple(JsonbTypeAdapter.class.getName());
public static final DotName JSONB_VISIBILITY = DotName.createSimple(JsonbVisibility.class.getName());
public static final DotName JSONB_NILLABLE = DotName.createSimple(JsonbNillable.class.getName());
public static final DotName JSONB_PROPERTY_ORDER = DotName.createSimple(JsonbPropertyOrder.class.getName());
public static final DotName JSONB_NUMBER_FORMAT = DotName.createSimple(JsonbNumberFormat.class.getName());
public static final DotName JSONB_DATE_FORMAT = DotName.createSimple(JsonbDateFormat.class.getName());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package io.quarkus.resteasy.jsonb.deployment;

import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;

public final class JandexUtil {

private JandexUtil() {
}

// includes annotations on superclasses, all interfaces and package-info
public static Map<DotName, AnnotationInstance> getEffectiveClassAnnotations(DotName classDotName, IndexView index) {
Map<DotName, AnnotationInstance> result = new HashMap<>();
getEffectiveClassAnnotationsRec(classDotName, index, result);

// we need these because they could contain jsonb annotations that alter the default behavior for all
// classes in the package
Collection<AnnotationInstance> annotationsFromPackage = getAnnotationsOfPackage(classDotName, index);
if (!annotationsFromPackage.isEmpty()) {
for (AnnotationInstance packageAnnotation : annotationsFromPackage) {
if (!result.containsKey(packageAnnotation.name())) {
result.put(packageAnnotation.name(), packageAnnotation);
}
}
}
return result;
}

private static void getEffectiveClassAnnotationsRec(DotName classDotName, IndexView index,
Map<DotName, AnnotationInstance> collected) {
// annotations previously collected have higher "priority" so we need to make sure we don't add them again
ClassInfo classInfo = index.getClassByName(classDotName);
if (classInfo == null) {
return;
}

Collection<AnnotationInstance> newInstances = classInfo.classAnnotations();
for (AnnotationInstance newInstance : newInstances) {
if (!collected.containsKey(newInstance.name())) {
collected.put(newInstance.name(), newInstance);
}
}

// collect annotations from the super type until we reach object
if (!DotNames.OBJECT.equals(classInfo.superName())) {
getEffectiveClassAnnotationsRec(classInfo.superName(), index, collected);
}

// collect annotations from all interfaces
for (DotName interfaceDotName : classInfo.interfaceNames()) {
getEffectiveClassAnnotationsRec(interfaceDotName, index, collected);
}
}

private static Collection<AnnotationInstance> getAnnotationsOfPackage(DotName classDotName, IndexView index) {
String className = classDotName.toString();
if (!className.contains(".")) {
return Collections.emptyList();
}
int i = className.lastIndexOf('.');
String packageName = className.substring(0, i);
ClassInfo packageClassInfo = index.getClassByName(DotName.createSimple(packageName + ".package-info"));
if (packageClassInfo == null) {
return Collections.emptyList();
}

return packageClassInfo.classAnnotations();
}

// determine whether the class contains any interface (however far up the tree) that contains a default method
public static boolean containsInterfacesWithDefaultMethods(ClassInfo classInfo, IndexView index) {
List<DotName> interfaceNames = classInfo.interfaceNames();
for (DotName interfaceName : interfaceNames) {
ClassInfo interfaceClassInfo = index.getClassByName(interfaceName);
if (interfaceClassInfo == null) {
continue;
}
final List<MethodInfo> methods = interfaceClassInfo.methods();
for (MethodInfo method : methods) {
// essentially the same as java.lang.reflect.Method#isDefault
if (((method.flags() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC)) {
return true;
}
}
return containsInterfacesWithDefaultMethods(interfaceClassInfo, index);
}
return false;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package io.quarkus.resteasy.jsonb.deployment;

import java.util.Locale;
import java.util.Map;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Singleton;
import javax.json.bind.Jsonb;
import javax.json.bind.serializer.JsonbSerializer;
import javax.json.spi.JsonProvider;

import org.eclipse.yasson.YassonProperties;
import org.eclipse.yasson.internal.JsonbContext;
import org.eclipse.yasson.internal.MappingContext;
import org.eclipse.yasson.internal.serializer.ContainerSerializerProvider;

import io.quarkus.arc.DefaultBean;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.resteasy.jsonb.runtime.serializers.QuarkusJsonbBinding;
import io.quarkus.resteasy.jsonb.runtime.serializers.SimpleContainerSerializerProvider;

public class JsonbBeanProducerGenerator {

public static String JSONB_PRODUCER = "io.quarkus.jsonb.JsonbProducer";

private final JsonbConfig jsonbConfig;

public JsonbBeanProducerGenerator(JsonbConfig jsonbConfig) {
this.jsonbConfig = jsonbConfig;
}

void generateJsonbContextResolver(ClassOutput classOutput, Map<String, String> typeToGeneratedSerializers) {
try (ClassCreator cc = ClassCreator.builder()
.classOutput(classOutput).className(JSONB_PRODUCER)
.build()) {

cc.addAnnotation(ApplicationScoped.class);

try (MethodCreator createJsonb = cc.getMethodCreator("createJsonb", Jsonb.class)) {

createJsonb.addAnnotation(Singleton.class);
createJsonb.addAnnotation(Produces.class);
createJsonb.addAnnotation(DefaultBean.class);

Class<javax.json.bind.JsonbConfig> jsonbConfigClass = javax.json.bind.JsonbConfig.class;

// create the JsonbConfig object
ResultHandle config = createJsonb.newInstance(MethodDescriptor.ofConstructor(jsonbConfigClass));

// create the jsonbContext object
ResultHandle provider = createJsonb
.invokeStaticMethod(MethodDescriptor.ofMethod(JsonProvider.class, "provider", JsonProvider.class));
ResultHandle jsonbContext = createJsonb.newInstance(
MethodDescriptor.ofConstructor(JsonbContext.class, jsonbConfigClass, JsonProvider.class),
config, provider);
ResultHandle mappingContext = createJsonb.invokeVirtualMethod(
MethodDescriptor.ofMethod(JsonbContext.class, "getMappingContext", MappingContext.class),
jsonbContext);

//handle locale
ResultHandle locale = null;
if (jsonbConfig.locale.isPresent()) {
locale = createJsonb.invokeStaticMethod(
MethodDescriptor.ofMethod(JsonbSupportClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER, "get",
Locale.class));
createJsonb.invokeVirtualMethod(
MethodDescriptor.ofMethod(jsonbConfigClass, "withLocale", jsonbConfigClass, Locale.class),
config, locale);
}

// handle date format
if (jsonbConfig.dateFormat.isPresent()) {
createJsonb.invokeVirtualMethod(
MethodDescriptor.ofMethod(jsonbConfigClass, "withDateFormat", jsonbConfigClass, String.class,
Locale.class),
config,
createJsonb.load(jsonbConfig.dateFormat.get()),
locale != null ? locale : createJsonb.loadNull());
}

// handle serializeNullValues
createJsonb.invokeVirtualMethod(
MethodDescriptor.ofMethod(jsonbConfigClass, "withNullValues", jsonbConfigClass, Boolean.class),
config,
createJsonb.invokeStaticMethod(
MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class),
createJsonb.load(jsonbConfig.serializeNullValues)));

// handle propertyOrderStrategy
createJsonb.invokeVirtualMethod(
MethodDescriptor.ofMethod(jsonbConfigClass, "withPropertyOrderStrategy", jsonbConfigClass,
String.class),
config, createJsonb.load(jsonbConfig.propertyOrderStrategy.toUpperCase()));

// handle encoding
if (jsonbConfig.encoding.isPresent()) {
createJsonb.invokeVirtualMethod(
MethodDescriptor.ofMethod(jsonbConfigClass, "withEncoding", jsonbConfigClass,
String.class),
config, createJsonb.load(jsonbConfig.encoding.get()));
}

// handle failOnUnknownProperties
createJsonb.invokeVirtualMethod(
MethodDescriptor.ofMethod(jsonbConfigClass, "setProperty", jsonbConfigClass, String.class,
Object.class),
config,
createJsonb.load(YassonProperties.FAIL_ON_UNKNOWN_PROPERTIES),
createJsonb.invokeStaticMethod(
MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class),
createJsonb.load(jsonbConfig.failOnUnknownProperties)));

// add generated serializers to config
if (!typeToGeneratedSerializers.isEmpty()) {
ResultHandle serializersArray = createJsonb.newArray(JsonbSerializer.class,
createJsonb.load(typeToGeneratedSerializers.size()));
int i = 0;
for (Map.Entry<String, String> entry : typeToGeneratedSerializers.entrySet()) {

ResultHandle serializer = createJsonb
.newInstance(MethodDescriptor.ofConstructor(entry.getValue()));

// build up the serializers array that will be passed to JsonbConfig
createJsonb.writeArrayValue(serializersArray, createJsonb.load(i), serializer);

// add a ContainerSerializerProvider for the serializer
ResultHandle serializerProvider = createJsonb.newInstance(
MethodDescriptor.ofConstructor(SimpleContainerSerializerProvider.class, JsonbSerializer.class),
serializer);
createJsonb.invokeVirtualMethod(
MethodDescriptor.ofMethod(MappingContext.class, "addSerializerProvider", void.class,
Class.class, ContainerSerializerProvider.class),
mappingContext, createJsonb.loadClass(entry.getKey()), serializerProvider);

i++;
}
createJsonb.invokeVirtualMethod(
MethodDescriptor.ofMethod(jsonbConfigClass, "withSerializers", jsonbConfigClass,
JsonbSerializer[].class),
config, serializersArray);
}

// create jsonb from QuarkusJsonbBinding
ResultHandle jsonb = createJsonb.newInstance(
MethodDescriptor.ofConstructor(QuarkusJsonbBinding.class, JsonbContext.class), jsonbContext);

createJsonb.returnValue(jsonb);
}
}
}
}
Loading