diff --git a/patient-generated-data/src/main/java/gov/va/api/health/patientgenerateddata/JacksonMapperConfig.java b/patient-generated-data/src/main/java/gov/va/api/health/patientgenerateddata/JacksonMapperConfig.java new file mode 100644 index 00000000..5df78ee2 --- /dev/null +++ b/patient-generated-data/src/main/java/gov/va/api/health/patientgenerateddata/JacksonMapperConfig.java @@ -0,0 +1,24 @@ +package gov.va.api.health.patientgenerateddata; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gov.va.api.health.autoconfig.configuration.JacksonConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonMapperConfig { + + private final MagicReferenceConfig magicReferences; + + @Autowired + public JacksonMapperConfig(MagicReferenceConfig magicReferences) { + this.magicReferences = magicReferences; + } + + @Bean + public ObjectMapper objectMapper() { + ObjectMapper mapper = JacksonConfig.createMapper(); + return magicReferences.configure(mapper); + } +} diff --git a/patient-generated-data/src/main/java/gov/va/api/health/patientgenerateddata/MagicReferenceConfig.java b/patient-generated-data/src/main/java/gov/va/api/health/patientgenerateddata/MagicReferenceConfig.java new file mode 100644 index 00000000..6f529dd2 --- /dev/null +++ b/patient-generated-data/src/main/java/gov/va/api/health/patientgenerateddata/MagicReferenceConfig.java @@ -0,0 +1,102 @@ +package gov.va.api.health.patientgenerateddata; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import gov.va.api.health.fhir.api.IsReference; +import gov.va.api.health.r4.api.elements.Reference; +import java.util.List; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class MagicReferenceConfig { + private final String baseUrl; + + private final String r4BasePath; + + @Autowired + public MagicReferenceConfig( + @Value("${public-url}") String baseUrl, @Value("${public-r4-base-path}") String r4BasePath) { + this.baseUrl = baseUrl; + this.r4BasePath = r4BasePath; + } + + /** Configures and returns the mapper to support magic references. */ + public ObjectMapper configure(ObjectMapper mapper) { + mapper.registerModule(new MagicReferenceModule()); + return mapper; + } + + private final class QualifiedReferenceWriter extends BeanPropertyWriter { + + private QualifiedReferenceWriter(BeanPropertyWriter base) { + super(base); + } + + private String qualify(String reference) { + if (StringUtils.isBlank(reference)) { + return null; + } + if (reference.startsWith("http")) { + return reference; + } + if (reference.startsWith("/")) { + return baseUrl + "/" + r4BasePath + reference; + } + return baseUrl + "/" + r4BasePath + "/" + reference; + } + + @Override + @SneakyThrows + public void serializeAsField( + Object shouldBeReference, JsonGenerator gen, SerializerProvider prov) { + if (!(shouldBeReference instanceof IsReference)) { + throw new IllegalArgumentException( + "Qualified Reference writer cannot serialize: " + shouldBeReference); + } + IsReference reference = (IsReference) shouldBeReference; + String qualifiedReference = qualify(reference.reference()); + if (qualifiedReference != null) { + gen.writeStringField(getName(), qualifiedReference); + } + } + } + + private final class MagicReferenceModule extends SimpleModule { + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + context.addBeanSerializerModifier( + new BeanSerializerModifier() { + private void applyReferenceWriter(List beanProperties) { + for (int i = 0; i < beanProperties.size(); i++) { + BeanPropertyWriter beanPropertyWriter = beanProperties.get(i); + if ("reference".equals(beanPropertyWriter.getName())) { + beanProperties.set(i, new QualifiedReferenceWriter(beanPropertyWriter)); + } + } + } + + @Override + public List changeProperties( + SerializationConfig serialConfig, + BeanDescription beanDesc, + List beanProperties) { + if (beanDesc.getBeanClass() == Reference.class) { + applyReferenceWriter(beanProperties); + } + return super.changeProperties(serialConfig, beanDesc, beanProperties); + } + }); + } + } +} diff --git a/patient-generated-data/src/test/java/gov/va/api/health/patientgenerateddata/JacksonMapperConfigTest.java b/patient-generated-data/src/test/java/gov/va/api/health/patientgenerateddata/JacksonMapperConfigTest.java new file mode 100644 index 00000000..ac857a91 --- /dev/null +++ b/patient-generated-data/src/test/java/gov/va/api/health/patientgenerateddata/JacksonMapperConfigTest.java @@ -0,0 +1,145 @@ +package gov.va.api.health.patientgenerateddata; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import gov.va.api.health.autoconfig.configuration.JacksonConfig; +import gov.va.api.health.r4.api.DataAbsentReason; +import gov.va.api.health.r4.api.elements.Extension; +import gov.va.api.health.r4.api.elements.Reference; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.Singular; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +public class JacksonMapperConfigTest { + private static Reference reference(String path) { + return Reference.builder().display("display-value").reference(path).id("id-value").build(); + } + + @Test + @SneakyThrows + public void preExistingDarsArePreserved() { + FugaziReferenceMajig input = + FugaziReferenceMajig.builder() + .ref(reference("https://example.com/api/Practitioner/1234")) + .nope(null) + ._nope(DataAbsentReason.of(DataAbsentReason.Reason.error)) + .build(); + FugaziReferenceMajig expected = + FugaziReferenceMajig.builder() + .ref(reference("https://example.com/api/Practitioner/1234")) + .nope(null) + ._nope(DataAbsentReason.of(DataAbsentReason.Reason.error)) + .build(); + String serializedJson = + new JacksonMapperConfig(new MagicReferenceConfig("https://example.com", "r4")) + .objectMapper() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(input); + FugaziReferenceMajig actual = + JacksonConfig.createMapper().readValue(serializedJson, FugaziReferenceMajig.class); + assertThat(actual).isEqualTo(expected); + } + + @Test + @SneakyThrows + public void referencesAreQualified() { + FugaziReferenceMajig input = + FugaziReferenceMajig.builder() + .whocares("noone") + .me(true) + .ref(reference("AllergyIntolerance/1234")) + .nope(reference("https://example.com/r4/Location/1234")) + ._nope(DataAbsentReason.of(DataAbsentReason.Reason.unsupported)) + .alsoNo(reference("https://example.com/r4/Location/1234")) + .thing(reference(null)) + .thing(reference("")) + .thing(reference("http://qualified.is.not/touched")) + .thing(reference("no/slash")) + .thing(reference("/cool/a/slash")) + .thing(reference("Location")) + .thing(reference("Location/1234")) + .thing(reference("https://example.com/r4/Location/1234")) + .thing(reference("/Organization")) + .thing(reference("Organization/1234")) + .thing(reference("https://example.com/r4/Organization/1234")) + .thing(reference("Practitioner/987")) + .inner( + FugaziReferenceMajig.builder() + .ref( + Reference.builder() + .reference("Practitioner/615f31df-f0c7-5100-ac42-7fb952c630d0") + .display(null) + .build()) + .build()) + .build(); + FugaziReferenceMajig expected = + FugaziReferenceMajig.builder() + .whocares("noone") + .me(true) + .nope(reference("https://example.com/r4/Location/1234")) + ._nope(DataAbsentReason.of(DataAbsentReason.Reason.unsupported)) + .alsoNo(reference("https://example.com/r4/Location/1234")) + .ref(reference("https://example.com/r4/AllergyIntolerance/1234")) + .thing(reference(null)) + .thing(reference(null)) + .thing(reference("http://qualified.is.not/touched")) + .thing(reference("https://example.com/r4/no/slash")) + .thing(reference("https://example.com/r4/cool/a/slash")) + .thing(reference("https://example.com/r4/Location")) + .thing(reference("https://example.com/r4/Location/1234")) + .thing(reference("https://example.com/r4/Location/1234")) + .thing(reference("https://example.com/r4/Organization")) + .thing(reference("https://example.com/r4/Organization/1234")) + .thing(reference("https://example.com/r4/Organization/1234")) + .thing(reference("https://example.com/r4/Practitioner/987")) + .inner( + FugaziReferenceMajig.builder() + .ref( + Reference.builder() + .reference( + "https://example.com/r4/Practitioner/615f31df-f0c7-5100-ac42-7fb952c630d0") + .build()) + .build()) + .build(); + String qualifiedJson = + new JacksonMapperConfig(new MagicReferenceConfig("https://example.com", "r4")) + .objectMapper() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(input); + FugaziReferenceMajig actual = + JacksonConfig.createMapper().readValue(qualifiedJson, FugaziReferenceMajig.class); + assertThat(actual).isEqualTo(expected); + } + + @Data + @Builder + @NoArgsConstructor(access = AccessLevel.PRIVATE) + @AllArgsConstructor + @JsonAutoDetect( + fieldVisibility = JsonAutoDetect.Visibility.ANY, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) + static final class FugaziReferenceMajig { + Reference ref; + + Reference nope; + + Extension _nope; + + Reference alsoNo; + + @Singular List things; + + JacksonMapperConfigTest.FugaziReferenceMajig inner; + + String whocares; + + Boolean me; + } +} diff --git a/patient-generated-data/src/test/resources/application.properties b/patient-generated-data/src/test/resources/application.properties index 5e57dab5..2a709a5c 100644 --- a/patient-generated-data/src/test/resources/application.properties +++ b/patient-generated-data/src/test/resources/application.properties @@ -1,3 +1,5 @@ +public-r4-base-path=unset +public-url=unset spring.datasource.url=jdbc:h2:. ssl.enable-client=false web-exception-key=test