From a2d7b8a261dace8ab395b889d2794d6750cdc399 Mon Sep 17 00:00:00 2001 From: Timothy Rundle Date: Tue, 21 Jul 2026 07:28:06 -0400 Subject: [PATCH 1/4] Upgrade jacoco plugin from 0.8.6 to 0.8.15 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 168fa38..711f1a0 100644 --- a/pom.xml +++ b/pom.xml @@ -110,7 +110,7 @@ org.jacoco jacoco-maven-plugin - 0.8.6 + 0.8.15 default-prepare-agent From 175b2a2db9672133f473aec3962f0da7cbc70c9d Mon Sep 17 00:00:00 2001 From: Timothy Rundle Date: Mon, 20 Jul 2026 21:58:07 -0400 Subject: [PATCH 2/4] Added parent; move Jackson2 implementation into sub-module --- jackson-dataformat-hal-v2/pom.xml | 58 +++++++++++++++ .../jackson/dataformat/hal/HALLink.java | 0 .../jackson/dataformat/hal/HALMapper.java | 0 .../dataformat/hal/JacksonHALModule.java | 0 .../dataformat/hal/PackageVersion.java | 0 .../dataformat/hal/annotation/Curie.java | 0 .../dataformat/hal/annotation/Curies.java | 0 .../hal/annotation/EmbeddedResource.java | 0 .../dataformat/hal/annotation/Link.java | 0 .../dataformat/hal/annotation/Resource.java | 0 .../dataformat/hal/deser/CurieMap.java | 0 .../hal/deser/HALBeanDeserializer.java | 0 .../deser/HALBeanDeserializerModifier.java | 0 .../hal/deser/ReservedProperty.java | 0 .../jackson/dataformat/hal/package-info.java | 0 .../dataformat/hal/ser/HALBeanSerializer.java | 0 .../hal/ser/HALBeanSerializerModifier.java | 0 .../com.fasterxml.jackson.core.ObjectCodec | 0 .../dataformat/hal/deser/CurieMapTest.java | 0 .../hal/deser/HALBeanDeserializerIT.java | 0 .../deser/HALBeanDeserializerJsonViewIT.java | 0 .../deser/HALBeanDeserializerMethodAnnIT.java | 0 .../hal/deser/ReservedPropertyTest.java | 0 .../hal/ser/HALBeanSerializerIT.java | 0 .../hal/ser/HALBeanSerializerJsonViewIT.java | 0 .../hal/ser/HALBeanSerializerMethodAnnIT.java | 0 ...ializerPolymorphicWithExistingFieldIT.java | 0 ...anSerializerPolymorphicWithPropertyIT.java | 0 pom.xml | 72 ++++++------------- 29 files changed, 78 insertions(+), 52 deletions(-) create mode 100644 jackson-dataformat-hal-v2/pom.xml rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/HALLink.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/HALMapper.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/JacksonHALModule.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/PackageVersion.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curie.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curies.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/annotation/EmbeddedResource.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/annotation/Link.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/annotation/Resource.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/deser/CurieMap.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/deser/HALBeanDeserializer.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/deser/HALBeanDeserializerModifier.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/deser/ReservedProperty.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/package-info.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/ser/HALBeanSerializer.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/java/io/openapitools/jackson/dataformat/hal/ser/HALBeanSerializerModifier.java (100%) rename {src => jackson-dataformat-hal-v2/src}/main/resources/META-INF/services/com.fasterxml.jackson.core.ObjectCodec (100%) rename {src => jackson-dataformat-hal-v2/src}/test/java/io/openapitoools/jackson/dataformat/hal/deser/CurieMapTest.java (100%) rename {src => jackson-dataformat-hal-v2/src}/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerIT.java (100%) rename {src => jackson-dataformat-hal-v2/src}/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerJsonViewIT.java (100%) rename {src => jackson-dataformat-hal-v2/src}/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerMethodAnnIT.java (100%) rename {src => jackson-dataformat-hal-v2/src}/test/java/io/openapitoools/jackson/dataformat/hal/deser/ReservedPropertyTest.java (100%) rename {src => jackson-dataformat-hal-v2/src}/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerIT.java (100%) rename {src => jackson-dataformat-hal-v2/src}/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerJsonViewIT.java (100%) rename {src => jackson-dataformat-hal-v2/src}/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerMethodAnnIT.java (100%) rename {src => jackson-dataformat-hal-v2/src}/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerPolymorphicWithExistingFieldIT.java (100%) rename {src => jackson-dataformat-hal-v2/src}/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerPolymorphicWithPropertyIT.java (100%) diff --git a/jackson-dataformat-hal-v2/pom.xml b/jackson-dataformat-hal-v2/pom.xml new file mode 100644 index 0000000..ca06432 --- /dev/null +++ b/jackson-dataformat-hal-v2/pom.xml @@ -0,0 +1,58 @@ + + + + 4.0.0 + + + io.openapitools.jackson.dataformat + jackson-dataformat-hal-parent + 1.0.10-SNAPSHOT + ../pom.xml + + + Jackson HAL Dataformat + jackson-dataformat-hal + jar + + Dataformat extension for Jackson to handle the HAL JSON format. + + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + ${com.fasterxml.jackson.version} + + + org.slf4j + slf4j-api + ${org.slf4j.version} + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.slf4j + slf4j-simple + ${org.slf4j.version} + test + + + org.mockito + mockito-core + ${org.mockito.version} + test + + + + + diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/HALLink.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/HALLink.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/HALLink.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/HALLink.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/HALMapper.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/HALMapper.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/HALMapper.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/HALMapper.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/JacksonHALModule.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/JacksonHALModule.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/JacksonHALModule.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/JacksonHALModule.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/PackageVersion.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/PackageVersion.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/PackageVersion.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/PackageVersion.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curie.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curie.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curie.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curie.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curies.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curies.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curies.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curies.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/EmbeddedResource.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/EmbeddedResource.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/annotation/EmbeddedResource.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/EmbeddedResource.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Link.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Link.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Link.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Link.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Resource.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Resource.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Resource.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Resource.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/deser/CurieMap.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/deser/CurieMap.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/deser/CurieMap.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/deser/CurieMap.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/deser/HALBeanDeserializer.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/deser/HALBeanDeserializer.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/deser/HALBeanDeserializer.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/deser/HALBeanDeserializer.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/deser/HALBeanDeserializerModifier.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/deser/HALBeanDeserializerModifier.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/deser/HALBeanDeserializerModifier.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/deser/HALBeanDeserializerModifier.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/deser/ReservedProperty.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/deser/ReservedProperty.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/deser/ReservedProperty.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/deser/ReservedProperty.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/package-info.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/package-info.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/package-info.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/package-info.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/ser/HALBeanSerializer.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/ser/HALBeanSerializer.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/ser/HALBeanSerializer.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/ser/HALBeanSerializer.java diff --git a/src/main/java/io/openapitools/jackson/dataformat/hal/ser/HALBeanSerializerModifier.java b/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/ser/HALBeanSerializerModifier.java similarity index 100% rename from src/main/java/io/openapitools/jackson/dataformat/hal/ser/HALBeanSerializerModifier.java rename to jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/ser/HALBeanSerializerModifier.java diff --git a/src/main/resources/META-INF/services/com.fasterxml.jackson.core.ObjectCodec b/jackson-dataformat-hal-v2/src/main/resources/META-INF/services/com.fasterxml.jackson.core.ObjectCodec similarity index 100% rename from src/main/resources/META-INF/services/com.fasterxml.jackson.core.ObjectCodec rename to jackson-dataformat-hal-v2/src/main/resources/META-INF/services/com.fasterxml.jackson.core.ObjectCodec diff --git a/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/CurieMapTest.java b/jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/CurieMapTest.java similarity index 100% rename from src/test/java/io/openapitoools/jackson/dataformat/hal/deser/CurieMapTest.java rename to jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/CurieMapTest.java diff --git a/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerIT.java b/jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerIT.java similarity index 100% rename from src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerIT.java rename to jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerIT.java diff --git a/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerJsonViewIT.java b/jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerJsonViewIT.java similarity index 100% rename from src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerJsonViewIT.java rename to jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerJsonViewIT.java diff --git a/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerMethodAnnIT.java b/jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerMethodAnnIT.java similarity index 100% rename from src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerMethodAnnIT.java rename to jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/HALBeanDeserializerMethodAnnIT.java diff --git a/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/ReservedPropertyTest.java b/jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/ReservedPropertyTest.java similarity index 100% rename from src/test/java/io/openapitoools/jackson/dataformat/hal/deser/ReservedPropertyTest.java rename to jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/ReservedPropertyTest.java diff --git a/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerIT.java b/jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerIT.java similarity index 100% rename from src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerIT.java rename to jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerIT.java diff --git a/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerJsonViewIT.java b/jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerJsonViewIT.java similarity index 100% rename from src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerJsonViewIT.java rename to jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerJsonViewIT.java diff --git a/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerMethodAnnIT.java b/jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerMethodAnnIT.java similarity index 100% rename from src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerMethodAnnIT.java rename to jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerMethodAnnIT.java diff --git a/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerPolymorphicWithExistingFieldIT.java b/jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerPolymorphicWithExistingFieldIT.java similarity index 100% rename from src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerPolymorphicWithExistingFieldIT.java rename to jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerPolymorphicWithExistingFieldIT.java diff --git a/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerPolymorphicWithPropertyIT.java b/jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerPolymorphicWithPropertyIT.java similarity index 100% rename from src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerPolymorphicWithPropertyIT.java rename to jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/ser/HALBeanSerializerPolymorphicWithPropertyIT.java diff --git a/pom.xml b/pom.xml index 711f1a0..4dfe234 100644 --- a/pom.xml +++ b/pom.xml @@ -1,30 +1,31 @@ - - + 4.0.0 - - Jackson HAL Dataformat + io.openapitools.jackson.dataformat + jackson-dataformat-hal-parent 1.0.10-SNAPSHOT - jackson-dataformat-hal - jar + pom - Dataformat extension for Jackson to handle the HAL JSON format. + Jackson HAL Dataformat Parent + Parent project for jackson-dataformat-hal modules. https://github.com/openapi-tools/jackson-dataformat-hal - MIT License - http://www.opensource.org/licenses/mit-license.php + MIT License + http://www.opensource.org/licenses/mit-license.php - Thor Anker Kvisgård Lange - thor.lange@gmail.com - Open API Tools - https://github.com/openapitools + Thor Anker Kvisgård Lange + thor.lange@gmail.com + Open API Tools + https://github.com/openapitools @@ -32,7 +33,7 @@ scm:git:https://github.com/openapi-tools/jackson-dataformat-hal.git scm:git:https://github.com/openapi-tools/jackson-dataformat-hal.git https://github.com/openapi-tools/jackson-dataformat-hal/tree/master - HEAD + HEAD @@ -50,48 +51,15 @@ UTF-8 UTF-8 2.13.4 + 3.0.3 5.7.0 3.6.28 1.7.30 - - - com.fasterxml.jackson.jaxrs - jackson-jaxrs-json-provider - ${com.fasterxml.jackson.version} - - - org.slf4j - slf4j-api - ${org.slf4j.version} - - - - org.junit.jupiter - junit-jupiter-api - ${junit.version} - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test - - - org.slf4j - slf4j-simple - ${org.slf4j.version} - test - - - org.mockito - mockito-core - ${org.mockito.version} - test - - + + jackson-dataformat-hal-v2 + @@ -198,5 +166,5 @@ - + From ab8ff9cd3db087a70d50fc9d8409ae4c6275b7e2 Mon Sep 17 00:00:00 2001 From: Timothy Rundle Date: Tue, 21 Jul 2026 06:10:07 -0400 Subject: [PATCH 3/4] Extract common code into core module --- jackson-dataformat-hal-core/pom.xml | 53 +++++++++++++++++++ .../jackson/dataformat/hal/HALLink.java | 0 .../dataformat/hal/annotation/Curie.java | 0 .../dataformat/hal/annotation/Curies.java | 0 .../hal/annotation/EmbeddedResource.java | 0 .../dataformat/hal/annotation/Link.java | 0 .../dataformat/hal/annotation/Resource.java | 0 .../dataformat/hal/deser/CurieMap.java | 0 .../dataformat/hal/deser/CurieMapTest.java | 0 jackson-dataformat-hal-v2/pom.xml | 7 ++- pom.xml | 4 +- 11 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 jackson-dataformat-hal-core/pom.xml rename {jackson-dataformat-hal-v2 => jackson-dataformat-hal-core}/src/main/java/io/openapitools/jackson/dataformat/hal/HALLink.java (100%) rename {jackson-dataformat-hal-v2 => jackson-dataformat-hal-core}/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curie.java (100%) rename {jackson-dataformat-hal-v2 => jackson-dataformat-hal-core}/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curies.java (100%) rename {jackson-dataformat-hal-v2 => jackson-dataformat-hal-core}/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/EmbeddedResource.java (100%) rename {jackson-dataformat-hal-v2 => jackson-dataformat-hal-core}/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Link.java (100%) rename {jackson-dataformat-hal-v2 => jackson-dataformat-hal-core}/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Resource.java (100%) rename {jackson-dataformat-hal-v2 => jackson-dataformat-hal-core}/src/main/java/io/openapitools/jackson/dataformat/hal/deser/CurieMap.java (100%) rename {jackson-dataformat-hal-v2 => jackson-dataformat-hal-core}/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/CurieMapTest.java (100%) diff --git a/jackson-dataformat-hal-core/pom.xml b/jackson-dataformat-hal-core/pom.xml new file mode 100644 index 0000000..4e4c4eb --- /dev/null +++ b/jackson-dataformat-hal-core/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + io.openapitools.jackson.dataformat + jackson-dataformat-hal-parent + 1.0.10-SNAPSHOT + ../pom.xml + + + jackson-dataformat-hal-core + jar + + Jackson HAL Dataformat Core + Core module for shared jackson-dataformat-hal functionality. + + + + com.fasterxml.jackson.core + jackson-annotations + ${com.fasterxml.jackson.annotations.version} + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.slf4j + slf4j-simple + ${org.slf4j.version} + test + + + org.mockito + mockito-core + ${org.mockito.version} + test + + + + diff --git a/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/HALLink.java b/jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/HALLink.java similarity index 100% rename from jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/HALLink.java rename to jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/HALLink.java diff --git a/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curie.java b/jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curie.java similarity index 100% rename from jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curie.java rename to jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curie.java diff --git a/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curies.java b/jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curies.java similarity index 100% rename from jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curies.java rename to jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Curies.java diff --git a/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/EmbeddedResource.java b/jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/EmbeddedResource.java similarity index 100% rename from jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/EmbeddedResource.java rename to jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/EmbeddedResource.java diff --git a/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Link.java b/jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Link.java similarity index 100% rename from jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Link.java rename to jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Link.java diff --git a/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Resource.java b/jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Resource.java similarity index 100% rename from jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Resource.java rename to jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/annotation/Resource.java diff --git a/jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/deser/CurieMap.java b/jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/deser/CurieMap.java similarity index 100% rename from jackson-dataformat-hal-v2/src/main/java/io/openapitools/jackson/dataformat/hal/deser/CurieMap.java rename to jackson-dataformat-hal-core/src/main/java/io/openapitools/jackson/dataformat/hal/deser/CurieMap.java diff --git a/jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/CurieMapTest.java b/jackson-dataformat-hal-core/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/CurieMapTest.java similarity index 100% rename from jackson-dataformat-hal-v2/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/CurieMapTest.java rename to jackson-dataformat-hal-core/src/test/java/io/openapitoools/jackson/dataformat/hal/deser/CurieMapTest.java diff --git a/jackson-dataformat-hal-v2/pom.xml b/jackson-dataformat-hal-v2/pom.xml index ca06432..8321090 100644 --- a/jackson-dataformat-hal-v2/pom.xml +++ b/jackson-dataformat-hal-v2/pom.xml @@ -17,6 +17,11 @@ Dataformat extension for Jackson to handle the HAL JSON format. + + io.openapitools.jackson.dataformat + jackson-dataformat-hal-core + ${project.version} + com.fasterxml.jackson.jaxrs jackson-jaxrs-json-provider @@ -53,6 +58,4 @@ test - - diff --git a/pom.xml b/pom.xml index 4dfe234..b77e1ac 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,8 @@ UTF-8 UTF-8 - 2.13.4 + 2.21.3 + 2.21 3.0.3 5.7.0 3.6.28 @@ -58,6 +59,7 @@ + jackson-dataformat-hal-core jackson-dataformat-hal-v2 From 19e9219d3855784060db11953a9cc16f9b11be8a Mon Sep 17 00:00:00 2001 From: Timothy Rundle Date: Mon, 20 Jul 2026 22:15:49 -0400 Subject: [PATCH 4/4] Add Jackson 3 module --- README.md | 8 +- jackson-dataformat-hal-v3/pom.xml | 61 +++ .../openapitools/jackson/hal/HALMapper.java | 17 + .../jackson/hal/JacksonHALModule.java | 26 ++ .../jackson/hal/PackageVersion.java | 50 ++ .../hal/deser/HALBeanDeserializer.java | 74 +++ .../deser/HALBeanDeserializerModifier.java | 74 +++ .../jackson/hal/deser/ReservedProperty.java | 101 +++++ .../jackson/hal/package-info.java | 14 + .../jackson/hal/ser/HALBeanSerializer.java | 363 +++++++++++++++ .../hal/ser/HALBeanSerializerModifier.java | 26 ++ .../jackson/hal/JsonTestUtils.java | 27 ++ .../hal/deser/HALBeanDeserializerIT.java | 102 +++++ .../deser/HALBeanDeserializerJsonViewIT.java | 227 ++++++++++ .../deser/HALBeanDeserializerMethodAnnIT.java | 427 ++++++++++++++++++ .../hal/deser/ReservedPropertyTest.java | 90 ++++ .../jackson/hal/ser/HALBeanSerializerIT.java | 97 ++++ .../hal/ser/HALBeanSerializerJsonViewIT.java | 205 +++++++++ .../hal/ser/HALBeanSerializerMethodAnnIT.java | 198 ++++++++ ...ializerPolymorphicWithExistingFieldIT.java | 198 ++++++++ ...anSerializerPolymorphicWithPropertyIT.java | 187 ++++++++ pom.xml | 1 + 22 files changed, 2569 insertions(+), 4 deletions(-) create mode 100755 jackson-dataformat-hal-v3/pom.xml create mode 100755 jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/HALMapper.java create mode 100755 jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/JacksonHALModule.java create mode 100755 jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/PackageVersion.java create mode 100755 jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/HALBeanDeserializer.java create mode 100755 jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/HALBeanDeserializerModifier.java create mode 100755 jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/ReservedProperty.java create mode 100755 jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/package-info.java create mode 100755 jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/ser/HALBeanSerializer.java create mode 100755 jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/ser/HALBeanSerializerModifier.java create mode 100644 jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/JsonTestUtils.java create mode 100755 jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerIT.java create mode 100755 jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerJsonViewIT.java create mode 100755 jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerMethodAnnIT.java create mode 100755 jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/ReservedPropertyTest.java create mode 100755 jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerIT.java create mode 100755 jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerJsonViewIT.java create mode 100755 jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerMethodAnnIT.java create mode 100644 jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerPolymorphicWithExistingFieldIT.java create mode 100644 jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerPolymorphicWithPropertyIT.java diff --git a/README.md b/README.md index a87f9e4..a102033 100755 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ Serialization is similar to the normal JSON serialization using the `HALMapper` `ObjectMapper`. ```java -ObjectMapper halMapper = new HALMapper(); +ObjectMapper halMapper = HALMapper.create(); String json = halMapper.writeValueAsString(new POJO()); ``` @@ -196,7 +196,7 @@ String json = halMapper.writeValueAsString(new POJO()); Deserialization is also similar to the normal JSON handling using the `HALMapper`. ```java -ObjectMapper halMapper = new HALMapper(); +ObjectMapper halMapper = HALMapper.create(); POJO value = halMapper.readValue("{..json..}", POJO.class); ``` @@ -211,7 +211,7 @@ public class MyApplication extends Application { private final Set singletons = new HashSet<>(); public MyApplication() { - singletons.add(new JacksonJsonProvider(new HALMapper())); + singletons.add(new JacksonJsonProvider(HALMapper.create())); } @Override @@ -228,7 +228,7 @@ jackson-jaxrs-providers can be used to register the `HALMapper`. ```java ClientBuilder cb = ClientBuilder.newBuilder(); -Client client = cb.register(new JacksonJsonProvider(new HALMapper())).build(); +Client client = cb.register(new JacksonJsonProvider(HALMapper.create())).build(); ``` ## HALLink extended with temporal aspect The HALLink is extended with a `seen` property. This is beyond the current specification of HAL. diff --git a/jackson-dataformat-hal-v3/pom.xml b/jackson-dataformat-hal-v3/pom.xml new file mode 100755 index 0000000..03dda68 --- /dev/null +++ b/jackson-dataformat-hal-v3/pom.xml @@ -0,0 +1,61 @@ + + + + 4.0.0 + + + io.openapitools.jackson.dataformat + jackson-dataformat-hal-parent + 1.0.10-SNAPSHOT + ../pom.xml + + + Jackson3 HAL Dataformat + jackson-hal + jar + + Dataformat extension for Jackson to handle the HAL JSON format. + + + + io.openapitools.jackson.dataformat + jackson-dataformat-hal-core + ${project.version} + + + tools.jackson.core + jackson-databind + ${tools.jackson.version} + + + org.slf4j + slf4j-api + ${org.slf4j.version} + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.slf4j + slf4j-simple + ${org.slf4j.version} + test + + + org.mockito + mockito-core + ${org.mockito.version} + test + + + diff --git a/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/HALMapper.java b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/HALMapper.java new file mode 100755 index 0000000..3650176 --- /dev/null +++ b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/HALMapper.java @@ -0,0 +1,17 @@ +package io.openapitools.jackson.hal; + +import tools.jackson.databind.json.JsonMapper; + +public class HALMapper { + + private HALMapper() {} + + public static JsonMapper.Builder builder() { + return JsonMapper.builder() + .addModule(new JacksonHALModule()); + } + + public static JsonMapper create() { + return builder() .build(); + } +} diff --git a/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/JacksonHALModule.java b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/JacksonHALModule.java new file mode 100755 index 0000000..f21478b --- /dev/null +++ b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/JacksonHALModule.java @@ -0,0 +1,26 @@ +package io.openapitools.jackson.hal; + +import io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource; +import io.openapitools.jackson.dataformat.hal.annotation.Link; +import io.openapitools.jackson.dataformat.hal.annotation.Resource; +import io.openapitools.jackson.hal.deser.HALBeanDeserializerModifier; +import io.openapitools.jackson.hal.ser.HALBeanSerializerModifier; +import tools.jackson.databind.module.SimpleModule; + +/** + * Module registering handlers for the {@link Resource} and {@link EmbeddedResource} annonations as well as the + * {@link Link} annotation. + */ +public class JacksonHALModule extends SimpleModule { + + public JacksonHALModule() { + super("JacksonHALModule", PackageVersion.VERSION); + } + + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + context.addSerializerModifier(new HALBeanSerializerModifier()); + context.addDeserializerModifier(new HALBeanDeserializerModifier()); + } +} diff --git a/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/PackageVersion.java b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/PackageVersion.java new file mode 100755 index 0000000..d244a4a --- /dev/null +++ b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/PackageVersion.java @@ -0,0 +1,50 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package io.openapitools.jackson.hal; + +import tools.jackson.core.Version; +import tools.jackson.core.Versioned; +import tools.jackson.core.util.VersionUtil; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Reading module version from Maven properties files. + */ +public final class PackageVersion implements Versioned { + + public static final Version VERSION; + + private static final String MAVEN_PROPERTIES = "/META-INF/maven/io.openapitools.jackson.dataformat/jackson-dataformat-hal/pom.properties"; + + static { + Version version = Version.unknownVersion(); + InputStream is = PackageVersion.class.getResourceAsStream(MAVEN_PROPERTIES); + if (is != null) { + try { + Properties p = new Properties(); + p.load(is); + version = VersionUtil.parseVersion(p.getProperty("version"), p.getProperty("groupId"), p.getProperty("artifactId")); + } catch (IOException e) { + // Silently ignore + } finally { + try { + is.close(); + } catch (IOException ex) { + // Ignore + } + } + } + VERSION = version; + } + + @Override + public Version version() { + return VERSION; + } + +} diff --git a/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/HALBeanDeserializer.java b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/HALBeanDeserializer.java new file mode 100755 index 0000000..56ba433 --- /dev/null +++ b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/HALBeanDeserializer.java @@ -0,0 +1,74 @@ +package io.openapitools.jackson.hal.deser; + +import io.openapitools.jackson.dataformat.hal.deser.CurieMap; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.TreeNode; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.deser.bean.BeanDeserializerBase; +import tools.jackson.databind.deser.std.DelegatingDeserializer; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; +import java.net.URI; +import java.util.Map; +import java.util.stream.StreamSupport; + +/** + * Deserializer to handle incoming application/hal+json. The de-serializer is responsible for intercepting + * the reserved properties (_links and _embedded) and mapping the properties of these + * objects in the incoming json to the uniquely assigned properties of the POJO class. + */ +public class HALBeanDeserializer extends DelegatingDeserializer { + + public HALBeanDeserializer(BeanDeserializerBase delegate) { + super(delegate); + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + TreeNode tn = ctxt.readTree(p); + if (tn.isObject()) { + ObjectNode root = (ObjectNode) tn; + for (ReservedProperty rp : ReservedProperty.values()) { + ObjectNode on = (ObjectNode) tn.get(rp.getPropertyName()); + if (on != null) { + CurieMap curieMap = createCurieMap(rp, on); + on.remove("curies"); + + for (Map.Entry jn : on.properties()) { + String propertyName = curieMap.resolve(jn.getKey()).map(URI::toString).orElse(jn.getKey()); + root.set(rp.alternateName(propertyName), jn.getValue()); + } + + root.remove(rp.getPropertyName()); + } + + } + } + + final JsonParser modifiedParser = tn.traverse(p.objectReadContext()); + modifiedParser.nextToken(); + return _delegatee.deserialize(modifiedParser, ctxt); + } + + private CurieMap createCurieMap(ReservedProperty rp, ObjectNode on) { + if (ReservedProperty.LINKS.equals(rp) && on.has("curies")) { + ArrayNode curies = (ArrayNode) on.get("curies"); + return new CurieMap(StreamSupport.stream(curies.spliterator(), false) + .map(n -> createMapping((ObjectNode) n)).toArray(CurieMap.Mapping[]::new)); + } else { + return new CurieMap(); + } + } + + private CurieMap.Mapping createMapping(ObjectNode node) { + return new CurieMap.Mapping(node.get("name").asString(), node.get("href").asString()); + } + + @Override + protected ValueDeserializer newDelegatingInstance(ValueDeserializer newDelegatee) { + return new HALBeanDeserializer((BeanDeserializerBase) newDelegatee); + } +} diff --git a/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/HALBeanDeserializerModifier.java b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/HALBeanDeserializerModifier.java new file mode 100755 index 0000000..3302eb0 --- /dev/null +++ b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/HALBeanDeserializerModifier.java @@ -0,0 +1,74 @@ +package io.openapitools.jackson.hal.deser; + +import io.openapitools.jackson.dataformat.hal.deser.CurieMap; +import tools.jackson.databind.BeanDescription; +import tools.jackson.databind.DeserializationConfig; +import tools.jackson.databind.PropertyName; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.deser.ValueDeserializerModifier; +import tools.jackson.databind.deser.bean.BeanDeserializer; +import tools.jackson.databind.introspect.BeanPropertyDefinition; +import io.openapitools.jackson.dataformat.hal.annotation.Curie; +import io.openapitools.jackson.dataformat.hal.annotation.Curies; +import io.openapitools.jackson.dataformat.hal.annotation.Resource; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * Modify the deserialization of classes annotated with {@link Resource}. Deserialization will handle the reserved + * properties _links and _embedded by assigning a unique property name to each of the + * properties that are part of these sections. + */ +public class HALBeanDeserializerModifier extends ValueDeserializerModifier { + + @Override + public ValueDeserializer modifyDeserializer(DeserializationConfig config, + BeanDescription.Supplier beanDescRef, + ValueDeserializer deserializer) { + + Resource ann = beanDescRef.getClassAnnotations().get(Resource.class); + if (ann != null) { + return new HALBeanDeserializer((BeanDeserializer) deserializer); + } + return deserializer; + } + + @Override + public List updateProperties(DeserializationConfig config, + BeanDescription.Supplier beanDescRef, + List propDefs) { + Resource ann = beanDescRef.getClassAnnotations().get(Resource.class); + if (ann != null) { + CurieMap map = createCurieMap(beanDescRef); + List modified = new ArrayList<>(); + Iterator properties = propDefs.iterator(); + while (properties.hasNext()) { + BeanPropertyDefinition property = properties.next(); + for (ReservedProperty rp : ReservedProperty.values()) { + String alternateName = rp.alternateName(property, map); + if (!property.getName().equals(alternateName)) { + modified.add(property.withName(new PropertyName(alternateName))); + properties.remove(); + } + } + } + propDefs.addAll(modified); + } + return propDefs; + } + + private CurieMap createCurieMap(BeanDescription.Supplier beanDesc) { + ArrayList mappings = new ArrayList<>(); + Curie sc = beanDesc.getClassAnnotations().get(Curie.class); + if (sc != null) { + mappings.add(new CurieMap.Mapping(sc)); + } + Curies cs = beanDesc.getClassAnnotations().get(Curies.class); + if (cs != null) { + Arrays.stream(cs.value()).forEach(c -> mappings.add(new CurieMap.Mapping(c))); + } + return new CurieMap(mappings.toArray(new CurieMap.Mapping[0])); + } +} diff --git a/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/ReservedProperty.java b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/ReservedProperty.java new file mode 100755 index 0000000..6ea9b21 --- /dev/null +++ b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/deser/ReservedProperty.java @@ -0,0 +1,101 @@ +package io.openapitools.jackson.hal.deser; + +import io.openapitools.jackson.dataformat.hal.deser.CurieMap; +import tools.jackson.databind.introspect.AnnotatedMember; +import tools.jackson.databind.introspect.BeanPropertyDefinition; +import io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource; +import io.openapitools.jackson.dataformat.hal.annotation.Link; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Optional; +import java.util.UUID; + +/** + * Modelling reserved HAL properties namely _links and _embedded. + */ +public enum ReservedProperty { + + LINKS("_links", Link.class), EMBEDDED("_embedded", EmbeddedResource.class); + + private final String name; + private final UUID prefix = UUID.randomUUID(); + private final Class annotation; + private final Method valueMethod; + + ReservedProperty(String name, Class annotation) { + this.name = name; + this.annotation = annotation; + try { + valueMethod = annotation.getDeclaredMethod("value"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + public String getPropertyName() { + return name; + } + + /** + * Assign an alternate name to POJO property that is unique outside of the the reserved + * object - if it is a link or embedded property. + * + * @param bpd The property to process. + * @param map Curie map applying to properties of _links section. + * @return An alternate name if relevant otherwise the original name is maintained. + */ + public String alternateName(BeanPropertyDefinition bpd, CurieMap map) { + String originalName = bpd.getName(); + + AnnotatedMember annotatedMember = firstNonNull(bpd.getField(), bpd.getSetter(), bpd.getGetter()); + if (annotatedMember == null) { + return originalName; + } + + Annotation o = annotatedMember.getAnnotation(annotation); + if (o == null) { + return originalName; + } + + String name; + try { + String alternateName = (String) valueMethod.invoke(o); + name = alternateName.isEmpty() ? originalName : alternateName; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new RuntimeException(e); + } + + if (o.annotationType() == Link.class) { + String curiePrefix = ((Link) o).curie(); + if (!curiePrefix.isEmpty()) { + Optional resolved = map.resolve(curiePrefix + ":" + name); + return alternateName(resolved.map(URI::toString).orElse(name)); + } + } + + return alternateName(name); + } + + /** + * Given a property that should belong to this reserved object instance a name is assigned which will + * be unique outside of the reserved property. + * + * @param originalName Property name within this reserved object that should be unique outside. + * @return Alternate name for the given original name. + */ + public String alternateName(String originalName) { + return prefix.toString() + ":" + originalName; + } + + @SafeVarargs + static T firstNonNull(T... vals) { + for (T v : vals) { + if (v != null) + return v; + } + + return null; + } +} diff --git a/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/package-info.java b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/package-info.java new file mode 100755 index 0000000..fd0f1ea --- /dev/null +++ b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/package-info.java @@ -0,0 +1,14 @@ +/** + * Package containing classes to help generating HAL compliant JSON output using the Jackson framework. The module is activated + * by adding a Jackson module to the Jackson {@link tools.jackson.databind.ObjectMapper}: + *
+ * JsonMapper jm = JsonMapper.builder().addModule(new JacksonHALModule()).build();
+ * 
+ * + * To signal some bean is to be treated as HAL resource it should be annotated with the {@link io.openapitools.jackson.dataformat.hal.annotation.Resource} + * annotation. To mark a property of a HAL resource for inclusion as embedded resource the property should be annotations with + * {@link io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource} - this is true for both collections of resources and for one-to-one + * relationships. Adding a link is done by creating a property of type {@link io.openapitools.jackson.dataformat.hal.HALLink} annotated with + * {@link io.openapitools.jackson.dataformat.hal.annotation.Link} - this is also true for collections of links + */ +package io.openapitools.jackson.hal; diff --git a/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/ser/HALBeanSerializer.java b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/ser/HALBeanSerializer.java new file mode 100755 index 0000000..d0fbcb6 --- /dev/null +++ b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/ser/HALBeanSerializer.java @@ -0,0 +1,363 @@ +package io.openapitools.jackson.hal.ser; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonToken; +import tools.jackson.core.type.WritableTypeId; +import tools.jackson.databind.BeanDescription; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.jsontype.TypeSerializer; +import tools.jackson.databind.ser.BeanPropertyWriter; +import tools.jackson.databind.ser.bean.BeanSerializerBase; +import tools.jackson.databind.ser.impl.ObjectIdWriter; +import io.openapitools.jackson.dataformat.hal.HALLink; +import io.openapitools.jackson.dataformat.hal.annotation.Curie; +import io.openapitools.jackson.dataformat.hal.annotation.Curies; +import io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource; +import io.openapitools.jackson.dataformat.hal.annotation.Link; + +import java.util.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tools.jackson.databind.util.NameTransformer; + +/** + * Serializer to handle {@link io.openapitools.jackson.dataformat.hal.annotation.Resource} beans ensuring they are serialized according to the HAL + * specification. This implies placing links inside the _links property and embedded objects inside the _embedded + * property. + */ +public class HALBeanSerializer extends BeanSerializerBase { + + private static final Logger LOG = LoggerFactory.getLogger(HALBeanSerializer.class); + private final BeanDescription.Supplier beanDescription; + + public HALBeanSerializer(BeanSerializerBase src, BeanDescription.Supplier beanDescription) { + super(src); + this.beanDescription = beanDescription; + } + + // Secondary mutant constructor required for Jackson 3 abstract base implementations + protected HALBeanSerializer(HALBeanSerializer src, ObjectIdWriter objectIdWriter) { + super(src, objectIdWriter); + this.beanDescription = src.beanDescription; + } + + protected HALBeanSerializer(HALBeanSerializer src, ObjectIdWriter objectIdWriter, Object filterId) { + super(src, objectIdWriter, filterId); + this.beanDescription = src.beanDescription; + } + + protected HALBeanSerializer(HALBeanSerializer src, Set toIgnore, Set toInclude) { + super(src, toIgnore, toInclude); + this.beanDescription = src.beanDescription; + } + + protected HALBeanSerializer(HALBeanSerializer src, BeanPropertyWriter[] properties, BeanPropertyWriter[] filteredProperties) { + super(src, properties, filteredProperties); + this.beanDescription = src.beanDescription; + } + + @Override + public BeanSerializerBase withObjectIdWriter(ObjectIdWriter objectIdWriter) { + return new HALBeanSerializer(this, objectIdWriter); + } + + @Override + public BeanSerializerBase withFilterId(Object o) { + return new HALBeanSerializer(this, _objectIdWriter, o); + } + + @Override + protected BeanSerializerBase withProperties(BeanPropertyWriter[] properties, BeanPropertyWriter[] filteredProperties) { + return new HALBeanSerializer(this, properties, filteredProperties); + } + + @Override + protected BeanSerializerBase asArraySerializer() { + return this; + } + + @Override + protected BeanSerializerBase withByNameInclusion(Set toIgnore, Set toInclude) { + return new HALBeanSerializer(this, toIgnore, toInclude); + } + + @Override + public ValueSerializer unwrappingSerializer(NameTransformer unwrapper) { + // Return this instance directly, or wrap with tools.jackson.databind.ser.impl.UnwrappingBeanSerializer if needed + return this; + } + + @Override + public void serialize(Object bean, JsonGenerator jgen, SerializationContext provider) throws JacksonException { + FilteredProperties filtered = new FilteredProperties(bean, provider, beanDescription); + + // this serializer acts autonomous and creates the complete object + jgen.writeStartObject(bean); + filtered.serialize(bean, jgen, provider); + jgen.writeEndObject(); + } + + @Override + public void serializeWithType(Object bean, JsonGenerator jgen, SerializationContext provider, + TypeSerializer typeSer) throws JacksonException { + FilteredProperties filtered = new FilteredProperties(bean, provider, beanDescription); + + // this serializer lets the TypeSerializer create the outer frame and inserts the rest of the object + WritableTypeId typeIdDef = _typeIdDef(typeSer, bean, JsonToken.START_OBJECT); + WritableTypeId handledTypeId = typeSer.writeTypePrefix(jgen, provider, typeIdDef); + filtered.serialize(bean, jgen, provider); + typeSer.writeTypeSuffix(jgen, provider, handledTypeId); + } + + /** + * Modeling the properties of the bean segmented into HAL categories: links, embedded resources, and state + */ + private class FilteredProperties { + + private List state = new ArrayList<>(); + private Map links = new TreeMap<>(); + private Map embedded = new TreeMap<>(); + + // All of the possible curies that COULD be used (provided via Curie/Curies annotations) + private Map curieMap = new TreeMap<>(); + // All of the curies that actually ARE being used (provided via Link annotations) + private Set curiesInUse = new TreeSet<>(); + + public FilteredProperties(Object bean, SerializationContext provider, + BeanDescription.Supplier beanDescription) throws JacksonException { + + populateCurieMap(beanDescription); + + BeanPropertyWriter[] props; + + if (_filteredProps != null && provider.getActiveView() != null) { + props = Arrays.stream(_filteredProps).filter(bpw -> { + if (bpw == null || bpw.getViews() == null || bpw.getViews().length == 0) { + return provider.getConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION); + } + return Arrays.stream(bpw.getViews()).anyMatch(clazz -> clazz.isAssignableFrom(provider.getActiveView())); + }).toArray(BeanPropertyWriter[]::new); + } else { + props = _props; + } + + for (BeanPropertyWriter prop : props) { + try { + if (prop.getAnnotation(EmbeddedResource.class) != null) { + Object object = prop.get(bean); + if (object != null) { + EmbeddedResource er = prop.getAnnotation(EmbeddedResource.class); + String val = "".equals(er.value()) ? prop.getName() : er.value(); + addEmbeddedProperty(val, prop); + } + } else if (prop.getAnnotation(Link.class) != null) { + Link l = prop.getAnnotation(Link.class); + String relation = "".equals(l.value()) ? prop.getName() : l.value(); + String curie = "".equals(l.curie()) ? null : l.curie(); + if (!"".equals(l.curie())) { + curiesInUse.add(l.curie()); + } + Object value = prop.get(bean); + if (value instanceof Collection) { + addLinks(relation, (Collection) prop.get(bean), curie); + } else if (value instanceof HALLink) { + addLink(relation, (HALLink) prop.get(bean), curie); + } + } else { + state.add(prop); + } + } catch (Exception e) { + wrapAndThrow(provider, e, bean, prop.getName()); + } + } + + if (!curiesInUse.isEmpty()) { + addCurieLinks(); + } + } + + private void addCurieLinks() { + Collection curieLinks = new ArrayList<>(); + for (String curie: curiesInUse) { + if (curieMap.containsKey(curie)) { + curieLinks.add(new HALLink.Builder(curieMap.get(curie)) + .name(curie) + .build()); + } else { + LOG.warn("No Curie/Curies annotation provided for [{}]", curie); + } + } + addLinks("curies", curieLinks, null); + } + + private void populateCurieMap(BeanDescription.Supplier beanDescription) { + + // Curies should only be shown if they are being used by some other link. + // Populate CurieMap now so that it can be referred to later during link + // serialisation. Note - either a single Curie annotation can be used by + // itself or a collection can be wrapped using Curies. + + List curieAnnotations = new ArrayList<>(); + if (null != beanDescription.getClassAnnotations().get(Curie.class)) { + curieAnnotations.add(beanDescription.getClassAnnotations().get(Curie.class)); + } + if (null != beanDescription.getClassAnnotations().get(Curies.class)) { + curieAnnotations.addAll(Arrays.asList(beanDescription.getClassAnnotations().get(Curies.class).value())); + } + + for (Curie curie : curieAnnotations) { + if (curieMap.containsKey(curie.prefix())) { + LOG.warn("Curie annotation already exists [{}]", curie.prefix()); + } + curieMap.put(curie.prefix(), curie.href()); + } + } + + public void serialize(Object bean, JsonGenerator jgen, SerializationContext provider) throws JacksonException { + if (!links.isEmpty()) { + jgen.writeName("_links"); + jgen.writeStartObject(); + for (String rel : links.keySet()) { + jgen.writeName(rel); + links.get(rel).serialize(jgen); + } + jgen.writeEndObject(); + } + + if (!embedded.isEmpty()) { + jgen.writeName("_embedded"); + jgen.writeStartObject(); + for (String rel : embedded.keySet()) { + try { + // use the relation as new name for the original property + BeanPropertyWriter prop = renameBeanProperty(embedded.get(rel), rel); + + // serialize the field as normal field + prop.serializeAsProperty(bean, jgen, provider); + } catch (Exception e) { + wrapAndThrow(provider, e, bean, rel); + } + } + jgen.writeEndObject(); + } + + for (BeanPropertyWriter prop : state) { + try { + prop.serializeAsProperty(bean, jgen, provider); + } catch (Exception e) { + wrapAndThrow(provider, e, bean, prop.getName()); + } + } + } + + private void addEmbeddedProperty(String rel, BeanPropertyWriter property) { + if (embedded.put(rel, property) != null) { + LOG.warn("Embedded resource already existed with rel [{}] in class [{}]", rel, _handledType); + } + } + + private void addLink(String rel, HALLink link, String curie) { + if (links.put(applyCurieToRel(rel, curie), new LinkProperty(link)) != null) { + LOG.warn("Link resource already existed with rel [{}] in class [{}]", rel, _handledType); + } + } + + private void addLinks(String rel, Collection links, String curie) { + if (this.links.put(applyCurieToRel(rel, curie), new LinkProperty(links)) != null) { + LOG.warn("Link resource already existed with rel [{}] in class [{}]", rel, _handledType); + } + } + + private String applyCurieToRel(String rel, String curie) { + return (null == curie) ? rel : curie + ":" + rel; + } + + private BeanPropertyWriter renameBeanProperty(BeanPropertyWriter prop, String newName) { + return prop.rename(new NameTransformer() { + @Override + public String transform(String name) { + return newName; + } + + @Override + public String reverse(String transformed) { + return null; + } + }); + } + } + + + /** + * Representing either a single link (one-to-one relation) or a collection of links. + */ + private static class LinkProperty { + + private HALLink link; + private Collection links; + + public LinkProperty(HALLink link) { + this.link = link; + } + + public LinkProperty(Collection links) { + this.links = links == null ? new HashSet<>() : links; + } + + public void serialize(JsonGenerator jgen) { + if (link != null) { + writeLinkObject(jgen, link); + } else if (links != null) { + jgen.writeStartArray(); + for (HALLink curLink : links) { + writeLinkObject(jgen, curLink); + } + jgen.writeEndArray(); + } + } + + /** + * Write HALLink fields directly without delegating to Jackson's POJO serializer. + * Using writePOJO() would propagate the active @JsonView into HALLink's own fields + * (href, templated, etc.), which have no @JsonView annotations and would be filtered + * out when DEFAULT_VIEW_INCLUSION=false. The @JsonView on the resource only controls + * whether a link is included at all — not its internal structure. + */ + private void writeLinkObject(JsonGenerator jgen, HALLink link) { + jgen.writeStartObject(); + if (link.getHref() != null) { + jgen.writeStringProperty("href", link.getHref()); + } + if (link.getTemplated() != null) { + jgen.writeBooleanProperty("templated", link.getTemplated()); + } + if (link.getType() != null) { + jgen.writeStringProperty("type", link.getType()); + } + if (link.getDeprecation() != null) { + jgen.writeStringProperty("deprecation", link.getDeprecation().toString()); + } + if (link.getName() != null) { + jgen.writeStringProperty("name", link.getName()); + } + if (link.getProfile() != null) { + jgen.writeStringProperty("profile", link.getProfile().toString()); + } + if (link.getTitle() != null) { + jgen.writeStringProperty("title", link.getTitle()); + } + if (link.getHreflang() != null) { + jgen.writeStringProperty("hreflang", link.getHreflang()); + } + if (link.getSeen() != null) { + jgen.writeStringProperty("seen", link.getSeen()); + } + jgen.writeEndObject(); + } + + } +} diff --git a/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/ser/HALBeanSerializerModifier.java b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/ser/HALBeanSerializerModifier.java new file mode 100755 index 0000000..6339cbd --- /dev/null +++ b/jackson-dataformat-hal-v3/src/main/java/io/openapitools/jackson/hal/ser/HALBeanSerializerModifier.java @@ -0,0 +1,26 @@ +package io.openapitools.jackson.hal.ser; + +import tools.jackson.databind.BeanDescription; +import tools.jackson.databind.SerializationConfig; +import tools.jackson.databind.ValueSerializer; +import io.openapitools.jackson.dataformat.hal.annotation.Resource; +import tools.jackson.databind.ser.ValueSerializerModifier; +import tools.jackson.databind.ser.bean.BeanSerializerBase; + +/** + * Modifier ensuring that beans annotated with {@link Resource} is handled by the {@link HALBeanSerializer}. + */ +public class HALBeanSerializerModifier extends ValueSerializerModifier { + @Override + public ValueSerializer modifySerializer( + SerializationConfig config, + BeanDescription.Supplier beanDesc, + ValueSerializer serializer + ) { + Resource ann = beanDesc.getClassAnnotations().get(Resource.class); + if (ann != null && serializer instanceof BeanSerializerBase) { + return new HALBeanSerializer((BeanSerializerBase) serializer, beanDesc); + } + return serializer; + } +} diff --git a/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/JsonTestUtils.java b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/JsonTestUtils.java new file mode 100644 index 0000000..7c9e7cd --- /dev/null +++ b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/JsonTestUtils.java @@ -0,0 +1,27 @@ +package io.openapitoools.jackson.hal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import io.openapitools.jackson.hal.HALMapper; + +/** + * Shared test utilities for JSON assertion. + */ +public final class JsonTestUtils { + + private JsonTestUtils() {} + + /** + * Order-agnostic JSON comparison: parses both strings as maps and compares them. + * This avoids brittleness from property ordering differences between Jackson versions. + */ + @SuppressWarnings("unchecked") + public static void assertJsonEquals(String expected, String actual) throws Exception { + Map expectedMap = HALMapper.create().readValue(expected, Map.class); + Map actualMap = HALMapper.create().readValue(actual, Map.class); + assertEquals(expectedMap, actualMap); + } +} + diff --git a/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerIT.java b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerIT.java new file mode 100755 index 0000000..dc83c30 --- /dev/null +++ b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerIT.java @@ -0,0 +1,102 @@ +package io.openapitoools.jackson.hal.deser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.StringReader; +import java.util.Collection; + +import org.junit.jupiter.api.Test; + +import io.openapitools.jackson.dataformat.hal.HALLink; +import io.openapitools.jackson.hal.HALMapper; +import io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource; +import io.openapitools.jackson.dataformat.hal.annotation.Link; +import io.openapitools.jackson.dataformat.hal.annotation.Resource; +import tools.jackson.databind.json.JsonMapper; + + +public class HALBeanDeserializerIT { + static final String HAL_DOC = "{" + + "\"_links\":{" + + "\"child\":[{\"href\":\"/top/1/child/1\"},{\"href\":\"/top/1/child/2\"}]," + + "\"empty:list\":[]," + + "\"null:list\":[]," + + "\"self\":{\"href\":\"/top/1\"}," + + "\"templated\":{\"href\":\"/uri/{id}\",\"templated\":true, \"title\":\"top-title\", \"seen\":\"2017-08-10Z22:00:00\"}" + + "}," + + "\"_embedded\":{" + + "\"child\":[" + + "{\"_links\":{\"self\":{\"href\":\"/top/1/child/1\"}},\"id\":\"1\"}," + + "{\"_links\":{\"self\":{\"href\":\"/top/1/child/2\"}},\"id\":\"2\"}]" + + "}," + + "\"id\":\"1\"}"; + + JsonMapper om = HALMapper.create(); + + @Test + public void testDeserialization() throws Exception { + TopResource tr = om.readValue(new StringReader(HAL_DOC), TopResource.class); + assertEquals("1", tr.id); + assertEquals("/top/1", tr.self.getHref()); + assertEquals("/uri/{id}", tr.templated.getHref()); + assertTrue(tr.templated.getTemplated()); + assertEquals(2, tr.childLinks.size()); + assertEquals(2, tr.children.size()); + System.out.println("tr: " + tr); + } + + @Test + public void testDetailedDeserialization() throws Exception { + TopResource tr = om.readValue(new StringReader(HAL_DOC), TopResource.class); + assertEquals("1", tr.id); + assertEquals("/top/1", tr.self.getHref()); + assertEquals("/uri/{id}", tr.templated.getHref()); + assertTrue(tr.templated.getTemplated()); + assertEquals("top-title", tr.templated.getTitle()); + assertEquals("2017-08-10Z22:00:00", tr.templated.getSeen()); + assertEquals(2, tr.childLinks.size()); + assertEquals(2, tr.children.size()); + System.out.println("tr: " + tr); + } + + @Resource + public static class TopResource { + + public String id; + + @Link + public HALLink self; + + @Link + public HALLink templated; + + @Link + public HALLink nullLink; + + @Link("child") + public Collection childLinks; + + @Link("empty:list") + public Collection childLinksEmpty; + + @Link("null:list") + public Collection childLinksNull; + + @EmbeddedResource("child") + public Collection children; + + @EmbeddedResource + public String nullString; + } + + @Resource + public static class ChildResource { + + public String id; + + @Link + public HALLink self; + } + +} diff --git a/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerJsonViewIT.java b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerJsonViewIT.java new file mode 100755 index 0000000..59bc733 --- /dev/null +++ b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerJsonViewIT.java @@ -0,0 +1,227 @@ +package io.openapitoools.jackson.hal.deser; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; +import java.io.StringReader; + +import com.fasterxml.jackson.annotation.JsonView; +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.MapperFeature; + +import io.openapitools.jackson.dataformat.hal.HALLink; +import io.openapitools.jackson.hal.HALMapper; +import io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource; +import io.openapitools.jackson.dataformat.hal.annotation.Link; +import io.openapitools.jackson.dataformat.hal.annotation.Resource; + + +public class HALBeanDeserializerJsonViewIT { + static final String HAL_DOC_VIEW = "{" + + "\"_links\":{" + + "\"linkWithView1\":{" + + "\"href\":\"/link/with/view/1\"," + + "\"templated\":false" + + "}}," + + "\"_embedded\":{" + + "\"childWithView1\":{" + + "\"_links\":" + + "{\"self\":" + + "{\"href\":\"/top/1/child/childWithView1\"" + + "}}," + + "\"id\":\"childWithView1\"" + + "}}," + + "\"stateWithView1\":\"stateWithView1\"" + + "}"; + + static final String HAL_DOC_INCLUSIVE_VIEW = "{\"_links\":{" + + "\"linkWithView1\":{" + + "\"href\":\"/link/with/view/1\"," + + "\"templated\":false" + + "}," + + "\"linkWithoutView\":{" + + "\"href\":\"/link/without/view\"," + + "\"templated\":false" + + "}}," + + "\"_embedded\":{" + + "\"childWithView1\":{" + + "\"_links\":{" + + "\"self\":{" + + "\"href\":\"/top/1/child/childWithView1\"" + + "}}," + + "\"id\":\"childWithView1\"" + + "}," + + "\"childWithoutView\":{" + + "\"_links\":{" + + "\"self\":{" + + "\"href\":\"/top/1/child/childWithoutView\"" + + "}}," + + "\"id\":\"childWithoutView\"" + + "}}," + + "\"stateWithoutView\":\"stateWithoutView\"," + + "\"stateWithView1\":\"stateWithView1\"" + + "}"; + + static final String HAL_DOC_NO_VIEW = "{\"_links\":{" + + "\"linkWithView1\":{" + + "\"href\":\"/link/with/view/1\"," + + "\"templated\":false" + + "}," + + "\"linkWithView2\":{" + + "\"href\":\"/link/with/view/2\",\"templated\":false" + + "}," + + "\"linkWithoutView\":{" + + "\"href\":\"/link/without/view\"," + + "\"templated\":false" + + "}}," + + "\"_embedded\":{" + + "\"childWithView1\":{" + + "\"_links\":{" + + "\"self\":{" + + "\"href\":\"/top/1/child/childWithView1\"" + + "}}," + + "\"id\":\"childWithView1\"" + + "}," + + "\"childWithView2\":{" + + "\"_links\":{" + + "\"self\":{" + + "\"href\":\"/top/1/child/childWithView2\"" + + "}}," + + "\"id\":\"childWithView2\"" + + "}," + + "\"childWithoutView\":{" + + "\"_links\":{" + + "\"self\":{" + + "\"href\":\"/top/1/child/childWithoutView\"" + + "}}," + + "\"id\":\"childWithoutView\"" + + "}}," + + "\"stateWithoutView\":\"stateWithoutView\"," + + "\"stateWithView1\":\"stateWithView1\"" + + ",\"stateWithView2\":\"stateWithView2\"" + + "}"; + + @Test + public void testDeserializationOfView() throws IOException { + TopResource tr = HALMapper.builder() + .configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false) + .build() + .reader() + .withView(ResourceView1.class) + .forType(TopResource.class) + .readValue(new StringReader(HAL_DOC_INCLUSIVE_VIEW)); + + assertNull(tr.childWithoutView); + assertNull(tr.linkWithoutView); + assertNull(tr.stateWithoutView); + + assertNotNull(tr.childWithView1); + assertNotNull(tr.linkWithView1); + assertNotNull(tr.stateWithView1); + + assertNull(tr.childWithView2); + assertNull(tr.linkWithView2); + assertNull(tr.stateWithView2); + + } + + @Test + public void testDeserializationOfViewWithInclusion() throws IOException { + TopResource tr = HALMapper.builder() + .configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true) + .build() + .reader() + .withView(ResourceView1.class) + .forType(TopResource.class) + .readValue(new StringReader(HAL_DOC_INCLUSIVE_VIEW)); + + assertNotNull(tr.childWithoutView); + assertNotNull(tr.linkWithoutView); + assertNotNull(tr.stateWithoutView); + + assertNotNull(tr.childWithView1); + assertNotNull(tr.linkWithView1); + assertNotNull(tr.stateWithView1); + + assertNull(tr.childWithView2); + assertNull(tr.linkWithView2); + assertNull(tr.stateWithView2); + } + + @Test + public void testDeserializationOfNoView() throws IOException { + TopResource tr = HALMapper.create().readValue(new StringReader(HAL_DOC_NO_VIEW), TopResource.class); + + assertNotNull(tr.childWithoutView); + assertNotNull(tr.linkWithoutView); + assertNotNull(tr.stateWithoutView); + + assertNotNull(tr.childWithView1); + assertNotNull(tr.linkWithView1); + assertNotNull(tr.stateWithView1); + + assertNotNull(tr.childWithView2); + assertNotNull(tr.linkWithView2); + assertNotNull(tr.stateWithView2); + } + + @Resource + public static class TopResource { + + // No View + public String stateWithoutView; + + @Link + public HALLink linkWithoutView; + + @EmbeddedResource("childWithoutView") + public ChildResource childWithoutView; + + // Resource View 1 + @JsonView(HALBeanDeserializerJsonViewIT.ResourceView1.class) + public String stateWithView1; + + @Link("linkWithView1") + @JsonView(HALBeanDeserializerJsonViewIT.ResourceView1.class) + public HALLink linkWithView1; + + @EmbeddedResource("childWithView1") + @JsonView(HALBeanDeserializerJsonViewIT.ResourceView1.class) + public ChildResource childWithView1; + + // Resource View 2 + @JsonView(HALBeanDeserializerJsonViewIT.ResourceView2.class) + public String stateWithView2; + + @Link("linkWithView2") + @JsonView(HALBeanDeserializerJsonViewIT.ResourceView2.class) + public HALLink linkWithView2; + + @EmbeddedResource("childWithView2") + @JsonView(HALBeanDeserializerJsonViewIT.ResourceView2.class) + public ChildResource childWithView2; + } + + @Resource + @JsonView({ + HALBeanDeserializerJsonViewIT.ResourceView1.class, + HALBeanDeserializerJsonViewIT.ResourceView2.class + }) + public static class ChildResource { + + public String id; + + @Link + public HALLink self; + } + + public interface ResourceView1 { + + } + + public interface ResourceView2 { + + } +} diff --git a/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerMethodAnnIT.java b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerMethodAnnIT.java new file mode 100755 index 0000000..5cdee1f --- /dev/null +++ b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/HALBeanDeserializerMethodAnnIT.java @@ -0,0 +1,427 @@ +package io.openapitoools.jackson.hal.deser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.exc.UnrecognizedPropertyException; + +import io.openapitools.jackson.dataformat.hal.HALLink; +import io.openapitools.jackson.hal.HALMapper; +import io.openapitools.jackson.dataformat.hal.annotation.Curie; +import io.openapitools.jackson.dataformat.hal.annotation.Curies; +import io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource; +import io.openapitools.jackson.dataformat.hal.annotation.Link; +import io.openapitools.jackson.dataformat.hal.annotation.Resource; + +/** + * Verify that HAL annotations can be used on methods as well as fields. + */ +public class HALBeanDeserializerMethodAnnIT { + JsonMapper om = HALMapper.create(); + + @Test + public void testSingleCurie() throws IOException { + final String json = "{" + + " \"name\": \"POJO name\"," + + " \"_links\": {" + + " \"curies\": [{" + + " \"href\": \"http://my.example.com/doc/{rel}\"," + + " \"name\": \"cur1\"," + + " \"templated\": true" + + " }]," + + " \"self\": {" + + " \"href\": \"http://self.url\"," + + " \"templated\": false" + + " }," + + " \"link1\": {" + + " \"href\": \"http://other.link.url\"," + + " \"templated\": false" + + " }" + + " }," + + " \"_embedded\": {" + + " \"extras\": \"the more you know\"" + + " }" + + "}"; + + TestSingleCurieResource resource = om.readValue(json, TestSingleCurieResource.class); + assertEquals("POJO name", resource.fields.get("name")); + + HALLink self = (HALLink) resource.fields.get("self"); + assertEquals("http://self.url", self.getHref()); + assertFalse(self.getTemplated()); + + HALLink other = (HALLink) resource.fields.get("otherlink"); + assertEquals("http://other.link.url", other.getHref()); + + assertEquals("the more you know", resource.fields.get("extras")); + } + + @Test + public void testCuries() throws IOException { + final String json = "{" + + " \"name\": \"POJO name\"," + + " \"_links\": {" + + " \"curies\": [{" + + " \"href\": \"http://docs.my.site/{rel}\"," + + " \"name\": \"cur1\"," + + " \"templated\": true" + + " },{" + + " \"href\": \"http://docs.myother.site/cur2\"," + + " \"name\": \"cur2\"," + + " \"templated\": false" + + " }]," + + " \"self\": {" + + " \"href\": \"http://self.url\"," + + " \"templated\": false" + + " }," + + " \"link1\": {" + + " \"href\": \"http://other.link.url\"," + + " \"templated\": false" + + " }," + + " \"cur1:link2\": {" + + " \"href\": \"http://link2.url\"," + + " \"templated\": true" + + " }," + + " \"cur2:link3\": {" + + " \"href\": \"http://link3.url{?id}\"," + + " \"templated\": true" + + " }" + + " }," + + " \"_embedded\": {" + + " \"extras\": \"the more you know\"" + + " }" + + "}"; + + + TestCuriesResource resource = om.readValue(json, TestCuriesResource.class); + assertEquals("POJO name", resource.fields.get("name")); + + HALLink self = (HALLink) resource.fields.get("self"); + assertEquals("http://self.url", self.getHref()); + assertFalse(self.getTemplated()); + + HALLink other = (HALLink) resource.fields.get("otherlink"); + assertEquals("http://other.link.url", other.getHref()); + + HALLink curie2 = (HALLink) resource.fields.get("link2"); + assertEquals("http://link2.url", curie2.getHref()); + assertEquals(true, curie2.getTemplated()); + + HALLink curie3 = (HALLink) resource.fields.get("link3"); + assertEquals("http://link3.url{?id}", curie3.getHref()); + assertEquals(true, curie3.getTemplated()); + + HALLink curie4 = (HALLink) resource.fields.get("link4"); + assertNull(curie4); + + assertEquals("the more you know", resource.fields.get("extras")); + } + + @Test + public void testCuriesRenamed() throws IOException { + final String json = "{" + + " \"name\": \"POJO name\"," + + " \"_links\": {" + + " \"curies\": [{" + + " \"href\": \"http://docs.my.site/{rel}\"," + + " \"name\": \"cur11\"," + + " \"templated\": true" + + " },{" + + " \"href\": \"http://docs.myother.site/cur2\"," + + " \"name\": \"cur22\"," + + " \"templated\": false" + + " }]," + + " \"self\": {" + + " \"href\": \"http://self.url\"," + + " \"templated\": false" + + " }," + + " \"link1\": {" + + " \"href\": \"http://other.link.url\"," + + " \"templated\": false" + + " }," + + " \"cur11:link2\": {" + + " \"href\": \"http://link2.url\"," + + " \"templated\": true" + + " }," + + " \"cur22:link3\": {" + + " \"href\": \"http://link3.url{?id}\"," + + " \"templated\": true" + + " }" + + " }," + + " \"_embedded\": {" + + " \"extras\": \"the more you know\"" + + " }" + + "}"; + + + TestCuriesResource resource = om.readValue(json, TestCuriesResource.class); + assertEquals("POJO name", resource.fields.get("name")); + + HALLink self = (HALLink) resource.fields.get("self"); + assertEquals("http://self.url", self.getHref()); + assertFalse(self.getTemplated()); + + HALLink other = (HALLink) resource.fields.get("otherlink"); + assertEquals("http://other.link.url", other.getHref()); + + HALLink curie2 = (HALLink) resource.fields.get("link2"); + assertEquals("http://link2.url", curie2.getHref()); + assertEquals(true, curie2.getTemplated()); + + HALLink curie3 = (HALLink) resource.fields.get("link3"); + assertEquals("http://link3.url{?id}", curie3.getHref()); + assertEquals(true, curie3.getTemplated()); + + HALLink curie4 = (HALLink) resource.fields.get("link4"); + assertNull(curie4); + + assertEquals("the more you know", resource.fields.get("extras")); + } + + @Test + public void testUndefinedCurie() throws IOException { + final String json = "{" + + " \"name\": \"POJO name\"," + + " \"_links\": {" + + " \"curies\": [{" + + " \"href\": \"http://docs.my.site/{rel}\"," + + " \"name\": \"cur1\"," + + " \"templated\": true" + + " },{" + + " \"href\": \"http://docs.myother.site/cur2\"," + + " \"name\": \"cur2\"," + + " \"templated\": false" + + " }]," + + " \"self\": {" + + " \"href\": \"http://self.url\"," + + " \"templated\": false" + + " }," + + " \"link1\": {" + + " \"href\": \"http://other.link.url\"," + + " \"templated\": false" + + " }," + + " \"cur1:link2\": {" + + " \"href\": \"http://link2.url\"," + + " \"templated\": true" + + " }," + + " \"cur2:link3\": {" + + " \"href\": \"http://link3.url{?id}\"," + + " \"templated\": true" + + " }," + + " \"cur3:extralink\": {" + + " \"href\": \"http://extralink.url\"," + + " \"templated\": false" + + " }" + + " }," + + " \"_embedded\": {" + + " \"extras\": \"the more you know\"" + + " }" + + "}"; + + // With FAIL_ON_UNKNOWN_PROPERTIES=true (strict), an undefined curie prefix produces + // an unrecognized property that Jackson cannot map, so an exception must be thrown. + JsonMapper strictMapper = HALMapper.builder() + .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + try { + strictMapper.readValue(json, TestCuriesResource.class); + fail("There should be a failure due to non-defined curie prefix with strict mapper"); + } catch (UnrecognizedPropertyException upe) { + //expected + } + + // With FAIL_ON_UNKNOWN_PROPERTIES=false (Jackson 3 default), the unresolved curie + // link is silently ignored and deserialization succeeds. + JsonMapper lenientMapper = HALMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + lenientMapper.readValue(json, TestCuriesResource.class); // must not throw + } + + @Test + public void testCurieLinkURI() throws IOException { + final String json = "{" + + " \"name\": \"POJO name\"," + + " \"_links\": {" + + " \"curies\": [{" + + " \"href\": \"http://docs.my.site/{rel}\"," + + " \"name\": \"cur1\"," + + " \"templated\": true" + + " },{" + + " \"href\": \"http://docs.myother.site/cur2\"," + + " \"name\": \"cur2\"," + + " \"templated\": false" + + " }]," + + " \"self\": {" + + " \"href\": \"http://self.url\"," + + " \"templated\": false" + + " }," + + " \"link1\": {" + + " \"href\": \"http://other.link.url\"," + + " \"templated\": false" + + " }," + + " \"cur1:link2\": {" + + " \"href\": \"/link2.url\"," + + " \"templated\": true" + + " }," + + " \"cur2:link3\": {" + + " \"href\": \"http://link3.url{?id}\"," + + " \"templated\": true" + + " }" + + " }," + + " \"_embedded\": {" + + " \"extras\": \"the more you know\"" + + " }" + + "}"; + + + TestCuriesResource resource = om.readValue(json, TestCuriesResource.class); + assertEquals("POJO name", resource.fields.get("name")); + + HALLink self = (HALLink) resource.fields.get("self"); + assertEquals("http://self.url", self.getHref()); + assertFalse(self.getTemplated()); + + HALLink other = (HALLink) resource.fields.get("otherlink"); + assertEquals("http://other.link.url", other.getHref()); + + HALLink curie2 = (HALLink) resource.fields.get("link2"); + assertEquals("/link2.url", curie2.getHref()); + assertEquals(true, curie2.getTemplated()); + + HALLink curie3 = (HALLink) resource.fields.get("link3"); + assertEquals("http://link3.url{?id}", curie3.getHref()); + assertEquals(true, curie3.getTemplated()); + + HALLink curie4 = (HALLink) resource.fields.get("link4"); + assertNull(curie4); + + assertEquals("the more you know", resource.fields.get("extras")); + } + + @Test + public void testCurieResolved() throws IOException { + final String json = "{" + + " \"name\": \"POJO name\"," + + " \"_links\": {" + + " \"self\": {" + + " \"href\": \"http://self.url\"," + + " \"templated\": false" + + " }," + + " \"link1\": {" + + " \"href\": \"http://other.link.url\"," + + " \"templated\": false" + + " }," + + " \"http://docs.my.site/link2\": {" + + " \"href\": \"http://link2.url\"," + + " \"templated\": true" + + " }," + + " \"http://docs.myother.site/cur2\": {" + + " \"href\": \"http://link3.url{?id}\"," + + " \"templated\": true" + + " }" + + " }," + + " \"_embedded\": {" + + " \"extras\": \"the more you know\"" + + " }" + + "}"; + + + TestCuriesResource resource = om.readValue(json, TestCuriesResource.class); + + HALLink link2 = (HALLink) resource.fields.get("link2"); + assertEquals("http://link2.url", link2.getHref()); + assertEquals(true, link2.getTemplated()); + } + + + @Curie(prefix = "cur1", href = "http://my.example.com/doc/{rel}") + @Resource + public static class TestSingleCurieResource { + // Guard against Jackson unintentionally using class fields for deserialization. + private Map fields = new HashMap<>(); + + public void setName(String name) { + fields.put("name", name); + } + + @Link("self") + public void setSelfLink(HALLink link) { + fields.put("self", link); + } + + @Link("link1") + public void setOtherlink(HALLink link) { + fields.put("otherlink", link); + } + + @Link(value = "link2", curie = "cur1") + public void setLink2(HALLink link) { + fields.put("http://link2.url", link); + } + + @EmbeddedResource("extras") + public void setExtras(String extras) { + fields.put("extras", extras); + } + } + + @Curies({@Curie(href = "http://docs.my.site/{rel}", prefix = "cur1"), + @Curie(href = "http://docs.myother.site/cur2", prefix = "cur2"), + @Curie(href = "http://docs.another.site/{rel}", prefix = "cur3"), + @Curie(href = "http://docs.my.unusedsite/{rel}", prefix = "cur4-not-used")}) + @Resource + public static class TestCuriesResource { + // Guard against Jackson unintentionally using class fields for deserialization. + private Map fields = new HashMap<>(); + + public void setName(String name) { + fields.put("name", name); + } + + @Link("self") + public void setSelfLink(HALLink link) { + fields.put("self", link); + } + + @Link("link1") + public void setOtherlink(HALLink link) { + fields.put("otherlink", link); + } + + @Link(value = "link2", curie = "cur1") + public void setLink2(HALLink link) { + fields.put("link2", link); + } + + @Link(value = "link3", curie = "cur2") + public void setLink3(HALLink link) { + fields.put("link3", link); + } + + @Link(value = "link4", curie = "cur3") + public void setLink4(HALLink link) { + fields.put("link4", link); + } + + @Link(value = "link5", curie = "cur4") + public void setLink5(HALLink link) { + fields.put("link5", link); + } + + @EmbeddedResource("extras") + public void setExtras(String extras) { + fields.put("extras", extras); + } + } +} diff --git a/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/ReservedPropertyTest.java b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/ReservedPropertyTest.java new file mode 100755 index 0000000..56a3325 --- /dev/null +++ b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/deser/ReservedPropertyTest.java @@ -0,0 +1,90 @@ +package io.openapitoools.jackson.hal.deser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.Collections; + +import io.openapitools.jackson.dataformat.hal.deser.CurieMap; +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.introspect.AnnotatedField; +import tools.jackson.databind.introspect.AnnotationMap; +import tools.jackson.databind.introspect.BeanPropertyDefinition; + +import io.openapitools.jackson.dataformat.hal.annotation.Link; +import io.openapitools.jackson.hal.deser.ReservedProperty; + +public class ReservedPropertyTest { + + @Test + public void testAlternateNameNone() { + CurieMap cm = mock(CurieMap.class); + BeanPropertyDefinition bpd = mock(BeanPropertyDefinition.class); + when(bpd.getName()).thenReturn("orig-name"); + + String alternateName = ReservedProperty.LINKS.alternateName(bpd,cm ); + + assertEquals("orig-name", alternateName); + } + + @Test + public void testAlternateNameFieldNoAnnotation() throws Exception { + CurieMap cm = mock(CurieMap.class); + BeanPropertyDefinition bpd = mock(BeanPropertyDefinition.class); + AnnotatedField af = new AnnotatedField(null, POJO.class.getDeclaredField("bareField"), null); + when(bpd.getField()).thenReturn(af); + when(bpd.getName()).thenReturn("orig-name"); + + String alternateName = ReservedProperty.LINKS.alternateName(bpd, cm); + + assertEquals("orig-name", alternateName); + } + + @Test + public void testAlternateNameFieldNoValue() throws Exception { + CurieMap cm = mock(CurieMap.class); + BeanPropertyDefinition bpd = mock(BeanPropertyDefinition.class); + + final Field field = POJO.class.getDeclaredField("annotatedField"); + AnnotationMap am = AnnotationMap.of(Collections.singleton(field.getAnnotation(Link.class))); + AnnotatedField af = new AnnotatedField(null, field, am); + when(bpd.getField()).thenReturn(af); + when(bpd.getName()).thenReturn("orig-name"); + + String alternateName = ReservedProperty.LINKS.alternateName(bpd, cm); + + assertTrue(alternateName.matches(".+:orig-name")); + } + + @Test + public void testAlternateNameField() throws Exception { + CurieMap cm = mock(CurieMap.class); + BeanPropertyDefinition bpd = mock(BeanPropertyDefinition.class); + + final Field field = POJO.class.getDeclaredField("annotatedFieldWithValue"); + AnnotationMap am = AnnotationMap.of(Collections.singleton(field.getAnnotation(Link.class))); + AnnotatedField af = new AnnotatedField(null, field, am); + when(bpd.getField()).thenReturn(af); + when(bpd.getName()).thenReturn("orig-name"); + + String alternateName = ReservedProperty.LINKS.alternateName(bpd, cm); + + assertTrue(alternateName.matches(".+:alternate-name")); + } + + static class POJO { + + String bareField; + + @Link + String annotatedField; + + @Link(value = "alternate-name") + String annotatedFieldWithValue; + } + +} diff --git a/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerIT.java b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerIT.java new file mode 100755 index 0000000..879648c --- /dev/null +++ b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerIT.java @@ -0,0 +1,97 @@ +package io.openapitoools.jackson.hal.ser; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import org.junit.jupiter.api.Test; + +import io.openapitools.jackson.dataformat.hal.HALLink; +import io.openapitools.jackson.hal.HALMapper; +import io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource; +import io.openapitools.jackson.dataformat.hal.annotation.Link; +import io.openapitools.jackson.dataformat.hal.annotation.Resource; +import tools.jackson.databind.json.JsonMapper; + + +public class HALBeanSerializerIT { + + JsonMapper om = HALMapper.create(); + + @Test + public void testSerialization() throws Exception { + TopResource res1 = new TopResource(); + String json = om.writeValueAsString(res1); + assertEquals("{" + + "\"_links\":{" + + "\"child\":[{\"href\":\"/top/1/child/1\"},{\"href\":\"/top/1/child/2\"}]," + + "\"empty:list\":[]," + + "\"self\":{\"href\":\"/top/1\"}," + + "\"templated\":{\"href\":\"/uri/{id}\",\"templated\":true}" + + "}," + + "\"_embedded\":{" + + "\"child\":[" + + "{\"_links\":{\"self\":{\"href\":\"/top/1/child/1\"}},\"id\":\"1\"}," + + "{\"_links\":{\"self\":{\"href\":\"/top/1/child/2\"}},\"id\":\"2\"}]" + + "}," + + "\"id\":\"1\"}", + json); + } + + @Resource + public static class TopResource { + + public String id = "1"; + + @Link + public HALLink self = new HALLink.Builder(URI.create("/top/1")).build(); + + @Link("child") + public HALLink childLink = new HALLink.Builder(URI.create("/should/be/overridden")).build(); + + @Link + public HALLink templated = new HALLink.Builder("/uri/{id}").build(); + + @Link + public HALLink nullLink = null; + + @Link("child") + public Collection childLinks = Arrays.asList( + new HALLink.Builder(URI.create("/top/1/child/1")).build(), + new HALLink.Builder(URI.create("/top/1/child/2")).build()); + + @Link("empty:list") + public Collection childLinksEmpty = new ArrayList(); + + @Link("null:list") + public Collection childLinksNull = null; + + @EmbeddedResource // This should be overridden... + public ChildResource child = new ChildResource("42"); + + @EmbeddedResource("child") + public Collection children = Arrays.asList(new ChildResource("1"), new ChildResource("2")); + + @EmbeddedResource + public String nullString = null; + } + + @Resource + public static class ChildResource { + + public String id; + + @Link + public HALLink self; + + public ChildResource(String id) { + this.id = id; + self = new HALLink.Builder(URI.create("/top/1/child/" + id)).build(); + } + + } + +} diff --git a/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerJsonViewIT.java b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerJsonViewIT.java new file mode 100755 index 0000000..70b0513 --- /dev/null +++ b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerJsonViewIT.java @@ -0,0 +1,205 @@ +package io.openapitoools.jackson.hal.ser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static io.openapitoools.jackson.hal.JsonTestUtils.assertJsonEquals; + +import java.net.URI; + +import com.fasterxml.jackson.annotation.JsonView; +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.MapperFeature; + +import io.openapitools.jackson.dataformat.hal.HALLink; +import io.openapitools.jackson.hal.HALMapper; +import io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource; +import io.openapitools.jackson.dataformat.hal.annotation.Link; +import io.openapitools.jackson.dataformat.hal.annotation.Resource; + + +public class HALBeanSerializerJsonViewIT { + + + @Test + public void testSerializationOfView() throws Exception { + TopResource res1 = new TopResource(); + String json = HALMapper.builder() + .configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false) + .build() + .writer() + .withView(ResourceView1.class) + .writeValueAsString(res1); + + assertEquals("{" + + "\"_links\":{" + + "\"linkWithView1\":{" + + "\"href\":\"/link/with/view/1\"," + + "\"templated\":false" + + "}}," + + "\"_embedded\":{" + + "\"childWithView1\":{" + + "\"_links\":" + + "{\"self\":" + + "{\"href\":\"/top/1/child/childWithView1\"" + + "}}," + + "\"id\":\"childWithView1\"" + + "}}," + + "\"stateWithView1\":\"stateWithView1\"" + + "}", + json); + } + + @Test + public void testSerializationOfViewWithInclusion() throws Exception { + TopResource res1 = new TopResource(); + String json = HALMapper.builder() + .configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true) + .build() + .writer() + .withView(ResourceView1.class) + .writeValueAsString(res1); + + String expected = "{\"_links\":{" + + "\"linkWithView1\":{" + + "\"href\":\"/link/with/view/1\"," + + "\"templated\":false" + + "}," + + "\"linkWithoutView\":{" + + "\"href\":\"/link/without/view\"," + + "\"templated\":false" + + "}}," + + "\"_embedded\":{" + + "\"childWithView1\":{" + + "\"_links\":{" + + "\"self\":{" + + "\"href\":\"/top/1/child/childWithView1\"" + + "}}," + + "\"id\":\"childWithView1\"" + + "}," + + "\"childWithoutView\":{" + + "\"_links\":{" + + "\"self\":{" + + "\"href\":\"/top/1/child/childWithoutView\"" + + "}}," + + "\"id\":\"childWithoutView\"" + + "}}," + + "\"stateWithoutView\":\"stateWithoutView\"," + + "\"stateWithView1\":\"stateWithView1\"" + + "}"; + + // Use order-agnostic comparison to avoid brittleness from Jackson version differences + assertJsonEquals(expected, json); + } + + @Test + public void testSerializationOfNoView() throws Exception { + TopResource res1 = new TopResource(); + String json = HALMapper.create().writeValueAsString(res1); + + String expected = "{\"_links\":{" + + "\"linkWithView1\":{" + + "\"href\":\"/link/with/view/1\"," + + "\"templated\":false" + + "}," + + "\"linkWithView2\":{" + + "\"href\":\"/link/with/view/2\",\"templated\":false" + + "}," + + "\"linkWithoutView\":{" + + "\"href\":\"/link/without/view\"," + + "\"templated\":false" + + "}}," + + "\"_embedded\":{" + + "\"childWithView1\":{" + + "\"_links\":{" + + "\"self\":{" + + "\"href\":\"/top/1/child/childWithView1\"" + + "}}," + + "\"id\":\"childWithView1\"" + + "}," + + "\"childWithView2\":{" + + "\"_links\":{" + + "\"self\":{" + + "\"href\":\"/top/1/child/childWithView2\"" + + "}}," + + "\"id\":\"childWithView2\"" + + "}," + + "\"childWithoutView\":{" + + "\"_links\":{" + + "\"self\":{" + + "\"href\":\"/top/1/child/childWithoutView\"" + + "}}," + + "\"id\":\"childWithoutView\"" + + "}}," + + "\"stateWithoutView\":\"stateWithoutView\"," + + "\"stateWithView1\":\"stateWithView1\"" + + ",\"stateWithView2\":\"stateWithView2\"" + + "}"; + + // Use order-agnostic comparison to avoid brittleness from Jackson version differences + assertJsonEquals(expected, json); + } + + @Resource + public static class TopResource { + + // No View + public String stateWithoutView = "stateWithoutView"; + + @Link + public HALLink linkWithoutView = new HALLink.Builder("/link/without/view").build(); + + @EmbeddedResource("childWithoutView") + public ChildResource childWithoutView = new ChildResource("childWithoutView"); + + // Resource View 1 + @JsonView(HALBeanSerializerJsonViewIT.ResourceView1.class) + public String stateWithView1 = "stateWithView1"; + + @Link("linkWithView1") + @JsonView(HALBeanSerializerJsonViewIT.ResourceView1.class) + public HALLink linkWithView1 = new HALLink.Builder("/link/with/view/1").build(); + + @EmbeddedResource("childWithView1") + @JsonView(HALBeanSerializerJsonViewIT.ResourceView1.class) + public ChildResource childWithView1 = new ChildResource("childWithView1"); + + // Resource View 2 + @JsonView(HALBeanSerializerJsonViewIT.ResourceView2.class) + public String stateWithView2 = "stateWithView2"; + + @Link("linkWithView2") + @JsonView(HALBeanSerializerJsonViewIT.ResourceView2.class) + public HALLink linkWithView2 = new HALLink.Builder("/link/with/view/2").build(); + + @EmbeddedResource("childWithView2") + @JsonView(HALBeanSerializerJsonViewIT.ResourceView2.class) + public ChildResource childWithView2 = new ChildResource("childWithView2"); + } + + @Resource + @JsonView({ + HALBeanSerializerJsonViewIT.ResourceView1.class, + HALBeanSerializerJsonViewIT.ResourceView2.class + }) + public static class ChildResource { + + public String id; + + @Link + public HALLink self; + + public ChildResource(String id) { + this.id = id; + self = new HALLink.Builder(URI.create("/top/1/child/" + id)).build(); + } + + } + + public interface ResourceView1 { + + } + + public interface ResourceView2 { + + } +} diff --git a/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerMethodAnnIT.java b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerMethodAnnIT.java new file mode 100755 index 0000000..f4207fc --- /dev/null +++ b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerMethodAnnIT.java @@ -0,0 +1,198 @@ +package io.openapitoools.jackson.hal.ser; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; + +import io.openapitools.jackson.dataformat.hal.HALLink; +import io.openapitools.jackson.hal.HALMapper; +import io.openapitools.jackson.dataformat.hal.annotation.Curie; +import io.openapitools.jackson.dataformat.hal.annotation.Curies; +import io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource; +import io.openapitools.jackson.dataformat.hal.annotation.Link; +import io.openapitools.jackson.dataformat.hal.annotation.Resource; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Verify that HAL annotations can be used on methods as well as fields. + */ +public class HALBeanSerializerMethodAnnIT { + JsonMapper om = HALMapper.create(); + + @Test + public void testCuries() { + TestResource resource = new TestResource(); + resource.setSelf(new HALLink.Builder("http://self.url").build()); + resource.setRelatedLink1(new HALLink.Builder("http://other.link.url").build()); + resource.setRelatedLink2(new HALLink.Builder("http://other.link.url2").build()); + resource.setRelatedLink3(new HALLink.Builder("http://other.link.url3{?id}").build()); + resource.setRelatedLink4(new HALLink.Builder("/other.link.url4{?id}").build()); + resource.setName("POJO name"); + resource.setAuxData("added information"); + + final String json = om.writeValueAsString(resource); + + // Verify generated json + JsonNode jsonNode = JsonMapper.builder().build().readTree(json); + + JsonNode links = jsonNode.path("_links"); + assertFalse(links.isMissingNode()); + assertEquals(6, links.size()); + + JsonNode curies = links.get("curies"); + assertFalse(curies.isMissingNode()); + assertEquals(3, curies.size()); + + JsonNode relative2UsingCurie = links.get("cur1:relative2"); + assertFalse(relative2UsingCurie.isMissingNode()); + assertEquals("http://other.link.url2", relative2UsingCurie.get("href").asString()); + assertFalse(relative2UsingCurie.get("templated").asBoolean()); + + JsonNode relative3UsingCurie = links.get("cur2:relative3"); + assertFalse(relative3UsingCurie.isMissingNode()); + assertEquals("http://other.link.url3{?id}", relative3UsingCurie.get("href").asString()); + assertTrue(relative3UsingCurie.get("templated").asBoolean()); + + + JsonNode relative4UsingCurie = links.get("cur3:relative4"); + assertFalse(relative4UsingCurie.isMissingNode()); + assertEquals("/other.link.url4{?id}", relative4UsingCurie.get("href").asString()); + assertTrue(relative4UsingCurie.get("templated").asBoolean()); + + JsonNode curie4 = curies.get("cur4-not-used"); + assertNull(curie4); + + JsonNode embedded = jsonNode.path("_embedded"); + assertFalse(embedded.isMissingNode()); + assertEquals(1, embedded.size()); + + assertEquals("POJO name", jsonNode.get("name").asString()); + } + + @Test + public void singleCurieTest() throws Exception { + + // test support for @Curie as opposed to @Curies + + SingleCurieResource resource = new SingleCurieResource(); + resource.setRelatedLink1(new HALLink.Builder("http://other.link.url").build()); + + final String json = om.writeValueAsString(resource); + + // Verify generated json + JsonNode jsonNode = JsonMapper.builder().build().readTree(json); + + JsonNode links = jsonNode.path("_links"); + assertFalse(links.isMissingNode()); + assertEquals(2, links.size()); + + JsonNode curies = links.get("curies"); + assertFalse(curies.isMissingNode()); + // prune empty which means only 1 exixts + assertEquals(1, curies.size()); + + JsonNode curie1 = curies.get(0); + assertNotNull(curie1); + assertEquals("cur1", curie1.get("name").asString()); + assertEquals("http://docs.my.site/{rel}", curie1.get("href").asString()); + + JsonNode relative1UsingCurie = links.get("cur1:relative1"); + assertFalse(relative1UsingCurie.isMissingNode()); + } + + @Resource + @Curies({@Curie(href = "http://docs.my.site/{rel}", prefix = "cur1"), + @Curie(href = "http://docs.myother.site/{rel}", prefix = "cur2"), + @Curie(href = "http://docs.another.site/{rel}", prefix = "cur3"), + @Curie(href = "http://docs.my.site/{rel}", prefix = "cur4-not-used")}) + public static class TestResource { + // Guard against Jackson unintentionally using class fields for serialization. + private final Map fields = new HashMap<>(); + + public void setName(String name) { + fields.put("name", name); + } + + public String getName() { + return (String) fields.get("name"); + } + + @Link + public HALLink getSelf() { + return (HALLink) fields.get("self"); + } + + public void setSelf(HALLink link) { + fields.put("self", link); + } + + public HALLink getRelatedLink1() { + return (HALLink) fields.get("friend1"); + } + + public HALLink getRelatedLink2() { + return (HALLink) fields.get("friend2"); + } + + public HALLink getRelatedLink3() { + return (HALLink) fields.get("friend3"); + } + + public HALLink getRelatedLink4() { + return (HALLink) fields.get("friend4"); + } + + @Link("relative1") + public void setRelatedLink1(HALLink link) { + fields.put("friend1", link); + } + + @Link(value = "relative2", curie = "cur1") + public void setRelatedLink2(HALLink link) { + fields.put("friend2", link); + } + + @Link(value = "relative3", curie = "cur2") + public void setRelatedLink3(HALLink link) { + fields.put("friend3", link); + } + + @Link(value = "relative4", curie = "cur3") + public void setRelatedLink4(HALLink link) { + fields.put("friend4", link); + } + + @EmbeddedResource + public void setAuxData(String auxData) { + fields.put("auxdata", auxData); + } + + public String getAuxData() { + return (String) fields.get("auxdata"); + } + } + + // test support for @Curie as opposed to @Curies + @Resource + @Curie(href = "http://docs.my.site/{rel}", prefix = "cur1") + public static class SingleCurieResource { + + private final Map fields = new HashMap<>(); + + public HALLink getRelatedLink1() { + return (HALLink) fields.get("friend1"); + } + + @Link(value = "relative1", curie = "cur1") + public void setRelatedLink1(HALLink link) { + fields.put("friend1", link); + } + + } +} + diff --git a/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerPolymorphicWithExistingFieldIT.java b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerPolymorphicWithExistingFieldIT.java new file mode 100644 index 0000000..5e82fba --- /dev/null +++ b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerPolymorphicWithExistingFieldIT.java @@ -0,0 +1,198 @@ +package io.openapitoools.jackson.hal.ser; + +import static io.openapitoools.jackson.hal.JsonTestUtils.assertJsonEquals; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.json.JsonMapper; + +import io.openapitools.jackson.dataformat.hal.HALLink; +import io.openapitools.jackson.hal.HALMapper; +import io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource; +import io.openapitools.jackson.dataformat.hal.annotation.Link; +import io.openapitools.jackson.dataformat.hal.annotation.Resource; + +public class HALBeanSerializerPolymorphicWithExistingFieldIT { + + JsonMapper om = HALMapper.create(); + + @Test + public void testSerializationForResourceWithEmbeddableList() throws Exception { + @Resource + @SuppressWarnings("unused") + class TopResource { + public String id = "1"; + @Link + public HALLink self = new HALLink.Builder(URI.create("/top/1")).build(); + @Link("child") + public List childResourcesLink = Arrays.asList( + new HALLink.Builder(URI.create("/top/1/child/1")).build(), + new HALLink.Builder(URI.create("/top/1/child/2")).build()); + @EmbeddedResource("child") + public Collection children = Arrays.asList(new ChildResource("1"), new OtherChildResource("2", "Max")); + } + + TopResource resource = new TopResource(); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"_links\":{" + + "\"child\":[{\"href\":\"/top/1/child/1\"},{\"href\":\"/top/1/child/2\"}]," + + "\"self\":{\"href\":\"/top/1\"}" + + "}," + + "\"_embedded\":{" + + "\"child\":[" + + "{\"_links\":{\"self\":{\"href\":\"/top/1/child/1\"}},\"type\":\"ChildResource\",\"id\":\"1\"}," + + "{\"_links\":{\"self\":{\"href\":\"/top/1/child/2\"}},\"type\":\"OtherChildResource\",\"id\":\"2\",\"name\":\"Max\"}]" + + "}," + + "\"id\":\"1\"}", + json); + } + + @Test + public void testSerializationForResourceWithList() throws Exception { + @Resource + @SuppressWarnings("unused") + class TopResourceWithoutEmbedded { + public String id = "1"; + @Link + public HALLink self = new HALLink.Builder(URI.create("/top/1")).build(); + public Collection children = Arrays.asList(new ChildResource("1"), + new OtherChildResource("2", "Max")); + } + + TopResourceWithoutEmbedded resource = new TopResourceWithoutEmbedded(); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"_links\":{" + + "\"self\":{\"href\":\"/top/1\"}" + + "}," + + "\"id\":\"1\"," + + "\"children\":[" + + "{\"_links\":{\"self\":{\"href\":\"/top/1/child/1\"}},\"type\":\"ChildResource\",\"id\":\"1\"}," + + "{\"_links\":{\"self\":{\"href\":\"/top/1/child/2\"}},\"type\":\"OtherChildResource\",\"id\":\"2\",\"name\":\"Max\"}]" + + "}", + json); + } + + @Test + public void testSerializationForResourceEmbedded() throws Exception { + @Resource + @SuppressWarnings("unused") + class SimpleTopResourceEmbedded { + public String id = "1"; + @Link + public HALLink self = new HALLink.Builder(URI.create("/top/1")).build(); + @EmbeddedResource + public ChildResource child = new ChildResource("1"); + } + + SimpleTopResourceEmbedded resource = new SimpleTopResourceEmbedded(); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"_links\":{" + + "\"self\":{\"href\":\"/top/1\"}" + + "}," + + "\"_embedded\":{" + + "\"child\":{\"_links\":{\"self\":{\"href\":\"/top/1/child/1\"}},\"type\":\"ChildResource\",\"id\":\"1\"}" + + "}," + + "\"id\":\"1\"}", + json); + } + + @Test + public void testSerializationForResource() throws Exception { + @Resource + @SuppressWarnings("unused") + class SimpleTopResource { + public String id = "1"; + @Link public HALLink self = new HALLink.Builder(URI.create("/top/1")).build(); + public ChildResource child = new ChildResource("1"); + } + + SimpleTopResource resource = new SimpleTopResource(); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"_links\":{" + + "\"self\":{\"href\":\"/top/1\"}" + + "}," + + "\"id\":\"1\"," + + "\"child\":{" + + "\"_links\":{\"self\":{\"href\":\"/top/1/child/1\"}},\"type\":\"ChildResource\",\"id\":\"1\"" + + "}}", + json); + } + + @Test + public void testSerializationForAnnotatedPolymorphicObject() throws Exception { + ChildResource resource = new ChildResource("1"); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"_links\":{" + + "\"self\":{\"href\":\"/top/1/child/1\"}" + + "}," + + "\"type\":\"ChildResource\"," + + "\"id\":\"1\"}", + json); + } + + @Test + public void testSerializationForNotAnnotatedPolymorphicObject() throws Exception { + ChildResource resource = new OtherChildResource("1", "Max"); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"_links\":{" + + "\"self\":{\"href\":\"/top/1/child/1\"}" + + "}," + + "\"type\":\"OtherChildResource\"," + + "\"id\":\"1\"," + + "\"name\":\"Max\"}", + json); + } + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + defaultImpl = ChildResource.class) + @JsonSubTypes({@JsonSubTypes.Type(value = OtherChildResource.class, name = "OtherChildResource")}) + @Resource + public static class ChildResource { + + public String type = "ChildResource"; + + public String id; + + @Link public HALLink self; + + public ChildResource(String id) { + this.id = id; + self = new HALLink.Builder(URI.create("/top/1/child/" + id)).build(); + } + } + + @Resource + public static class OtherChildResource extends ChildResource { + + public String type = "OtherChildResource"; + + public String name; + + public OtherChildResource(String id, String name) { + super(id); + this.name = name; + } + } +} diff --git a/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerPolymorphicWithPropertyIT.java b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerPolymorphicWithPropertyIT.java new file mode 100644 index 0000000..d45a71f --- /dev/null +++ b/jackson-dataformat-hal-v3/src/test/java/io/openapitoools/jackson/hal/ser/HALBeanSerializerPolymorphicWithPropertyIT.java @@ -0,0 +1,187 @@ +package io.openapitoools.jackson.hal.ser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static io.openapitoools.jackson.hal.JsonTestUtils.assertJsonEquals; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.json.JsonMapper; + +import io.openapitools.jackson.dataformat.hal.HALLink; +import io.openapitools.jackson.hal.HALMapper; +import io.openapitools.jackson.dataformat.hal.annotation.EmbeddedResource; +import io.openapitools.jackson.dataformat.hal.annotation.Link; +import io.openapitools.jackson.dataformat.hal.annotation.Resource; + +public class HALBeanSerializerPolymorphicWithPropertyIT { + JsonMapper om = HALMapper.create(); + + @Test + public void testSerializationForResourceWithEmbeddableList() throws Exception { + @Resource + @SuppressWarnings("unused") + class TopResource { + public String id = "1"; + @Link + public HALLink self = new HALLink.Builder(URI.create("/top/1")).build(); + @Link("child") + public List childResourcesLink = Arrays.asList( + new HALLink.Builder(URI.create("/top/1/child/1")).build(), + new HALLink.Builder(URI.create("/top/1/child/2")).build()); + @EmbeddedResource("child") + public Collection children = Arrays.asList(new ChildResource("1"), + new OtherChildResource("2", "Max")); + } + TopResource resource = new TopResource(); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"_links\":{" + + "\"child\":[{\"href\":\"/top/1/child/1\"},{\"href\":\"/top/1/child/2\"}]," + + "\"self\":{\"href\":\"/top/1\"}" + + "}," + + "\"_embedded\":{" + + "\"child\":[" + + "{\"@type\":\"ChildResource\",\"_links\":{\"self\":{\"href\":\"/top/1/child/1\"}},\"id\":\"1\"}," + + "{\"@type\":\"OtherChildResource\",\"_links\":{\"self\":{\"href\":\"/top/1/child/2\"}},\"id\":\"2\",\"name\":\"Max\"}]" + + "}," + + "\"id\":\"1\"" + + "}", json); + } + + @Test + public void testSerializationForResourceWithList() throws Exception { + @Resource + @SuppressWarnings("unused") + class TopResourceWithoutEmbedded { + public String id = "1"; + @Link + public HALLink self = new HALLink.Builder(URI.create("/top/1")).build(); + public Collection children = Arrays.asList(new ChildResource("1"), + new OtherChildResource("2", "Max")); + } + TopResourceWithoutEmbedded resource = new TopResourceWithoutEmbedded(); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"_links\":{" + + "\"self\":{\"href\":\"/top/1\"}" + + "}," + + "\"id\":\"1\"," + + "\"children\":[" + + "{\"@type\":\"ChildResource\",\"_links\":{\"self\":{\"href\":\"/top/1/child/1\"}},\"id\":\"1\"}," + + "{\"@type\":\"OtherChildResource\",\"_links\":{\"self\":{\"href\":\"/top/1/child/2\"}},\"id\":\"2\",\"name\":\"Max\"}]"+ + "}", json); + } + + @Test + public void testSerializationForResourceEmbedded() throws Exception { + @Resource + @SuppressWarnings("unused") + class SimpleTopResourceEmbedded { + public String id = "1"; + @Link + public HALLink self = new HALLink.Builder(URI.create("/top/1")).build(); + @EmbeddedResource + public ChildResource child = new ChildResource("1"); + } + SimpleTopResourceEmbedded resource = new SimpleTopResourceEmbedded(); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"_links\":{" + + "\"self\":{\"href\":\"/top/1\"}" + + "}," + + "\"_embedded\":{" + + "\"child\":{\"@type\":\"ChildResource\",\"_links\":{\"self\":{\"href\":\"/top/1/child/1\"}},\"id\":\"1\"}" + + "}," + + "\"id\":\"1\"}", json); + } + + @Test + public void testSerializationForResource() throws Exception { + @Resource + @SuppressWarnings("unused") + class SimpleTopResource { + public String id = "1"; + @Link + public HALLink self = new HALLink.Builder(URI.create("/top/1")).build(); + public ChildResource child = new ChildResource("1"); + } + SimpleTopResource resource = new SimpleTopResource(); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"_links\":{" + + "\"self\":{\"href\":\"/top/1\"}" + + "}," + + "\"id\":\"1\"," + + "\"child\":{" + + "\"@type\":\"ChildResource\",\"_links\":{\"self\":{\"href\":\"/top/1/child/1\"}},\"id\":\"1\"" + + "}}", + json); + } + + @Test + public void testSerializationForAnnotatedPolymorphicObject() throws Exception { + ChildResource resource = new ChildResource("1"); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"@type\":\"ChildResource\"," + + "\"_links\":{" + + "\"self\":{\"href\":\"/top/1/child/1\"}" + + "}," + + "\"id\":\"1\"" + + "}", json); + } + + @Test + public void testSerializationForNotAnnotatedPolymorphicObject() throws Exception { + ChildResource resource = new OtherChildResource("1", "Max"); + String json = om.writeValueAsString(resource); + assertJsonEquals( + "{" + + "\"@type\":\"OtherChildResource\"," + + "\"_links\":{" + + "\"self\":{\"href\":\"/top/1/child/1\"}" + + "}," + + "\"id\":\"1\"," + + "\"name\":\"Max\"" + + "}", json); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, defaultImpl = ChildResource.class) + @JsonSubTypes({ @JsonSubTypes.Type(value = OtherChildResource.class, name = "OtherChildResource") }) + @JsonTypeName("ChildResource") + @Resource + public static class ChildResource { + public String id; + @Link + public HALLink self; + + public ChildResource(String id) { + this.id = id; + self = new HALLink.Builder(URI.create("/top/1/child/" + id)).build(); + } + } + + @Resource + @JsonTypeName("OtherChildResource") + public static class OtherChildResource extends ChildResource { + public String name; + + public OtherChildResource(String id, String name) { + super(id); + this.name = name; + } + } +} diff --git a/pom.xml b/pom.xml index b77e1ac..7c103ce 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,7 @@ jackson-dataformat-hal-core jackson-dataformat-hal-v2 + jackson-dataformat-hal-v3