Skip to content

Commit

Permalink
Allow JSON/XML serialization customization in ORM
Browse files Browse the repository at this point in the history
- add qualifiers to mark FormatMapper for JSON/XML formatting
- use a Quarkus-configured ObjectMapper if Jackson is present and user did not specify a format mapper
- use a Quarkus-configured Jsonb if Jsonb is present and user did not specify a format mapper
  • Loading branch information
marko-bekhta authored and yrodiere committed Nov 22, 2023
1 parent 0bc38e2 commit 47ecd0a
Show file tree
Hide file tree
Showing 29 changed files with 764 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ public interface Capability {
String LRA_PARTICIPANT = QUARKUS_PREFIX + ".lra.participant";

String JACKSON = QUARKUS_PREFIX + ".jackson";
String JSONB = QUARKUS_PREFIX + ".jsonb";

String KOTLIN = QUARKUS_PREFIX + ".kotlin";
String JAXB = QUARKUS_PREFIX + ".jaxb";
String JAXP = QUARKUS_PREFIX + ".jaxp";

String JSONB = QUARKUS_PREFIX + ".jsonb";
String KOTLIN = QUARKUS_PREFIX + ".kotlin";

String HAL = QUARKUS_PREFIX + ".hal";

Expand Down
79 changes: 79 additions & 0 deletions docs/src/main/asciidoc/hibernate-orm.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1242,3 +1242,82 @@ to tell Quarkus it should be used in the default persistence unit.
+
For <<multiple-persistence-units,named persistence units>>, use `@PersistenceUnitExtension("nameOfYourPU")`
<2> Implement `org.hibernate.engine.jdbc.spi.StatementInspector`.

[[json_xml_serialization_deserialization]]

Check warning on line 1246 in docs/src/main/asciidoc/hibernate-orm.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Customizing JSON/XML serialization/deserialization'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Customizing JSON/XML serialization/deserialization'.", "location": {"path": "docs/src/main/asciidoc/hibernate-orm.adoc", "range": {"start": {"line": 1246, "column": 36}}}, "severity": "INFO"}
== Customizing JSON/XML serialization/deserialization

By default, Quarkus will try to automatically configure format mappers depending on available extensions.
Globally configured `ObjectMapper` (or `Jsonb`) will be used for serialization/deserialization operations when Jackson (or JSON-B) is available.
Jackson will take precedence if both Jackson and JSON-B are available at the same time.

JSON and XML serialization/deserialization in Hibernate ORM can be customized by implementing a `org.hibernate.type.format.FormatMapper`
and annotating the implementation with the appropriate qualifiers:

[source,java]
----
@JsonFormat // <1>
@PersistenceUnitExtension // <2>
public class MyJsonFormatMapper implements FormatMapper { // <3>
@Override
public String inspect(String sql) {
// ...
return sql;
}
@Override
public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, WrapperOptions wrapperOptions) {
// ...
}
@Override
public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapperOptions) {
// ...
}
}
----
<1> Annotate the format mapper implementation with the `@JsonFormat` qualifier
to tell Quarkus that this mapper is specific to JSON serialization/deserialization.
+
<2> Annotate the format mapper implementation with the `@PersistenceUnitExtension` qualifier
to tell Quarkus it should be used in the default persistence unit.
+
For <<multiple-persistence-units,named persistence units>>, use `@PersistenceUnitExtension("nameOfYourPU")`
<3> Implement `org.hibernate.type.format.FormatMapper`.

In case of a custom XML format mapper, a different CDI qualifier must be applied:

[source,java]
----
@XmlFormat // <1>
@PersistenceUnitExtension // <2>
public class MyJsonFormatMapper implements FormatMapper { // <3>
@Override
public String inspect(String sql) {
// ...
return sql;
}
@Override
public <T> T fromString(CharSequence charSequence, JavaType<T> javaType, WrapperOptions wrapperOptions) {
// ...
}
@Override
public <T> String toString(T value, JavaType<T> javaType, WrapperOptions wrapperOptions) {
// ...
}
}
----
<1> Annotate the format mapper implementation with the `@XmlFormat` qualifier
to tell Quarkus that this mapper is specific to XML serialization/deserialization.
+
<2> Annotate the format mapper implementation with the `@PersistenceUnitExtension` qualifier
to tell Quarkus it should be used in the default persistence unit.
+
For <<multiple-persistence-units,named persistence units>>, use `@PersistenceUnitExtension("nameOfYourPU")`
<3> Implement `org.hibernate.type.format.FormatMapper`.

[NOTE]
====
Format mappers *must* have both `@PersistenceUnitExtension` and either `@JsonFormat` or `@XmlFormat` CDI qualifiers applied.
Having multiple JSON (or XML) format mappers registered for the same persistence unit will result in an exception, because of the ambiguity.
====
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ private static DotName createConstant(String fqcn) {

public static final DotName INTERCEPTOR = createConstant("org.hibernate.Interceptor");
public static final DotName STATEMENT_INSPECTOR = createConstant("org.hibernate.resource.jdbc.spi.StatementInspector");
public static final DotName FORMAT_MAPPER = createConstant("org.hibernate.type.format.FormatMapper");
public static final DotName JSON_FORMAT = createConstant("io.quarkus.hibernate.orm.JsonFormat");
public static final DotName XML_FORMAT = createConstant("io.quarkus.hibernate.orm.XmlFormat");

public static final List<DotName> GENERATORS = List.of(
createConstant("org.hibernate.generator.internal.CurrentTimestampGeneration"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public class HibernateOrmCdiProcessor {
ClassNames.TENANT_RESOLVER,
ClassNames.TENANT_CONNECTION_RESOLVER,
ClassNames.INTERCEPTOR,
ClassNames.STATEMENT_INSPECTOR);
ClassNames.STATEMENT_INSPECTOR,
ClassNames.FORMAT_MAPPER);

@BuildStep
AnnotationsTransformerBuildItem convertJpaResourceAnnotationsToQualifier(
Expand Down Expand Up @@ -247,11 +248,13 @@ void generateDataSourceBeans(HibernateOrmRecorder recorder,
@BuildStep
void registerAnnotations(BuildProducer<AdditionalBeanBuildItem> additionalBeans,
BuildProducer<BeanDefiningAnnotationBuildItem> beanDefiningAnnotations) {
// add the @PersistenceUnit and @PersistenceUnitExtension classes
// add the @PersistenceUnit, @PersistenceUnitExtension, @JsonFormat and @XmlFormat classes
// otherwise they won't be registered as qualifiers
additionalBeans.produce(AdditionalBeanBuildItem.builder()
.addBeanClasses(ClassNames.QUARKUS_PERSISTENCE_UNIT.toString(),
ClassNames.PERSISTENCE_UNIT_EXTENSION.toString())
ClassNames.PERSISTENCE_UNIT_EXTENSION.toString(),
ClassNames.JSON_FORMAT.toString(),
ClassNames.XML_FORMAT.toString())
.build());

// Register the default scope for @PersistenceUnitExtension and make such beans unremovable by default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ public void configurationDescriptorBuilding(
hibernateOrmConfig.database.ormCompatibilityVersion, Collections.emptyMap()),
null,
jpaModel.getXmlMappings(persistenceXmlDescriptorBuildItem.getDescriptor().getName()),
false, true));
false, true, capabilities));
}

if (impliedPU.shouldGenerateImpliedBlockingPersistenceUnit()) {
Expand Down Expand Up @@ -1124,7 +1124,7 @@ private static void producePersistenceUnitDescriptorFromConfig(
persistenceUnitConfig.unsupportedProperties),
persistenceUnitConfig.multitenantSchemaDatasource.orElse(null),
xmlMappings,
false, false));
false, false, capabilities));
}

private static void collectDialectConfig(String persistenceUnitName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

import java.util.Collection;
import java.util.List;
import java.util.Optional;

import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;

import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.hibernate.orm.runtime.boot.QuarkusPersistenceUnitDefinition;
import io.quarkus.hibernate.orm.runtime.boot.xml.RecordableXmlMapping;
import io.quarkus.hibernate.orm.runtime.customized.FormatMapperKind;
import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationStaticDescriptor;
import io.quarkus.hibernate.orm.runtime.recording.RecordedConfig;

Expand All @@ -30,19 +34,23 @@ public final class PersistenceUnitDescriptorBuildItem extends MultiBuildItem {
private final List<RecordableXmlMapping> xmlMappings;
private final boolean isReactive;
private final boolean fromPersistenceXml;
private final Optional<FormatMapperKind> jsonMapper;
private final Optional<FormatMapperKind> xmlMapper;

public PersistenceUnitDescriptorBuildItem(ParsedPersistenceXmlDescriptor descriptor, String configurationName,
RecordedConfig config,
String multiTenancySchemaDataSource,
List<RecordableXmlMapping> xmlMappings,
boolean isReactive, boolean fromPersistenceXml) {
boolean isReactive, boolean fromPersistenceXml, Capabilities capabilities) {
this.descriptor = descriptor;
this.configurationName = configurationName;
this.config = config;
this.multiTenancySchemaDataSource = multiTenancySchemaDataSource;
this.xmlMappings = xmlMappings;
this.isReactive = isReactive;
this.fromPersistenceXml = fromPersistenceXml;
this.jsonMapper = json(capabilities);
this.xmlMapper = xml(capabilities);
}

public Collection<String> getManagedClassNames() {
Expand Down Expand Up @@ -80,6 +88,23 @@ public boolean isFromPersistenceXml() {
public QuarkusPersistenceUnitDefinition asOutputPersistenceUnitDefinition(
List<HibernateOrmIntegrationStaticDescriptor> integrationStaticDescriptors) {
return new QuarkusPersistenceUnitDefinition(descriptor, configurationName, config,
xmlMappings, isReactive, fromPersistenceXml, integrationStaticDescriptors);
xmlMappings, isReactive, fromPersistenceXml, jsonMapper, xmlMapper, integrationStaticDescriptors);
}

private Optional<FormatMapperKind> json(Capabilities capabilities) {
if (capabilities.isPresent(Capability.JACKSON)) {
return Optional.of(FormatMapperKind.JACKSON);
}
if (capabilities.isPresent(Capability.JSONB)) {
return Optional.of(FormatMapperKind.JSONB);
}
return Optional.empty();
}

private Optional<FormatMapperKind> xml(Capabilities capabilities) {
if (capabilities.isPresent(Capability.JAXB)) {
return Optional.of(FormatMapperKind.JAXB);
}
return Optional.empty();
}
}
15 changes: 15 additions & 0 deletions extensions/hibernate-orm/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,21 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-caffeine</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jsonb</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jaxb</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.quarkus.hibernate.orm;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.inject.Qualifier;

import org.hibernate.type.format.FormatMapper;

/**
* CDI qualifier for beans implementing a {@link FormatMapper}.
* <p>
* This mapper will be used by Hibernate ORM for serialization and deserialization of JSON properties.
* <p>
* <strong>Must</strong> be used in a combination with a {@link PersistenceUnitExtension} qualifier to define the persistence
* unit the mapper should be associated with.
*/
@Target({ TYPE, FIELD, METHOD, PARAMETER })
@Retention(RUNTIME)
@Documented
@Qualifier
public @interface JsonFormat {
class Literal extends AnnotationLiteral<JsonFormat> implements JsonFormat {
public static JsonFormat INSTANCE = new Literal();

private Literal() {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.quarkus.hibernate.orm;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.inject.Qualifier;

import org.hibernate.type.format.FormatMapper;

/**
* CDI qualifier for beans implementing a {@link FormatMapper}.
* <p>
* This mapper will be used by Hibernate ORM for serialization and deserialization of XML properties.
* <p>
* <strong>Must</strong> be used in a combination with a {@link PersistenceUnitExtension} qualifier to define the persistence
* unit the mapper should be associated with.
*/
@Target({ TYPE, FIELD, METHOD, PARAMETER })
@Retention(RUNTIME)
@Documented
@Qualifier
public @interface XmlFormat {
class Literal extends AnnotationLiteral<XmlFormat> implements XmlFormat {
public static XmlFormat INSTANCE = new Literal();

private Literal() {
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.hibernate.orm.runtime;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Locale;

Expand All @@ -22,8 +24,10 @@ public static boolean isDefaultPersistenceUnit(String name) {
}

public static <T> InjectableInstance<T> singleExtensionInstanceForPersistenceUnit(Class<T> beanType,
String persistenceUnitName) {
InjectableInstance<T> instance = extensionInstanceForPersistenceUnit(beanType, persistenceUnitName);
String persistenceUnitName,
Annotation... additionalQualifiers) {
InjectableInstance<T> instance = extensionInstanceForPersistenceUnit(beanType, persistenceUnitName,
additionalQualifiers);
if (instance.isAmbiguous()) {
throw new IllegalStateException(String.format(Locale.ROOT,
"Multiple instances of %1$s were found for persistence unit %2$s. "
Expand All @@ -33,9 +37,15 @@ public static <T> InjectableInstance<T> singleExtensionInstanceForPersistenceUni
return instance;
}

public static <T> InjectableInstance<T> extensionInstanceForPersistenceUnit(Class<T> beanType, String persistenceUnitName) {
return Arc.container().select(beanType,
new PersistenceUnitExtension.Literal(persistenceUnitName));
public static <T> InjectableInstance<T> extensionInstanceForPersistenceUnit(Class<T> beanType, String persistenceUnitName,
Annotation... additionalQualifiers) {
if (additionalQualifiers.length == 0) {
return Arc.container().select(beanType, new PersistenceUnitExtension.Literal(persistenceUnitName));
} else {
Annotation[] qualifiers = Arrays.copyOf(additionalQualifiers, additionalQualifiers.length + 1);
qualifiers[additionalQualifiers.length] = new PersistenceUnitExtension.Literal(persistenceUnitName);
return Arc.container().select(beanType, qualifiers);
}
}

public static class PersistenceUnitNameComparator implements Comparator<String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@
import org.hibernate.tool.schema.spi.CommandAcceptanceException;
import org.hibernate.tool.schema.spi.DelayedDropRegistryNotAvailableImpl;
import org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator;
import org.hibernate.type.format.FormatMapper;

import io.quarkus.arc.InjectableInstance;
import io.quarkus.hibernate.orm.JsonFormat;
import io.quarkus.hibernate.orm.XmlFormat;
import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil;
import io.quarkus.hibernate.orm.runtime.RuntimeSettings;
import io.quarkus.hibernate.orm.runtime.migration.MultiTenancyStrategy;
Expand Down Expand Up @@ -211,6 +214,17 @@ protected void populate(String persistenceUnitName, SessionFactoryOptionsBuilder
if (!statementInspectorInstance.isUnsatisfied()) {
options.applyStatementInspector(statementInspectorInstance.get());
}

InjectableInstance<FormatMapper> jsonFormatMapper = PersistenceUnitUtil.singleExtensionInstanceForPersistenceUnit(
FormatMapper.class, persistenceUnitName, JsonFormat.Literal.INSTANCE);
if (!jsonFormatMapper.isUnsatisfied()) {
options.applyJsonFormatMapper(jsonFormatMapper.get());
}
InjectableInstance<FormatMapper> xmlFormatMapper = PersistenceUnitUtil.singleExtensionInstanceForPersistenceUnit(
FormatMapper.class, persistenceUnitName, XmlFormat.Literal.INSTANCE);
if (!xmlFormatMapper.isUnsatisfied()) {
options.applyXmlFormatMapper(xmlFormatMapper.get());
}
}

private static class ServiceRegistryCloser implements SessionFactoryObserver {
Expand Down
Loading

0 comments on commit 47ecd0a

Please sign in to comment.