From 17ae3f71422aa95f39f1fa8a6fce8e26799ec3c7 Mon Sep 17 00:00:00 2001 From: Basilio Bogado <541149+basiliskus@users.noreply.github.com> Date: Wed, 6 Dec 2023 02:20:34 +0800 Subject: [PATCH] FilePartnerMetadataStorage implementation (#704) * Added initial implementation for localfile saving + added custom exception * Added missing excpetion handling in SendOrderUseCase * Updated Jackson formatter to handle Instant * Added implementation for readMetadata + exception handling * Added test coverage * Added JavaDoc for PartnerMetadataException * 672: Test PartnerMetadataException and PartnerMetadata * Fixed sonarcloud security risk --------- Co-authored-by: halprin --- .../metadata/PartnerMetadataException.java | 9 +++ .../etor/metadata/PartnerMetadataStorage.java | 4 +- .../etor/orders/SendOrderUseCase.java | 7 +- .../localfile/FilePartnerMetadataStorage.java | 53 ++++++++++++++- .../PartnerMetadataExceptionTest.groovy | 20 ++++++ .../etor/metadata/PartnerMetadataTest.groovy | 35 ++++++++++ .../FilePartnerMetadataStorageTest.groovy | 67 +++++++++++++++++++ shared/build.gradle | 1 + .../external/jackson/Jackson.java | 10 +++ 9 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataException.java create mode 100644 etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataExceptionTest.groovy create mode 100644 etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataTest.groovy create mode 100644 etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorageTest.groovy diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataException.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataException.java new file mode 100644 index 000000000..5abed8c11 --- /dev/null +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataException.java @@ -0,0 +1,9 @@ +package gov.hhs.cdc.trustedintermediary.etor.metadata; + +/** Custom exception class use to catch partner metadata exceptions */ +public class PartnerMetadataException extends Exception { + + public PartnerMetadataException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataStorage.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataStorage.java index 660fe5edb..12e51da84 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataStorage.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataStorage.java @@ -2,7 +2,7 @@ /** Interface to store and retrieve our partner-facing metadata. */ public interface PartnerMetadataStorage { - PartnerMetadata readMetadata(String uniqueId); + PartnerMetadata readMetadata(String uniqueId) throws PartnerMetadataException; /** * This method will do "upserts". If the record doesn't exist, it is created. If the record @@ -10,5 +10,5 @@ public interface PartnerMetadataStorage { * * @param metadata The metadata to save. */ - void saveMetadata(PartnerMetadata metadata); + void saveMetadata(PartnerMetadata metadata) throws PartnerMetadataException; } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCase.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCase.java index 6ba944f44..fa7e378dd 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCase.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/orders/SendOrderUseCase.java @@ -2,6 +2,7 @@ import gov.hhs.cdc.trustedintermediary.etor.metadata.EtorMetadataStep; import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadata; +import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataException; import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataStorage; import gov.hhs.cdc.trustedintermediary.wrappers.MetricMetadata; import java.time.Instant; @@ -25,7 +26,11 @@ public void convertAndSend(final Order order) throws UnableToSendOrderExcepti var partnerMetadata = new PartnerMetadata( "uniqueId", "senderName", "receiverName", Instant.now(), "abcd"); - partnerMetadataStorage.saveMetadata(partnerMetadata); + try { + partnerMetadataStorage.saveMetadata(partnerMetadata); + } catch (PartnerMetadataException e) { + throw new UnableToSendOrderException("Unable to save metadata for the order", e); + } var omlOrder = converter.convertMetadataToOmlOrder(order); metadata.put(order.getFhirResourceId(), EtorMetadataStep.ORDER_CONVERTED_TO_OML); diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorage.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorage.java index 68e74cf20..4b0e4ca80 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorage.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorage.java @@ -1,13 +1,40 @@ package gov.hhs.cdc.trustedintermediary.external.localfile; import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadata; +import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataException; import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataStorage; +import gov.hhs.cdc.trustedintermediary.wrappers.Logger; +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter; +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.FormatterProcessingException; +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermissions; +import javax.inject.Inject; /** Implements the {@link PartnerMetadataStorage} using local files. */ public class FilePartnerMetadataStorage implements PartnerMetadataStorage { private static final FilePartnerMetadataStorage INSTANCE = new FilePartnerMetadataStorage(); + @Inject Formatter formatter; + @Inject Logger logger; + + private static final Path metadataTempDirectory; + + static { + try { + FileAttribute onlyOwnerAttrs = + PosixFilePermissions.asFileAttribute( + PosixFilePermissions.fromString("rwx------")); + metadataTempDirectory = Files.createTempDirectory("metadata", onlyOwnerAttrs); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + private FilePartnerMetadataStorage() {} public static FilePartnerMetadataStorage getInstance() { @@ -15,10 +42,30 @@ public static FilePartnerMetadataStorage getInstance() { } @Override - public PartnerMetadata readMetadata(final String uniqueId) { - return null; + public PartnerMetadata readMetadata(final String uniqueId) throws PartnerMetadataException { + Path filePath = getFilePath(uniqueId); + try { + String content = Files.readString(filePath); + return formatter.convertJsonToObject(content, new TypeReference<>() {}); + } catch (IOException | FormatterProcessingException e) { + throw new PartnerMetadataException("Unable to read the metadata file", e); + } } @Override - public void saveMetadata(final PartnerMetadata metadata) {} + public void saveMetadata(final PartnerMetadata metadata) throws PartnerMetadataException { + Path metadataFilePath = getFilePath(metadata.uniqueId()); + try { + String content = formatter.convertToJsonString(metadata); + Files.writeString(metadataFilePath, content); + logger.logInfo("Saved metadata for " + metadata.uniqueId() + " to " + metadataFilePath); + } catch (IOException | FormatterProcessingException e) { + throw new PartnerMetadataException( + "Error saving metadata for " + metadata.uniqueId(), e); + } + } + + private Path getFilePath(String uniqueId) { + return metadataTempDirectory.resolve(uniqueId + ".json"); + } } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataExceptionTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataExceptionTest.groovy new file mode 100644 index 000000000..a2a617b42 --- /dev/null +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataExceptionTest.groovy @@ -0,0 +1,20 @@ +package gov.hhs.cdc.trustedintermediary.etor.metadata + + +import spock.lang.Specification + +class PartnerMetadataExceptionTest extends Specification { + def "constructor works"() { + + given: + def message = "something blew up!" + def cause = new NullPointerException() + + when: + def exception = new PartnerMetadataException(message, cause) + + then: + exception.getMessage() == message + exception.getCause() == cause + } +} diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataTest.groovy new file mode 100644 index 000000000..fa17e4706 --- /dev/null +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/metadata/PartnerMetadataTest.groovy @@ -0,0 +1,35 @@ +package gov.hhs.cdc.trustedintermediary.etor.metadata + + +import gov.hhs.cdc.trustedintermediary.PojoTestUtils +import java.time.Instant +import spock.lang.Specification + +class PartnerMetadataTest extends Specification { + def "test getters and setters"() { + when: + PojoTestUtils.validateGettersAndSetters(PartnerMetadata) + + then: + noExceptionThrown() + } + + def "test constructor"() { + given: + def uniqueId = "uniqueId" + def sender = "sender" + def receiver = "receiver" + def timeReceived = Instant.now() + def hash = "abcd" + + when: + def metadata = new PartnerMetadata(uniqueId, sender, receiver, timeReceived, hash) + + then: + metadata.uniqueId() == uniqueId + metadata.sender() == sender + metadata.receiver() == receiver + metadata.timeReceived() == timeReceived + metadata.hash() == hash + } +} diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorageTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorageTest.groovy new file mode 100644 index 000000000..e313e2218 --- /dev/null +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/localfile/FilePartnerMetadataStorageTest.groovy @@ -0,0 +1,67 @@ +package gov.hhs.cdc.trustedintermediary.external.localfile + +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadata +import gov.hhs.cdc.trustedintermediary.etor.metadata.PartnerMetadataException +import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.FormatterProcessingException +import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference +import spock.lang.Specification + +import java.time.Instant + +class FilePartnerMetadataStorageTest extends Specification { + + def setup() { + TestApplicationContext.reset() + TestApplicationContext.init() + TestApplicationContext.register(FilePartnerMetadataStorage, FilePartnerMetadataStorage.getInstance()) + } + + def "save and read metadata successfully"() { + given: + def expectedUniqueId = "uniqueId" + PartnerMetadata metadata = new PartnerMetadata(expectedUniqueId, "sender", "receiver", Instant.parse("2023-12-04T18:51:48.941875Z"), "abcd") + + TestApplicationContext.register(Formatter, Jackson.getInstance()) + TestApplicationContext.injectRegisteredImplementations() + + when: + FilePartnerMetadataStorage.getInstance().saveMetadata(metadata) + def actualMetadata = FilePartnerMetadataStorage.getInstance().readMetadata(expectedUniqueId) + + then: + actualMetadata == metadata + } + + def "saveMetadata throws PartnerMetadataException when unable to save file"() { + given: + PartnerMetadata metadata = new PartnerMetadata("uniqueId", "sender", "receiver", Instant.now(), "abcd") + + def mockFormatter = Mock(Formatter) + mockFormatter.convertToJsonString(_ as PartnerMetadata) >> {throw new FormatterProcessingException("error", new Exception())} + TestApplicationContext.register(Formatter, mockFormatter) + TestApplicationContext.injectRegisteredImplementations() + + when: + FilePartnerMetadataStorage.getInstance().saveMetadata(metadata) + + then: + thrown(PartnerMetadataException) + } + + def "readMetadata throws PartnerMetadataException when unable to read file"() { + given: + def mockFormatter = Mock(Formatter) + mockFormatter.convertJsonToObject(_ as String, _ as TypeReference) >> {throw new FormatterProcessingException("error", new Exception())} + TestApplicationContext.register(Formatter, mockFormatter) + TestApplicationContext.injectRegisteredImplementations() + + when: + FilePartnerMetadataStorage.getInstance().readMetadata("uniqueId") + + then: + thrown(PartnerMetadataException) + } +} diff --git a/shared/build.gradle b/shared/build.gradle index aabbfa8b9..cb83797a8 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-core:2.16.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.0' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.16.0' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.0' //fhir api 'ca.uhn.hapi.fhir:hapi-fhir-base:6.10.0' diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/jackson/Jackson.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/jackson/Jackson.java index 02c55a61a..dd546f289 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/jackson/Jackson.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/jackson/Jackson.java @@ -3,8 +3,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import gov.hhs.cdc.trustedintermediary.wrappers.Logger; import gov.hhs.cdc.trustedintermediary.wrappers.YamlCombiner; import gov.hhs.cdc.trustedintermediary.wrappers.YamlCombinerException; @@ -30,6 +32,14 @@ public class Jackson implements Formatter, YamlCombiner { @Inject Logger logger; + static { + JavaTimeModule javaTimeModule = new JavaTimeModule(); + JSON_OBJECT_MAPPER.registerModule(javaTimeModule); + YAML_OBJECT_MAPPER.registerModule(javaTimeModule); + JSON_OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + YAML_OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } + private Jackson() {} public static Jackson getInstance() {