From 04722ab01c833995f8d7cf00fcf68255e2b9da6c Mon Sep 17 00:00:00 2001 From: Alexandre Pocinho <apo@kereval.com> Date: Thu, 1 Feb 2024 16:00:12 +0000 Subject: [PATCH] Implements ITI-104 transaction + external validation --- .gitlab-ci.yml | 50 +- PixM_Feed_Documentation/README_PixMFeed.md | 536 --------- .../pictures/iti_93_interaction_diagram.png | Bin 14156 -> 0 bytes .../pictures/pixm_connector.png | Bin 111903 -> 0 bytes .../pictures/pixm_scheme_total.png | Bin 36542 -> 0 bytes README.md | 350 +++--- http-validator-client/pom.xml | 62 + .../validation/HttpValidationService.java | 65 ++ .../HttpValidationServiceFactory.java | 14 + .../ws/HttpValidatorServerClientImpl.java | 66 ++ .../HttpValidationServiceFactoryTest.java | 15 + .../validation/HttpValidationServiceTest.java | 63 ++ .../mock/HttpValidationServiceMock.java | 14 + .../HttpValidatorServerClientImplMock.java | 47 + .../test/resources/http_request_iti104.http | 66 ++ .../src/test/resources/response_failed.json | 226 ++++ .../src/test/resources/response_passed.json | 226 ++++ .../resources/response_throw_exception.json | 3 + matchbox-client/pom.xml | 79 ++ .../CustomPatientServiceFactory.java | 14 + .../CustomPatientValidationService.java | 78 ++ .../interlay/ws/IGFhirServerClientImpl.java | 85 ++ .../CustomPatientServiceFactoryTest.java | 16 + .../CustomPatientValidationServiceTest.java | 58 + .../CustomPatientValidationServiceMock.java | 15 + .../ws/mock/IgFhirServerClientMock.java | 55 + .../test/resources/post_request_passed.json | 60 + .../src/test/resources/post_response.json | 14 + .../test/resources/post_response_failed.json | 14 + pixm-connector-service/pom.xml | 330 ++++++ .../connector/ConversionException.java | 2 +- .../FhirToGazelleRegistryConverter.java | 247 ++++ .../GazelleRegistryToFhirConverter.java | 180 +++ .../adapter/preferences/Namespaces.java | 0 .../OperationalPreferencesPIXm.java | 1 + .../adapter/preferences/Preferences.java | 0 .../adapter/servlet/IheHapiFhirServer.java | 37 + .../application/ConfigurationAdapter.java | 12 + .../PatientRegistryFeedClient.java | 340 ++++++ .../PatientRegistrySearchClient.java | 215 ++++ .../PatientRegistryXRefSearchClient.java | 44 +- .../application/ProfilesValidators.java | 126 +++ .../ProfilesValidatorsFactory.java | 23 + .../interceptor/LegacyLoggingInterceptor.java | 6 +- .../interceptor/NewLoggingInterceptor.java | 0 .../provider/IhePatientResourceProvider.java | 357 ++++++ .../service/RequestValidatorService.java | 27 + .../ITI104PatientFeedQueryProfile.java | 26 + .../ITI104PatientFeedQueryProfileFactory.java | 24 + .../profiles/ITI83GetPIXmQueryProfile.java | 21 + .../ITI83GetPIXmQueryProfileFactory.java | 24 + .../profiles/ITI83PostPIXmQueryProfile.java | 27 + .../ITI83PostPIXmQueryProfileFactory.java | 23 + .../ProfilesValidatorsFactoryProvider.java | 26 + .../src}/main/resources/META-INF/ejb-jar.xml | 2 +- ...elle.application.ProfilesValidatorsFactory | 3 + .../SoapUI/IHE-Pixm-soapui-project.xml | 1002 +++++++++++++++++ .../src/main/resources/deployment.properties | 3 + .../src}/main/webapp/WEB-INF/beans.xml | 4 +- .../src/main/webapp/WEB-INF/jboss-web.xml | 7 + .../src/main/webapp/WEB-INF/web.xml | 4 + .../FhirToGazelleRegistryConverterTest.java | 50 +- .../GazelleRegistryToFhirConverterTest.java | 53 +- .../application/PatientFeedClientTest.java | 541 +++++++++ .../PatientRegistrySearchClientTest.java | 317 ++++++ .../PatientRegistryXRefSearchClientTest.java | 68 +- .../mock/PatientFeedClientMock.java | 27 + .../application/mock/SearchClientMock.java | 33 + .../mock}/XRefSearchClientMock.java | 2 +- .../IhePatientResourceProviderTest.java | 479 ++++++++ .../mock/ConfigurationAdapterMock.java | 20 + .../provider/mock/HttpFormatException.java | 7 + .../provider/mock/HttpRequestParser.java | 118 ++ .../provider/mock/HttpServletRequestMock.java | 377 +++++++ .../mock/PatientRegistryFeedClientMock.java | 77 ++ .../mock/PatientRegistrySearchClientMock.java | 25 + .../PatientRegistryXRefSearchClientMock.java | 64 ++ .../mock/RequestValidatorServiceMock.java | 73 ++ ...ProfilesValidatorsFactoryProviderTest.java | 73 ++ ...elle.application.ProfilesValidatorsFactory | 3 + .../archives/post_request_1_entry.json | 37 + .../archives/post_request_NO_PATIENT.json | 47 + .../resources/archives/post_response.json | 36 + .../resources/http_request_get_iti83.http | 5 + .../test/resources/http_request_iti104.http | 27 + .../http_request_post_urn_iti83.http | 28 + .../src/test/resources/operationOutcome.json | 14 + .../src/test/resources/validationReport.json | 54 + pom.xml | 675 ++++------- settings.xml | 16 +- .../BundleToPatientRegistryConverter.java | 226 ---- .../connector/BusinessToFhirConverter.java | 251 ----- .../adapter/connector/DeletionBundle.java | 52 - .../adapter/connector/UpdateBundle.java | 43 - .../adapter/servlet/ChHapiFhirServer.java | 78 -- .../adapter/servlet/IheHapiFhirServer.java | 69 -- .../PatientRegistryFeedClient.java | 263 ----- .../PatientRegistrySearchClient.java | 208 ---- .../business/provider/CHBundleProvider.java | 148 --- .../provider/ChPatientResourceProvider.java | 303 ----- .../provider/IhePatientResourceProvider.java | 182 --- .../PatientResourceProviderException.java | 25 - .../SoapUI/IHE-Pixm-soapui-project.xml | 78 -- src/main/resources/deployment.properties | 3 - src/main/webapp/WEB-INF/jboss-web.xml | 4 - src/main/webapp/WEB-INF/web.xml | 6 - .../ResourcesToTestPurpose/post_request.json | 127 --- .../post_request_1_entry.json | 37 - .../post_request_NO_PATIENT.json | 48 - .../ResourcesToTestPurpose/post_response.json | 36 - .../application/PatientFeedClientMock.java | 28 - .../application/PatientFeedClientTest.java | 568 ---------- .../PatientRegistrySearchClientTest.java | 321 ------ .../gazelle/application/SearchClientMock.java | 34 - .../provider/CHBundleProviderMock.java | 40 - .../provider/CHBundleProviderTest.java | 164 --- .../provider/CHPatientProviderMock.java | 71 -- .../ChPatientResourceProviderTest.java | 424 ------- .../IhePatientResourceProviderTest.java | 242 ---- .../PatientRegistryFeedClientMock.java | 77 -- .../PatientRegistrySearchClientMock.java | 43 - .../PatientRegistryXRefSearchClientMock.java | 61 - .../PatientResourceProviderExceptionTest.java | 67 -- 123 files changed, 7518 insertions(+), 5589 deletions(-) delete mode 100644 PixM_Feed_Documentation/README_PixMFeed.md delete mode 100644 PixM_Feed_Documentation/pictures/iti_93_interaction_diagram.png delete mode 100644 PixM_Feed_Documentation/pictures/pixm_connector.png delete mode 100644 PixM_Feed_Documentation/pictures/pixm_scheme_total.png create mode 100644 http-validator-client/pom.xml create mode 100644 http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationService.java create mode 100644 http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceFactory.java create mode 100644 http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/ws/HttpValidatorServerClientImpl.java create mode 100644 http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceFactoryTest.java create mode 100644 http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceTest.java create mode 100644 http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/mock/HttpValidationServiceMock.java create mode 100644 http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/ws/mock/HttpValidatorServerClientImplMock.java create mode 100644 http-validator-client/src/test/resources/http_request_iti104.http create mode 100644 http-validator-client/src/test/resources/response_failed.json create mode 100644 http-validator-client/src/test/resources/response_passed.json create mode 100644 http-validator-client/src/test/resources/response_throw_exception.json create mode 100644 matchbox-client/pom.xml create mode 100644 matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientServiceFactory.java create mode 100644 matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientValidationService.java create mode 100644 matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/ws/IGFhirServerClientImpl.java create mode 100644 matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientServiceFactoryTest.java create mode 100644 matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientValidationServiceTest.java create mode 100644 matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/mock/CustomPatientValidationServiceMock.java create mode 100644 matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/ws/mock/IgFhirServerClientMock.java create mode 100644 matchbox-client/src/test/resources/post_request_passed.json create mode 100644 matchbox-client/src/test/resources/post_response.json create mode 100644 matchbox-client/src/test/resources/post_response_failed.json create mode 100644 pixm-connector-service/pom.xml rename {src => pixm-connector-service/src}/main/java/net/ihe/gazelle/adapter/connector/ConversionException.java (99%) create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/connector/FhirToGazelleRegistryConverter.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/connector/GazelleRegistryToFhirConverter.java rename {src => pixm-connector-service/src}/main/java/net/ihe/gazelle/adapter/preferences/Namespaces.java (100%) rename {src => pixm-connector-service/src}/main/java/net/ihe/gazelle/adapter/preferences/OperationalPreferencesPIXm.java (90%) rename {src => pixm-connector-service/src}/main/java/net/ihe/gazelle/adapter/preferences/Preferences.java (100%) create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/servlet/IheHapiFhirServer.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/application/ConfigurationAdapter.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/application/PatientRegistryFeedClient.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/application/PatientRegistrySearchClient.java rename {src => pixm-connector-service/src}/main/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClient.java (83%) create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/application/ProfilesValidators.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/application/ProfilesValidatorsFactory.java rename {src => pixm-connector-service/src}/main/java/net/ihe/gazelle/business/interceptor/LegacyLoggingInterceptor.java (94%) rename {src => pixm-connector-service/src}/main/java/net/ihe/gazelle/business/interceptor/NewLoggingInterceptor.java (100%) create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/business/provider/IhePatientResourceProvider.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/business/service/RequestValidatorService.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI104PatientFeedQueryProfile.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI104PatientFeedQueryProfileFactory.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83GetPIXmQueryProfile.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83GetPIXmQueryProfileFactory.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83PostPIXmQueryProfile.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83PostPIXmQueryProfileFactory.java create mode 100644 pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ProfilesValidatorsFactoryProvider.java rename {src => pixm-connector-service/src}/main/resources/META-INF/ejb-jar.xml (66%) create mode 100644 pixm-connector-service/src/main/resources/META-INF/services/net.ihe.gazelle.application.ProfilesValidatorsFactory create mode 100644 pixm-connector-service/src/main/resources/SoapUI/IHE-Pixm-soapui-project.xml create mode 100644 pixm-connector-service/src/main/resources/deployment.properties rename {src => pixm-connector-service/src}/main/webapp/WEB-INF/beans.xml (65%) create mode 100644 pixm-connector-service/src/main/webapp/WEB-INF/jboss-web.xml create mode 100644 pixm-connector-service/src/main/webapp/WEB-INF/web.xml rename src/test/java/net/ihe/gazelle/adapter/connector/BundleToPatientRegistryConverterTest.java => pixm-connector-service/src/test/java/net/ihe/gazelle/adapter/connector/FhirToGazelleRegistryConverterTest.java (81%) rename src/test/java/net/ihe/gazelle/adapter/connector/BusinessToFhirConverterTest.java => pixm-connector-service/src/test/java/net/ihe/gazelle/adapter/connector/GazelleRegistryToFhirConverterTest.java (85%) create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/application/PatientFeedClientTest.java create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/application/PatientRegistrySearchClientTest.java rename {src => pixm-connector-service/src}/test/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClientTest.java (92%) create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock/PatientFeedClientMock.java create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock/SearchClientMock.java rename {src/test/java/net/ihe/gazelle/application => pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock}/XRefSearchClientMock.java (95%) create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/IhePatientResourceProviderTest.java create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/ConfigurationAdapterMock.java create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpFormatException.java create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpRequestParser.java create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpServletRequestMock.java create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistryFeedClientMock.java create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistrySearchClientMock.java create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistryXRefSearchClientMock.java create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/RequestValidatorServiceMock.java create mode 100644 pixm-connector-service/src/test/java/net/ihe/gazelle/interlay/profiles/ProfilesValidatorsFactoryProviderTest.java create mode 100644 pixm-connector-service/src/test/resources/META-INF/services/net.ihe.gazelle.application.ProfilesValidatorsFactory create mode 100644 pixm-connector-service/src/test/resources/archives/post_request_1_entry.json create mode 100644 pixm-connector-service/src/test/resources/archives/post_request_NO_PATIENT.json create mode 100644 pixm-connector-service/src/test/resources/archives/post_response.json create mode 100644 pixm-connector-service/src/test/resources/http_request_get_iti83.http create mode 100644 pixm-connector-service/src/test/resources/http_request_iti104.http create mode 100644 pixm-connector-service/src/test/resources/http_request_post_urn_iti83.http create mode 100644 pixm-connector-service/src/test/resources/operationOutcome.json create mode 100644 pixm-connector-service/src/test/resources/validationReport.json delete mode 100644 src/main/java/net/ihe/gazelle/adapter/connector/BundleToPatientRegistryConverter.java delete mode 100644 src/main/java/net/ihe/gazelle/adapter/connector/BusinessToFhirConverter.java delete mode 100644 src/main/java/net/ihe/gazelle/adapter/connector/DeletionBundle.java delete mode 100644 src/main/java/net/ihe/gazelle/adapter/connector/UpdateBundle.java delete mode 100644 src/main/java/net/ihe/gazelle/adapter/servlet/ChHapiFhirServer.java delete mode 100644 src/main/java/net/ihe/gazelle/adapter/servlet/IheHapiFhirServer.java delete mode 100644 src/main/java/net/ihe/gazelle/application/PatientRegistryFeedClient.java delete mode 100644 src/main/java/net/ihe/gazelle/application/PatientRegistrySearchClient.java delete mode 100644 src/main/java/net/ihe/gazelle/business/provider/CHBundleProvider.java delete mode 100644 src/main/java/net/ihe/gazelle/business/provider/ChPatientResourceProvider.java delete mode 100644 src/main/java/net/ihe/gazelle/business/provider/IhePatientResourceProvider.java delete mode 100644 src/main/java/net/ihe/gazelle/business/provider/PatientResourceProviderException.java delete mode 100644 src/main/resources/SoapUI/IHE-Pixm-soapui-project.xml delete mode 100644 src/main/resources/deployment.properties delete mode 100644 src/main/webapp/WEB-INF/jboss-web.xml delete mode 100644 src/main/webapp/WEB-INF/web.xml delete mode 100644 src/test/ResourcesToTestPurpose/post_request.json delete mode 100644 src/test/ResourcesToTestPurpose/post_request_1_entry.json delete mode 100644 src/test/ResourcesToTestPurpose/post_request_NO_PATIENT.json delete mode 100644 src/test/ResourcesToTestPurpose/post_response.json delete mode 100644 src/test/java/net/ihe/gazelle/application/PatientFeedClientMock.java delete mode 100644 src/test/java/net/ihe/gazelle/application/PatientFeedClientTest.java delete mode 100644 src/test/java/net/ihe/gazelle/application/PatientRegistrySearchClientTest.java delete mode 100644 src/test/java/net/ihe/gazelle/application/SearchClientMock.java delete mode 100644 src/test/java/net/ihe/gazelle/business/provider/CHBundleProviderMock.java delete mode 100644 src/test/java/net/ihe/gazelle/business/provider/CHBundleProviderTest.java delete mode 100644 src/test/java/net/ihe/gazelle/business/provider/CHPatientProviderMock.java delete mode 100644 src/test/java/net/ihe/gazelle/business/provider/ChPatientResourceProviderTest.java delete mode 100644 src/test/java/net/ihe/gazelle/business/provider/IhePatientResourceProviderTest.java delete mode 100644 src/test/java/net/ihe/gazelle/business/provider/PatientRegistryFeedClientMock.java delete mode 100644 src/test/java/net/ihe/gazelle/business/provider/PatientRegistrySearchClientMock.java delete mode 100644 src/test/java/net/ihe/gazelle/business/provider/PatientRegistryXRefSearchClientMock.java delete mode 100644 src/test/java/net/ihe/gazelle/business/provider/PatientResourceProviderExceptionTest.java diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6cac78d..56eaddf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,45 +1,67 @@ # Define templates include: - project: 'gazelle/private/gitlab-ci-templates' - file: 'extends.yaml' - ref: 'master' + file: 'extends-v2.yaml' + ref: '1.0.2' # Define stages stages: - build - - postbuild + - predeploy - deploy - - tests + - test + - publish - release - upgrade - - cleanup + - clean # Define global variables variables: P_NAME: "pixm-connector" P_APP_TYPE: "java" P_CODE_SRC_PATH: "." + P_MAVEN_IMAGE_TAG: "3.8.1-openjdk-17-slim" + SERVICE_PATH: "${CI_PROJECT_NAME}-service" # Define jobs -code: +compile/unit-test: stage: build extends: - - .buildCodeForJavaWithWildfly + - .buildJavaMavenTest + +package: + stage: build + extends: + - .buildJavaMavenPackage + needs: + - job: compile/unit-test + artifacts: true variables: - P_MAVEN_IMAGE_TAG: "wildfly-3.6.3-18.0.1.Final" + P_MAVEN_EXTRA_ARGS: "-DskipTests" -quality: - stage: tests +static-analysis: + stage: test extends: - - .testQualityForJavaWithSonarqube + - .testJavaMavenSonarAnalysis + needs: + - job: compile/unit-test + artifacts: true + variables: + P_MAVEN_EXTRA_ARGS: "-Psonar -DskipTests" +install-cache-repo: + stage: publish + extends: + - .publishJavaMavenInstall + needs: + - job: package + artifacts: true variables: - P_MAVEN_IMAGE_TAG: "wildfly-3.6.3-18.0.1.Final" + P_MAVEN_EXTRA_ARGS: "-DskipTests" + release/code: stage: release extends: - .releaseCodeForJava - variables: - P_MAVEN_IMAGE_TAG: "wildfly-3.6.3-18.0.1.Final" diff --git a/PixM_Feed_Documentation/README_PixMFeed.md b/PixM_Feed_Documentation/README_PixMFeed.md deleted file mode 100644 index 5dffc95..0000000 --- a/PixM_Feed_Documentation/README_PixMFeed.md +++ /dev/null @@ -1,536 +0,0 @@ -# PixM Feed - ->Version : 1.0.0 <br /> ->Date : 2021-08-26 - -PixM Feed lets a user to proceed some operations with Cross-Referenced (X-ref) Patient. -The User can do the following ITI-93 transactions : Create, Update, Delete and Merge Patient (called CUDM operations) -Each operation will be decribed later. - -The PixM Feed has 2 components : -* a PixM Consumer : the Client part, where the User can do CUDM operations through a SOAP UI project uploaded in Gazelle Webservice Tester (GWT). The SOAP UI project can perform also all cases covered by the standard and shall send/receive all the required requests. -* a PixM Manager : the Server part, where all the CUDM operations are done in order to feed the Patient database. The PixM Manager has 2 parts : - + PixM Connector: interpret REST FHIR requests and data to transform it into a Gazelle Patient Model for the transfert toward Patient Registry. - + Patient Registry : manage transaction I/O of the Patient Database, CUDM and also X-ref. The access to PatReg has to be done with GIT-B webservices.<br /> - -The communication between PixM Connector and Patient Registry is done by a Java Client in order to translate REST operation into SOAP operation done with GIT-B webservices. - - - - -All the transactions between the PixM Client and the PixM Manager are ruled byt ITI-93 transactions.<br /> -https://fhir.ch/ig/ch-epr-mhealth/iti-93.html<br /> -https://www.ihe.net/uploadedFiles/Documents/ITI/IHE_ITI_Suppl_PMIR.pdf - -<br /> - -## ITI-93 transactions -_____________________________________________________________________________________ -ITI-93 transactions are basically Bundle messages exchanged between a Supplier and a Consumer. -The Supplier sends a Mobile Patient Feed Request to a Consumer. This event is trigerred every time patients are created, updated, merged or deleted by the Supplier. -The Consumer sends back a Mobile Patient feed Response to a Supplier. - -  - -The **Bundle PixMFeed request** has 2 entries (https://fhir.ch/ig/ch-epr-mhealth/Bundle-BundlePIXmFeed.json.html) : -- a Message header -- a History Bundle, containing the Patient Resource (https://www.hl7.org/fhir/patient.html) - -This two entries are mandatory for each transactions. -We will see further that not every fields are mandatory to proceed operations. - -The **Bundle PixMFeed response** has 1 entry (https://fhir.ch/ig/ch-epr-mhealth/Bundle-BundlePIXmResponse.json.html): -- a message response that acknowledge the transaction is done. - -For our purpose, the message response will also return in case of : -- CREATE : the uuid of the newly created patient will be returned in the message -- UPDATE : the whole Patient will be returned -- MERGE : The uuid of the original Patient is returned -- DELETE : The DONE or GONE status - -<br /> - -## PIXM Consumer : SOAP UI Project -__________________________________________________________________ -The SOAP UI Project for CHPixM Feed is splitted in 2 parts : -- the one that the user can interact with to do its request to PixM Server, can test nominal and errors requests -- the MOCK server test implemented methods and mimic the desired comportment from the PixM Server - -4 methods are implemented and their related TestSuite & TestCase -- CREATE (C) -- DELETE (D) -- UPDATE (U) -- MERGE (M) - -For each TestCase, the user can filled a prefilled body with following fields : - -- name.given (CU) -- name.family (CU) -- birthDate (CU) -- gender (CU) -- resource (CUDM) -- system (CU) -- value (CU) -- uuid (UMD) -- uuidToRedirect (M) - -These fields can be accessed in each TestSuite > TestCase -then on the bottom-left corner click on the tab "Custom properties" - - -### CREATE METHOD : -__________________________________________________________________ - -#### NOMINAL CASE : - -Send the following REST Request - - POST : {serverAdress}/ch_fhir/Bundle/ - - -With a prefilled body as following : - -```json -{ - "resourceType" : "Bundle", - "id" : "BundlePIXmFeed", - "meta" : { - "profile" : [ - "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle" - ] - }, - "type" : "message", - "entry" : [ - { - "fullUrl" : "http://example.com/fhir/MessageHeader/1", - "resource" : { - "resourceType" : "MessageHeader", - "id" : "1", - "text" : { - "status" : "generated", - "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative</b></p><p><b>event</b>: <code>urn:ihe:iti:pmir:2019:patient-feed</code></p><h3>Destinations</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientEndpoint\">http://example.com/patientEndpoint</a></td></tr></table><h3>Sources</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientSource\">http://example.com/patientSource</a></td></tr></table><p><b>focus</b>: <a href=\"#Bundle_abc\">See above (Bundle/abc)</a></p></div>" - }, - "eventUri" : "urn:ihe:iti:pmir:2019:patient-feed", - "destination" : [ - { - "endpoint" : "http://example.com/patientEndpoint" - } - ], - "source" : { - "endpoint" : "http://example.com/patientSource" - }, - "focus" : [ - { - "reference" : "Bundle/abc" - } - ] - } - }, - { - "fullUrl" : "http://example.com/fhir/Bundle/abc", - "resource" : { - "resourceType" : "Bundle", - "id" : "abc", - "type" : "history", - "entry" : [ - { - "fullUrl" : "http://example.com/fhir/Patient/PatientPIXmFeed", - "resource" : { - "resourceType" : "Patient", - "id" : "PatientPIXmFeed", - "text" : { - "status" : "generated", - "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative</b></p><p><b>id</b>: PatientPIXmFeed</p><p><b>meta</b>: </p><p><b>identifier</b>: Medical record number = 8734</p><p><b>name</b>: Franz Muster , Muster </p><p><b>gender</b>: male</p><p><b>birthDate</b>: 1995-01-27</p></div>" - }, - "contained" : [ - { - "resourceType" : "Organization", - "id" : "org1", - "identifier" : [ - { - "system" : "${system}", - "value" : "${value}" - } - ], - "address" : [ - { - "use" : "work", - "line" : [ - "Doktorgasse", - "2" - ], - "city" : "Musterhausen", - "postalCode" : "8888", - "country" : "CH" - } - ] - } - ], - "identifier" : [ - { - "type" : { - "coding" : [ - { - "system" : "http://terminology.hl7.org/CodeSystem/v2-0203", - "code" : "MR" - } - ] - }, - "system" : "${#system}", - "value" : "${#value}" - } - ], - "name" : [ - { - "family" : "${#name.family}", - "given" : [ - "${#name.given}" - ] - }, - { - "family" : "${#name.family}", - "_family" : { - "extension" : [ - { - "url" : "http://hl7.org/fhir/StructureDefinition/iso21090-EN-qualifier", - "valueCode" : "BR" - } - ] - } - } - ], - "gender" : "${#gender}", - "birthDate" : "${#birthDate}", - "managingOrganization" : { - "reference" : "#org1" - } - }, - "request" : { - "method" : "POST", - "url" : "Patient" - }, - "response" : { - "status" : "200" - } - } - ] - } - } - ] - } -``` - - -These parameters are mandatory in CREATE method -* name.given -* name.family -* birthdate -* gender - - - -The server then should answer a response bundle like : - ```json -{ - "resourceType" : "Bundle", - "id" : "BundlePIXmResponse", - "meta" : { - "profile" : [ - "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle-response" - ] - }, - "type" : "message", - "entry" : [ - { - "fullUrl" : "http://example.com/fhir/MessageHeader/1", - "resource" : { - "resourceType" : "MessageHeader", - "id" : "1", - "text" : { - "status" : "generated", - "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative</b></p><p><b>event</b>: <code>urn:ihe:iti:pmir:2019:patient-feed</code></p><h3>Destinations</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientEndpoint\">http://example.com/patientEndpoint</a></td></tr></table><h3>Sources</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientSource\">http://example.com/patientSource</a></td></tr></table><h3>Responses</h3><table class=\"grid\"><tr><td>-</td><td><b>Identifier</b></td><td><b>Code</b></td></tr><tr><td>*</td><td>1</td><td>ok</td></tr></table></div>" - }, - "eventUri" : "urn:ihe:iti:pmir:2019:patient-feed", - "destination" : [ - { - "endpoint" : "http://example.com/patientEndpoint" - } - ], - "source" : { - "endpoint" : "http://example.com/patientSource" - }, - "response" : { - "identifier" : "1", - "code" : "ok" - } - } - } - ] - } - -+ The ID of the new patient newly created -``` - - -The following assertion will be tested when the body is sent : -- if the returned HTTP Status is "200" -- if the gender is : male OR female OR unknown OR other -- if the birthDay is an existed date in Calendar AND in the YYYY-MM-DD format AND NOT greater than the Current day -- if the sourceIdentifier (if given) respect the following regex: ->urn:oid:([0-9]+)(\.[0-9]+)+\|([ -~]+) <br /> ->{start with "urn:oid:"} then {"any number"} then {sequence of "."+"any number"} then a pipe ["|"] then {"any string"} - -<br/> - -#### TEST CASE (400) - ->Resource could not be parsed or failed basic FHIR validation rules | malformed bundle message.<br/> - -Same as Nominal Test excepting : - -* The Bundle message is intentionally malformed - -* The following assertion will be tested when the body is sent : - - if the returned HTTP Status is "400" - -<br/> - - -#### TEST CASE (404) ->Resource type not supported, or not a FHIR end-point : request URL is not valid - -The following assertion will be tested when the body is sent : -- if the returned HTTP Status is "404" - -#### TEST CASE (422) - ->The proposed resource violated applicable FHIR profiles or server business rules. This should be accompanied by an OperationOutcome resource providing additional detail. - -The request is a right one but the submitted birthdate OR gender OR sourceIdentifier do NOT respect the assertion rules. - -The following assertion will be tested when the body is sent : - - if the returned HTTP Status is "422" - - -### DELETE METHOD -______________________________________________________________________ - - -#### NOMINAL CASE : -Send the following REST Request ->DELETE : {serverAdress}/{resource}/{id} - - -Same body as a create except : -* request method must be delete at the end : -```json - - "request" : { - "method" : "DELETE", - "url" : "Patient" - }, -``` - - -* Mandatory field to fill in case of DELETE method : - * uuid - -In the case of the DELETE method, none of the fields are taking into account, only the UUID is check, and the Server shall answer 2 DELETE status : -- DONE if the Patient existed and is now deleted -- GONE if the Patient to delete does NOT exist in Databse - -The following assertion will be tested when the body is sent : -- if the returned HTTP Status is "204" - - -<br/> - -#### TEST CASE (404) - ->case where uuid not exist in database - - -Same as NOMINAL except : - -The following assertion will be tested when the body is sent : -- if the returned HTTP Status is "404" - - -### UPDATE METHOD -__________________________________________________________________________________ - -#### NOMINAL CASE : -Send the following REST Request ->PUT : {serverAdress}/{resource}/{id} - - -Same body as CREATE method but : -* the request method should be put AND the patient id is replaced by an UUID -```json - "request" : { - "method" : "PUT", - "url" : "Patient" - }, - -``` - -* Mandatory fields : uuid - -* The body response shall return the modified Patient. - -<br/> - -#### TEST CASE (400) - ->resource could not be parsed or failed basic FHIR validation rules | malformed bundle message - - -Same as Nominal Test excepting : - -The Bundle message is intentionally malformed - -The following assertion will be tested when the body is sent : -- if the returned HTTP Status is "400" - - - - - -#### TEST CASE (422) -> the proposed resource violated applicable FHIR profiles or server business rules. This should be accompanied by an OperationOutcome resource providing additional detail. - -Same as Nominal Test excepting : - - -The following assertion will be tested when the body is sent : -- if the returned HTTP Status is "422" - - <br/> - -### MERGE METHOD (should be updated in further version) -____________________________________________________________________________________ -Send the following REST Request : ->PUT : {serverAdress}/{resource}/{id} - -The body is different because the MERGE is a used in the case where the same patient was created 2 times and the user would like to redirect the duplicated patient to the original one in the database. - -```json -{ - "resourceType": "Bundle", - "type": "message", - "entry": [{ - "fullUrl": "http://qualification.ihe-europe.net/fhir/MessageHeader/1", - "resource": { - "resourceType": "MessageHeader", - "id": "1", - "eventUri": "urn:ihe:iti:pmir:2019:patient-feed", - "source": { - "endpoint": "http://qualification.ihe-europe.net/patientSource" - }, - "focus": [{ - "reference": "Bundle/abc" - }], - "destination": [{ - "endpoint": "http://qualification.ihe-europe.net/patientEndpoint" - }] - } - }, { - "fullUrl": "http://qualification.ihe-europe.net/fhir/Bundle/abc", - "resource": { - "resourceType": "Bundle", - "id": "abc", - "type": "history", - "entry": [{ - "fullUrl": "http://qualification.ihe-europe.net/fhir/Patient/${uuid}", - "resource": { - "resourceType": "Patient", - "id": "${uuid}", - "active": false, - "link": [{ - "other": { - "reference": "http://qualification.ihe-europe.net/fhir/Patient/${uuidToRedirect}" - }, - "type": "replaced-by" - }] - }, - "request": { - "method": "PUT", - "url": "Patient/${uuid}" - }, - "response": { - "status": "200" - } - }] - } - }] -} -``` -* Mandatory fields to fill in MERGE request : - * uuid - * uuidToRedirect - -* The response should be as Following : - - - a Patient Identifier is mandatory [1..*] - - On the GWT, I can choose a project {projectName} and select the Merge Test Suite and modify : the endpoint, the formerSourceIdentifier I want to deprecate, the newSourceIdentifier I want for my Patient. - - 200 status is returned if the patient is merged - - 400 status is returned if I provided malformed Bundle Json - - 404 status is returned if I provided well-formed sourceIdentifier but not exist in PatReg - - 405 status is returned if I provide a sourceIdentifier for a deactivated patient - - 422 status is returned if I provide a malformed sourceIdentifier - -<br/> - -## PIX-M MANAGER -__________________________________________________________________________________ - -### PixM Connector - -PixM Connector is a FHIR application server exposing REST services. It uses the HAPI FHIR Server framework (a fast solution to deploy a FHIR Server and implement a specific treatment for the request). - -The PixM Connector is a Java Project which receives the SUT Request and transfer it to the Java Client for Patreg. -It shall manage received CREATE, UPDATE, DELETE, MERGE request from SUT. -It has also to convert a FHIR Bundle Object from ITI-93 transaction into a manageable Patient Object by Patient Registry. -It has to received response message from Patient Registry to transfer to the SUT. - -It has 3 parts : -* **server part** : define the context of the deployed FHIR Server -* **provider part** : feed the server with the methods that it needs to expose. In our case it defines the treatment of parameters that needs to be transferred -* **call-the-java-client part** : in this part, the module initialize the Java client with de Patient Registry adress. It uses the JAVA client to transfer the request and interpret the response and reported errors. It also prepares the content of the FHIR reponse and sends back to the SUT. - -<br/> - -### Java Client (part of PixM Connector) - -It acts like a connector to Patient Registry. -Its role is to initialize the connection with Patient Registry and translate the research parameters into GITB Model. -It implements the same interface as the service. This connector is embedded in PIXm-Connector to communicate with this module. - -<br/> - -### Patient Registry - -Patient Registry is a module that allows you to interact with the database of Patient Manager database. -This module has no web interface. It is only accessible through its webservices just like PIX-Connector. -With one exception, the latter uses REST webservices while Patient Registry uses Soap technology with a GITB overlay. - -The GITB model is a standardization project initiated by the European Commission. -The specifications aim at the interoperability of the test tools in a test bucket. It is about -defining a common interface between the tools and a test bench. This common interface -defines the data exchanges via the webservices. They use a standardized data type standardized data type that requires a correspondence with the gazelle data models. -It is expected that in the next few years that the GITB standard will migrate to REST webservices. - -The Patient Registry Webservice implements the GITB model. It only receives requests in this format. It identifies the operation to be performed (here a cross-reference search operation) and -operation) and establishes the reverse correspondence, from the GITB model to the -Gazelle model. - -The webservice then transmits these parameters to the service application layer which -returns the result or the exceptions raised during the operation. Again, we perform the -the results in a report indicating the status and the result of the operation. -the result of the operation. This report is then translated and interpreted by the Java client. - - - - - - diff --git a/PixM_Feed_Documentation/pictures/iti_93_interaction_diagram.png b/PixM_Feed_Documentation/pictures/iti_93_interaction_diagram.png deleted file mode 100644 index d65ca11ee29cda0694cd6f2d5b2d25e81006542e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14156 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 Ru<|&7@=JC3TG=-t{|8ne?`8l1 diff --git a/PixM_Feed_Documentation/pictures/pixm_connector.png b/PixM_Feed_Documentation/pictures/pixm_connector.png deleted file mode 100644 index e2eb7b8aeb0b979b5bf7854aabde52452c89a8c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111903 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�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 Nl}kz&v(6j(|9|OqRH^^~ diff --git a/PixM_Feed_Documentation/pictures/pixm_scheme_total.png b/PixM_Feed_Documentation/pictures/pixm_scheme_total.png deleted file mode 100644 index ba1ad2f1ea3f7eb9b9ba0ffa63403eaa850929d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36542 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 Sm<Iv;5fhRYEauno`hNgM`H?CB diff --git a/README.md b/README.md index 74cf42d..b713a87 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,21 @@ - -# Pixm Connector +# PIXm Connector --- -### Prerequisites -- Java 11 with maven. -- WildFly 18. + +## Prerequisites + +- Java 17 with maven. +- WildFly 30.0.1 is deployed +- [Patient Registry](https://gazelle.ihe.net/gazelle-documentation/Patient-Registry/installation.html) is installed. +- [HTTP Validator](https://gitlab.inria.fr/gazelle/applications/test-execution/validator/http-validator) is installed. +- [Matchbox](https://www.matchbox.health/) is installed. + --- + ### Build project locally -After cloning this repository to your local installation launch +After cloning this repository to your local installation launch. +Check that the JAVA_HOME is set correctly to Java 17. ```bash > mvn clean install @@ -16,232 +23,235 @@ After cloning this repository to your local installation launch from the project root directory. - -The artifact pixm_fhir_server.war will be created in target/ directory. - +The artifact `pixm-connector.war` will be created in target/ directory. --- + ### Deploy on WildFly server -After building the project through Maven, the artifact created just has to be added to your local WildFly installation in the folder -```bash +After building the project through Maven, the artifact created just has to be added to your local WildFly installation +in the folder + +```http {$wildfly.home}/standalone/deployments ``` ---- -### Overview -Here is a quick overview of the available functionnality from PIXm connector +A `deployment.properties` file has to be created in folder `/opt/pixm-connector` with following content: + +```properties + patientregistry.url = https://{{host}}/patient-registry/PatientProcessingService/patient-processing-service?wsdl + xrefpatientregistry.url = https://{{host}}/patient-registry/CrossReferenceService/xref-processing-service?wsdl + PROFILE_ID_CREATE_UPDATE_DELETE_ITI_104="IHE_ITI-104-PatientFeed_Query" + PROFILE_ID_POST_ITI_83="IHE_ITI-83_POST_PIXm_Query" + PROFILE_ID_GET_ITI_83="IHE_ITI-83_GET_PIXm_Query" + APP_HTTP_VALIDATOR_SERVER="https://{{host}}/http-validator" + APP_IG_FHIR_SERVER="https://{{host}}/matchboxv3/fhir" + PIXM_PATIENT_PROFILE="https://profiles.ihe.net/ITI/PIXm/StructureDefinition/IHE.PIXm.Patient" + PIXM_PARAMETERS_PROFILE="https://profiles.ihe.net/ITI/PIXm/StructureDefinition/IHE.PIXm.Parameters"% +``` -|Method|URL to call|Entry parameter|Returned value| -|-|-|-|-| -|Create Patient|```{FHIR.server.address}/Patient/$create```|ITI-93 Bundle| Patient Bundle -|Delete Patient|```{FHIR.server.address}/Patient/$delete```|Patient SourceIdentifier|/ -|Read Patient|```{FHIR.server.address}/Patient```|Patient SourceIdentifier|Patient Bundle -|Update Patient|```{FHIR.server.address}/Patient/$update```|ITI-93 Bundle with a Patient SourceIdentifier|Patient Bundle -|Merge Patient|```{FHIR.server.address}/Patient/$merge```|2 Patient SourceIdentifier|Patient Bundle -|Check Cross Referenced Patient|```{FHIR.server.address}/Patient/$ihe-pix```|A Patient sourceIdentifier and a TargetDomain|Patient Bundle +## Overview +___ ---- -### Request a Patient Cross Reference on a specific Target Identifier (ITI-83) +Here is a quick overview of the available functionality from PIXm connector -Once the project deployed on your WildFly, you can now call it to request a cross Referenced Patient in the Patient Registry. +| Operation | HTTP Methods | URL to call | Entry parameter | Returned value | +|--------------------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------|------------------------------------------| +| Create/Update Patient | PUT | ```{FHIR.server.address}/Patient?identifier={{patient.system}}\|{{patient.id}}``` | ITI-104 Patient identifier | ITI-104 FHIR Patient | +| Delete Patient | DELETE | ```{FHIR.server.address}/Patient/?identifier={{patient.system}}\|{{patient.id}}``` | ITI-104 Patient identifier | / | +| Merge Patient | PUT | ```{FHIR.server.address}/Patient/?identifier={{patient.system}}\|{{patient.id}}``` | ITI-104 Patient identifier w/ patient.link to Patient to keep | ITI-104 FHIR Patient | +| Check Cross Referenced Patient | GET | ```{FHIR.server.address}/Patient/$ihe-pix?sourceIdentifier={{patient.system}}\|{{patient.id}}&targetSystem={{targetSystem}}``` | A Patient sourceIdentifier and a TargetDomain | ITI-83 FHIR Parameters with X-ref values | -Parameters allowed are : +Capability statement of the application can be found with : <https://example.com/pixm-connector/fhir/metadata> -- The Patient Identifier and the Target System attributed to this identifier -- The Target System you want the cross reference from. -- The format returned. -Cardinality for these parameters will be described in each profile since it's the main difference between each. +As described in [HAPI FHIR resources](https://hapifhir.io/hapi-fhir/docs/server_plain/rest_operations_operations.html), some strings are automatically escaped when the FHIR server parses URLs: -#### IHE Profile +- `"|"` → `"%7C"` +- `"=>="` → `"=%3E%3D"` +- `"=<="` → `"=%3C%3D"` +- `"=>"` → `"=%3E"` +- `"=<"` → `"=%3C"` -The URL to call is -```http - {wildfly18.address}/pixm_fhir_server/fhir_ihe/Patient/$ihe-pix -``` +## Validation process +___ -|Parameter name|Cardinality|Parameter Type|Description| -|--------------|-----------|--------------|-----------| -|sourceIdentifier|1..1|Token|The Patient identifier search parameter that will be used by the Patient Identifier Cross-reference Manager to find cross matching identifiers associated with the Patient Resource -|targetSystem|0..*|uri|The target Patient Identifier Assigning Authority from which the returned identifiers should be selected.| -|_format|0..1|mime-type|The requested format of the response. Accepted values : JSON and XML| +Each operation implies a validation of requests for both ITI-104 and ITI-83 transactions. +Validation is done by calling: -#### CH Profile +- [HTTP Validator](https://gitlab.inria.fr/gazelle/applications/test-execution/validator/http-validator) for URL and Headers. +- [Matchbox](https://www.matchbox.health/) for Body with FHIR Resource. -The URL to call is -```http - {wildfly18.address}/pixm_fhir_server/fhir_ch/Patient/$ihe-pix -``` +Both validators allow to perform validation and have high customization if specifications changed for both transactions without refactoring pixm-connector application. +An error during validation process will result with an OperationOutcome with error `400 Bad Request` with issues describing where it failed. +## Request a Patient Cross Reference on a specific Target Identifier (ITI-83) +___ -|Parameter name|Cardinality|Parameter Type|Description| -|--------------|-----------|--------------|-----------| -|sourceIdentifier|1..1|Token|The Patient identifier search parameter that will be used by the Patient Identifier Cross-reference Manager to find cross matching identifiers associated with the Patient Resource -|targetSystem|1..2|uri|The target Patient Identifier Assigning Authority from which the returned identifiers should be selected.| -|_format|0..1|mime-type|The requested format of the response. Accepted values : JSON and XML| +- [IHE Specifications](https://profiles.ihe.net/ITI/PIXm/ITI-83.html) -For example : -Given the Patient with the ID 69420 in the Target System 1.3.6.1.4.1.21367.13.20.3000 -And you want the cross referenced patient in the target system 1.3.6.1.4.1.21367.13.20.1000 - -The corresponding url will be : +Request a cross-referenced Patient is possible thanks to the `$ihe-pixm`. -```http - {wildfly18.address}/pixm_fhir_server/fhir_ihe/Patient/$ihe-pix?sourceIdentifier=urn:oid:1.3.6.1.4.1.21367.13.20.3000|69420&targetSystem=urn:oid:1.3.6.1.4.1.21367.13.20.1000 -``` +Parameters allowed are : ---- -#### Error returned -Malformed requests can cause different types of error, here is a quick overview of how to troubleshoot them : +- The Patient Identifier and the Target System attributed to this identifier as `sourceIdentfier` +- The Target System you want the cross-reference from as `targetSystem`. +- The format returned as `_format`. `xml` AND `json` are the only values. - -An error 400 Bad Request is returned when the source domain given within the source identifier parameter is not recognized by the Patient Registry as an asigning authority. -In the case of our request above, the value "urn:oid:1.3.6.1.4.1.21367.13.20.3000" is not a valid source domain able to register Patients +The URL to call is: -Common mistakes with the source domain include : -- Forgetting the namespace in front of the adress (urn:oid:) -- Malformed source domain. -The source domain can have the form of an url ```http -sourceIdentifier=https://your.domain|id -``` -or of an adress, which must follows the pattern x.x.x.x.x.x.x.x.x.x -```http -sourceIdentifier=urn:oid:x.x.x.x.x.x.x.x.x.x|id + GET {wildfly18.address}/pixm-connector/fhir/Patient/$ihe-pix ``` ---- - +For example : +Given the Patient with the `id=69420` with the `system=urn:oid:1.3.6.1.4.1.21367.13.20.3000` as `sourceIdentifer=system|id` +And you want the cross-referenced patient in the `targetSystem=1.3.6.1.4.1.21367.13.20.1000` +And you want the returned response as a `json`. -An error 403 Forbidden is returned when a target domain given in the target system parameter is not recognized by the Patient Registry. -In the case of our request above, the value "urn:oid:1.3.6.1.4.1.21367.13.20.1000" is not recognized as a valid target domain containing Patients. +The corresponding url will be : -Common mistakes with the target system are the same as the aformentioned error 400 since the target system and the source domain have the same representation. +```http + {wildfly18.address}/pixm-connector/fhir/Patient/$ihe-pix?sourceIdentifier=urn:oid:1.3.6.1.4.1.21367.13.20.3000|69420&targetSystem=urn:oid:1.3.6.1.4.1.21367.13.20.1000&_format=json +``` --- - -An error 404 Not Found is returned when the patient identifier given within the source identifier parameter is not recognized by the Patient registry. -In the case of our request above, the value "69420" is not a valid Identifier linked to an existing Patient. +## Requests on Patient resources (ITI-104) ---- -### Requests on Patient resources (ITI-93) - -PIXm connector implements query for ITI-93 +___ -Many of those requests will take an ITI-93 Bundle as a Pramaeter +- [IHE Specifications](https://profiles.ihe.net/ITI/PIXm/ITI-104.html) --- -#### Create - -PIXm connector accepts the creation of a Patient in the Patient Manager. -Although a Patient could be created without any information in the HL7 model, PIXm connector will only allow a Patient to be created with minimum information to permits cross referenciation. -The request takes only one argument, a bundle with three required components : -- A Message header. -- A Bundle of type history, describing the Patient resource to be created. +### Create/Update -The "required" fields in the Patient are : -- A name (either Family, or Given, or both). -- A Country. -- A Gender. -- An Identifier. (?) +Link: <https://profiles.ihe.net/ITI/PIXm/ITI-104.html#2310441-add-or-revise-patient> +PIXm connector accepts the creation of a Patient in the Patient Manager. +Although a Patient could be created without any information in the HL7 model, PIXm connector will only allow a Patient +to be created with minimum and/or mandatory information to permits cross-reference thanks to validation with Matchbox. -A MessageHeader shall contains -|Componant Name|cardinality|example or description| -|-|-|-| -|eventUri|[1..1]|urn:ihe:iti:pmir:2019:patient-feed| -|focus|[1..1]|shall reference the url of Bundle| -|sourceEndpoint|[1..1]|Actual message source address or id| -|destination|[1..*]|The destination(s) of this feed| - -A history type bundle shall contains -|Componant Name|cardinality|example or description| -|-|-|-| -|type|[1..1]|should be History -|entry|[1..*]|the same Patient Resource shall not appear twice in this Bundle -|entry.request.method|[1..1]|POST PUT DELETE -|entry.request.url|[1..1]|The URL for this entry, relative to the root (the address to which the request is posted). -|entry.response.status|[1..1]|The response status shall be an HTTP response status of 200 - - -If the Creation is succesful, an http response 200 will be returned, with a Bundle containing the Patient information, and an Endpoint to view the Patient in browser. - -If it's not the case, a myriad of error can be returned to help you pinpoint why your request cannot be interpreted. +The Resource could not be parsed or failed basic FHIR validation rules. +In the case of an error `400 Bad Request` or `422 Unprocessable Entity` being returned, +please check the following guidelines to verify your query. ---- - -The Resource could not be parsed or failed basic FHIR validation rules. In the case of an error 400 being returned, please check the following guidelines to verify your query. -- The Bundle does not countain minimum information needed for creation verify if the following arguments are given in the Patient and have the corresponding types and size. - - A name (either Family, or Given, or both). - - A Country. - - A Gender. - - An Identifier. (?) - -The Bundle might be malformed, and does not countain one of the main component - - A Message header. - - A Bundle of type history, describing the Patient resource to be created. - ---- - -Two main event can cause an 404 response. -- The endpoint provided for the request might be wrong, check the validity of the url on your endpoint. -Usually, your endpoint should look like +Create/Update request is done through a FHIR conditional update mechanism ([cond-update](http://hl7.org/fhir/http.html#cond-update)) where the patient identifier has to be given as following. ```http - {wildfly18.address}/pixm_fhir_server/fhir_ihe/Patient/$create?{Bundle} + PUT {wildfly18.address}/pixm-connector/fhir/Patient/identifier=urn:oid:1.3.6.1.4.1.21367.13.20.1000|IHERED-m94 +``` + +With body : + +```json + +{ + "resourceType" : "Patient", + "id" : "Patient-MaidenAlice-Red", + "meta" : { + "profile" : [ + "https://profiles.ihe.net/ITI/PIXm/StructureDefinition/IHE.PIXm.Patient" + ] + }, + "text" : { + "status" : "generated", + "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p style=\"border: 1px #661aff solid; background-color: #e6e6ff; padding: 10px;\"><b>ALICE MOHR </b> female, DoB: 1958-01-30 ( id:\u00a0IHERED-m94)</p><hr/><table class=\"grid\"><tr><td style=\"background-color: #f3f5da\" title=\"Record is active\">Active:</td><td colspan=\"3\">true</td></tr></table></div>" + }, + "identifier" : [ + { + "system" : "urn:oid:1.3.6.1.4.1.21367.13.20.1000", + "value" : "IHERED-m94" + } + ], + "active" : true, + "name" : [ + { + "family" : "MOHR", + "given" : [ + "ALICE" + ] + } + ], + "gender" : "female", + "birthDate" : "1958-01-30" +} ``` -Check your wildfly18 address then, by opening it in a web browser you should get a 403 Forbidden error thrown out, indicating you're indeed pointing at the right server. -- The other common reason causing this error is the Resource type asked not being supported by PIXm. This means the part "/Patient" in the url is not right, either because you choosed another Resource type (like an Observation or a Bundle instead of Patient) which does not support the operation create, or because you mistyped it. +If the patient exists then it is updated otherwise it is created. +The response will return the updated/created patient. --- - -This error is complementary to the 400 Bad Request error. Whereas the 400 happens when the bundle is "syntactically" malformed. For example, a required element is not present in the bundle. The inside value of the parameter might be completly abnormal. In the case of an error 422, the Bundle object sent is **SEMANTICALLY** erroneous. +### Merge for resolving duplicated patient -In this case, check the core of your request, maybe the Message Header Value is incoherent. Maybe one of the value in the Patient does not have any endpoint. +Link : <https://profiles.ihe.net/ITI/PIXm/ITI-104.html#2310442-resolve-duplicate-patient> -In this case the best way to solve this error is by checking the Payload sent to the server, maybe a Value is erroneous. This error case sends an Operation Outcome REsource with it, you should check it as it contains additional information about where the error shoud come from exactly. +The merge method allows the user to merge two patients together if two registered patients represent the same people. +This action is **irreversible** as it deactivates a resource making it only readable and immutable. ---- - -This error is surely none of your doing. If one of this occurs, send a Ticket to the corresponding authority providing you the service. +The request is a PUT method with the Patient to deactivate with a `activate: false` attribute and a `link` field with identifier for the kept patient resource. ---- -#### Update - -The Update method allows the user to update a Patient in the Patient Registry through PIXm. The Request sent is the same as the create method, The endpoint is the only difference between the two, since it encapsulates the method used. It also needs the sourceIdentifier from the Patient to modify, if you're not sure about it, you can always use the @Read Method described under. +```http +PUT http://example.org/fhir/Patient?identifier=urn:oid:1.3.6.1.4.1.21367.13.20.1000|IHERED-m94 HTTP/1.1 +Accept: application/fhir+json +Content-Type: application/fhir+json +``` +```json +{ + "resourceType": "Patient", + "identifier": [ + { + "system": "urn:oid:1.3.6.1.4.1.21367.13.20.1000", + "value": "IHERED-m94" + } + ], + "active": false, + "name": [ + { + "family": "MOHR", + "given": [ + "MAIDEN" + ] + } + ], + "gender": "female", + "birthDate": "1958-01-30", + "link": [ + { + "other": { + "identifier": { + "system": "urn:oid:1.3.6.1.4.1.21367.13.20.1000", + "value": "IHERED-994" + } + }, + "type": "replaced-by" + } + ] +} +``` -You can then follow the create operation explanation above for in-detail details about the parameters needed. +### Delete one or more patient(s). -The returned value is the Updated Patient. -Error values returned contain the same as the Create operation and even more. -We will not go through the errors already explained in the [Create section](#create) but will add information on the errors : +Link: <https://profiles.ihe.net/ITI/PIXm/ITI-104.html#2310443-remove-patient> ---- - -Is returned when the token for authentication is not valid to perform this method. +The delete operation allows suppression of the patient with its identifier thanks to a conditional deletion. +This application allows multiple deletion if the identifier returned more than one Patient. ---- - -When the resource pointed is not found ---- - -When the resource sended is not of the same version as the resource stored in patient registry +````http +DELETE http://example.org/fhir/Patient?identifier=urn:oid:1.3.6.1.4.1.21367.13.20.1000|IHERED-994 HTTP/1.1 +Accept: application/fhir+json +```` ---- -#### Merge -The merge method allows the user to merge two patients together if it is deemed reasonable to think those two registered patients represent the same people +if the delete is successful the application returns a `200 OK` response otherwise it would be a `204 No Content`. ---- -#### Delete +## Errors returned --- -#### Read +Malformed requests can cause different types of error, for now `422 Unprocessable Entity` is mostly returned. +Future features will allow a better granularity for code returned. diff --git a/http-validator-client/pom.xml b/http-validator-client/pom.xml new file mode 100644 index 0000000..092b5d1 --- /dev/null +++ b/http-validator-client/pom.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>net.ihe.gazelle</groupId> + <artifactId>pixm-connector</artifactId> + <version>3.0.0-SNAPSHOT</version> + </parent> + + <artifactId>http-validator-client</artifactId> + <name>HTTP Validator Client</name> + <version>3.0.0-SNAPSHOT</version> + + <properties> + <maven.compiler.source>17</maven.compiler.source> + <maven.compiler.target>17</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + <dependencies> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>2.0.3</version> + <scope>compile</scope> + </dependency> + <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5 --> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>4.5.13</version> + <scope>compile</scope> + </dependency> + + <dependency> + <groupId>jakarta.platform</groupId> + <artifactId>jakarta.jakartaee-api</artifactId> + </dependency> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>validation-api</artifactId> + </dependency> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>validation-jaxrs-api</artifactId> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <version>2.0.2-beta</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <version>${junit.jupiter.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + +</project> \ No newline at end of file diff --git a/http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationService.java b/http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationService.java new file mode 100644 index 0000000..ebdb82c --- /dev/null +++ b/http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationService.java @@ -0,0 +1,65 @@ +package net.ihe.gazelle.http.validator.client.interlay.validation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.json.JsonMapper; +import net.ihe.gazelle.http.validator.client.interlay.ws.HttpValidatorServerClientImpl; +import net.ihe.gazelle.validation.api.application.ValidationService; +import net.ihe.gazelle.validation.api.domain.metadata.structure.ValidationProfile; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationReport; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationTestResult; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationItem; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationRequest; +import net.ihe.gazelle.validation.interlay.dto.report.ValidationReportDTO; +import net.ihe.gazelle.validation.interlay.dto.request.ValidationRequestDTO; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class HttpValidationService implements ValidationService { + + public static final String HTTP_VALIDATION = "http-validation"; + public static final String APP_HTTP_VALIDATOR_SERVER = System.getenv("APP_HTTP_VALIDATOR_SERVER"); + public static final String RULES_FAILED_DURING_HTTP_VALIDATION = "Some rules failed during http-validation"; + List<ValidationItem> validationItemList = new ArrayList<>(); + private ValidationReport validationReport = new ValidationReport(); + + @Override + public ValidationReport validate(ValidationRequest validationRequest) { + ValidationRequestDTO validationRequestDTO = new ValidationRequestDTO(validationRequest); + JsonMapper jsonMapper = new JsonMapper(); + try { + String httpMessage = jsonMapper.writeValueAsString(validationRequestDTO); + String response = getResponseFromHttpValidation(httpMessage); + validationReport = jsonMapper.readValue(response, ValidationReportDTO.class); + if (ValidationTestResult.FAILED.equals(validationReport.getOverallResult())) { + addMessageToValidationItem(RULES_FAILED_DURING_HTTP_VALIDATION); + } + } catch (JsonProcessingException e) { + validationReport.setOverallResult(ValidationTestResult.FAILED); + addMessageToValidationItem(e.getMessage()); + } finally { + validationReport.setValidationItems(validationItemList); + } + return validationReport; + } + + + protected String getResponseFromHttpValidation(String content) { + HttpValidatorServerClientImpl httpValidatorServerClient = new HttpValidatorServerClientImpl(APP_HTTP_VALIDATOR_SERVER); + return httpValidatorServerClient.sendMessageToValidation(content); + + } + + private void addMessageToValidationItem(String message) { + ValidationItem vi = new ValidationItem(); + vi.setItemId(HTTP_VALIDATION); + vi.setContent(message.getBytes(StandardCharsets.UTF_8)); + validationItemList.add(vi); + } + + //TODO : implements this methods + public List<ValidationProfile> getValidationProfiles() { + return new ArrayList<>(); + } +} diff --git a/http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceFactory.java b/http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceFactory.java new file mode 100644 index 0000000..c5bc8f1 --- /dev/null +++ b/http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceFactory.java @@ -0,0 +1,14 @@ +package net.ihe.gazelle.http.validator.client.interlay.validation; + +import jakarta.enterprise.inject.Default; +import jakarta.ws.rs.Produces; +import net.ihe.gazelle.validation.api.application.ValidationService; + +public class HttpValidationServiceFactory { + + @Produces + @Default + public ValidationService getValidationService() { + return new HttpValidationService(); + } +} diff --git a/http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/ws/HttpValidatorServerClientImpl.java b/http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/ws/HttpValidatorServerClientImpl.java new file mode 100644 index 0000000..e93272b --- /dev/null +++ b/http-validator-client/src/main/java/net/ihe/gazelle/http/validator/client/interlay/ws/HttpValidatorServerClientImpl.java @@ -0,0 +1,66 @@ +package net.ihe.gazelle.http.validator.client.interlay.ws; + + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + + +public class HttpValidatorServerClientImpl { + + private static final Logger LOGGER = LoggerFactory.getLogger(HttpValidatorServerClientImpl.class); + private String serverUrl; + + /** + * Default constructor for the class. + * + * @param serverUrl URL of the server. + */ + public HttpValidatorServerClientImpl(String serverUrl) { + this.serverUrl = serverUrl; + } + + /** + * Send the message to the server. + * + * @param messageToValidate literal content of the message to validate. + * @return the literal value of the response entity. + */ + public String sendMessageToValidation(String messageToValidate) { + String result = null; + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpPost httppost = new HttpPost(serverUrl + "/rest/validation/validate"); + StringEntity stringEntity = new StringEntity(messageToValidate, StandardCharsets.UTF_8); + stringEntity.setContentType(ContentType.APPLICATION_JSON.getMimeType()); + httppost.setEntity(stringEntity); + HttpResponse response = httpclient.execute(httppost); + // reponse + HttpEntity entity = response.getEntity(); + if (entity != null) { + try (InputStream inputStream = entity.getContent()) { + BufferedReader reader = new BufferedReader( + new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + result = reader.lines() + .collect(Collectors.joining("\n")); + reader.close(); + } + } + } catch (IOException e) { + LOGGER.error("Error contacting remote HTTP Validator Server for validation.", e); + } + return result; + } +} diff --git a/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceFactoryTest.java b/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceFactoryTest.java new file mode 100644 index 0000000..09ede6c --- /dev/null +++ b/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceFactoryTest.java @@ -0,0 +1,15 @@ +package net.ihe.gazelle.http.validator.client.interlay.validation; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + + +class HttpValidationServiceFactoryTest { + + HttpValidationServiceFactory factory = new HttpValidationServiceFactory(); + + @Test + void testGetValidationService() { + Assertions.assertInstanceOf(HttpValidationService.class, factory.getValidationService()); + } +} \ No newline at end of file diff --git a/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceTest.java b/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceTest.java new file mode 100644 index 0000000..ccab378 --- /dev/null +++ b/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/HttpValidationServiceTest.java @@ -0,0 +1,63 @@ +package net.ihe.gazelle.http.validator.client.interlay.validation; + +import net.ihe.gazelle.http.validator.client.interlay.validation.mock.HttpValidationServiceMock; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationReport; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationItem; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; + +class HttpValidationServiceTest { + + static HttpValidationServiceMock httpValidationService = new HttpValidationServiceMock(); + + static ValidationRequest request = new ValidationRequest(); + static ValidationItem item = new ValidationItem(); + static File httpRequestFile = new File("src/test/resources/http_request_iti104.http"); + static String httpRequest; + + @BeforeAll + static public void initialize() { + try { + httpRequest = Files.readString(Path.of(httpRequestFile.getAbsolutePath())); + } catch (IOException e) { + throw new RuntimeException(e); + } + item.setContent(httpRequest.getBytes()); + request.addValidationItem(item); + + } + + @Test + void testValidatePassed() { + request.setValidationProfileId("passed"); + ValidationReport report = httpValidationService.validate(request); + Assertions.assertEquals("PASSED", report.getOverallResult().name()); + } + + @Test + void testValidateFailed() { + request.setValidationProfileId("failed"); + ValidationReport report = httpValidationService.validate(request); + Assertions.assertEquals("FAILED", report.getOverallResult().name()); + } + + @Test + void testValidateFailedWithCaughtJsonProcessingException() { + request.setValidationProfileId("error"); + ValidationReport report = httpValidationService.validate(request); + Assertions.assertEquals("FAILED", report.getOverallResult().name()); + } + + @Test + void testGetValidationProfiles() { + Assertions.assertInstanceOf(ArrayList.class, httpValidationService.getValidationProfiles()); + } +} \ No newline at end of file diff --git a/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/mock/HttpValidationServiceMock.java b/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/mock/HttpValidationServiceMock.java new file mode 100644 index 0000000..c8ccca4 --- /dev/null +++ b/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/mock/HttpValidationServiceMock.java @@ -0,0 +1,14 @@ +package net.ihe.gazelle.http.validator.client.interlay.validation.mock; + +import net.ihe.gazelle.http.validator.client.interlay.validation.HttpValidationService; +import net.ihe.gazelle.http.validator.client.interlay.validation.ws.mock.HttpValidatorServerClientImplMock; + +public class HttpValidationServiceMock extends HttpValidationService { + + protected String getResponseFromHttpValidation(String content) { + HttpValidatorServerClientImplMock httpValidatorServerClient = new HttpValidatorServerClientImplMock(System.getenv("http://localhost")); + return httpValidatorServerClient.sendMessageToValidation(content); + } + + +} diff --git a/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/ws/mock/HttpValidatorServerClientImplMock.java b/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/ws/mock/HttpValidatorServerClientImplMock.java new file mode 100644 index 0000000..794332d --- /dev/null +++ b/http-validator-client/src/test/java/net/ihe/gazelle/http/validator/client/interlay/validation/ws/mock/HttpValidatorServerClientImplMock.java @@ -0,0 +1,47 @@ +package net.ihe.gazelle.http.validator.client.interlay.validation.ws.mock; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import net.ihe.gazelle.http.validator.client.interlay.ws.HttpValidatorServerClientImpl; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class HttpValidatorServerClientImplMock extends HttpValidatorServerClientImpl { + + String passedResponsePath = "src/test/resources/response_passed.json"; + String failedResponsePath = "src/test/resources/response_failed.json"; + String throwExceptionErrorPath = "src/test/resources/response_thrown_exception.json"; + + /** + * Default constructor for the class. + * + * @param serverUrl URL of the server. + */ + public HttpValidatorServerClientImplMock(String serverUrl) { + super(serverUrl); + } + + public String sendMessageToValidation(String messageToValidate) { + String response = ""; + + try { + File responseFile = new File(getResponse(messageToValidate)); + response = Files.readString(Path.of(responseFile.getAbsolutePath())); + } catch (Exception e) { + System.out.println("Error during process"); + } + return response; + } + + + private String getResponse(String message) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(message); + String profile = jsonNode.get("validationProfileId").asText(); + return "passed".equals(profile) ? passedResponsePath : "failed".equals(profile) ? failedResponsePath : throwExceptionErrorPath; + + } +} diff --git a/http-validator-client/src/test/resources/http_request_iti104.http b/http-validator-client/src/test/resources/http_request_iti104.http new file mode 100644 index 0000000..c902418 --- /dev/null +++ b/http-validator-client/src/test/resources/http_request_iti104.http @@ -0,0 +1,66 @@ +POST https://hapi.fhir.org/baseR4/Patient/$validate HTTP/1.1 +Content-Type: application/fhir+json; charset=utf-8 +Accept: application/fhir+json; charset=utf-8 +Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW +Host: hapi.fhir.org + +{ + "resourceType": "Patient", + "id": "Passed", + "meta": { + "profile": [ + "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-patient" + ], + "security": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActReason", + "code": "HTEST" + } + ] + }, + "text": { + "status": "generated", + "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p style=\"border: 1px #661aff solid; background-color: #e6e6ff; padding: 10px;\"><b>Franz Muster </b> male, DoB: 1995-01-27 ( Medical record number: 8734)</p><hr/><table class=\"grid\"><tr><td style=\"background-color: #f3f5da\" title=\"Alternate names (see the one above)\">Alt. Name:</td><td colspan=\"3\">Muster </td></tr><tr><td style=\"background-color: #f3f5da\" title=\"Patient Links\">Links:</td><td colspan=\"3\"><ul><li>Managing Organization: <span/></li></ul></td></tr></table></div>" + }, + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "urn:oid:2.16.756.888888.3.1", + "value": "8734" + } + ], + "name": [ + { + "family": "Muster", + "given": [ + "Franz" + ] + }, + { + "family": "Muster", + "_family": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/iso21090-EN-qualifier", + "valueCode": "BR" + } + ] + } + } + ], + "gender": "male", + "birthDate": "1995-01-27", + "managingOrganization": { + "identifier": { + "system": "urn:oid:2.51.1.3", + "value": "7601000201041" + } + } +} \ No newline at end of file diff --git a/http-validator-client/src/test/resources/response_failed.json b/http-validator-client/src/test/resources/response_failed.json new file mode 100644 index 0000000..a2a0d35 --- /dev/null +++ b/http-validator-client/src/test/resources/response_failed.json @@ -0,0 +1,226 @@ +{ + "modelVersion": "0.1", + "uuid": "dc51dd67-2d99-47d3-9b68-3987a48b73bb", + "dateTime": "2023-12-13T10:02:34.033+0000", + "disclaimer": "This report is generated by HTTP Validator Web Service", + "overallResult": "FAILED", + "validationMethod": { + "validationServiceName": "http-validator", + "validationServiceVersion": "0.3.0", + "validationProfileID": "WADO-RS_Request_Validation_Profile", + "validationProfileVersion": "1.0" + }, + "validationItems": null, + "reports": [ + { + "name": "Well Formed Validation", + "standards": [ + "HTTP" + ], + "subReportResult": "PASSED", + "subReports": null, + "assertionReports": [ + { + "assertionID": "Well Formed Validation", + "assertionType": null, + "description": "Success: The document you have validated is supposed to be a well-formed document.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + } + ], + "subCounters": { + "numberOfFailedWithInfos": 0, + "numberOfFailedWithWarnings": 0, + "numberOfFailedWithErrors": 0, + "numberOfUnexpectedErrors": 0, + "numberOfAssertions": 1 + }, + "unexpectedErrors": null + }, + { + "name": "WADO_HTTPRequest", + "standards": null, + "subReportResult": "PASSED", + "subReports": null, + "assertionReports": [ + { + "assertionID": "GETMethodChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "HTTP1VersionChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "URIRegexChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "QueryParametersAbsenceChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "AcceptHeaderPresenceChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "AcceptHeaderContentChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "AcceptHeaderBoundaryPresentOnce", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "AcceptHeaderTransferSyntaxPresentOnce", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "TransferSyntaxValuesChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "AcceptHeaderQPresentOnce", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "QValuesChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "KOSSOPInstanceUIDHeaderPresenceChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "KOSSOPInstanceUIDHeaderFieldValueChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + } + ], + "subCounters": { + "numberOfFailedWithInfos": 0, + "numberOfFailedWithWarnings": 0, + "numberOfFailedWithErrors": 0, + "numberOfUnexpectedErrors": 0, + "numberOfAssertions": 13 + }, + "unexpectedErrors": null + } + ], + "counters": { + "numberOfFailedWithInfos": 0, + "numberOfFailedWithWarnings": 0, + "numberOfFailedWithErrors": 0, + "numberOfUnexpectedErrors": 0, + "numberOfAssertions": 14 + }, + "additionalMetadata": null +} \ No newline at end of file diff --git a/http-validator-client/src/test/resources/response_passed.json b/http-validator-client/src/test/resources/response_passed.json new file mode 100644 index 0000000..519f1e5 --- /dev/null +++ b/http-validator-client/src/test/resources/response_passed.json @@ -0,0 +1,226 @@ +{ + "modelVersion": "0.1", + "uuid": "dc51dd67-2d99-47d3-9b68-3987a48b73bb", + "dateTime": "2023-12-13T10:02:34.033+0000", + "disclaimer": "This report is generated by HTTP Validator Web Service", + "overallResult": "PASSED", + "validationMethod": { + "validationServiceName": "http-validator", + "validationServiceVersion": "0.3.0", + "validationProfileID": "WADO-RS_Request_Validation_Profile", + "validationProfileVersion": "1.0" + }, + "validationItems": null, + "reports": [ + { + "name": "Well Formed Validation", + "standards": [ + "HTTP" + ], + "subReportResult": "PASSED", + "subReports": null, + "assertionReports": [ + { + "assertionID": "Well Formed Validation", + "assertionType": null, + "description": "Success: The document you have validated is supposed to be a well-formed document.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + } + ], + "subCounters": { + "numberOfFailedWithInfos": 0, + "numberOfFailedWithWarnings": 0, + "numberOfFailedWithErrors": 0, + "numberOfUnexpectedErrors": 0, + "numberOfAssertions": 1 + }, + "unexpectedErrors": null + }, + { + "name": "WADO_HTTPRequest", + "standards": null, + "subReportResult": "PASSED", + "subReports": null, + "assertionReports": [ + { + "assertionID": "GETMethodChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "HTTP1VersionChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "URIRegexChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "QueryParametersAbsenceChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "AcceptHeaderPresenceChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "AcceptHeaderContentChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "AcceptHeaderBoundaryPresentOnce", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "AcceptHeaderTransferSyntaxPresentOnce", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "TransferSyntaxValuesChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "AcceptHeaderQPresentOnce", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "QValuesChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "KOSSOPInstanceUIDHeaderPresenceChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + }, + { + "assertionID": "KOSSOPInstanceUIDHeaderFieldValueChecking", + "assertionType": null, + "description": "INFO : The content is valid.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + } + ], + "subCounters": { + "numberOfFailedWithInfos": 0, + "numberOfFailedWithWarnings": 0, + "numberOfFailedWithErrors": 0, + "numberOfUnexpectedErrors": 0, + "numberOfAssertions": 13 + }, + "unexpectedErrors": null + } + ], + "counters": { + "numberOfFailedWithInfos": 0, + "numberOfFailedWithWarnings": 0, + "numberOfFailedWithErrors": 0, + "numberOfUnexpectedErrors": 0, + "numberOfAssertions": 14 + }, + "additionalMetadata": null +} \ No newline at end of file diff --git a/http-validator-client/src/test/resources/response_throw_exception.json b/http-validator-client/src/test/resources/response_throw_exception.json new file mode 100644 index 0000000..a122389 --- /dev/null +++ b/http-validator-client/src/test/resources/response_throw_exception.json @@ -0,0 +1,3 @@ +{ + "modelVersion": "0.1" +} \ No newline at end of file diff --git a/matchbox-client/pom.xml b/matchbox-client/pom.xml new file mode 100644 index 0000000..e59b5fd --- /dev/null +++ b/matchbox-client/pom.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>net.ihe.gazelle</groupId> + <artifactId>pixm-connector</artifactId> + <version>3.0.0-SNAPSHOT</version> + </parent> + + <artifactId>matchbox-client</artifactId> + <name>Matchbox Client</name> + <version>3.0.0-SNAPSHOT</version> + + <properties> + <maven.compiler.source>17</maven.compiler.source> + <maven.compiler.target>17</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>validation-api</artifactId> + </dependency> + <!-- This dependency includes the core HAPI-FHIR classes --> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-base</artifactId> + </dependency> + + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-server</artifactId> + </dependency> + + <!-- At least one "structures" JAR must also be included --> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-structures-r4</artifactId> + </dependency> + + <!-- Used for validation --> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-validation-resources-r4</artifactId> + </dependency> + <dependency> + <groupId>jakarta.platform</groupId> + <artifactId>jakarta.jakartaee-api</artifactId> + </dependency> + <dependency> + <groupId>jakarta.servlet</groupId> + <artifactId>jakarta.servlet-api</artifactId> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>4.5.13</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.json</groupId> + <artifactId>json</artifactId> + <version>20230618</version> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <version>${junit.jupiter.version}</version> + </dependency> + </dependencies> + +</project> \ No newline at end of file diff --git a/matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientServiceFactory.java b/matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientServiceFactory.java new file mode 100644 index 0000000..de8b1a8 --- /dev/null +++ b/matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientServiceFactory.java @@ -0,0 +1,14 @@ +package net.ihe.gazelle.matchbox.client.interlay.validation; + +import jakarta.enterprise.inject.Default; +import jakarta.ws.rs.Produces; +import net.ihe.gazelle.validation.api.application.ValidationService; + +public class CustomPatientServiceFactory { + + @Produces + @Default + public ValidationService getValidationService() { + return new CustomPatientValidationService(); + } +} diff --git a/matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientValidationService.java b/matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientValidationService.java new file mode 100644 index 0000000..6cd0718 --- /dev/null +++ b/matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientValidationService.java @@ -0,0 +1,78 @@ +package net.ihe.gazelle.matchbox.client.interlay.validation; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import net.ihe.gazelle.matchbox.client.interlay.ws.IGFhirServerClientImpl; +import net.ihe.gazelle.validation.api.application.ValidationService; +import net.ihe.gazelle.validation.api.domain.metadata.structure.ValidationProfile; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationReport; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationTestResult; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationItem; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationRequest; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.OperationOutcome; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class CustomPatientValidationService implements ValidationService { + public static final String MATCHBOX_VALIDATION = "matchbox-validation"; + public static final String APP_IG_FHIR_SERVER = System.getenv("APP_IG_FHIR_SERVER"); + private final ValidationReport validationReport = new ValidationReport(); + List<ValidationItem> validationItemList = new ArrayList<>(); + + @Override + public ValidationReport validate(ValidationRequest validationRequest) { + ByteArrayInputStream bais = new ByteArrayInputStream(validationRequest.getValidationItems().get(0).getContent()); + String iti104Patient = new String(bais.readAllBytes(), StandardCharsets.UTF_8); + String response = getResponseFromFhirValidation(iti104Patient, validationRequest.getValidationProfileId()); + addMessageToValidationItem(response); + hasAnyErrorsInIssues(response); + validationReport.setValidationItems(validationItemList); + return validationReport; + } + + private void addMessageToValidationItem(String message) { + ValidationItem vi = new ValidationItem(); + vi.setItemId(MATCHBOX_VALIDATION); + vi.setContent(message.getBytes(StandardCharsets.UTF_8)); + validationItemList.add(vi); + } + + protected String getResponseFromFhirValidation(String iti104Patient, String fhirProfile) { + IGFhirServerClientImpl igFhirServerClient = new IGFhirServerClientImpl(APP_IG_FHIR_SERVER); + return igFhirServerClient.sendMessageToValidation(iti104Patient, fhirProfile); + + } + + private <T extends IBaseResource> T convertStringIntoFhirResource(Class<T> tClass, String input) { + FhirContext ctx = FhirContext.forR4(); + IParser parser = ctx.newJsonParser(); + return parser.parseResource(tClass, input); + } + + private void hasAnyErrorsInIssues(String response) { + OperationOutcome operationOutcome = convertStringIntoFhirResource(OperationOutcome.class, response); + validationReport.setOverallResult(ValidationTestResult.PASSED); + for (OperationOutcome.OperationOutcomeIssueComponent issue : operationOutcome.getIssue()) { + if (isErrorOrFatal(issue.getSeverity())) { + validationReport.setOverallResult(ValidationTestResult.FAILED); + return; + } + } + } + + private boolean isErrorOrFatal(OperationOutcome.IssueSeverity severity) { + return switch (severity) { + case FATAL, ERROR, NULL -> true; + default -> false; + }; + } + + public List<ValidationProfile> getValidationProfiles() { + return new ArrayList<>(); + } + +} diff --git a/matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/ws/IGFhirServerClientImpl.java b/matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/ws/IGFhirServerClientImpl.java new file mode 100644 index 0000000..fa462c4 --- /dev/null +++ b/matchbox-client/src/main/java/net/ihe/gazelle/matchbox/client/interlay/ws/IGFhirServerClientImpl.java @@ -0,0 +1,85 @@ +package net.ihe.gazelle.matchbox.client.interlay.ws; + + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + + +public class IGFhirServerClientImpl { + + private static final Logger LOGGER = LoggerFactory.getLogger(IGFhirServerClientImpl.class); + private String serverUrl; + + /** + * Default constructor for the class. + * + * @param serverUrl URL of the server. + */ + public IGFhirServerClientImpl(String serverUrl) { + this.serverUrl = serverUrl; + } + + + /** + * Send the message to the server. + * + * @param messageToValidate literal content of the message to validate. + * @param profile literal value of the url of the profile to validate against. + * @return the literal value of the response entity. + */ + public String sendMessageToValidation(String messageToValidate, String profile) { + String result = null; + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpPost httppost = new HttpPost(serverUrl + "/$validate?profile=" + + URLEncoder.encode(profile, StandardCharsets.UTF_8)); + + StringEntity stringEntity = new StringEntity(messageToValidate, StandardCharsets.UTF_8); + httppost.setEntity(stringEntity); + HttpResponse response = httpclient.execute(httppost); + HttpEntity entity = response.getEntity(); + if (entity != null) { + try (InputStream inputStream = entity.getContent()) { + BufferedReader reader = new BufferedReader( + new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + result = reader.lines() + .collect(Collectors.joining("\n")); + reader.close(); + } + } + } catch (IOException e) { + LOGGER.error("Error contacting remote FHIR Server for validation.", e); + result = getExceptionReportMessage(messageToValidate, e); + } + return result; + } + + + /** + * Return the literal value of the message to pur in the report + * + * @param e exception to report + * @param message message to display with the error + * @return literal value of the message. + */ + private String getExceptionReportMessage(String message, Exception e) { + StringBuilder finalMessage = new StringBuilder(message); + if (e.getMessage() != null && !e.getMessage().isEmpty()) { + finalMessage.append(String.format("with message : [ %s ]", e.getMessage())); + } + return finalMessage.toString(); + } +} diff --git a/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientServiceFactoryTest.java b/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientServiceFactoryTest.java new file mode 100644 index 0000000..9fc9848 --- /dev/null +++ b/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientServiceFactoryTest.java @@ -0,0 +1,16 @@ +package net.ihe.gazelle.matchbox.client.interlay.validation; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class CustomPatientServiceFactoryTest { + + CustomPatientServiceFactory factory = new CustomPatientServiceFactory(); + + @Test + void testGetValidationService() { + Assertions.assertInstanceOf(CustomPatientValidationService.class, factory.getValidationService()); + } + + +} \ No newline at end of file diff --git a/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientValidationServiceTest.java b/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientValidationServiceTest.java new file mode 100644 index 0000000..e6b0856 --- /dev/null +++ b/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/CustomPatientValidationServiceTest.java @@ -0,0 +1,58 @@ +package net.ihe.gazelle.matchbox.client.interlay.validation; + +import net.ihe.gazelle.matchbox.client.interlay.validation.mock.CustomPatientValidationServiceMock; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationReport; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationItem; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationRequest; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; + +class CustomPatientValidationServiceTest { + + static CustomPatientValidationService customPatientValidationService = new CustomPatientValidationServiceMock(); + + + static ValidationRequest request = new ValidationRequest(); + static ValidationItem item = new ValidationItem(); + static File patientFranzFile = new File("src/test/resources/post_request_passed.json"); + static String patientFranz; + + + @BeforeAll + static public void initialize() { + try { + patientFranz = Files.readString(Path.of(patientFranzFile.getAbsolutePath())); + } catch (IOException e) { + throw new RuntimeException(e); + } + item.setContent(patientFranz.getBytes()); + request.addValidationItem(item); + + } + + @Test + void validatePassed() { + request.setValidationProfileId("passed"); + ValidationReport report = customPatientValidationService.validate(request); + Assertions.assertEquals("PASSED", report.getOverallResult().name()); + } + + @Test + void validateFailed() { + request.setValidationProfileId("failed"); + ValidationReport report = customPatientValidationService.validate(request); + Assertions.assertEquals("FAILED", report.getOverallResult().name()); + } + + @Test + void getValidationProfiles() { + Assertions.assertInstanceOf(ArrayList.class, customPatientValidationService.getValidationProfiles()); + } +} \ No newline at end of file diff --git a/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/mock/CustomPatientValidationServiceMock.java b/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/mock/CustomPatientValidationServiceMock.java new file mode 100644 index 0000000..e16825d --- /dev/null +++ b/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/validation/mock/CustomPatientValidationServiceMock.java @@ -0,0 +1,15 @@ +package net.ihe.gazelle.matchbox.client.interlay.validation.mock; + +import net.ihe.gazelle.matchbox.client.interlay.validation.CustomPatientValidationService; +import net.ihe.gazelle.matchbox.client.interlay.ws.mock.IgFhirServerClientMock; + +public class CustomPatientValidationServiceMock extends CustomPatientValidationService { + + protected String getResponseFromFhirValidation(String iti104Patient, String fhirProfile) { + IgFhirServerClientMock igFhirServerClient = new IgFhirServerClientMock("http://localhost"); + return igFhirServerClient.sendMessageToValidation(iti104Patient, fhirProfile); + + } + + +} diff --git a/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/ws/mock/IgFhirServerClientMock.java b/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/ws/mock/IgFhirServerClientMock.java new file mode 100644 index 0000000..0a5bd19 --- /dev/null +++ b/matchbox-client/src/test/java/net/ihe/gazelle/matchbox/client/interlay/ws/mock/IgFhirServerClientMock.java @@ -0,0 +1,55 @@ +package net.ihe.gazelle.matchbox.client.interlay.ws.mock; + +import net.ihe.gazelle.matchbox.client.interlay.ws.IGFhirServerClientImpl; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; + +public class IgFhirServerClientMock extends IGFhirServerClientImpl { + + String passedResponsePath = "src/test/resources/post_response.json"; + String failedResponsePath = "src/test/resources/post_response_failed.json"; + + + /** + * Default constructor for the class. + * + * @param serverUrl URL of the server. + */ + public IgFhirServerClientMock(String serverUrl) { + super(serverUrl); + } + + public String sendMessageToValidation(String messageToValidate, String profile) { + String response = ""; + File responseFile = new File(getResponse(profile)); + try { + response = Files.readString(Path.of(responseFile.getAbsolutePath())); + + } catch (Exception e) { + System.out.println("Problem during readString: " + e); + } + return response; + } + + + /** + * Return the literal value of the message to pur in the report + * + * @param e exception to report + * @param message message to display with the error + * @return literal value of the message. + */ + private String getExceptionReportMessage(String message, Exception e) { + StringBuilder finalMessage = new StringBuilder(message); + if (e.getMessage() != null && !e.getMessage().isEmpty()) { + finalMessage.append(String.format("with message : [ %s ]", e.getMessage())); + } + return finalMessage.toString(); + } + + private String getResponse(String profile) { + return "passed".equals(profile) ? passedResponsePath : failedResponsePath; + } +} diff --git a/matchbox-client/src/test/resources/post_request_passed.json b/matchbox-client/src/test/resources/post_request_passed.json new file mode 100644 index 0000000..e1087f5 --- /dev/null +++ b/matchbox-client/src/test/resources/post_request_passed.json @@ -0,0 +1,60 @@ +{ + "resourceType": "Patient", + "id": "Passed", + "meta": { + "profile": [ + "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-patient" + ], + "security": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActReason", + "code": "HTEST" + } + ] + }, + "text": { + "status": "generated", + "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p style=\"border: 1px #661aff solid; background-color: #e6e6ff; padding: 10px;\"><b>Franz Muster </b> male, DoB: 1995-01-27 ( Medical record number: 8734)</p><hr/><table class=\"grid\"><tr><td style=\"background-color: #f3f5da\" title=\"Alternate names (see the one above)\">Alt. Name:</td><td colspan=\"3\">Muster </td></tr><tr><td style=\"background-color: #f3f5da\" title=\"Patient Links\">Links:</td><td colspan=\"3\"><ul><li>Managing Organization: <span/></li></ul></td></tr></table></div>" + }, + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "urn:oid:2.16.756.888888.3.1", + "value": "8734" + } + ], + "name": [ + { + "family": "Muster", + "given": [ + "Franz" + ] + }, + { + "family": "Muster", + "_family": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/iso21090-EN-qualifier", + "valueCode": "BR" + } + ] + } + } + ], + "gender": "male", + "birthDate": "1995-01-27", + "managingOrganization": { + "identifier": { + "system": "urn:oid:2.51.1.3", + "value": "7601000201041" + } + } +} \ No newline at end of file diff --git a/matchbox-client/src/test/resources/post_response.json b/matchbox-client/src/test/resources/post_response.json new file mode 100644 index 0000000..ebaf46f --- /dev/null +++ b/matchbox-client/src/test/resources/post_response.json @@ -0,0 +1,14 @@ +{ + "resourceType": "OperationOutcome", + "text": { + "status": "generated", + "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><h1>Operation Outcome</h1><table border=\"0\"><tr><td style=\"font-weight: bold;\">INFORMATION</td><td>[]</td><td>No issues detected during validation</td></tr></table></div>" + }, + "issue": [ + { + "severity": "information", + "code": "informational", + "diagnostics": "No issues detected during validation" + } + ] +} \ No newline at end of file diff --git a/matchbox-client/src/test/resources/post_response_failed.json b/matchbox-client/src/test/resources/post_response_failed.json new file mode 100644 index 0000000..3870f75 --- /dev/null +++ b/matchbox-client/src/test/resources/post_response_failed.json @@ -0,0 +1,14 @@ +{ + "resourceType": "OperationOutcome", + "text": { + "status": "generated", + "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><h1>Operation Outcome</h1><table border=\"0\"><tr><td style=\"font-weight: bold;\">ERROR</td><td>[]</td><td>HAPI-0450: Failed to parse request body as JSON resource. Error was: HAPI-1821: [element="gender"] Invalid attribute value "toto": Unknown AdministrativeGender code 'toto'</td></tr></table></div>" + }, + "issue": [ + { + "severity": "error", + "code": "processing", + "diagnostics": "HAPI-0450: Failed to parse request body as JSON resource. Error was: HAPI-1821: [element=\"gender\"] Invalid attribute value \"toto\": Unknown AdministrativeGender code 'toto'" + } + ] +} \ No newline at end of file diff --git a/pixm-connector-service/pom.xml b/pixm-connector-service/pom.xml new file mode 100644 index 0000000..97ccc2d --- /dev/null +++ b/pixm-connector-service/pom.xml @@ -0,0 +1,330 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>net.ihe.gazelle</groupId> + <artifactId>pixm-connector</artifactId> + <version>3.0.0-SNAPSHOT</version> + </parent> + + <artifactId>pixm-connector-service</artifactId> + <name>PIXm Connector Service</name> + <version>3.0.0-SNAPSHOT</version> + <packaging>war</packaging> + + <properties> + <maven.compiler.source>17</maven.compiler.source> + <maven.compiler.target>17</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <app.patient.registry.version>2.2.0</app.patient.registry.version> + </properties> + + <build> + <finalName>pixm-connector</finalName> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-release-plugin</artifactId> + <version>${maven.release.plugin.version}</version> + <configuration> + <tagNameFormat>@{project.version}</tagNameFormat> + <autoVersionSubmodules>true</autoVersionSubmodules> + <releaseProfiles>release</releaseProfiles> + </configuration> + </plugin> + <!-- Tell Maven which Java source version you want to use --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>${maven.compiler.plugin.version}</version> + <configuration> + <source>${java.version}</source> + <target>${java.version}</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>${maven.surefire.plugin.version}</version> + <configuration> + <properties> + <property> + <name>listener</name> + <value>io.qameta.allure.junit5.AllureJunit5</value> + </property> + </properties> + <dependenciesToScan> + <dependency>net.ihe.gazelle:lib.unit-test</dependency> + </dependenciesToScan> + <testFailureIgnore>false</testFailureIgnore> + <argLine> + -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + </argLine> + <systemProperties> + <property> + <name>junit.jupiter.extensions.autodetection.enabled</name> + <value>true</value> + </property> + <property> + <name>allure.results.directory</name> + <value>${project.basedir}/target/allure-results</value> + </property> + </systemProperties> + <argLine>${argLine}</argLine> + </configuration> + <dependencies> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <version>${junit.jupiter.version}</version> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <version>${junit.jupiter.version}</version> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>${junit.jupiter.version}</version> + </dependency> + <dependency> + <groupId>uk.org.webcompere</groupId> + <artifactId>system-stubs-jupiter</artifactId> + <version>${system.stubs.jupiter.version}</version> + </dependency> + <dependency> + <groupId>org.aspectj</groupId> + <artifactId>aspectjweaver</artifactId> + <version>${aspectj.version}</version> + </dependency> + </dependencies> + </plugin> + <plugin> + <groupId>io.qameta.allure</groupId> + <artifactId>allure-maven</artifactId> + <version>${allure.maven.version}</version> + <configuration> + <allureDownloadUrl> + https://github.com/allure-framework/allure-maven/archive/refs/tags/${allure.maven.version}.zip + </allureDownloadUrl> + </configuration> + </plugin> + + <!-- The configuration here tells the WAR plugin to include the FHIR Tester + overlay. You can omit it if you are not using that feature. --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-war-plugin</artifactId> + <version>3.3.1</version> + <configuration> + <overlays> + <overlay> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-testpage-overlay</artifactId> + </overlay> + </overlays> + </configuration> + </plugin> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <version>${jacoco.maven.plugin.version}</version> + <executions> + <execution> + <id>pre-unit-test</id> + <goals> + <goal>prepare-agent</goal> + </goals> + </execution> + <execution> + <id>default-report</id> + <phase>package</phase> + <goals> + <goal>report</goal> + </goals> + <configuration> + <dataFile>${project.build.directory}/target/jacoco.exec</dataFile> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <configuration> + <doclint>none</doclint> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>validation-api</artifactId> + </dependency> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>framework.preferences-model-api</artifactId> + <version>1.0.1</version> + </dependency> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>framework.operational-preferences-service</artifactId> + <version>1.0.1</version> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <version>${junit.jupiter.version}</version> + <scope>test</scope> + </dependency> + + <!-- This dependency includes the core HAPI-FHIR classes --> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-base</artifactId> + </dependency> + + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-server</artifactId> + </dependency> + + <!-- At least one "structures" JAR must also be included --> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-structures-r4</artifactId> + </dependency> + + <!-- Used for validation --> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-validation-resources-r4</artifactId> + </dependency> + <dependency> + <groupId>com.phloc</groupId> + <artifactId>phloc-schematron</artifactId> + <version>${phloc.schematron.version}</version> + </dependency> + + <!-- This dependency is used for the "FHIR Tester" web app overlay --> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-testpage-overlay</artifactId> + <version>${hapi.fhir.version}</version> + <type>war</type> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-testpage-overlay</artifactId> + <version>${hapi.fhir.version}</version> + <classifier>classes</classifier> + <scope>provided</scope> + </dependency> + + <!-- HAPI-FHIR uses Logback for logging support. The logback library is + included automatically by Maven as a part of the hapi-fhir-base dependency, + but you also need to include a logging library. Logback is used here, but + log4j would also be fine. --> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <version>${logback.classic.version}</version> + </dependency> + + + <!-- Needed for JEE/Servlet support --> + <dependency> + <groupId>jakarta.platform</groupId> + <artifactId>jakarta.jakartaee-api</artifactId> + </dependency> + <dependency> + <groupId>jakarta.servlet</groupId> + <artifactId>jakarta.servlet-api</artifactId> + </dependency> + + <!-- If you are using HAPI narrative generation, you will need to include + Thymeleaf as well. Otherwise the following can be omitted. --> + <dependency> + <groupId>org.thymeleaf</groupId> + <artifactId>thymeleaf</artifactId> + <version>${thymeleaf.version}</version> + </dependency> + <dependency> + <groupId>org.fhir</groupId> + <artifactId>ucum</artifactId> + </dependency> + <dependency> + <groupId>com.github.ben-manes.caffeine</groupId> + <artifactId>caffeine</artifactId> + <version>${caffeine.version}</version> + </dependency> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>app.patient-registry-xref-search-client</artifactId> + <version>${app.patient.registry.version}</version> + </dependency> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>app.patient-registry-search-client</artifactId> + <version>${app.patient.registry.version}</version> + </dependency> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>app.patient-registry-feed-client</artifactId> + <version>${app.patient.registry.version}</version> + </dependency> + <!-- Used for CORS support --> + <dependency> + <groupId>org.ebaysf.web</groupId> + <artifactId>cors-filter</artifactId> + <version>${cors.filter.version}</version> + <exclusions> + <exclusion> + <artifactId>servlet-api</artifactId> + <groupId>javax.servlet</groupId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-junit-jupiter</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.qameta.allure</groupId> + <artifactId>allure-junit5</artifactId> + <version>${allure.junit5.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>matchbox-client</artifactId> + <version>3.0.0-SNAPSHOT</version> + </dependency> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>http-validator-client</artifactId> + <version>3.0.0-SNAPSHOT</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>uk.org.webcompere</groupId> + <artifactId>system-stubs-jupiter</artifactId> + <version>${system.stubs.jupiter.version}</version> + </dependency> + </dependencies> + +</project> \ No newline at end of file diff --git a/src/main/java/net/ihe/gazelle/adapter/connector/ConversionException.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/connector/ConversionException.java similarity index 99% rename from src/main/java/net/ihe/gazelle/adapter/connector/ConversionException.java rename to pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/connector/ConversionException.java index a610d08..c444d27 100644 --- a/src/main/java/net/ihe/gazelle/adapter/connector/ConversionException.java +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/connector/ConversionException.java @@ -1,7 +1,7 @@ package net.ihe.gazelle.adapter.connector; public class ConversionException extends Exception { - + public ConversionException() { super(); } diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/connector/FhirToGazelleRegistryConverter.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/connector/FhirToGazelleRegistryConverter.java new file mode 100644 index 0000000..7ae59da --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/connector/FhirToGazelleRegistryConverter.java @@ -0,0 +1,247 @@ +package net.ihe.gazelle.adapter.connector; + +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; +import net.ihe.gazelle.app.patientregistryapi.business.GenderCode; +import net.ihe.gazelle.app.patientregistryapi.business.Person; +import net.ihe.gazelle.app.patientregistryapi.business.PersonName; +import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Address.AddressUse; +import org.hl7.fhir.r4.model.Patient.ContactComponent; +import org.jetbrains.annotations.NotNull; + +/** + * Class containing static methods to convert + * + * @author pvm + */ +public class FhirToGazelleRegistryConverter { + + public static final String SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND = "sourceIdentifier Patient Identifier not found"; + public static final String SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_HTTP_NOR_URN_OID = "sourceIdentifier Patient Identifier does not contain http nor urn:oid:"; + public static final String SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND = "sourceIdentifier Assigning Authority not found"; + public static final String ISO = "ISO"; + private static final String URN_OID = "urn:oid:"; + private static final String HTTP_PROTOCOL = "http"; + + /** + * private constructor to override the public implicit one. + */ + private FhirToGazelleRegistryConverter() { + } + + public static net.ihe.gazelle.app.patientregistryapi.business.Patient convertPatient(Patient fhirPatient) throws ConversionException { + net.ihe.gazelle.app.patientregistryapi.business.Patient registryPatient = new net.ihe.gazelle.app.patientregistryapi.business.Patient(); + if (fhirPatient.hasActive()) { + registryPatient.setActive(fhirPatient.getActive()); + } + else{ + registryPatient.setActive(true); + } + + if (fhirPatient.hasBirthDate()) { + registryPatient.setDateOfBirth(fhirPatient.getBirthDate()); + } + + if (fhirPatient.hasDeceased()) { + registryPatient.setDateOfDeath(fhirPatient.getDeceasedDateTimeType().getValue()); + } + + if (fhirPatient.hasGender()) { + registryPatient.setGender(convertGender(fhirPatient.getGender())); + } + + if (fhirPatient.hasMultipleBirth()) { + registryPatient.setMultipleBirthOrder(fhirPatient.getMultipleBirthIntegerType().getValue()); + } + + if (fhirPatient.hasId()) { + registryPatient.setUuid(fhirPatient.getId()); + } + + convertAddresses(fhirPatient, registryPatient); + convertIdentifiers(fhirPatient, registryPatient); + convertContacts(fhirPatient, registryPatient); + convertNames(fhirPatient, registryPatient); + + return registryPatient; + } + + private static void convertAddresses(Patient fhirPatient, net.ihe.gazelle.app.patientregistryapi.business.Patient registryPatient) throws ConversionException { + if (fhirPatient.hasAddress()) { + for (Address address : fhirPatient.getAddress()) { + registryPatient.addAddress(convertAddress(address)); + } + } + } + + private static void convertContacts(Patient fhirPatient, net.ihe.gazelle.app.patientregistryapi.business.Patient registryPatient) throws ConversionException { + if (fhirPatient.hasContact()) { + for (ContactComponent contact : fhirPatient.getContact()) { + registryPatient.addContact(convertContact(contact)); + } + } + } + + private static void convertNames(Patient fhirPatient, net.ihe.gazelle.app.patientregistryapi.business.Patient registryPatient) { + if (fhirPatient.hasName()) { + for (HumanName name : fhirPatient.getName()) { + registryPatient.addName(convertName(name)); + } + } + } + + private static void convertIdentifiers(Patient fhirPatient, net.ihe.gazelle.app.patientregistryapi.business.Patient registryPatient) { + if (fhirPatient.hasIdentifier()) { + for (Identifier id : fhirPatient.getIdentifier()) { + addEntity(registryPatient, id); + } + } + } + + private static void addEntity(net.ihe.gazelle.app.patientregistryapi.business.Patient registryPatient, Identifier id) { + EntityIdentifier entityIdentifier = new EntityIdentifier(); + if (id.hasSystem()) { + String systemID = id.getSystem(); + systemID = systemID.replace(URN_OID, ""); + entityIdentifier.setSystemIdentifier(systemID); + entityIdentifier.setType(ISO); + } + if (id.hasValue()) { + entityIdentifier.setValue(id.getValue()); + } else { + throw new InvalidRequestException("Cannot create Patient without any Identifier"); + } + registryPatient.addIdentifier(entityIdentifier); + } + + private static GenderCode convertGender(Enumerations.AdministrativeGender gender) throws ConversionException { + return switch (gender) { + case MALE -> GenderCode.MALE; + case FEMALE -> GenderCode.FEMALE; + case UNKNOWN -> GenderCode.UNDEFINED; + case OTHER -> GenderCode.OTHER; + default -> throw new ConversionException(String.format("Cannot map GenderCode : %s", gender)); + }; + } + + private static net.ihe.gazelle.app.patientregistryapi.business.Address convertAddress(Address fhirAddress) throws ConversionException { + net.ihe.gazelle.app.patientregistryapi.business.Address registryAddress = new net.ihe.gazelle.app.patientregistryapi.business.Address(); + + if (fhirAddress.hasCity()) { + registryAddress.setCity(fhirAddress.getCity()); + } + + if (fhirAddress.hasCountry()) { + registryAddress.setCountryIso3(fhirAddress.getCountry()); + } + + if (fhirAddress.hasPostalCode()) { + registryAddress.setPostalCode(fhirAddress.getPostalCode()); + } + + if (fhirAddress.hasState()) { + registryAddress.setState(fhirAddress.getState()); + } + + if (fhirAddress.hasUse()) { + registryAddress.setUse(convertAddressUse(fhirAddress.getUse())); + } + + if (fhirAddress.hasLine()) { + for (StringType addressLine : fhirAddress.getLine()) { + registryAddress.addLine(addressLine.getValue()); + } + } + return registryAddress; + } + + private static net.ihe.gazelle.app.patientregistryapi.business.AddressUse convertAddressUse(AddressUse addressUse) throws ConversionException { + return switch (addressUse) { + case BILLING -> net.ihe.gazelle.app.patientregistryapi.business.AddressUse.BILLING; + case HOME -> net.ihe.gazelle.app.patientregistryapi.business.AddressUse.HOME; + case OLD -> net.ihe.gazelle.app.patientregistryapi.business.AddressUse.BAD; + case TEMP -> net.ihe.gazelle.app.patientregistryapi.business.AddressUse.TEMPORARY; + case WORK -> net.ihe.gazelle.app.patientregistryapi.business.AddressUse.WORK; + default -> throw new ConversionException(String.format("Cannot map address use : %s", addressUse)); + }; + } + + private static Person convertContact(ContactComponent fhirContact) throws ConversionException { + Person registryContact = new Person(); + + if (fhirContact.hasGender()) { + registryContact.setGender(convertGender(fhirContact.getGender())); + } + + if (fhirContact.hasAddress()) { + registryContact.addAddress(convertAddress(fhirContact.getAddress())); + } + + if (fhirContact.hasName()) { + registryContact.addName(convertName(fhirContact.getName())); + } + return registryContact; + } + + private static PersonName convertName(HumanName fhirName) { + PersonName registryName = new PersonName(); + + if (fhirName.hasUse()) { + registryName.setUse(fhirName.getUse().toString()); + } + + if (fhirName.hasFamily()) { + registryName.setFamily(fhirName.getFamily()); + } + + if (fhirName.hasGiven()) { + for (StringType givenName : fhirName.getGiven()) { + registryName.addGiven(givenName.getValue()); + } + } + + if (fhirName.hasPrefix()) { + for (StringType prefixName : fhirName.getPrefix()) { + registryName.setPrefix(prefixName.getValue()); + } + } + + if (fhirName.hasSuffix()) { + for (StringType suffixName : fhirName.getSuffix()) { + registryName.setSuffix(suffixName.getValue()); + } + } + + return registryName; + } + + @NotNull + public static EntityIdentifier convertSourceIdentiferToEntityIdentifier(String sourceIdentifierSystem, String sourceIdentifierValue) { + if (sourceIdentifierSystem == null) { + throw new UnprocessableEntityException(SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND); + } + if (sourceIdentifierValue == null) { + throw new UnprocessableEntityException(SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND); + } + if (sourceIdentifierSystem.isBlank() || sourceIdentifierValue.isBlank()) { + throw new UnprocessableEntityException(SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND); + } + + EntityIdentifier wellFormedEntityIdentifier = new EntityIdentifier(); + if (sourceIdentifierSystem.contains(URN_OID)) { + wellFormedEntityIdentifier.setSystemIdentifier(sourceIdentifierSystem.replace(URN_OID, "")); + wellFormedEntityIdentifier.setType(ISO); + wellFormedEntityIdentifier.setValue(sourceIdentifierValue); + } else if (sourceIdentifierSystem.contains(HTTP_PROTOCOL)) { + wellFormedEntityIdentifier.setSystemIdentifier(sourceIdentifierSystem); + wellFormedEntityIdentifier.setType(ISO); + wellFormedEntityIdentifier.setValue(sourceIdentifierValue); + } else { + throw new UnprocessableEntityException(SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_HTTP_NOR_URN_OID); + } + return wellFormedEntityIdentifier; + } + +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/connector/GazelleRegistryToFhirConverter.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/connector/GazelleRegistryToFhirConverter.java new file mode 100644 index 0000000..9d7b1a2 --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/connector/GazelleRegistryToFhirConverter.java @@ -0,0 +1,180 @@ +package net.ihe.gazelle.adapter.connector; + +import net.ihe.gazelle.app.patientregistryapi.business.*; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.HumanName; + +import java.util.List; + +/** + * Converter to transform Patient Registry's {@link Patient} to {@link org.hl7.fhir.r4.model.Patient}. + * + * @author pvm + */ +public class GazelleRegistryToFhirConverter { + + private static final String URN_PREFIX = "urn:oid:"; + + /** + * private constructor to override the implicit one. + */ + private GazelleRegistryToFhirConverter() { + } + + public static org.hl7.fhir.r4.model.Patient convertPatient(Patient registryPatient) throws ConversionException { + org.hl7.fhir.r4.model.Patient fhirPatient = new org.hl7.fhir.r4.model.Patient(); + fhirPatient.setId(registryPatient.getUuid()); + fhirPatient.setGender(getGenderCode(registryPatient)); + fhirPatient.setBirthDate(registryPatient.getDateOfBirth()); + fhirPatient.setActive(registryPatient.isActive()); + setNames(fhirPatient, registryPatient); + setCrossIdentifier(fhirPatient, registryPatient); + setAddresses(fhirPatient, registryPatient); + setTelecom(fhirPatient, registryPatient); + return fhirPatient; + + } + + private static void setNames(org.hl7.fhir.r4.model.Patient fhirPatient, Patient registryPatient) throws ConversionException { + List<PersonName> patientNames = registryPatient.getNames(); + if (patientNames != null) { + for (PersonName personName : patientNames) { + fhirPatient.addName(convertRegistryNameToFhirName(personName)); + } + } + } + + private static HumanName convertRegistryNameToFhirName(PersonName registryName) throws ConversionException { + HumanName fhirName = new HumanName(); + if (registryName != null) { + fhirName.setFamily(registryName.getFamily()); + for (String given : registryName.getGivens()) { + fhirName.addGiven(given); + } + try { + fhirName.setUse(HumanName.NameUse.fromCode(registryName.getUse())); + } catch (FHIRException e) { + throw new ConversionException(String.format("Cannot convert PersonName use : %s", registryName.getUse()), e); + } + fhirName.addPrefix(registryName.getPrefix()); + fhirName.addSuffix(registryName.getSuffix()); + } + return fhirName; + } + + private static Enumerations.AdministrativeGender getGenderCode(Patient registryPatient) { + if (registryPatient.getGender() == null) { + return null; + } else { + return switch (registryPatient.getGender()) { + case MALE -> Enumerations.AdministrativeGender.MALE; + case FEMALE -> Enumerations.AdministrativeGender.FEMALE; + case UNDEFINED -> Enumerations.AdministrativeGender.UNKNOWN; + case OTHER -> Enumerations.AdministrativeGender.OTHER; + }; + } + } + + private static void setCrossIdentifier(org.hl7.fhir.r4.model.Patient fhirPatient, Patient registryPatient) { + if (registryPatient.getIdentifiers() != null) { + for (EntityIdentifier currentPatientIdentifier : registryPatient.getIdentifiers()) { + if (currentPatientIdentifier.getSystemIdentifier() != null) { + String fhirSystem = getUniversalIDAsUrn(currentPatientIdentifier.getSystemIdentifier()); + fhirPatient.addIdentifier().setSystem(fhirSystem).setValue(currentPatientIdentifier.getValue()); + } + } + } + } + + private static void setAddresses(org.hl7.fhir.r4.model.Patient fhirPatient, Patient patient) throws ConversionException { + List<Address> addressList = patient.getAddresses(); + if (addressList != null) { + for (Address address : addressList) { + org.hl7.fhir.r4.model.Address fhirAddress = new org.hl7.fhir.r4.model.Address(); + for (String line : address.getLines()) { + fhirAddress.addLine(line); + } + fhirAddress.setCity(address.getCity()); + fhirAddress.setCountry(address.getCountryIso3()); + fhirAddress.setPostalCode(address.getPostalCode()); + fhirAddress.setState(address.getState()); + fhirAddress.setUse(getAddressUse(address.getUse())); + fhirPatient.addAddress(fhirAddress); + } + } + } + + private static void setTelecom(org.hl7.fhir.r4.model.Patient fhirPatient, Patient patient) throws ConversionException { + List<ContactPoint> contactPoints = patient.getContactPoints(); + if (contactPoints != null) { + for (ContactPoint contactPoint : contactPoints) { + if (contactPoint != null) { + org.hl7.fhir.r4.model.ContactPoint fhirContactPoint = new org.hl7.fhir.r4.model.ContactPoint(); + fhirContactPoint.setSystem(getContactPointSystem(contactPoint.getType())); + fhirContactPoint.setValue(contactPoint.getValue()); + fhirContactPoint.setUse(getContactPointUse(contactPoint.getUse())); + fhirPatient.addTelecom(fhirContactPoint); + } + } + } + } + + private static String getUniversalIDAsUrn(String universalID) { + String urnToReturn = null; + if (universalID != null) { + urnToReturn = (universalID.startsWith(URN_PREFIX) ? universalID : URN_PREFIX + universalID); + } + return urnToReturn; + } + + private static org.hl7.fhir.r4.model.Address.AddressUse getAddressUse(AddressUse addressUse) throws ConversionException { + if (addressUse == null) { + return null; + } + return switch (addressUse) { + case HOME, PRIMARY_HOME -> org.hl7.fhir.r4.model.Address.AddressUse.HOME; + case WORK -> org.hl7.fhir.r4.model.Address.AddressUse.WORK; + case VACATION_HOME, TEMPORARY -> org.hl7.fhir.r4.model.Address.AddressUse.TEMP; + case BAD -> org.hl7.fhir.r4.model.Address.AddressUse.OLD; + case BILLING -> org.hl7.fhir.r4.model.Address.AddressUse.BILLING; + default -> throw new ConversionException(String.format("Cannot convert AddressUse : %s", addressUse)); + }; + + } + + private static org.hl7.fhir.r4.model.ContactPoint.ContactPointUse getContactPointUse(ContactPointUse contactPointUse) + throws ConversionException { + if (contactPointUse == null) { + return null; + } + return switch (contactPointUse) { + case HOME, PRIMARY_HOME -> org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.HOME; + case MOBILE -> org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.MOBILE; + case WORK -> org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.WORK; + case TEMPORARY -> org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.TEMP; + default -> + throw new ConversionException(String.format("Cannot convert ContactPointUse : %s", contactPointUse.value())); + }; + } + + private static org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem getContactPointSystem(ContactPointType contactPointType) + throws ConversionException { + if (contactPointType == null) { + return null; + } + return switch (contactPointType) { + case BEEPER -> org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.PAGER; + case PHONE -> org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.PHONE; + case FAX -> org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.FAX; + case URL -> org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.URL; + case EMAIL -> org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.EMAIL; + case SMS -> org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.SMS; + case OTHER -> org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.OTHER; + default -> + throw new ConversionException(String.format("Cannot convert ContactPointType : %s", contactPointType)); + }; + + } + +} diff --git a/src/main/java/net/ihe/gazelle/adapter/preferences/Namespaces.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/preferences/Namespaces.java similarity index 100% rename from src/main/java/net/ihe/gazelle/adapter/preferences/Namespaces.java rename to pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/preferences/Namespaces.java diff --git a/src/main/java/net/ihe/gazelle/adapter/preferences/OperationalPreferencesPIXm.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/preferences/OperationalPreferencesPIXm.java similarity index 90% rename from src/main/java/net/ihe/gazelle/adapter/preferences/OperationalPreferencesPIXm.java rename to pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/preferences/OperationalPreferencesPIXm.java index a4a2a98..0e16a07 100644 --- a/src/main/java/net/ihe/gazelle/adapter/preferences/OperationalPreferencesPIXm.java +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/preferences/OperationalPreferencesPIXm.java @@ -14,6 +14,7 @@ public class OperationalPreferencesPIXm implements OperationalPreferencesClientA List<String> deploymentPreferences = new ArrayList<>(); deploymentPreferences.add(Preferences.XREF_PATREG.toString()); + deploymentPreferences.add(Preferences.PATIENT_REGISTRY_URL.toString()); mandatoryPreferences.put(Namespaces.DEPLOYMENT.getValue(), deploymentPreferences); return mandatoryPreferences; diff --git a/src/main/java/net/ihe/gazelle/adapter/preferences/Preferences.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/preferences/Preferences.java similarity index 100% rename from src/main/java/net/ihe/gazelle/adapter/preferences/Preferences.java rename to pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/preferences/Preferences.java diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/servlet/IheHapiFhirServer.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/servlet/IheHapiFhirServer.java new file mode 100644 index 0000000..ecee679 --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/adapter/servlet/IheHapiFhirServer.java @@ -0,0 +1,37 @@ +package net.ihe.gazelle.adapter.servlet; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +import net.ihe.gazelle.business.provider.IhePatientResourceProvider; + +import jakarta.inject.Inject; +import jakarta.servlet.annotation.WebServlet; + +import java.io.Serial; +import java.util.ArrayList; +import java.util.List; + + +@WebServlet(urlPatterns = {"/fhir/*"}, displayName = "FHIR Server IHE") +public class IheHapiFhirServer extends RestfulServer { + @Inject + private IhePatientResourceProvider ihePatientResourceProvider; + @Serial + private static final long serialVersionUID = 1L; + public IheHapiFhirServer() { + super(FhirContext.forR4()); + } + + /** + * This method is called automatically when the + * servlet is initializing. + */ + @Override + public void initialize() { + List<IResourceProvider> providers = new ArrayList<>(); + providers.add(ihePatientResourceProvider); + setResourceProviders(providers); + + } +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/application/ConfigurationAdapter.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/ConfigurationAdapter.java new file mode 100644 index 0000000..4f445b2 --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/ConfigurationAdapter.java @@ -0,0 +1,12 @@ +package net.ihe.gazelle.application; + +public class ConfigurationAdapter { + + public String getProfileIdCreateUpdateDeleteIti104() { return System.getenv("PROFILE_ID_CREATE_UPDATE_DELETE_ITI_104"); } + public String getProfileIdPostIti83() { + return System.getenv("PROFILE_ID_POST_ITI_83"); + } + public String getProfileIdGetIti83() { + return System.getenv("PROFILE_ID_GET_ITI_83"); + } +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/application/PatientRegistryFeedClient.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/PatientRegistryFeedClient.java new file mode 100644 index 0000000..7501639 --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/PatientRegistryFeedClient.java @@ -0,0 +1,340 @@ +package net.ihe.gazelle.application; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import jakarta.inject.Named; +import jakarta.xml.ws.WebServiceException; +import net.ihe.gazelle.adapter.connector.ConversionException; +import net.ihe.gazelle.adapter.connector.FhirToGazelleRegistryConverter; +import net.ihe.gazelle.adapter.connector.GazelleRegistryToFhirConverter; +import net.ihe.gazelle.adapter.preferences.OperationalPreferencesPIXm; +import net.ihe.gazelle.adapter.preferences.Preferences; +import net.ihe.gazelle.app.patientregistryapi.application.PatientFeedException; +import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; +import net.ihe.gazelle.app.patientregistryapi.business.PersonName; +import net.ihe.gazelle.app.patientregistryfeedclient.adapter.PatientFeedClient; +import net.ihe.gazelle.framework.loggerservice.application.GazelleLogger; +import net.ihe.gazelle.framework.loggerservice.application.GazelleLoggerFactory; +import net.ihe.gazelle.framework.operationalpreferencesservice.adapter.JNDIPropertiesLoader; +import net.ihe.gazelle.framework.operationalpreferencesservice.application.PreferencesServiceFactory; +import net.ihe.gazelle.framework.preferencesmodelapi.application.NamespaceException; +import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesClientApplication; +import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; +import net.ihe.gazelle.framework.preferencesmodelapi.application.PreferenceException; +import net.ihe.gazelle.lib.annotations.Package; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Patient; + +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.List; + +@Named("PatientRegistryFeedClient") +public class PatientRegistryFeedClient { + public static final String NO_UUID = "No UUID was retrieved from the created Patient"; + public static final String NO_PATIENT_PARAMETER = "No Patient were given to create"; + public static final String CLIENT_NOT_SET = "client not set"; + public static final String INVALID_PARAMETERS = "Invalid parameters"; + public static final String MANDATORY_FIELD_ARE_MISSING = "Mandatory fields are missing: family name, Date of Birth or Gender"; + private static final String UUID = "PatientPIXmFeed"; + private static final GazelleLogger logger = GazelleLoggerFactory.getInstance().getLogger(PatientRegistryFeedClient.class); + public static final String UUID_CANNOT_BE_NULL_OR_EMPTY = "The uuid cannot be null or empty"; + public static final String CANNOT_PROCEED_TO_DELETE = "Cannot proceed to delete"; + private String serverUrl; + private OperationalPreferencesService operationalPreferencesService; + private PatientFeedClient client; + + public PatientRegistryFeedClient() { + } + + /** + * Default constructor, used for injection. + * + * @param operationalPreferencesService {@link OperationalPreferencesService} used to retrieve Patient Registry's URL. + */ + @Package + public PatientRegistryFeedClient(OperationalPreferencesService operationalPreferencesService) { + + this.operationalPreferencesService = operationalPreferencesService; + } + + /** + * Package-private constructor used for test purposes. + * + * @param url {@link URL} to be used by this to instantiate the processing service to retrieve patients. + */ + @Package + PatientRegistryFeedClient(URL url) { + this.client = new PatientFeedClient(url); + } + + private static boolean areMandatoryFieldsPresent(net.ihe.gazelle.app.patientregistryapi.business.Patient patient) { + PersonName name = patient.getNames().get(0); + return name.getFamily().isBlank() + || name.getGivens().isEmpty() + || patient.getDateOfBirth() == null + || patient.getGender() == null; + } + + private static boolean isNullOrBlank(String uuid) { + return uuid == null || uuid.isBlank(); + } + + @Package + public void setClient(PatientFeedClient client) { + this.client = client; + } + + /** + * Initialize the Search Client using the Operational Preferences Service. + * + * @throws PatientFeedException if the service cannot be instantiated. + */ + private void initializeClient() throws PatientFeedException { + + + + String patientRegistryUrl = null; + getUrlOfServer(); + try { + PreferencesServiceFactory preferencesServiceFactory = new PreferencesServiceFactory(); + OperationalPreferencesService preferencesService = preferencesServiceFactory.createOperationalPreferencesService(new JNDIPropertiesLoader(), getPrefForMyApp()); + + String patRegNamespaceValue = Preferences.PATIENT_REGISTRY_URL.getNamespace().getValue(); + String patRegUrlKey = Preferences.PATIENT_REGISTRY_URL.getKey(); + patientRegistryUrl = preferencesService.getStringValue(patRegNamespaceValue,patRegUrlKey); + this.client = new PatientFeedClient(new URL(patientRegistryUrl)); + } catch (NamespaceException | PreferenceException e) { + throw new PatientFeedException(String.format("Unable to retrieve [%s] Preference !", Preferences.PATIENT_REGISTRY_URL.getKey())); + } catch (MalformedURLException e) { + throw new PatientFeedException(String.format("Preference [%s] with value [%s] is not a valid URL !", + Preferences.PATIENT_REGISTRY_URL.getKey(), + patientRegistryUrl)); + } catch (WebServiceException e) { + logger.warn(e.getMessage()); + throw new PatientFeedException(String.format("Can't connect to patient registry ! at address [%s]", patientRegistryUrl)); + } + } + + OperationalPreferencesClientApplication getPrefForMyApp(){ + return new OperationalPreferencesPIXm(); + } + + /** + * Method called to create a Patient in the Patient Registry Database + * + * @param patient : the patient to add in database, represented in patientRegistry model + * @return a string corresponding to the uuid of the newly created patient in the database. + */ + public Patient createPatient(net.ihe.gazelle.app.patientregistryapi.business.Patient patient) throws PatientFeedException, ConversionException { + if (client == null) { + logger.info(CLIENT_NOT_SET); + initializeClient(); + } + if (patient == null) { + throw new InvalidRequestException(NO_PATIENT_PARAMETER); + } + + if (areMandatoryFieldsPresent(patient)) { + throw new InvalidRequestException(MANDATORY_FIELD_ARE_MISSING); + } + String uuid; + try { + patient.setUuid(UUID); + patient.setActive(true); + uuid = client.createPatient(patient); + patient.setUuid(uuid); + + if (uuid.isBlank()) { + throw new InternalErrorException(NO_UUID); + } + + } catch (PatientFeedException e) { + switch (e.getCause().getMessage()) { + case "Impossible to cross reference the patient (not saved)" -> + throw new InternalErrorException("Impossible to cross reference the patient, it will not be saved !", e); + case "Unexpected Exception persisting Patient !" -> + throw new InternalErrorException("Unexpected Exception persisting Patient !", e); + default -> treatClientBaseErrors(e); + } + } + return GazelleRegistryToFhirConverter.convertPatient(patient); + } + + /** + * Method called to update a Patient in the Patient Registry Database. + * + * @param patient : the Patient object we want to insert in DB + * @return a String corresponding to the uuid confirming the transaction has been successful. + */ + public Patient updatePatient(net.ihe.gazelle.app.patientregistryapi.business.Patient patient, EntityIdentifier identifier) throws PatientFeedException, ConversionException { + if (client == null) { + logger.info(CLIENT_NOT_SET); + initializeClient(); + } + if (patient == null ) { + throw new InvalidRequestException(INVALID_PARAMETERS); + } + patient.setUuid(UUID); + + Patient updatedPatient = null; + try { + updatedPatient = GazelleRegistryToFhirConverter.convertPatient(client.updatePatient(patient, identifier)); + } catch (PatientFeedException e) { + treatClientBaseErrors(e); + } + return updatedPatient; + + + } + + /** + * Method called to merge two Patients in the Patient Registry Database. + * + * @param uuidOriginal : the Patient object we want to insert in DB + * @param uuidDuplicated : The uuid of the patient corresponding to it in DB + * @return a String corresponding to the uuid confirming the transaction has been successful. + */ + //TODO: Implement response for merge + public Bundle mergePatient(String uuidOriginal, String uuidDuplicated) throws PatientFeedException { + if (client == null) { + logger.info(CLIENT_NOT_SET); + initializeClient(); + } + if (isNullOrBlank(uuidOriginal) || isNullOrBlank(uuidDuplicated)) { + throw new InvalidRequestException(INVALID_PARAMETERS); + } + + Bundle response = new Bundle(); + try { + + response.setId(client.mergePatient(uuidOriginal, uuidDuplicated)); + + } catch (PatientFeedException e) { + treatClientBaseErrors(e); + } + return response; + } + + /** + * Deactivate a patient in the Patient Registry Database by adding all linked identifiers to the patient. + * @param itiPatient the patient to deactivate + * @return the deactivated patient + * @throws PatientFeedException If deactivation cannot be performed. + */ + public Patient deactivatePatient(Patient itiPatient, EntityIdentifier identifier) throws PatientFeedException { + if(itiPatient.getActive()){ + throw new InvalidRequestException("Patient not intended to be deactivated"); + } + List<Identifier> identifiersToAdd = itiPatient + .getLink() + .stream() + .map(elm -> elm.getOther().getIdentifier()) + .toList(); + itiPatient.getIdentifier().addAll(identifiersToAdd); + try { + return this.updatePatient(FhirToGazelleRegistryConverter.convertPatient(itiPatient), identifier); + } catch (PatientFeedException | ConversionException e) { + throw new PatientFeedException("Unable to deactivate patient", e); + } + } + + /*** + * + * @param identifier + * @return + * @throws PatientFeedException + */ + public boolean delete(String identifier) throws PatientFeedException { + boolean deletionStatus = Boolean.FALSE; + if (client == null) { + logger.info(CLIENT_NOT_SET); + initializeClient(); + } + if (identifier.isBlank()) { + throw new InvalidRequestException("Invalid parameter"); + } + try { + deletionStatus = client.deletePatient(identifier); + if (!deletionStatus) { + throw new ResourceGoneException("Patient with UUID " + identifier); + } + } catch (PatientFeedException exception) { + switch (exception.getCause().getMessage()) { + case UUID_CANNOT_BE_NULL_OR_EMPTY -> + throw new InternalErrorException(UUID_CANNOT_BE_NULL_OR_EMPTY, exception); + case CANNOT_PROCEED_TO_DELETE -> + throw new InternalErrorException(CANNOT_PROCEED_TO_DELETE, exception); + default -> treatClientBaseErrors(exception); + } + } + return deletionStatus; + } + + public EntityIdentifier delete(EntityIdentifier identifier) throws PatientFeedException { + EntityIdentifier deleted = null; + if (client == null) { + logger.info(CLIENT_NOT_SET); + initializeClient(); + } + if (identifier == null || identifier.getValue() == null) { + throw new InvalidRequestException("Invalid identifier"); + } + try { + deleted = client.deletePatient(identifier); + if (deleted == null) { + throw new ResourceGoneException("Cannot delete patient with identifier " + identifier); + } + } catch (PatientFeedException exception) { + switch (exception.getCause().getMessage()) { + case UUID_CANNOT_BE_NULL_OR_EMPTY -> + throw new InternalErrorException(UUID_CANNOT_BE_NULL_OR_EMPTY, exception); + case CANNOT_PROCEED_TO_DELETE -> + throw new InternalErrorException(CANNOT_PROCEED_TO_DELETE, exception); + default -> treatClientBaseErrors(exception); + } + } + return deleted; + } + + /** + * The method is used to retrieve the url of the current server. + */ + private void getUrlOfServer() { + InetAddress ip; + try { + ip = InetAddress.getLocalHost(); + serverUrl = ip.getCanonicalHostName(); + logger.info(String.format("Your current Hostname : %s", serverUrl)); + + } catch (UnknownHostException exception) { + logger.error("Unable to find serverUrl"); + serverUrl = "ReplaceByTheRightEndpoint"; + } + } + + /** + * Private Method which goal is to process the first levels exceptions thrown by the Patient feed client. + * + * @param e the exception thrown + * @throws InternalErrorException throws back the exception with the right http code and the stack trace. + */ + private void treatClientBaseErrors(Exception e) { + switch (e.getMessage()) { + case "Exception while Mapping with GITB elements !" -> + throw new InternalErrorException("Exception while Mapping with GITB elements !", e); + case "Invalid Response from distant PatientFeedProcessingService !" -> + throw new InternalErrorException("Invalid Response from distant PatientFeedProcessingService !", e); + case "Invalid operation used on distant PatientFeedProcessingService !" -> + throw new InternalErrorException("Invalid operation used on distant PatientFeedProcessingService !", e); + case "Invalid Request sent to distant PatientFeedProcessingService !" -> + throw new InternalErrorException("Invalid Request sent to distant PatientFeedProcessingService !", e); + default -> throw new InternalErrorException("An unhandled error was thrown.", e); + } + } + +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/application/PatientRegistrySearchClient.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/PatientRegistrySearchClient.java new file mode 100644 index 0000000..73bf5e1 --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/PatientRegistrySearchClient.java @@ -0,0 +1,215 @@ +package net.ihe.gazelle.application; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import com.gitb.ps.ProcessingService; +import net.ihe.gazelle.adapter.connector.ConversionException; +import net.ihe.gazelle.adapter.connector.GazelleRegistryToFhirConverter; +import net.ihe.gazelle.adapter.preferences.OperationalPreferencesPIXm; +import net.ihe.gazelle.adapter.preferences.Preferences; +import net.ihe.gazelle.app.patientregistryapi.business.Patient; +import net.ihe.gazelle.app.patientregistryapi.business.PatientSearchCriterionKey; +import net.ihe.gazelle.app.patientregistrysearchclient.adapter.PatientSearchClient; +import net.ihe.gazelle.framework.loggerservice.application.GazelleLogger; +import net.ihe.gazelle.framework.loggerservice.application.GazelleLoggerFactory; +import net.ihe.gazelle.framework.operationalpreferencesservice.adapter.JNDIPropertiesLoader; +import net.ihe.gazelle.framework.operationalpreferencesservice.application.PreferencesServiceFactory; +import net.ihe.gazelle.framework.preferencesmodelapi.application.NamespaceException; +import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesClientApplication; +import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; +import net.ihe.gazelle.framework.preferencesmodelapi.application.PreferenceException; +import net.ihe.gazelle.lib.annotations.Package; +import net.ihe.gazelle.lib.searchmodelapi.business.SearchCriteria; +import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; +import net.ihe.gazelle.lib.searchmodelapi.business.searchcriterion.SearchCriterion; +import net.ihe.gazelle.lib.searchmodelapi.business.searchcriterion.StringSearchCriterion; +import org.hl7.fhir.r4.model.OperationOutcome; + +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.xml.ws.WebServiceException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +@Named("patientRegistrySearchClient") +public class PatientRegistrySearchClient { + + public static final String PROCESSING_UUID_OF_PATIENT = "Processing uuid of Patient"; + public static final String NOT_FOUND = "not-found"; + private static final String INVALID_REQUEST = "Invalid Request sent to distant PatientFeedProcessingService !"; + private static final String INVALID_OPERATION = "Invalid operation used on distant PatientFeedProcessingService !"; + private static final String INVALID_RESPONSE = "Invalid Response from distant PatientFeedProcessingService !"; + private static final String INVALID_MAPPING = "Exception while Mapping with GITB elements !"; + private static final String CONVERSION_ERROR = "A problem occured when Converting Patient from Patient Registry model to hl7 v4"; + private static final String NO_UUID = "No patient Uuid given in parameter"; + private static final String NO_PATIENT_FOUND = "No Patient were found"; + private static final String MORE_THAN_ONE_PATIENT_FOUND = "More than one Patient was found for this ID"; + + private static final GazelleLogger logger = GazelleLoggerFactory.getInstance().getLogger(PatientRegistrySearchClient.class); + private OperationalPreferencesService operationalPreferencesService; + private PatientSearchClient client; + + /** + * Default constructor, used for injection. + * + * @param operationalPreferencesService {@link OperationalPreferencesService} used to retrieve Patient Registry's URL. + */ + @Inject + public PatientRegistrySearchClient(OperationalPreferencesService operationalPreferencesService) { + + this.operationalPreferencesService = operationalPreferencesService; + } + + public PatientRegistrySearchClient() { + + } + + /** + * Package-private constructor used for test purposes. + * + * @param url {@link URL} to be used by this to instantiate the processing service to retrieve patients. + */ + @Package + PatientRegistrySearchClient(URL url) { + this.client = new PatientSearchClient(url); + } + + + /** + * Package-private constructor used for test purposes. + * + * @param service {@link ProcessingService} to be used by this to retrieve Patients. + */ + @Package + PatientRegistrySearchClient(ProcessingService service) { + this.client = new PatientSearchClient(service); + } + + @Package + public void setClient(PatientSearchClient client) { + this.client = client; + } + + /** + * Method To call the Search Client from Patient Registry with the uuid of a Patient. + * + * @param patientUuid the uuid of the Patient as a String + * @return a single Patient (in hl7 v4 model) corresponding to the uuid given + * @throws SearchException + */ + public org.hl7.fhir.r4.model.Patient searchPatient(String patientUuid) throws SearchException { + if (patientUuid == null || patientUuid.isEmpty()) { + logger.error(NO_UUID); + throw new InvalidRequestException(NO_UUID); + } + + if (client == null) { + logger.info("client not set"); + initializeSearchClient(); + } + + logger.info(PROCESSING_UUID_OF_PATIENT); + + + SearchCriteria searchCriteria = new SearchCriteria(); + SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); + searchCriterion.setValue(patientUuid); + searchCriteria.addSearchCriterion(searchCriterion); + + List<org.hl7.fhir.r4.model.Patient> resources = new ArrayList<>(); + + try { + + logger.info("Sending request to patient registry"); + List<Patient> patients = client.search(searchCriteria); + + if (patients.isEmpty()) { + logger.error("No Patient found"); + throw new ResourceNotFoundException(NO_PATIENT_FOUND, generateOperationOutcome(NOT_FOUND, NO_PATIENT_FOUND)); + } + + if (patients.size() > 1) { + logger.error("More than one Patient were found"); + throw new ResourceNotFoundException(MORE_THAN_ONE_PATIENT_FOUND, generateOperationOutcome(NOT_FOUND, MORE_THAN_ONE_PATIENT_FOUND)); + } + + logger.info("Search complete"); + for (Patient pat : patients) { + logger.info("converting patient to right model"); + resources.add(GazelleRegistryToFhirConverter.convertPatient(pat)); + } + } catch (ConversionException e) { + logger.error(CONVERSION_ERROR); + throw new InternalErrorException(CONVERSION_ERROR, e); + } catch (SearchException e) { + logger.warn(e.getMessage()); + switch (e.getMessage()) { + case INVALID_REQUEST: + throw new InvalidRequestException(INVALID_REQUEST, generateOperationOutcome("code-invalid", INVALID_REQUEST)); + case INVALID_OPERATION: + throw new InvalidRequestException(INVALID_OPERATION, generateOperationOutcome("code-invalid", INVALID_OPERATION)); + case INVALID_RESPONSE: + throw new ResourceNotFoundException(INVALID_RESPONSE, generateOperationOutcome(NOT_FOUND, INVALID_RESPONSE)); + case INVALID_MAPPING: + throw new InternalErrorException(INVALID_MAPPING); + default: + throw new InternalErrorException(e); + } + } + + + return resources.get(0); + } + + /** + * Initialize the Search Client using the Operational Preferences Service. + * + * @throws SearchException if the service cannot be instantiated. + */ + private void initializeSearchClient() throws SearchException { + String patientRegistryUrl = null; + try { + PreferencesServiceFactory preferencesServiceFactory = new PreferencesServiceFactory(); + OperationalPreferencesService operationalPreferencesService = preferencesServiceFactory.createOperationalPreferencesService(new JNDIPropertiesLoader(), getPrefForMyApp()); + + patientRegistryUrl = operationalPreferencesService. + getStringValue(Preferences.PATIENT_REGISTRY_URL.getNamespace().getValue(), Preferences.PATIENT_REGISTRY_URL.getKey()); + this.client = new PatientSearchClient(new URL(patientRegistryUrl)); + } catch (NamespaceException | PreferenceException e) { + throw new SearchException(String.format("Unable to retrieve [%s] Preference !", Preferences.PATIENT_REGISTRY_URL.getKey())); + } catch (MalformedURLException e) { + throw new SearchException(String.format("Preference [%s] with value [%s] is not a valid URL !", Preferences.PATIENT_REGISTRY_URL.getKey(), + patientRegistryUrl)); + } catch (WebServiceException e) { + logger.warn(e.getMessage()); + throw new SearchException(String.format("Can't connect to patient registry ! at address [%s]", patientRegistryUrl)); + } + + } + + OperationalPreferencesClientApplication getPrefForMyApp(){ + return new OperationalPreferencesPIXm(); + } + + + + /** + * Method to generate the Error outcome in the response + * + * @param codeString Code of the error catch + * @param diagnostics origin of the issue + * @return generated Operation outcome for the Fhir response + */ + private OperationOutcome generateOperationOutcome(String codeString, String diagnostics) { + OperationOutcome code = new OperationOutcome(); + OperationOutcome.OperationOutcomeIssueComponent issue = new OperationOutcome.OperationOutcomeIssueComponent(); + issue.setSeverity(OperationOutcome.IssueSeverity.ERROR); + issue.setCode(OperationOutcome.IssueType.fromCode(codeString)); + issue.setDiagnostics(diagnostics); + code.addIssue(issue); + return code; + } +} diff --git a/src/main/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClient.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClient.java similarity index 83% rename from src/main/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClient.java rename to pixm-connector-service/src/main/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClient.java index 11e4e77..b3ed05c 100644 --- a/src/main/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClient.java +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClient.java @@ -5,6 +5,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import com.gitb.ps.ProcessingService; +import net.ihe.gazelle.adapter.preferences.OperationalPreferencesPIXm; import net.ihe.gazelle.adapter.preferences.Preferences; import net.ihe.gazelle.app.patientregistryapi.application.SearchCrossReferenceException; import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; @@ -12,7 +13,10 @@ import net.ihe.gazelle.app.patientregistryapi.business.PatientAliases; import net.ihe.gazelle.app.patientregistryxrefsearchclient.adapter.XRefSearchClient; import net.ihe.gazelle.framework.loggerservice.application.GazelleLogger; import net.ihe.gazelle.framework.loggerservice.application.GazelleLoggerFactory; +import net.ihe.gazelle.framework.operationalpreferencesservice.adapter.JNDIPropertiesLoader; +import net.ihe.gazelle.framework.operationalpreferencesservice.application.PreferencesServiceFactory; import net.ihe.gazelle.framework.preferencesmodelapi.application.NamespaceException; +import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesClientApplication; import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; import net.ihe.gazelle.framework.preferencesmodelapi.application.PreferenceException; import net.ihe.gazelle.lib.annotations.Package; @@ -21,9 +25,9 @@ import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Reference; -import javax.inject.Inject; -import javax.inject.Named; -import javax.xml.ws.WebServiceException; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.xml.ws.WebServiceException; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; @@ -98,7 +102,10 @@ public class PatientRegistryXRefSearchClient { if (client == null) { String patientRegistryUrl = null; try { - patientRegistryUrl = this.operationalPreferencesService. + PreferencesServiceFactory preferencesServiceFactory = new PreferencesServiceFactory(); + OperationalPreferencesService operationalPreferencesService = preferencesServiceFactory.createOperationalPreferencesService(new JNDIPropertiesLoader(), getPrefForMyApp()); + + patientRegistryUrl = operationalPreferencesService. getStringValue(Preferences.XREF_PATREG.getNamespace().getValue(), Preferences.XREF_PATREG.getKey()); this.client = new XRefSearchClient(new URL(patientRegistryUrl)); @@ -119,6 +126,11 @@ public class PatientRegistryXRefSearchClient { } + OperationalPreferencesClientApplication getPrefForMyApp(){ + return new OperationalPreferencesPIXm(); + } + + /** * Proceed to the request using the XReferencePatientRegistry client and prepare the fhir response * @@ -134,18 +146,16 @@ public class PatientRegistryXRefSearchClient { patientAliases = client.search(sourceIdentifier, targetSystemList); } catch (SearchCrossReferenceException searchCrossReferenceException) { switch (searchCrossReferenceException.getCause().getMessage()) { - case ERROR_IN_THE_SOURCE_IDENTIFIER_SYSTEM_DOES_NOT_EXIT: - case THE_SYSTEM_IDENTIFIER_FROM_SOURCE_IDENTIFIER_CANNOT_BE_NULL_OR_EMPTY: - throw new InvalidRequestException(SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND, generateOperationOutcome("code-invalid", - SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND)); - case ERROR_IN_THE_SOURCE_IDENTIFIER_IT_DOES_NOT_MATCH_ANY_PATIENT: - case ERROR_IN_THE_SOURCE_IDENTIFIER_IT_DOES_NOT_MATCH_ANY_IDENTITY: - throw new ResourceNotFoundException(SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND, generateOperationOutcome("not-found", - SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND)); - case ONE_OF_THE_TARGET_DOMAIN_DOES_NOT_EXIST: - throw new ForbiddenOperationException(TARGET_SYSTEM_NOT_FOUND, generateOperationOutcome("code-invalid", TARGET_SYSTEM_NOT_FOUND)); - default: - throw new InternalErrorException("An internal error occurred", searchCrossReferenceException); + case ERROR_IN_THE_SOURCE_IDENTIFIER_SYSTEM_DOES_NOT_EXIT, THE_SYSTEM_IDENTIFIER_FROM_SOURCE_IDENTIFIER_CANNOT_BE_NULL_OR_EMPTY -> + throw new InvalidRequestException(SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND, generateOperationOutcome("code-invalid", + SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND)); + case ERROR_IN_THE_SOURCE_IDENTIFIER_IT_DOES_NOT_MATCH_ANY_PATIENT, ERROR_IN_THE_SOURCE_IDENTIFIER_IT_DOES_NOT_MATCH_ANY_IDENTITY -> + throw new ResourceNotFoundException(SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND, generateOperationOutcome("not-found", + SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND)); + case ONE_OF_THE_TARGET_DOMAIN_DOES_NOT_EXIST -> + throw new ForbiddenOperationException(TARGET_SYSTEM_NOT_FOUND, generateOperationOutcome("code-invalid", TARGET_SYSTEM_NOT_FOUND)); + default -> + throw new InternalErrorException("An internal error occurred", searchCrossReferenceException); } } @@ -174,7 +184,7 @@ public class PatientRegistryXRefSearchClient { Parameters.ParametersParameterComponent targetId = new Parameters.ParametersParameterComponent(); targetId.setName("targetId"); Reference ref = new Reference(); - ref.setReference("https://" + serverUrl + "/pixm-connector/fhir_ihe/Patient/" + pat.getUuid()); + ref.setReference("https://" + serverUrl + "/pixm-connector/fhir/ihe/Patient/" + pat.getUuid()); targetId.setValue(ref); parameters.addParameter(targetId); diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/application/ProfilesValidators.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/ProfilesValidators.java new file mode 100644 index 0000000..f70655d --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/ProfilesValidators.java @@ -0,0 +1,126 @@ +package net.ihe.gazelle.application; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import jakarta.servlet.http.HttpServletRequest; +import net.ihe.gazelle.http.validator.client.interlay.validation.HttpValidationServiceFactory; +import net.ihe.gazelle.matchbox.client.interlay.validation.CustomPatientServiceFactory; +import net.ihe.gazelle.validation.api.application.ValidationService; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationReport; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationSubReport; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationTestResult; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationItem; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationRequest; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.codesystems.HttpVerb; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +public interface ProfilesValidators { + + String CRLF = "\r\n"; + String PROBLEM_DURING_VALIDATION = "Problem during Validation of Pixm Patient: "; + + List<ValidationReport> validateRequest(HttpServletRequest request, Resource resource, String profileId); + + + default ValidationReport validateWithHttpValidator(HttpServletRequest request, String profileId) { + String messageToValidate = getFullHttpMessageFromRequest(request); + ValidationRequest validationRequest = constructValidationRequest(messageToValidate, profileId); + validationRequest.setValidationServiceName("HTTP Validator"); + ValidationService patientValidationService = new HttpValidationServiceFactory().getValidationService(); + return patientValidationService.validate(validationRequest); + } + + default ValidationRequest constructValidationRequest(Object obj, String profileId) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ValidationRequest validationRequest = new ValidationRequest(); + validationRequest.setValidationProfileId(profileId); + byte[] data; + + if (obj instanceof Resource) { + String resourceToString = FhirContext.forR4().newJsonParser().encodeResourceToString((IBaseResource) obj); + data = resourceToString.getBytes(StandardCharsets.UTF_8); + } else if (obj instanceof String) { + data = ((String) obj).getBytes(StandardCharsets.UTF_8); + } else { + try (ObjectOutputStream outputStream = new ObjectOutputStream(baos)) { + outputStream.writeObject(obj); + data = baos.toByteArray(); + } catch (IOException e) { + throw new UnprocessableEntityException(PROBLEM_DURING_VALIDATION, e); + } + } + validationRequest.setValidationItems(List.of(new ValidationItem().setContent(data))); + return validationRequest; + } + + default String getFullHttpMessageFromRequest(HttpServletRequest request) { + String url = (request != null) + ? getFullURL(request) + : ""; + String httpMessage = (request != null) + ? formatHttpMessageIntoOneString(request) + : ""; + + StringBuilder sb = new StringBuilder(url); + sb.append(CRLF) + .append(httpMessage) + .append(CRLF); + return sb.toString(); + } + + default String getFullURL(HttpServletRequest request) { + StringBuilder sb; + sb = new StringBuilder(); + sb.append(request.getMethod()); + sb.append(" "); + sb.append(request.getRequestURL()); + if(null != request.getQueryString()){ + sb.append("?"); + sb.append(request.getQueryString()); + } + sb.append(" "); + sb.append(request.getProtocol()); + return sb.toString(); + } + + default Map<String, String> getAllHeaders(HttpServletRequest request) { + return Collections.list(request.getHeaderNames()) + .stream() + .collect(Collectors.toMap(h -> h, request::getHeader)); + } + + default String formatHttpMessageIntoOneString(HttpServletRequest request) { + Map<String, String> headers = getAllHeaders(request); + StringBuilder sb = new StringBuilder(); + for (Map.Entry<String, String> header : headers.entrySet()) { + sb.append(header.getKey()) + .append(": ") + .append(header.getValue()) + .append(CRLF); + } + return sb.toString(); + } + + + default boolean isPostOrPutRequest(HttpServletRequest request) { + return HttpVerb.POST.toCode().equals(request.getMethod()) || HttpVerb.PUT.toCode().equals(request.getMethod()); + } + + default ValidationReport validateWithFhir(Resource resource, String profileId) { + ValidationRequest validationRequest; + validationRequest = constructValidationRequest(resource, profileId); + ValidationService patientValidationService = new CustomPatientServiceFactory().getValidationService(); + return patientValidationService.validate(validationRequest); + } + +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/application/ProfilesValidatorsFactory.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/ProfilesValidatorsFactory.java new file mode 100644 index 0000000..d6b2abe --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/application/ProfilesValidatorsFactory.java @@ -0,0 +1,23 @@ +package net.ihe.gazelle.application; + +/** + * The factory has the responsibility of creating the correct formatter for a given document type. + * The factory is also responsible for determining if it supports a given document type. + * To learn more about the factory pattern, + * see <a href="https://refactoring.guru/design-patterns/factory-method/java/example">Factory Method</a> + */ +public interface ProfilesValidatorsFactory { + + /** + * Creates a formatter for a given document type. + * The type is already known as each factory only supports one type. + * + * @return a formatter + */ + ProfilesValidators createProfileValidator(); + + boolean supports(String profilId); + + String getProfileId(); + +} diff --git a/src/main/java/net/ihe/gazelle/business/interceptor/LegacyLoggingInterceptor.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/business/interceptor/LegacyLoggingInterceptor.java similarity index 94% rename from src/main/java/net/ihe/gazelle/business/interceptor/LegacyLoggingInterceptor.java rename to pixm-connector-service/src/main/java/net/ihe/gazelle/business/interceptor/LegacyLoggingInterceptor.java index 4a94ec2..1678868 100644 --- a/src/main/java/net/ihe/gazelle/business/interceptor/LegacyLoggingInterceptor.java +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/business/interceptor/LegacyLoggingInterceptor.java @@ -9,14 +9,13 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; public class LegacyLoggingInterceptor extends InterceptorAdapter { private static final Logger ourLog = LoggerFactory.getLogger(LegacyLoggingInterceptor.class); - @Override public boolean incomingRequestPostProcessed(RequestDetails theRequest, HttpServletRequest theSrvRequest, HttpServletResponse theSrvResponse) { @@ -26,7 +25,6 @@ public class LegacyLoggingInterceptor extends InterceptorAdapter { return true; // Processing should continue } - @Override @Hook(Pointcut.SERVER_HANDLE_EXCEPTION) public boolean handleException( RequestDetails theRequestDetails, diff --git a/src/main/java/net/ihe/gazelle/business/interceptor/NewLoggingInterceptor.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/business/interceptor/NewLoggingInterceptor.java similarity index 100% rename from src/main/java/net/ihe/gazelle/business/interceptor/NewLoggingInterceptor.java rename to pixm-connector-service/src/main/java/net/ihe/gazelle/business/interceptor/NewLoggingInterceptor.java diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/business/provider/IhePatientResourceProvider.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/business/provider/IhePatientResourceProvider.java new file mode 100644 index 0000000..8ca812c --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/business/provider/IhePatientResourceProvider.java @@ -0,0 +1,357 @@ +package net.ihe.gazelle.business.provider; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.annotation.*; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.servlet.http.HttpServletRequest; +import net.ihe.gazelle.adapter.connector.ConversionException; +import net.ihe.gazelle.adapter.connector.FhirToGazelleRegistryConverter; +import net.ihe.gazelle.app.patientregistryapi.application.PatientFeedException; +import net.ihe.gazelle.app.patientregistryapi.application.SearchCrossReferenceException; +import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; +import net.ihe.gazelle.application.ConfigurationAdapter; +import net.ihe.gazelle.application.PatientRegistryFeedClient; +import net.ihe.gazelle.application.PatientRegistrySearchClient; +import net.ihe.gazelle.application.PatientRegistryXRefSearchClient; +import net.ihe.gazelle.business.service.RequestValidatorService; +import net.ihe.gazelle.lib.annotations.Package; +import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationReport; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationTestResult; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationItem; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is a resource provider which stores Patient resources in memory using a HashMap. This is obviously not a production-ready solution for many + * reasons, + * but it is useful to help illustrate how to build a fully-functional server. + */ +@Named("ihePatientResourceProvider") +public class IhePatientResourceProvider implements IResourceProvider { + + public static final String SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND = "sourceIdentifier or Patient Identifier not found"; + public static final String TARGET_SYSTEM_NOT_FOUND = "targetSystem not found"; + public static final String NO_IDENTIFIER_PROVIDED = "No Identifier provided"; + public static final String NO_PATIENT_PROVIDED = "No Patient Provided"; + public static final String PROBLEM_DURING_VALIDATION = "Problem during Validation of Pixm Patient: "; + public static final String SOURCE_IDENTIFIER = "sourceIdentifier"; + public static final String TARGET_SYSTEM = "targetSystem"; + public static final String FHIR_PATIENT_COULD_NOT_BE_CONVERTED_TO_REGISTRY_PATIENT = "Patient Could not be converted to HL7 Patient"; + public static final String PROBLEM_DURING_CREATING_PATIENT_IN_PATIENT_REGISTRY = "Problem during creating patient in Patient registry"; + public static final String ERROR_IN_DELETION = "Error in deletion"; + public static final String PATIENT_FEED_CLIENT_IS_NOT_SET = "Patient Feed client is not set"; + public static final String PATIENT_SUCCESSFULLY_FOUND = "Patient Successfully found"; + public static final String PATIENT_COULD_NOT_BE_RETRIEVED = "Patient could not be retrieved"; + private static final Logger patientLogger = LoggerFactory.getLogger(IhePatientResourceProvider.class); + private static final String URN_OID = "urn:oid:"; + @Inject + public RequestValidatorService requestValidatorService; + @Inject + protected PatientRegistryXRefSearchClient patientRegistryXRefSearchClient; + @Inject + protected PatientRegistrySearchClient patientRegistrySearchClient; + @Inject + protected PatientRegistryFeedClient patientRegistryFeedClient; + + @Inject + protected ConfigurationAdapter configurationAdapter; + + + + private IhePatientResourceProvider() { + } + + @Package + public IhePatientResourceProvider(PatientRegistryXRefSearchClient xRefClient, + PatientRegistrySearchClient client, + PatientRegistryFeedClient patientFeedClient, + RequestValidatorService requestValidatorService, + ConfigurationAdapter configurationAdapter) { + this.requestValidatorService = requestValidatorService; + this.patientRegistryXRefSearchClient = xRefClient; + this.patientRegistrySearchClient = client; + this.configurationAdapter = configurationAdapter; + this.patientRegistryFeedClient = patientFeedClient; + } + + /** + * The getResourceType method comes from IResourceProvider, and must be overridden to indicate what type of resource this provider supplies. + */ + @Override + public Class<? extends IBaseResource> getResourceType() { + return Patient.class; + } + + /** + * Method called to create a Patient on Patient Registry + * + * @param iti104Patient : the Patient to create. + * @return a MethodOutcome containing a representation of the patient created. + */ + @Create + public MethodOutcome create(@ResourceParam Patient iti104Patient, HttpServletRequest request) throws UnprocessableEntityException { + checkIfPatientExist(iti104Patient); + String profileId = configurationAdapter.getProfileIdCreateUpdateDeleteIti104(); + validateInputs(request, iti104Patient, profileId); + return addNewPatientIntoRegistry(iti104Patient); + } + + /** + * Method to Update a patient related to PIXm Delete Method + * + * @param theConditional the identifier of the Patient we want to update + * @param iti104Patient The Bundle content of the patient + * @return FhirBundle that contains the updated patient + */ + @Update + public MethodOutcome update(@IdParam IdType theId, @ConditionalUrlParam String theConditional, @ResourceParam Patient iti104Patient, HttpServletRequest request) { + checkIfPatientExist(iti104Patient); + checkIfIdIsPresent(theConditional, iti104Patient); + + + validateInputs(request, iti104Patient, configurationAdapter.getProfileIdCreateUpdateDeleteIti104()); + // the '?' is surely not at the beginning of the string + EntityIdentifier identifier = createEntityIdentifierFromConditional(theConditional); + if(identifier == null){ + throw new UnprocessableEntityException(NO_IDENTIFIER_PROVIDED); + } + try { + Patient patientUpdated; + if(iti104Patient.hasActive() && !iti104Patient.getActive()){ // this forces the true as a default value + patientUpdated = patientRegistryFeedClient.deactivatePatient(iti104Patient, identifier); + } + else{ + patientUpdated = patientRegistryFeedClient.updatePatient(FhirToGazelleRegistryConverter.convertPatient(iti104Patient), identifier); + } + + MethodOutcome methodOutcome = new MethodOutcome(); + methodOutcome.setResource(patientUpdated); + return methodOutcome; + } catch (ConversionException e) { + throw new UnprocessableEntityException(FHIR_PATIENT_COULD_NOT_BE_CONVERTED_TO_REGISTRY_PATIENT, e); + } catch (PatientFeedException e) { + throw new UnprocessableEntityException(PATIENT_FEED_CLIENT_IS_NOT_SET); + } + + } + + /** + * Method to delete a Patient related to PIXm Delete Method + * + * @param theConditional of the patient to delete + * @return FhirBundle that contains the deletion status + */ + @Delete + public MethodOutcome delete(@IdParam IdType theId, @ConditionalUrlParam (supportsMultiple = true) String theConditional, HttpServletRequest request) { + + if (theConditional == null || theConditional.isEmpty()) { + throw new UnprocessableEntityException(NO_IDENTIFIER_PROVIDED); + } + + validateInputs(request, null, configurationAdapter.getProfileIdCreateUpdateDeleteIti104()); + + try { + EntityIdentifier identifier = createEntityIdentifierFromConditional(theConditional); + EntityIdentifier deleted = patientRegistryFeedClient.delete(identifier); + MethodOutcome methodOutcome = new MethodOutcome(); + int responseStatusCode = deleted == null || deleted.getValue() == null || deleted.getValue().isBlank() + ? 204 + : 200; + methodOutcome.setResponseStatusCode(responseStatusCode); + return methodOutcome; + } catch (Exception e) { + throw new UnprocessableEntityException(ERROR_IN_DELETION, e); + } + } + + + /** + * Method to read a Patient through its uuid in the patient Manager database + * + * @param theId Id Type given in the url. represents the uuid of the searched patient + * @return One patient corresponding to the uuid given in entry. If many Patients are found, we consider it as an error and won't return ANY + * patient. + */ + @Read + public Patient read(@IdParam IdType theId) { + String uuid = theId.getIdPart(); + if (uuid == null || uuid.isBlank()) { + patientLogger.error(NO_IDENTIFIER_PROVIDED); + throw new UnprocessableEntityException(NO_IDENTIFIER_PROVIDED); + } + try { + Patient retrievedPatient = patientRegistrySearchClient.searchPatient(uuid); + patientLogger.info(PATIENT_SUCCESSFULLY_FOUND); + return retrievedPatient; + } catch (SearchException e) { + throw new UnprocessableEntityException(PATIENT_COULD_NOT_BE_RETRIEVED, e); + } + } + + /** + * Search method for a Patient using the source identifier required parameter + * and an optional list of target system + * + * @param sourceIdentifierParam : the source identifier of the patient, should be formatted "urn:oid:x.x.x.x.x.x.x.x.x.x|value" + * @param targetSystemParam: the target System(s) we want to find the Patient on, should be formatted "urn:oid:x.x.x.x.x.x.x.x.x.x, + * urn:oid:x.x.x.x.x.x.x.x.x.x" + * @return a Parameters element composed of a list of target identifier for every Patient found, and an url to the Patient in the server. + */ + @Operation(name = "$ihe-pix", idempotent = true) + public Parameters findCorrespondingIdentifiersWithGet(@OperationParam(name = SOURCE_IDENTIFIER) TokenAndListParam sourceIdentifierParam, + @OperationParam(name = TARGET_SYSTEM) StringAndListParam targetSystemParam, HttpServletRequest request) { + + validateInputs(request, null, configurationAdapter.getProfileIdGetIti83()); + + EntityIdentifier sourceIdentifier = createEntityIdentifierFromSourceIdentifier(sourceIdentifierParam); + List<String> targetSystemList = createTargetSystemListFromParam(targetSystemParam); + return getParametersFromIdentifierAndTarget(sourceIdentifier, targetSystemList); + + } + + //--------------------Private/protected methods-------------------// + + private Parameters getParametersFromIdentifierAndTarget(EntityIdentifier sourceIdentifier, List<String> targetSystemList) { + Parameters parametersResults; + try { + parametersResults = patientRegistryXRefSearchClient.process(sourceIdentifier, targetSystemList); + } catch (SearchCrossReferenceException | UnprocessableEntityException e) { + throw new UnprocessableEntityException(e.getMessage()); + } + return parametersResults; + } + + private EntityIdentifier createEntityIdentifierFromSourceIdentifier(TokenAndListParam sourceIdentifier) { + if (sourceIdentifier == null || sourceIdentifier.size() == 0) { + throw new UnprocessableEntityException(SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND); + } + TokenOrListParam tokenOrListParams = sourceIdentifier.getValuesAsQueryTokens().get(0); + TokenParam source = tokenOrListParams.getValuesAsQueryTokens().get(0); + String sourceIdentifierSystem = source.getSystem(); + String sourceIdentifierValue = source.getValue(); + return FhirToGazelleRegistryConverter.convertSourceIdentiferToEntityIdentifier(sourceIdentifierSystem, sourceIdentifierValue); + } + + private EntityIdentifier createEntityIdentifierFromConditional(String conditional){ + String identifier = conditional.contains("=") ? conditional.substring(conditional.indexOf("=")+1) : null; + //clear urn:oid: if present (need to be improved so this action handled by the UI) + if(identifier != null) { + identifier = identifier.replace(URN_OID, ""); + } + else{ + return null; + } + String[] valueAndSystem = identifier.split("%7C"); + if(valueAndSystem.length != 2){ + throw new UnprocessableEntityException("Identifier does not respect the format : value|system"); + } + EntityIdentifier entityIdentifier = new EntityIdentifier(); + entityIdentifier.setValue(valueAndSystem[1]); + entityIdentifier.setSystemIdentifier(valueAndSystem[0]); + return entityIdentifier; + } + + + private List<String> createTargetSystemListFromParam(StringAndListParam targetSystemParam) { + List<String> targetSystemList = new ArrayList<>(); + if (targetSystemParam == null || targetSystemParam.size() == 0) { + throw new UnprocessableEntityException(TARGET_SYSTEM_NOT_FOUND); + } + for (StringOrListParam listParam : targetSystemParam.getValuesAsQueryTokens()) { + List<StringParam> queryStrings = listParam.getValuesAsQueryTokens(); + buildTargetSystemList(queryStrings, targetSystemList); + } + return targetSystemList; + } + + private void buildTargetSystemList(List<StringParam> list, List<String> targetSystemList) { + for (StringParam singleParam : list) { + String singleParamValue = singleParam.getValue(); + if (singleParamValue.contains(URN_OID)) { + singleParamValue = singleParamValue.replace(URN_OID, ""); + } + targetSystemList.add(singleParamValue); + } + } + + + private void checkIfPatientExist(Patient iti104Patient) { + if (iti104Patient == null) { + patientLogger.error(NO_PATIENT_PROVIDED); + throw new UnprocessableEntityException(NO_PATIENT_PROVIDED); + } + } + + private void checkIfIdIsPresent(String theId, Patient iti104Patient) { + if (theId == null || theId.isBlank() || !iti104Patient.hasIdentifier()) { + patientLogger.error(NO_IDENTIFIER_PROVIDED); + throw new UnprocessableEntityException(NO_IDENTIFIER_PROVIDED); + } + } + + private MethodOutcome addNewPatientIntoRegistry(Patient iti104Patient) { + try { + Patient newPatient = patientRegistryFeedClient.createPatient(FhirToGazelleRegistryConverter.convertPatient(iti104Patient)); + if (newPatient != null) { + MethodOutcome methodOutcome = new MethodOutcome(); + methodOutcome.setResource(newPatient); + methodOutcome.setCreated(true); + return methodOutcome; + } else { + throw new UnprocessableEntityException(PROBLEM_DURING_CREATING_PATIENT_IN_PATIENT_REGISTRY); + } + } catch (ConversionException e) { + throw new UnprocessableEntityException(FHIR_PATIENT_COULD_NOT_BE_CONVERTED_TO_REGISTRY_PATIENT); + } catch (PatientFeedException e) { + throw new UnprocessableEntityException(PATIENT_FEED_CLIENT_IS_NOT_SET); + } + } + + private OperationOutcome getOperationOutcome(ValidationReport report) { + OperationOutcome oo = new OperationOutcome(); + for (ValidationItem item : report.getValidationItems()) { + if ("matchbox-validation".equals(item.getItemId())) { + String returnedMessage = new String(item.getContent()); + oo = createFhirResourceFromString(OperationOutcome.class, returnedMessage); + } + if ("http-validation".equals(item.getItemId())) { + OperationOutcome.OperationOutcomeIssueComponent issue = new OperationOutcome.OperationOutcomeIssueComponent(); + issue.setSeverity(OperationOutcome.IssueSeverity.ERROR); + issue.setCode(OperationOutcome.IssueType.PROCESSING); + issue.setDiagnostics(new String(item.getContent())); + oo.addIssue(issue); + } + } + return oo; + } + + protected <T extends IBaseResource> T createFhirResourceFromString(Class<T> tClass, String input) { + FhirContext ctx = FhirContext.forR4(); + IParser parser = ctx.newJsonParser(); + parser.setPrettyPrint(true); + return parser.parseResource(tClass, input); + } + + private void validateInputs(HttpServletRequest request, Resource resource, String profileId) throws InvalidRequestException { + List<ValidationReport> validationReports = requestValidatorService.validateRequest(request, resource, profileId); + for (ValidationReport vr : validationReports) { + if (!ValidationTestResult.PASSED.equals(vr.getOverallResult())) { + OperationOutcome oo = getOperationOutcome(vr); + throw new InvalidRequestException(PROBLEM_DURING_VALIDATION, oo); + } + } + } +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/business/service/RequestValidatorService.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/business/service/RequestValidatorService.java new file mode 100644 index 0000000..865b914 --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/business/service/RequestValidatorService.java @@ -0,0 +1,27 @@ +package net.ihe.gazelle.business.service; + +import jakarta.servlet.http.HttpServletRequest; +import net.ihe.gazelle.application.ProfilesValidators; +import net.ihe.gazelle.application.ProfilesValidatorsFactory; +import net.ihe.gazelle.interlay.profiles.ProfilesValidatorsFactoryProvider; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationReport; +import org.hl7.fhir.r4.model.Resource; + +import java.util.List; + +public class RequestValidatorService { + + private final ProfilesValidatorsFactoryProvider profilesValidatorsFactoryProvider; + + public RequestValidatorService() { + this.profilesValidatorsFactoryProvider = new ProfilesValidatorsFactoryProvider(); + } + + public List<ValidationReport> validateRequest(HttpServletRequest request, Resource resource, String profileId) { + ProfilesValidatorsFactory profilesValidatorsFactory = profilesValidatorsFactoryProvider.createProfileValidatorFactory(profileId); + ProfilesValidators validators = profilesValidatorsFactory.createProfileValidator(); + return validators.validateRequest(request, resource, profileId); + } + + +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI104PatientFeedQueryProfile.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI104PatientFeedQueryProfile.java new file mode 100644 index 0000000..439f652 --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI104PatientFeedQueryProfile.java @@ -0,0 +1,26 @@ +package net.ihe.gazelle.interlay.profiles; + +import jakarta.servlet.http.HttpServletRequest; +import net.ihe.gazelle.application.ProfilesValidators; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationReport; +import org.hl7.fhir.r4.model.Resource; +import java.util.ArrayList; +import java.util.List; + + +public class ITI104PatientFeedQueryProfile implements ProfilesValidators { + + public static final String PIXM_PATIENT_PROFILE = System.getenv("PIXM_PATIENT_PROFILE"); + + + @Override + public List<ValidationReport> validateRequest(HttpServletRequest request, Resource resource, String profileId) { + List<ValidationReport> reports = new ArrayList<>(); + if (isPostOrPutRequest(request)) { + reports.add(validateWithFhir(resource, PIXM_PATIENT_PROFILE)); + } + reports.add(validateWithHttpValidator(request, profileId)); + return reports; + } + +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI104PatientFeedQueryProfileFactory.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI104PatientFeedQueryProfileFactory.java new file mode 100644 index 0000000..42aec43 --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI104PatientFeedQueryProfileFactory.java @@ -0,0 +1,24 @@ +package net.ihe.gazelle.interlay.profiles; + +import net.ihe.gazelle.application.ProfilesValidators; +import net.ihe.gazelle.application.ProfilesValidatorsFactory; + +public class ITI104PatientFeedQueryProfileFactory implements ProfilesValidatorsFactory { + + public static final String PROFILE_ID_CREATE_UPDATE_DELETE_ITI_104 = "PROFILE_ID_CREATE_UPDATE_DELETE_ITI_104"; + + @Override + public ProfilesValidators createProfileValidator() { + return new ITI104PatientFeedQueryProfile(); + } + + @Override + public boolean supports(String profileValidatorId) { + return getProfileId().equals(profileValidatorId); + } + + @Override + public String getProfileId() { + return System.getenv(PROFILE_ID_CREATE_UPDATE_DELETE_ITI_104); + } +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83GetPIXmQueryProfile.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83GetPIXmQueryProfile.java new file mode 100644 index 0000000..795fbb0 --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83GetPIXmQueryProfile.java @@ -0,0 +1,21 @@ +package net.ihe.gazelle.interlay.profiles; + +import jakarta.servlet.http.HttpServletRequest; +import net.ihe.gazelle.application.ProfilesValidators; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationReport; +import org.hl7.fhir.r4.model.Resource; + +import java.util.ArrayList; +import java.util.List; + +public class ITI83GetPIXmQueryProfile implements ProfilesValidators { + + @Override + public List<ValidationReport> validateRequest(HttpServletRequest request, Resource resource, String profileId) { + List<ValidationReport> reports = new ArrayList<>(); + reports.add(validateWithHttpValidator(request, profileId)); + return reports; + } + + +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83GetPIXmQueryProfileFactory.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83GetPIXmQueryProfileFactory.java new file mode 100644 index 0000000..213681d --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83GetPIXmQueryProfileFactory.java @@ -0,0 +1,24 @@ +package net.ihe.gazelle.interlay.profiles; + +import net.ihe.gazelle.application.ProfilesValidators; +import net.ihe.gazelle.application.ProfilesValidatorsFactory; + +public class ITI83GetPIXmQueryProfileFactory implements ProfilesValidatorsFactory { + + public static final String PROFILE_ID_GET_ITI_83 = "PROFILE_ID_GET_ITI_83"; + + @Override + public ProfilesValidators createProfileValidator() { + return new ITI83GetPIXmQueryProfile(); + } + + @Override + public boolean supports(String profileValidatorId) { + return getProfileId().equals(profileValidatorId); + } + + @Override + public String getProfileId() { + return System.getenv(PROFILE_ID_GET_ITI_83); + } +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83PostPIXmQueryProfile.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83PostPIXmQueryProfile.java new file mode 100644 index 0000000..1685ec3 --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83PostPIXmQueryProfile.java @@ -0,0 +1,27 @@ +package net.ihe.gazelle.interlay.profiles; + +import jakarta.servlet.http.HttpServletRequest; +import net.ihe.gazelle.application.ProfilesValidators; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationReport; +import org.hl7.fhir.r4.model.Resource; + +import java.util.ArrayList; +import java.util.List; + +public class ITI83PostPIXmQueryProfile implements ProfilesValidators { + + public static final String PIXM_PARAMETERS_PROFILE = System.getenv("PIXM_PARAMETERS_PROFILE"); + + @Override + public List<ValidationReport> validateRequest(HttpServletRequest request, Resource resource, String profileId) { + List<ValidationReport> reports = new ArrayList<>(); + if (isPostOrPutRequest(request)) { + reports.add(validateWithFhir(resource, PIXM_PARAMETERS_PROFILE)); + } + reports.add(validateWithHttpValidator(request, profileId)); + + return reports; + } + + +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83PostPIXmQueryProfileFactory.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83PostPIXmQueryProfileFactory.java new file mode 100644 index 0000000..fdc7f7f --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ITI83PostPIXmQueryProfileFactory.java @@ -0,0 +1,23 @@ +package net.ihe.gazelle.interlay.profiles; + +import net.ihe.gazelle.application.ProfilesValidators; +import net.ihe.gazelle.application.ProfilesValidatorsFactory; + +public class ITI83PostPIXmQueryProfileFactory implements ProfilesValidatorsFactory { + public static final String PROFILE_ID_POST_ITI_83 = "PROFILE_ID_POST_ITI_83"; + + @Override + public ProfilesValidators createProfileValidator() { + return new ITI83PostPIXmQueryProfile(); + } + + @Override + public boolean supports(String profileValidatorId) { + return getProfileId().equals(profileValidatorId); + } + + @Override + public String getProfileId() { + return System.getenv(PROFILE_ID_POST_ITI_83); + } +} diff --git a/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ProfilesValidatorsFactoryProvider.java b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ProfilesValidatorsFactoryProvider.java new file mode 100644 index 0000000..e06223b --- /dev/null +++ b/pixm-connector-service/src/main/java/net/ihe/gazelle/interlay/profiles/ProfilesValidatorsFactoryProvider.java @@ -0,0 +1,26 @@ +package net.ihe.gazelle.interlay.profiles; + +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import net.ihe.gazelle.application.ProfilesValidatorsFactory; + +import java.util.ServiceLoader; + +public class ProfilesValidatorsFactoryProvider { + + public static final String NO_VALIDATOR_FOUND_FOR_THIS_PROFIL_ID = "No validator found for this profilID: "; + + public ProfilesValidatorsFactory createProfileValidatorFactory(String profilID) { + Iterable<ProfilesValidatorsFactory> allProfilesValidators = getListOfAllProfilesValidators(); + + for (ProfilesValidatorsFactory profilesValidatorsFactory : allProfilesValidators) { + if (profilesValidatorsFactory.supports(profilID)) { + return profilesValidatorsFactory; + } + } + throw new UnprocessableEntityException(NO_VALIDATOR_FOUND_FOR_THIS_PROFIL_ID + profilID); + } + + private Iterable<ProfilesValidatorsFactory> getListOfAllProfilesValidators() { + return ServiceLoader.load(ProfilesValidatorsFactory.class); + } +} diff --git a/src/main/resources/META-INF/ejb-jar.xml b/pixm-connector-service/src/main/resources/META-INF/ejb-jar.xml similarity index 66% rename from src/main/resources/META-INF/ejb-jar.xml rename to pixm-connector-service/src/main/resources/META-INF/ejb-jar.xml index ad79b32..8c7ac93 100644 --- a/src/main/resources/META-INF/ejb-jar.xml +++ b/pixm-connector-service/src/main/resources/META-INF/ejb-jar.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<ejb-jar xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" +<ejb-jar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd" version="3.0"> diff --git a/pixm-connector-service/src/main/resources/META-INF/services/net.ihe.gazelle.application.ProfilesValidatorsFactory b/pixm-connector-service/src/main/resources/META-INF/services/net.ihe.gazelle.application.ProfilesValidatorsFactory new file mode 100644 index 0000000..a89b120 --- /dev/null +++ b/pixm-connector-service/src/main/resources/META-INF/services/net.ihe.gazelle.application.ProfilesValidatorsFactory @@ -0,0 +1,3 @@ +net.ihe.gazelle.interlay.profiles.ITI104PatientFeedQueryProfileFactory +net.ihe.gazelle.interlay.profiles.ITI83GetPIXmQueryProfileFactory +net.ihe.gazelle.interlay.profiles.ITI83PostPIXmQueryProfileFactory \ No newline at end of file diff --git a/pixm-connector-service/src/main/resources/SoapUI/IHE-Pixm-soapui-project.xml b/pixm-connector-service/src/main/resources/SoapUI/IHE-Pixm-soapui-project.xml new file mode 100644 index 0000000..e6a11b5 --- /dev/null +++ b/pixm-connector-service/src/main/resources/SoapUI/IHE-Pixm-soapui-project.xml @@ -0,0 +1,1002 @@ +<?xml version="1.0" encoding="UTF-8"?> +<con:soapui-project xmlns:con="http://eviware.com/soapui/config" id="0f6914ec-af8e-40a6-9910-e95e833c773b" + activeEnvironment="Default" + name="IHE:Pixm" resourceRoot="" soapui-version="5.6.0" abortOnError="false" + runType="SEQUENTIAL"> + <con:settings> + <con:setting id="com.eviware.soapui.impl.wsdl.actions.iface.tools.soapui.TestRunnerAction@values-local"> + <![CDATA[<xml-fragment xmlns:con="http://eviware.com/soapui/config"> + <con:entry key="Report Format(s)" value=""/> + <con:entry key="Host:Port" value=""/> + <con:entry key="Export JUnit Results" value="false"/> + <con:entry key="Export All" value="false"/> + <con:entry key="Save After" value="false"/> + <con:entry key="Add Settings" value="false"/> + <con:entry key="WSS Password Type" value=""/> + <con:entry key="TestSuite" value="PixM - Code 200"/> + <con:entry key="Endpoint" value=""/> + <con:entry key="Select Report Type" value=""/> + <con:entry key="System Properties" value=""/> + <con:entry key="Password" value=""/> + <con:entry key="Print Report" value="false"/> + <con:entry key="Open Report" value="false"/> + <con:entry key="Export JUnit Results with test properties" value="false"/> + <con:entry key="Global Properties" value=""/> + <con:entry key="Project Properties" value=""/> + <con:entry key="Project Password" value=""/> + <con:entry key="TestCase" value="<all>"/> + <con:entry key="Username" value=""/> + <con:entry key="user-settings.xml Password" value=""/> + <con:entry key="TestRunner Path" value=""/> + <con:entry key="Environment" value="Default"/> + <con:entry key="Coverage Report" value="false"/> + <con:entry key="Enable UI" value="false"/> + <con:entry key="Root Folder" value=""/> + <con:entry key="Ignore Errors" value="false"/> + <con:entry key="Domain" value=""/> + <con:entry key="Tool Args" value=""/> + <con:entry key="Save Project" value="false"/> +</xml-fragment>]]></con:setting> + </con:settings> + <con:interface xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="con:RestService" + id="3aa45915-2866-4224-a6c0-61c79628c944" wadlVersion="http://wadl.dev.java.net/2009/02" + name="http://qualification.ihe-europe.net" + type="rest"> + <con:settings/> + <con:definitionCache type="TEXT" rootPart=""/> + <con:endpoints> + <con:endpoint>http://localhost:8580/pixm_fhir_server/fhir_ihe</con:endpoint> + <con:endpoint>http://qualification.ihe-europe.net/pixm_fhir_server/fhir_ihe</con:endpoint> + </con:endpoints> + <con:resource name="" path="/{resource}/{operation}" id="cf9419c5-50ad-4552-915e-bcb5492a80db"> + <con:settings/> + <con:parameters> + <con:parameter> + <con:name>resource</con:name> + <con:value/> + <con:style>TEMPLATE</con:style> + <con:default/> + <con:description xsi:nil="true"/> + </con:parameter> + <con:parameter> + <con:name>operation</con:name> + <con:value/> + <con:style>TEMPLATE</con:style> + <con:default/> + <con:description xsi:nil="true"/> + </con:parameter> + <con:parameter> + <con:name>sourceIdentifier</con:name> + <con:value/> + <con:style>QUERY</con:style> + <con:default/> + <con:description xsi:nil="true"/> + </con:parameter> + <con:parameter> + <con:name>targetSystem</con:name> + <con:value/> + <con:style>QUERY</con:style> + <con:default/> + <con:description xsi:nil="true"/> + </con:parameter> + <con:parameter> + <con:name>_format</con:name> + <con:value/> + <con:style>QUERY</con:style> + <con:default/> + <con:description xsi:nil="true"/> + </con:parameter> + </con:parameters> + <con:method name="GET from sourceIdentifier" id="a06fed5a-ee7b-4b4b-be29-9368040008a4" method="GET"> + <con:settings/> + <con:parameters/> + <con:representation type="FAULT"> + <con:mediaType>text/html; charset=iso-8859-1</con:mediaType> + <con:status>404</con:status> + <con:params/> + <con:element>html</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>404</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>404</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>404</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>404</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>404</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>404</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType>text/html; charset=iso-8859-1</con:mediaType> + <con:status>200</con:status> + <con:params/> + <con:element>html</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>404</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType>application/json</con:mediaType> + <con:status>200</con:status> + <con:params/> + <con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Response + </con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType>application/json</con:mediaType> + <con:status>404 400 403 401</con:status> + <con:params/> + <con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Fault + </con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>500</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>500</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>500</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>500</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType>fhir+json</con:mediaType> + <con:status>200</con:status> + <con:params/> + <con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Response + </con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType>fhir+xml</con:mediaType> + <con:status>200</con:status> + <con:params/> + <con:element>id</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType>application/fhir+xml</con:mediaType> + <con:status>403</con:status> + <con:params/> + <con:element>id</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType/> + <con:status>404</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>500</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>500</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>500</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>500</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>500</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>500</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>404</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType xsi:nil="true"/> + <con:status>500</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType>application/fhir+json</con:mediaType> + <con:status>404</con:status> + <con:params/> + <con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Fault + </con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType xsi:nil="true"/> + <con:status>0</con:status> + <con:params/> + <con:element>data</con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType>application/fhir+json; charset=UTF-8</con:mediaType> + <con:status>404 500 400</con:status> + <con:params/> + <con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Fault + </con:element> + </con:representation> + <con:representation type="RESPONSE"> + <con:mediaType>application/fhir+json; charset=UTF-8</con:mediaType> + <con:status>200</con:status> + <con:params/> + <con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Response + </con:element> + </con:representation> + <con:representation type="FAULT"> + <con:mediaType>application/fhir+xml; charset=UTF-8</con:mediaType> + <con:status>500 404</con:status> + <con:params/> + <con:element xmlns:fhir="http://hl7.org/fhir">fhir:OperationOutcome</con:element> + </con:representation> + <con:request name="code 200" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" + multiValueDelimiter=","> + <con:settings> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/> + </con:setting> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false + </con:setting> + </con:settings> + <con:endpoint>http://localhost:8089/</con:endpoint> + <con:request/> + <con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri> + <con:credentials> + <con:authType>No Authorization</con:authType> + </con:credentials> + <con:jmsConfig JMSDeliveryMode="PERSISTENT"/> + <con:jmsPropertyConfig/> + <con:parameters> + <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404"/> + <con:entry key="resource" value="Patient"/> + <con:entry key="operation" value="/$ihe-pix"/> + </con:parameters> + <con:parameterOrder> + <con:entry>resource</con:entry> + <con:entry>operation</con:entry> + <con:entry>sourceIdentifier</con:entry> + <con:entry>targetSystem</con:entry> + <con:entry>_format</con:entry> + </con:parameterOrder> + </con:request> + <con:request name="code 400" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" + multiValueDelimiter=","> + <con:settings> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/> + </con:setting> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false + </con:setting> + </con:settings> + <con:endpoint>http://localhost:8089/</con:endpoint> + <con:request/> + <con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri> + <con:credentials> + <con:authType>No Authorization</con:authType> + </con:credentials> + <con:jmsConfig JMSDeliveryMode="PERSISTENT"/> + <con:jmsPropertyConfig/> + <con:parameters> + <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2|NA5404 "/> + <con:entry key="resource" value="Patient"/> + <con:entry key="operation" value="/$ihe-pix"/> + </con:parameters> + <con:parameterOrder> + <con:entry>resource</con:entry> + <con:entry>operation</con:entry> + <con:entry>sourceIdentifier</con:entry> + <con:entry>targetSystem</con:entry> + <con:entry>_format</con:entry> + </con:parameterOrder> + </con:request> + <con:request name="code 403" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" + multiValueDelimiter=","> + <con:settings> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/> + </con:setting> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false + </con:setting> + </con:settings> + <con:endpoint>http://qualification.ihe-europe.net</con:endpoint> + <con:request/> + <con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri> + <con:credentials> + <con:authType>No Authorization</con:authType> + </con:credentials> + <con:jmsConfig JMSDeliveryMode="PERSISTENT"/> + <con:jmsPropertyConfig/> + <con:parameters> + <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404 "/> + <con:entry key="resource" value="Patient"/> + <con:entry key="operation" value="/$ihe-pix"/> + </con:parameters> + <con:parameterOrder> + <con:entry>resource</con:entry> + <con:entry>operation</con:entry> + <con:entry>sourceIdentifier</con:entry> + <con:entry>targetSystem</con:entry> + <con:entry>_format</con:entry> + </con:parameterOrder> + </con:request> + <con:request name="code 404" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" + multiValueDelimiter=","> + <con:settings> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/> + </con:setting> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false + </con:setting> + </con:settings> + <con:endpoint>http://qualification.ihe-europe.net</con:endpoint> + <con:request/> + <con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri> + <con:credentials> + <con:authType>No Authorization</con:authType> + </con:credentials> + <con:jmsConfig JMSDeliveryMode="PERSISTENT"/> + <con:jmsPropertyConfig/> + <con:parameters> + <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404 "/> + <con:entry key="resource" value="Patient"/> + <con:entry key="operation" value="/$ihe-pix"/> + </con:parameters> + <con:parameterOrder> + <con:entry>resource</con:entry> + <con:entry>operation</con:entry> + <con:entry>sourceIdentifier</con:entry> + <con:entry>targetSystem</con:entry> + <con:entry>_format</con:entry> + </con:parameterOrder> + </con:request> + </con:method> + </con:resource> + </con:interface> + <con:testSuite id="4008d084-2b9b-4853-90b1-75ae50eda188" name="PixM - Code 200"> + <con:description>TestSuite generated for REST Service [http://qualification.ihe-europe.net]</con:description> + <con:settings/> + <con:runType>SEQUENTIAL</con:runType> + <con:testCase id="c9b20a8c-efd3-4613-8b00-ed4cbbf5a582" failOnError="true" failTestCaseOnErrors="true" + keepSession="false" maxResults="0" name="TestCase" searchProperties="true"> + <con:description>TestCase generated for REST Resource [] located at [/{resource}{operation}] + </con:description> + <con:settings/> + <con:testStep type="transfer" name="Property Transfer" id="2bbdf78b-4de4-4c6b-b73d-e88fca4f3bd0"> + <con:settings/> + <con:config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="con:PropertyTransfersStep"> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>sourceIdentifier</con:name> + <con:sourceType>sourceIdentifier</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>sourceIdentifier</con:targetType> + <con:targetStep>code 200</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>targetSystem</con:name> + <con:sourceType>targetSystem</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>targetSystem</con:targetType> + <con:targetStep>code 200</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>_format</con:name> + <con:sourceType>_format</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>_format</con:targetType> + <con:targetStep>code 200</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + </con:config> + </con:testStep> + <con:testStep type="restrequest" name="code 200" id="92a9dcef-29ec-4d16-a50b-8379860b7234"> + <con:settings/> + <con:config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + service="http://qualification.ihe-europe.net" + resourcePath="/{resource}/{operation}" methodName="GET from sourceIdentifier" + xsi:type="con:RestRequestStep"> + <con:restRequest name="code 200" id="79fc74bf-5277-4aef-8f00-744072264e5e" + mediaType="application/json" multiValueDelimiter=","> + <con:settings> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false + </con:setting> + </con:settings> + <con:endpoint>http://qualification.ihe-europe.net/pixm-connector/fhir_ihe</con:endpoint> + <con:request/> + <con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri> + <con:assertion type="Valid HTTP Status Codes" id="19207097-8fec-4017-89c0-f4ba61af51e4" + name="Valid HTTP Status Codes"> + <con:configuration> + <codes>200</codes> + </con:configuration> + </con:assertion> + <con:assertion type="GroovyScriptAssertion" id="22513178-52bd-4e7a-9cf0-0e6d2eb7649d" + name="Script Assertion"> + <con:configuration> + <scriptText>String sourceIdentifier = context.testCase.testSteps['code + 200'].getPropertyValue( "sourceIdentifier" ) + log.info sourceIdentifier + assert (sourceIdentifier ==~ /urn:oid:([0-9]*)(\.[0-9]*){10}\|([0-9A-Z])*/) + </scriptText> + </con:configuration> + </con:assertion> + <con:credentials> + <con:authType>No Authorization</con:authType> + </con:credentials> + <con:jmsConfig JMSDeliveryMode="PERSISTENT"/> + <con:jmsPropertyConfig/> + <con:parameters> + <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404"/> + <con:entry key="resource" value="Patient"/> + <con:entry key="_format" value="json"/> + <con:entry key="operation" value="$ihe-pix"/> + <con:entry key="targetSystem" value="domain1"/> + </con:parameters> + <con:parameterOrder> + <con:entry>resource</con:entry> + <con:entry>operation</con:entry> + <con:entry>sourceIdentifier</con:entry> + <con:entry>targetSystem</con:entry> + <con:entry>_format</con:entry> + </con:parameterOrder> + </con:restRequest> + </con:config> + </con:testStep> + <con:properties/> + </con:testCase> + <con:properties> + <con:property> + <con:name>Endpoint</con:name> + <con:value>http://localhost:8580/pixm_fhir_server/fhir_ihe</con:value> + </con:property> + <con:property> + <con:name>resource</con:name> + <con:value>Patient</con:value> + </con:property> + <con:property> + <con:name>operation</con:name> + <con:value>/$ihe-pix</con:value> + </con:property> + <con:property> + <con:name>sourceIdentifier</con:name> + <con:value>urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404</con:value> + </con:property> + <con:property> + <con:name>targetSystem</con:name> + <con:value>domain1</con:value> + </con:property> + <con:property> + <con:name>_format</con:name> + <con:value>json</con:value> + </con:property> + </con:properties> + </con:testSuite> + <con:testSuite id="6de478cb-4400-4297-ae33-55cbde01bbea" name="PixM - Code 400"> + <con:description>TestSuite generated for REST Service [http://qualification.ihe-europe.net]</con:description> + <con:settings/> + <con:runType>SEQUENTIAL</con:runType> + <con:testCase id="6cb2bee4-3d20-40f6-9b61-c58440ba620c" failOnError="true" failTestCaseOnErrors="true" + keepSession="false" maxResults="0" name="TestCase" searchProperties="true"> + <con:description>TestCase generated for REST Resource [] located at [/{resource}{operation}] + </con:description> + <con:settings/> + <con:testStep type="transfer" name="Property Transfer 400" id="b1f85dac-008d-477c-a678-e7e5b2dd3ff6"> + <con:settings/> + <con:config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="con:PropertyTransfersStep"> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>sourceIdentifier</con:name> + <con:sourceType>sourceIdentifier</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>sourceIdentifier</con:targetType> + <con:targetStep>code 400</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>targetSystem</con:name> + <con:sourceType>targetSystem</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>targetSystem</con:targetType> + <con:targetStep>code 400</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>_format</con:name> + <con:sourceType>_format</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>_format</con:targetType> + <con:targetStep>code 400</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + </con:config> + </con:testStep> + <con:testStep type="restrequest" name="code 400" id="ef0eb5bd-cd75-4804-b094-600c16de753f"> + <con:settings/> + <con:config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + service="http://qualification.ihe-europe.net" + resourcePath="/{resource}/{operation}" methodName="GET from sourceIdentifier" + xsi:type="con:RestRequestStep"> + <con:restRequest name="code 400" id="79fc74bf-5277-4aef-8f00-744072264e5e" + mediaType="application/json" multiValueDelimiter=","> + <con:settings> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false + </con:setting> + </con:settings> + <con:endpoint>http://localhost:8580/pixm_fhir_server/fhir_ihe</con:endpoint> + <con:request/> + <con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri> + <con:assertion type="GroovyScriptAssertion" id="92a5155f-cd74-43fb-bc66-9402e5fb602c" + name="Script Assertion"> + <con:configuration> + <scriptText>String sourceIdentifier = context.testCase.testSteps['code + 400'].getPropertyValue('sourceIdentifier') + assert !(sourceIdentifier ==~ /urn:oid:([0-9]*)(\.[0-9]*){10}\|([0-9A-Z])*/) + </scriptText> + </con:configuration> + </con:assertion> + <con:assertion type="Valid HTTP Status Codes" id="1661d742-bcb2-4e22-ae22-deb68f175e4b" + name="Valid HTTP Status Codes"> + <con:configuration> + <codes>400</codes> + </con:configuration> + </con:assertion> + <con:credentials> + <con:authType>No Authorization</con:authType> + </con:credentials> + <con:jmsConfig JMSDeliveryMode="PERSISTENT"/> + <con:jmsPropertyConfig/> + <con:parameters> + <con:entry key="sourceIdentifier" + value="urn:oid:1.3.6.1.4.1.21367.13.20.3000%7CIHEBLUE-998"/> + <con:entry key="resource" value="Patient"/> + <con:entry key="_format" value="json"/> + <con:entry key="operation" value="/$ihe-pix"/> + <con:entry key="targetSystem" value="domain1"/> + </con:parameters> + <con:parameterOrder> + <con:entry>resource</con:entry> + <con:entry>operation</con:entry> + <con:entry>sourceIdentifier</con:entry> + <con:entry>targetSystem</con:entry> + <con:entry>_format</con:entry> + </con:parameterOrder> + </con:restRequest> + </con:config> + </con:testStep> + <con:properties/> + </con:testCase> + <con:properties> + <con:property> + <con:name>Endpoint</con:name> + <con:value>http://localhost:8089</con:value> + </con:property> + <con:property> + <con:name>resource</con:name> + <con:value>Patient</con:value> + </con:property> + <con:property> + <con:name>operation</con:name> + <con:value>/$ihe-pix</con:value> + </con:property> + <con:property> + <con:name>sourceIdentifier</con:name> + <con:value>urn:oid:1.3.6.1.4.1.21367.2010.1.2|NA5404</con:value> + </con:property> + <con:property> + <con:name>targetSystem</con:name> + <con:value>domain1</con:value> + </con:property> + <con:property> + <con:name>_format</con:name> + <con:value>json</con:value> + </con:property> + </con:properties> + </con:testSuite> + <con:testSuite id="d5f31f17-d59f-4cbd-9107-44cc8015c0e0" name="PixM - Code 403"> + <con:description>TestSuite generated for REST Service [http://qualification.ihe-europe.net]</con:description> + <con:settings/> + <con:runType>SEQUENTIAL</con:runType> + <con:testCase id="206c3c04-8afe-4947-83f6-24ad0976216d" failOnError="true" failTestCaseOnErrors="true" + keepSession="false" maxResults="0" name="TestCase" searchProperties="true"> + <con:description>TestCase generated for REST Resource [] located at [/{resource}{operation}] + </con:description> + <con:settings/> + <con:testStep type="transfer" name="Property Transfer 403" id="f7a14909-71cf-4e8c-a2c9-6cf571a0b533"> + <con:settings/> + <con:config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="con:PropertyTransfersStep"> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>sourceIdentifier</con:name> + <con:sourceType>sourceIdentifier</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>sourceIdentifier</con:targetType> + <con:targetStep>code 403</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>targetSystem</con:name> + <con:sourceType>targetSystem</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>targetSystem</con:targetType> + <con:targetStep>code 403</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>_format</con:name> + <con:sourceType>_format</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>_format</con:targetType> + <con:targetStep>code 403</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + </con:config> + </con:testStep> + <con:testStep type="restrequest" name="code 403" id="b2d62938-5129-4372-ba8a-75d5e518f30f"> + <con:settings/> + <con:config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + service="http://qualification.ihe-europe.net" + resourcePath="/{resource}/{operation}" methodName="GET from sourceIdentifier" + xsi:type="con:RestRequestStep"> + <con:restRequest name="code 403" id="79fc74bf-5277-4aef-8f00-744072264e5e" + mediaType="application/json" multiValueDelimiter=","> + <con:settings> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false + </con:setting> + </con:settings> + <con:endpoint>http://localhost:8089/</con:endpoint> + <con:request/> + <con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri> + <con:assertion type="GroovyScriptAssertion" id="9b29c838-e6b9-4627-adb1-5c5f7fb1adae" + name="Script Assertion"> + <con:configuration> + <scriptText>String sourceIdentifier = context.testCase.testSteps['code + 403'].getPropertyValue('sourceIdentifier') + assert (sourceIdentifier ==~ /urn:oid:([0-9]*)(\.[0-9]*){10}\|([0-9A-Z])*/) + </scriptText> + </con:configuration> + </con:assertion> + <con:assertion type="Valid HTTP Status Codes" id="c04ae33e-63c1-47e3-aae2-a8bf46f30048" + name="Valid HTTP Status Codes"> + <con:configuration> + <codes>403</codes> + </con:configuration> + </con:assertion> + <con:credentials> + <con:authType>No Authorization</con:authType> + </con:credentials> + <con:jmsConfig JMSDeliveryMode="PERSISTENT"/> + <con:jmsPropertyConfig/> + <con:parameters> + <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404"/> + <con:entry key="resource" value="Patient"/> + <con:entry key="_format" value="json"/> + <con:entry key="operation" value="/$ihe-pix"/> + <con:entry key="targetSystem" value="domain4"/> + </con:parameters> + <con:parameterOrder> + <con:entry>resource</con:entry> + <con:entry>operation</con:entry> + <con:entry>sourceIdentifier</con:entry> + <con:entry>targetSystem</con:entry> + <con:entry>_format</con:entry> + </con:parameterOrder> + </con:restRequest> + </con:config> + </con:testStep> + <con:properties/> + </con:testCase> + <con:properties> + <con:property> + <con:name>Endpoint</con:name> + <con:value>http://localhost:8089</con:value> + </con:property> + <con:property> + <con:name>resource</con:name> + <con:value>Patient</con:value> + </con:property> + <con:property> + <con:name>operation</con:name> + <con:value>/$ihe-pix</con:value> + </con:property> + <con:property> + <con:name>sourceIdentifier</con:name> + <con:value>urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404</con:value> + </con:property> + <con:property> + <con:name>targetSystem</con:name> + <con:value>domain4</con:value> + </con:property> + <con:property> + <con:name>_format</con:name> + <con:value>json</con:value> + </con:property> + </con:properties> + </con:testSuite> + <con:testSuite id="b6325bf6-8648-4734-be8b-d30dfd6319c7" name="PixM - Code 404"> + <con:description>TestSuite generated for REST Service [http://qualification.ihe-europe.net]</con:description> + <con:settings/> + <con:runType>SEQUENTIAL</con:runType> + <con:testCase id="8c2a2ef4-00d7-4f85-8138-93863d113c1e" failOnError="true" failTestCaseOnErrors="true" + keepSession="false" maxResults="0" name="TestCase" searchProperties="true"> + <con:description>TestCase generated for REST Resource [] located at [/{resource}{operation}] + </con:description> + <con:settings/> + <con:testStep type="transfer" name="Property Transfer 404" id="3939b84b-910b-4e9a-9632-ff0e1474a2f4"> + <con:settings/> + <con:config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="con:PropertyTransfersStep"> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>sourceIdentifier</con:name> + <con:sourceType>sourceIdentifier</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>sourceIdentifier</con:targetType> + <con:targetStep>code 404</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>targetSystem</con:name> + <con:sourceType>targetSystem</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>targetSystem</con:targetType> + <con:targetStep>code 404</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + <con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" + ignoreEmpty="false" transferToAll="false" entitize="false" + transferChildNodes="false"> + <con:name>_format</con:name> + <con:sourceType>_format</con:sourceType> + <con:sourceStep>#TestSuite#</con:sourceStep> + <con:targetType>_format</con:targetType> + <con:targetStep>code 404</con:targetStep> + <con:upgraded>true</con:upgraded> + </con:transfers> + </con:config> + </con:testStep> + <con:testStep type="restrequest" name="code 404" id="e13bc6cd-1799-4521-8fe1-639565d1affe"> + <con:settings/> + <con:config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + service="http://qualification.ihe-europe.net" + resourcePath="/{resource}/{operation}" methodName="GET from sourceIdentifier" + xsi:type="con:RestRequestStep"> + <con:restRequest name="code 404" id="79fc74bf-5277-4aef-8f00-744072264e5e" + mediaType="application/json" multiValueDelimiter=","> + <con:settings> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting> + <con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false + </con:setting> + </con:settings> + <con:endpoint>http://qualification.ihe-europe.net/pixm_fhir_server/fhir_ihe</con:endpoint> + <con:request/> + <con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri> + <con:assertion type="Valid HTTP Status Codes" id="5e3da6e1-9f94-48f1-af24-f9ee0ca899cc" + name="Valid HTTP Status Codes"> + <con:configuration> + <codes>404</codes> + </con:configuration> + </con:assertion> + <con:assertion type="GroovyScriptAssertion" id="42c5b179-978b-4d0d-a279-fbb44cd81f41" + name="Script Assertion"> + <con:configuration> + <scriptText>String sourceIdentifier = context.testCase.testSteps['code + 404'].getPropertyValue('sourceIdentifier') + assert (sourceIdentifier ==~ /urn:oid:([0-9]*)(\.[0-9]*){10}\|([0-9A-Z])*/) + </scriptText> + </con:configuration> + </con:assertion> + <con:credentials> + <con:authType>No Authorization</con:authType> + </con:credentials> + <con:jmsConfig JMSDeliveryMode="PERSISTENT"/> + <con:jmsPropertyConfig/> + <con:parameters> + <con:entry key="sourceIdentifier" value=" "/> + <con:entry key="resource" value="Patient"/> + <con:entry key="_format" value="json"/> + <con:entry key="operation" value="$ihe-pix"/> + <con:entry key="targetSystem" value="domain1"/> + </con:parameters> + <con:parameterOrder> + <con:entry>resource</con:entry> + <con:entry>operation</con:entry> + <con:entry>sourceIdentifier</con:entry> + <con:entry>targetSystem</con:entry> + <con:entry>_format</con:entry> + </con:parameterOrder> + </con:restRequest> + </con:config> + </con:testStep> + <con:properties/> + </con:testCase> + <con:properties> + <con:property> + <con:name>Endpoint</con:name> + <con:value>http://localhost:8089</con:value> + </con:property> + <con:property> + <con:name>resource</con:name> + <con:value>Patient</con:value> + </con:property> + <con:property> + <con:name>operation</con:name> + <con:value>/$ihe-pix</con:value> + </con:property> + <con:property> + <con:name>sourceIdentifier</con:name> + <con:value>urn:oid:1.3.6.1.4.1.21367.2010.1.2.301|NA5404</con:value> + </con:property> + <con:property> + <con:name>targetSystem</con:name> + <con:value>domain1</con:value> + </con:property> + <con:property> + <con:name>_format</con:name> + <con:value>json</con:value> + </con:property> + </con:properties> + </con:testSuite> + <con:properties> + <con:property> + <con:name>resource</con:name> + <con:value>Patient</con:value> + </con:property> + <con:property> + <con:name>operation</con:name> + <con:value>/$ihe-pix</con:value> + </con:property> + </con:properties> + <con:wssContainer/> + <con:oAuth2ProfileContainer/> + <con:oAuth1ProfileContainer/> + <con:sensitiveInformation/> +</con:soapui-project> \ No newline at end of file diff --git a/pixm-connector-service/src/main/resources/deployment.properties b/pixm-connector-service/src/main/resources/deployment.properties new file mode 100644 index 0000000..05a73a0 --- /dev/null +++ b/pixm-connector-service/src/main/resources/deployment.properties @@ -0,0 +1,3 @@ +#Â Defines the URL of the PatientRegistry X-Ref Processing Service. +xrefpatientregistry.url=https://qualification.ihe-europe.net/patient-registry/CrossReferenceService/xref-processing-service?wsdl +patientregistry.url=https://qualification.ihe-europe.net/patient-registry/PatientProcessingService/patient-processing-service?wsdl \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/beans.xml b/pixm-connector-service/src/main/webapp/WEB-INF/beans.xml similarity index 65% rename from src/main/webapp/WEB-INF/beans.xml rename to pixm-connector-service/src/main/webapp/WEB-INF/beans.xml index 6f34698..2bd5e18 100644 --- a/src/main/webapp/WEB-INF/beans.xml +++ b/pixm-connector-service/src/main/webapp/WEB-INF/beans.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> -<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" +<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd" bean-discovery-mode="all"> diff --git a/pixm-connector-service/src/main/webapp/WEB-INF/jboss-web.xml b/pixm-connector-service/src/main/webapp/WEB-INF/jboss-web.xml new file mode 100644 index 0000000..a053187 --- /dev/null +++ b/pixm-connector-service/src/main/webapp/WEB-INF/jboss-web.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<jboss-web version="10.0" + xmlns="http://www.jboss.com/xml/ns/javaee" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-web_10_0.xsd"> + <context-root>pixm-connector</context-root> +</jboss-web> \ No newline at end of file diff --git a/pixm-connector-service/src/main/webapp/WEB-INF/web.xml b/pixm-connector-service/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..6f4a7e0 --- /dev/null +++ b/pixm-connector-service/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,4 @@ +<web-app version="4.0" xmlns="http://xmlns.jcp.org/xml/ns/javaee" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"> +</web-app> \ No newline at end of file diff --git a/src/test/java/net/ihe/gazelle/adapter/connector/BundleToPatientRegistryConverterTest.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/adapter/connector/FhirToGazelleRegistryConverterTest.java similarity index 81% rename from src/test/java/net/ihe/gazelle/adapter/connector/BundleToPatientRegistryConverterTest.java rename to pixm-connector-service/src/test/java/net/ihe/gazelle/adapter/connector/FhirToGazelleRegistryConverterTest.java index 3d5f075..368e1c7 100644 --- a/src/test/java/net/ihe/gazelle/adapter/connector/BundleToPatientRegistryConverterTest.java +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/adapter/connector/FhirToGazelleRegistryConverterTest.java @@ -2,11 +2,11 @@ package net.ihe.gazelle.adapter.connector; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import io.qameta.allure.*; -import net.ihe.gazelle.app.patientregistryapi.business.*; +import net.ihe.gazelle.app.patientregistryapi.business.AddressUse; +import net.ihe.gazelle.app.patientregistryapi.business.GenderCode; import net.ihe.gazelle.app.patientregistryapi.business.Person; +import net.ihe.gazelle.app.patientregistryapi.business.PersonName; import org.hl7.fhir.r4.model.*; -import org.hl7.fhir.r4.model.Address; -import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.Test; import java.time.LocalDate; @@ -14,14 +14,14 @@ import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; -@Feature("BundleConverter") -public class BundleToPatientRegistryConverterTest { +@Feature("FhirToGazelleConverter") +public class FhirToGazelleRegistryConverterTest { @Test @Description("Test on unitary conversion") @Severity(SeverityLevel.CRITICAL) @Story("Name conversion") - void TestPatientNameConversion(){ + void TestPatientNameConversion() { Patient pat = new Patient(); HumanName name = new HumanName(); name.addGiven("Patrick"); @@ -32,7 +32,7 @@ public class BundleToPatientRegistryConverterTest { pat.addName(name); try { - net.ihe.gazelle.app.patientregistryapi.business.Patient response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat); + net.ihe.gazelle.app.patientregistryapi.business.Patient response = FhirToGazelleRegistryConverter.convertPatient(pat); PersonName responseName = response.getNames().get(0); assertEquals("Patrick", responseName.getGivens().get(0)); assertEquals("TEMP", responseName.getUse()); @@ -49,7 +49,7 @@ public class BundleToPatientRegistryConverterTest { @Description("Test on unitary conversion") @Severity(SeverityLevel.CRITICAL) @Story("Gender conversion") - void TestPatientGenderConversion(){ + void TestPatientGenderConversion() { Patient pat1 = new Patient(); pat1.setGender(Enumerations.AdministrativeGender.FEMALE); @@ -64,16 +64,16 @@ public class BundleToPatientRegistryConverterTest { pat4.setGender(Enumerations.AdministrativeGender.UNKNOWN); try { - net.ihe.gazelle.app.patientregistryapi.business.Patient response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat1); + net.ihe.gazelle.app.patientregistryapi.business.Patient response = FhirToGazelleRegistryConverter.convertPatient(pat1); assertEquals(GenderCode.FEMALE, response.getGender()); - response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat2); + response = FhirToGazelleRegistryConverter.convertPatient(pat2); assertEquals(GenderCode.MALE, response.getGender()); - response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat3); + response = FhirToGazelleRegistryConverter.convertPatient(pat3); assertEquals(GenderCode.OTHER, response.getGender()); - response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat4); + response = FhirToGazelleRegistryConverter.convertPatient(pat4); assertEquals(GenderCode.UNDEFINED, response.getGender()); } catch (ConversionException e) { @@ -86,7 +86,7 @@ public class BundleToPatientRegistryConverterTest { @Description("Test on unitary conversion") @Severity(SeverityLevel.CRITICAL) @Story("Contact conversion") - void TestPatientContactConversion(){ + void TestPatientContactConversion() { Patient pat = new Patient(); Patient.ContactComponent comp = new Patient.ContactComponent(); @@ -98,7 +98,7 @@ public class BundleToPatientRegistryConverterTest { pat.addContact(comp); try { - net.ihe.gazelle.app.patientregistryapi.business.Patient response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat); + net.ihe.gazelle.app.patientregistryapi.business.Patient response = FhirToGazelleRegistryConverter.convertPatient(pat); Person contact = response.getContacts().get(0); assertEquals(GenderCode.OTHER, contact.getGender()); assertEquals("Henlo", contact.getAddresses().get(0).getLines().get(0)); @@ -114,7 +114,7 @@ public class BundleToPatientRegistryConverterTest { @Description("Test on unitary conversion") @Severity(SeverityLevel.CRITICAL) @Story("Address conversion") - void TestPatientAddressConversion(){ + void TestPatientAddressConversion() { Patient pat = new Patient(); Address address = new Address(); @@ -144,7 +144,7 @@ public class BundleToPatientRegistryConverterTest { pat4.addAddress(address4); try { - net.ihe.gazelle.app.patientregistryapi.business.Patient response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat); + net.ihe.gazelle.app.patientregistryapi.business.Patient response = FhirToGazelleRegistryConverter.convertPatient(pat); net.ihe.gazelle.app.patientregistryapi.business.Address responseAddress = response.getAddresses().get(0); assertEquals("Chicoutimi", responseAddress.getCity()); assertEquals("Canada", responseAddress.getCountryIso3()); @@ -153,19 +153,19 @@ public class BundleToPatientRegistryConverterTest { assertEquals(AddressUse.TEMPORARY, responseAddress.getUse()); assertEquals("4 boulevard de l'Université", responseAddress.getLines().get(0)); - response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat1); + response = FhirToGazelleRegistryConverter.convertPatient(pat1); responseAddress = response.getAddresses().get(0); assertEquals(AddressUse.BILLING, responseAddress.getUse()); - response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat2); + response = FhirToGazelleRegistryConverter.convertPatient(pat2); responseAddress = response.getAddresses().get(0); assertEquals(AddressUse.HOME, responseAddress.getUse()); - response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat3); + response = FhirToGazelleRegistryConverter.convertPatient(pat3); responseAddress = response.getAddresses().get(0); assertEquals(AddressUse.BAD, responseAddress.getUse()); - response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat4); + response = FhirToGazelleRegistryConverter.convertPatient(pat4); responseAddress = response.getAddresses().get(0); assertEquals(AddressUse.WORK, responseAddress.getUse()); @@ -179,7 +179,7 @@ public class BundleToPatientRegistryConverterTest { @Description("Test on unitary conversion") @Severity(SeverityLevel.CRITICAL) @Story("Single parameters conversion") - void TestPatientParametersConversion(){ + void TestPatientParametersConversion() { LocalDate dateValue = LocalDate.now(); @@ -189,7 +189,7 @@ public class BundleToPatientRegistryConverterTest { pat.setId("Hello"); try { - net.ihe.gazelle.app.patientregistryapi.business.Patient response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat); + net.ihe.gazelle.app.patientregistryapi.business.Patient response = FhirToGazelleRegistryConverter.convertPatient(pat); assertEquals(java.sql.Date.valueOf(dateValue), response.getDateOfBirth()); assertEquals("Hello", response.getUuid()); assertEquals(true, response.isActive()); @@ -204,7 +204,7 @@ public class BundleToPatientRegistryConverterTest { @Description("Test on unitary conversion") @Severity(SeverityLevel.CRITICAL) @Story("Single parameters conversion") - void TestPatientIdentifiersConversion(){ + void TestPatientIdentifiersConversion() { Patient pat = new Patient(); Identifier id = new Identifier(); @@ -218,11 +218,11 @@ public class BundleToPatientRegistryConverterTest { try { - net.ihe.gazelle.app.patientregistryapi.business.Patient response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat); + net.ihe.gazelle.app.patientregistryapi.business.Patient response = FhirToGazelleRegistryConverter.convertPatient(pat); assertEquals("Hello_Vincent", response.getIdentifiers().get(0).getSystemIdentifier()); assertEquals("69420", response.getIdentifiers().get(0).getValue()); - response = BundleToPatientRegistryConverter.fhirPatientToGazellePatient(pat1); + response = FhirToGazelleRegistryConverter.convertPatient(pat1); } catch (InvalidRequestException e) { assertEquals("Cannot create Patient without any Identifier", e.getMessage()); diff --git a/src/test/java/net/ihe/gazelle/adapter/connector/BusinessToFhirConverterTest.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/adapter/connector/GazelleRegistryToFhirConverterTest.java similarity index 85% rename from src/test/java/net/ihe/gazelle/adapter/connector/BusinessToFhirConverterTest.java rename to pixm-connector-service/src/test/java/net/ihe/gazelle/adapter/connector/GazelleRegistryToFhirConverterTest.java index 3261b7b..8a8da84 100644 --- a/src/test/java/net/ihe/gazelle/adapter/connector/BusinessToFhirConverterTest.java +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/adapter/connector/GazelleRegistryToFhirConverterTest.java @@ -1,10 +1,6 @@ package net.ihe.gazelle.adapter.connector; -import io.qameta.allure.Description; -import io.qameta.allure.Severity; -import io.qameta.allure.SeverityLevel; -import io.qameta.allure.Story; -import io.qameta.allure.Feature; +import io.qameta.allure.*; import net.ihe.gazelle.app.patientregistryapi.business.*; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Patient; @@ -14,13 +10,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @Feature("Fhir to Business Converter") -public class BusinessToFhirConverterTest { +public class GazelleRegistryToFhirConverterTest { @Test @Description("Test on unitary conversion") @Severity(SeverityLevel.CRITICAL) @Story("Gender conversion") - void TestPatientGenderConversion(){ + void TestPatientGenderConversion() { net.ihe.gazelle.app.patientregistryapi.business.Patient pat1 = new net.ihe.gazelle.app.patientregistryapi.business.Patient(); pat1.setGender(GenderCode.FEMALE); @@ -34,17 +30,20 @@ public class BusinessToFhirConverterTest { net.ihe.gazelle.app.patientregistryapi.business.Patient pat4 = new net.ihe.gazelle.app.patientregistryapi.business.Patient(); pat4.setGender(GenderCode.UNDEFINED); + net.ihe.gazelle.app.patientregistryapi.business.Patient pat5 = new net.ihe.gazelle.app.patientregistryapi.business.Patient(); + pat5.setGender(null); + try { - Patient response = BusinessToFhirConverter.patientToFhirPatient(pat1); + Patient response = GazelleRegistryToFhirConverter.convertPatient(pat1); assertEquals(Enumerations.AdministrativeGender.FEMALE, response.getGender()); - response = BusinessToFhirConverter.patientToFhirPatient(pat2); + response = GazelleRegistryToFhirConverter.convertPatient(pat2); assertEquals(Enumerations.AdministrativeGender.MALE, response.getGender()); - response = BusinessToFhirConverter.patientToFhirPatient(pat3); + response = GazelleRegistryToFhirConverter.convertPatient(pat3); assertEquals(Enumerations.AdministrativeGender.OTHER, response.getGender()); - response = BusinessToFhirConverter.patientToFhirPatient(pat4); + response = GazelleRegistryToFhirConverter.convertPatient(pat4); assertEquals(Enumerations.AdministrativeGender.UNKNOWN, response.getGender()); } catch (ConversionException e) { @@ -57,7 +56,7 @@ public class BusinessToFhirConverterTest { @Description("Test on unitary conversion") @Severity(SeverityLevel.CRITICAL) @Story("Address conversion") - void TestPatientAddressConversion(){ + void TestPatientAddressConversion() { net.ihe.gazelle.app.patientregistryapi.business.Patient pat = new net.ihe.gazelle.app.patientregistryapi.business.Patient(); Address address = new Address(); @@ -90,7 +89,7 @@ public class BusinessToFhirConverterTest { pat4.addAddress(address4); try { - Patient response = BusinessToFhirConverter.patientToFhirPatient(pat); + Patient response = GazelleRegistryToFhirConverter.convertPatient(pat); org.hl7.fhir.r4.model.Address responseAddress = response.getAddress().get(0); assertEquals("Chicoutimi", responseAddress.getCity()); assertEquals("Canada", responseAddress.getCountry()); @@ -99,19 +98,19 @@ public class BusinessToFhirConverterTest { assertEquals("4 boulevard de l'université", responseAddress.getLine().get(0).getValue()); assertEquals(org.hl7.fhir.r4.model.Address.AddressUse.WORK, responseAddress.getUse()); - response = BusinessToFhirConverter.patientToFhirPatient(pat1); + response = GazelleRegistryToFhirConverter.convertPatient(pat1); responseAddress = response.getAddress().get(0); assertEquals(org.hl7.fhir.r4.model.Address.AddressUse.HOME, responseAddress.getUse()); - response = BusinessToFhirConverter.patientToFhirPatient(pat2); + response = GazelleRegistryToFhirConverter.convertPatient(pat2); responseAddress = response.getAddress().get(0); assertEquals(org.hl7.fhir.r4.model.Address.AddressUse.TEMP, responseAddress.getUse()); - response = BusinessToFhirConverter.patientToFhirPatient(pat3); + response = GazelleRegistryToFhirConverter.convertPatient(pat3); responseAddress = response.getAddress().get(0); assertEquals(org.hl7.fhir.r4.model.Address.AddressUse.OLD, responseAddress.getUse()); - response = BusinessToFhirConverter.patientToFhirPatient(pat4); + response = GazelleRegistryToFhirConverter.convertPatient(pat4); responseAddress = response.getAddress().get(0); assertEquals(org.hl7.fhir.r4.model.Address.AddressUse.BILLING, responseAddress.getUse()); @@ -125,7 +124,7 @@ public class BusinessToFhirConverterTest { @Description("Test on unitary conversion") @Severity(SeverityLevel.CRITICAL) @Story("Gender conversion") - void TestPatientTelecomConversion(){ + void TestPatientTelecomConversion() { net.ihe.gazelle.app.patientregistryapi.business.Patient pat = new net.ihe.gazelle.app.patientregistryapi.business.Patient(); ContactPoint cp = new ContactPoint(); @@ -177,32 +176,32 @@ public class BusinessToFhirConverterTest { pat6.addContactPoint(cp6); try { - Patient response = BusinessToFhirConverter.patientToFhirPatient(pat); + Patient response = GazelleRegistryToFhirConverter.convertPatient(pat); assertEquals("Hello", response.getTelecom().get(0).getValue()); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.WORK, response.getTelecom().get(0).getUse()); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.PAGER, response.getTelecom().get(0).getSystem()); - response = BusinessToFhirConverter.patientToFhirPatient(pat1); + response = GazelleRegistryToFhirConverter.convertPatient(pat1); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.MOBILE, response.getTelecom().get(0).getUse()); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.PHONE, response.getTelecom().get(0).getSystem()); - response = BusinessToFhirConverter.patientToFhirPatient(pat2); + response = GazelleRegistryToFhirConverter.convertPatient(pat2); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.TEMP, response.getTelecom().get(0).getUse()); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.FAX, response.getTelecom().get(0).getSystem()); - response = BusinessToFhirConverter.patientToFhirPatient(pat3); + response = GazelleRegistryToFhirConverter.convertPatient(pat3); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.HOME, response.getTelecom().get(0).getUse()); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.URL, response.getTelecom().get(0).getSystem()); - response = BusinessToFhirConverter.patientToFhirPatient(pat4); + response = GazelleRegistryToFhirConverter.convertPatient(pat4); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.HOME, response.getTelecom().get(0).getUse()); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.EMAIL, response.getTelecom().get(0).getSystem()); - response = BusinessToFhirConverter.patientToFhirPatient(pat5); + response = GazelleRegistryToFhirConverter.convertPatient(pat5); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.HOME, response.getTelecom().get(0).getUse()); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.SMS, response.getTelecom().get(0).getSystem()); - response = BusinessToFhirConverter.patientToFhirPatient(pat6); + response = GazelleRegistryToFhirConverter.convertPatient(pat6); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.HOME, response.getTelecom().get(0).getUse()); assertEquals(org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.OTHER, response.getTelecom().get(0).getSystem()); @@ -216,7 +215,7 @@ public class BusinessToFhirConverterTest { @Description("Test on unitary conversion") @Severity(SeverityLevel.CRITICAL) @Story("CrossReference conversion") - void TestPatientCrossReferenceConversion(){ + void TestPatientCrossReferenceConversion() { net.ihe.gazelle.app.patientregistryapi.business.Patient pat = new net.ihe.gazelle.app.patientregistryapi.business.Patient(); @@ -226,7 +225,7 @@ public class BusinessToFhirConverterTest { pat.addIdentifier(id); try { - Patient response = BusinessToFhirConverter.patientToFhirPatient(pat); + Patient response = GazelleRegistryToFhirConverter.convertPatient(pat); assertEquals("urn:oid:Hello_Vincent", response.getIdentifier().get(0).getSystem()); assertEquals("69420", response.getIdentifier().get(0).getValue()); diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/application/PatientFeedClientTest.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/PatientFeedClientTest.java new file mode 100644 index 0000000..a59136d --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/PatientFeedClientTest.java @@ -0,0 +1,541 @@ +package net.ihe.gazelle.application; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import io.qameta.allure.*; +import net.ihe.gazelle.adapter.connector.ConversionException; +import net.ihe.gazelle.app.patientregistryapi.application.PatientFeedException; +import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; +import net.ihe.gazelle.app.patientregistryapi.business.GenderCode; +import net.ihe.gazelle.app.patientregistryapi.business.Patient; +import net.ihe.gazelle.app.patientregistryapi.business.PersonName; +import net.ihe.gazelle.app.patientregistryfeedclient.adapter.PatientFeedClient; +import net.ihe.gazelle.app.patientregistryfeedclient.adapter.PatientFeedProcessResponseException; +import net.ihe.gazelle.framework.preferencesmodelapi.application.NamespaceException; +import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; +import net.ihe.gazelle.framework.preferencesmodelapi.application.PreferenceException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import jakarta.xml.ws.WebServiceException; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; + +@Feature("PatientFeedClient") +public class PatientFeedClientTest { + + private static final String TEST_UUID = "123e4567-e89b-12d3-a456-426614174000"; + private static final String MALFORMED_UUID = "123e4567-e89b-12d3-a456-42661417400000000000000000000000000"; + + @Mock + static private PatientFeedClient patientFeedClientMock; + @Mock + static private OperationalPreferencesService operationalPreferencesService; + @Mock + private PatientRegistryFeedClient patientRegistryFeedClient; + + @BeforeAll + static public void initialize() { + patientFeedClientMock = Mockito.mock(PatientFeedClient.class); + operationalPreferencesService = Mockito.mock(OperationalPreferencesService.class); + } + + @Test + @Description("Test on initialization, when a namespace exception is thrown") + @Severity(SeverityLevel.CRITICAL) + @Story("initialization") + public void TestInitializeNameSpaceException() throws PreferenceException, NamespaceException { + + patientRegistryFeedClient = new PatientRegistryFeedClient(operationalPreferencesService); + + Mockito.doThrow(NamespaceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + + "/operational" + + "-preferences", "patientregistry.url"); + + Patient patient = new Patient(); + + assertThrows(PatientFeedException.class, + () -> patientRegistryFeedClient.createPatient(patient)); + } + + @Test + @Description("Test on initialization, when a preference exception is thrown") + @Severity(SeverityLevel.CRITICAL) + @Story("initialization") + public void TestInitializePreferenceException() throws PreferenceException, NamespaceException { + Mockito.doThrow(PreferenceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + + "/operational" + + "-preferences", "patientregistry.url"); + patientRegistryFeedClient = new PatientRegistryFeedClient(operationalPreferencesService); + + Patient patient = new Patient(); + + assertThrows(PatientFeedException.class, + () -> patientRegistryFeedClient.updatePatient(patient, null)); + } + + @Test + @Description("Test on initialization, when the url of the server is malformed") + @Severity(SeverityLevel.CRITICAL) + @Story("initialization") + public void TestInitializeMalformedURLException() throws PreferenceException, NamespaceException { + Mockito.doThrow(PreferenceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + + "/operational" + + "-preferences", "patientregistry.url"); + patientRegistryFeedClient = new PatientRegistryFeedClient(operationalPreferencesService); + + assertThrows(PatientFeedException.class, + () -> patientRegistryFeedClient.delete("")); + } + + @Test + @Description("Test on initialization, when a web service exception is thrown") + @Severity(SeverityLevel.CRITICAL) + @Story("initialization") + public void TestInitializeWebServiceException() throws PreferenceException, NamespaceException { + Mockito.doThrow(WebServiceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + + "/operational" + + "-preferences", "patientregistry.url"); + patientRegistryFeedClient = new PatientRegistryFeedClient(operationalPreferencesService); + + assertThrows(PatientFeedException.class, + () -> patientRegistryFeedClient.mergePatient("", "")); + } + + @Test + @Description("Test on create, nominal case") + @Severity(SeverityLevel.CRITICAL) + @Story("create") + public void TestNominalCreation() throws PatientFeedException, ConversionException { + + Patient patient = createPatient("name", "name", LocalDate.of(1990, 06, 19), GenderCode.MALE); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doReturn(TEST_UUID).when(patientFeedClientMock).createPatient(any()); + assertEquals(TEST_UUID, patientRegistryFeedClient.createPatient(patient).getId()); + } + + @Test + @Description("Test on create, null Patient") + @Severity(SeverityLevel.CRITICAL) + @Story("create") + public void TestNullPatientException() throws PatientFeedException { + + Patient patient = null; + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + assertThrows(InvalidRequestException.class, () -> patientRegistryFeedClient.createPatient(patient)); + try { + patientRegistryFeedClient.createPatient(patient); + } catch (InvalidRequestException | ConversionException e) { + assertEquals(PatientRegistryFeedClient.NO_PATIENT_PARAMETER, e.getMessage()); + } + } + + @Test + @Description("Test on create, when a blank uuid is returned") + @Severity(SeverityLevel.CRITICAL) + @Story("create") + public void TestBlankUuidReturnedException() throws PatientFeedException { + + Patient patient = createPatient("name", "name", LocalDate.of(1990, 06, 19), GenderCode.MALE); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doReturn("").when(patientFeedClientMock).createPatient(any()); + assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.createPatient(patient)); + try { + patientRegistryFeedClient.createPatient(patient); + } catch (InternalErrorException e) { + assertEquals(PatientRegistryFeedClient.NO_UUID, e.getMessage()); + } catch (ConversionException e) { + throw new RuntimeException(e); + } + } + + @Test + @Description("Test on create, when a Malformed UUID is returned") + @Severity(SeverityLevel.CRITICAL) + @Story("create") + public void TestMalformedUuidReturnedException() throws PatientFeedException { + + Patient patient = createPatient("name", "name", LocalDate.of(1990, 06, 19), GenderCode.MALE); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doReturn(MALFORMED_UUID).when(patientFeedClientMock).createPatient(any()); + try { + assertEquals(MALFORMED_UUID, patientRegistryFeedClient.createPatient(patient).getId()); + } catch (Exception e) { + fail(); + } + } + + @Test + @Description("Test on create, for particular exceptions returned from PatientFeedApplication") + @Severity(SeverityLevel.CRITICAL) + @Story("create") + public void TestFeedThrowsPatientFeedCannotCrossRef() throws PatientFeedException { + + Patient patient = createPatient("name", "name", LocalDate.of(1990, 06, 19), GenderCode.MALE); + + PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("Impossible to cross reference the patient (not saved)"); + PatientFeedException firstException = new PatientFeedException(embedException); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doThrow(firstException).when(patientFeedClientMock).createPatient(any()); + + assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.createPatient(patient)); + try { + patientRegistryFeedClient.createPatient(patient); + } catch (InternalErrorException | ConversionException e) { + assertEquals("Impossible to cross reference the patient, it will not be saved !", e.getMessage()); + } + } + + @Test + @Description("Test on create, for particular exceptions returned from PatientFeedApplication") + @Severity(SeverityLevel.CRITICAL) + @Story("create") + public void TestFeedThrowsPatientFeedPersistingPatient() throws PatientFeedException { + + Patient patient = createPatient("name", "name", LocalDate.of(1990, 06, 19), GenderCode.MALE); + + PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("Unexpected Exception persisting Patient !"); + PatientFeedException firstException = new PatientFeedException(embedException); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doThrow(firstException).when(patientFeedClientMock).createPatient(any()); + + assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.createPatient(patient)); + try { + patientRegistryFeedClient.createPatient(patient); + } catch (InternalErrorException e) { + assertEquals("Unexpected Exception persisting Patient !", e.getMessage()); + } catch (ConversionException e) { + throw new RuntimeException(e); + } + } + + @Test + @Description("Test on create, for particular exceptions returned from PatientFeedApplication") + @Severity(SeverityLevel.CRITICAL) + @Story("create") + public void TestFeedThrowsPatientFeedExceptionSystemNotFound() throws PatientFeedException { + + Patient patient = createPatient("name", "name", LocalDate.of(1990, 06, 19), GenderCode.MALE); + + PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("System not found"); + PatientFeedException firstException = new PatientFeedException("Exception while Mapping with GITB elements !", embedException); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doThrow(firstException).when(patientFeedClientMock).createPatient(any()); + + assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.createPatient(patient)); + try { + patientRegistryFeedClient.createPatient(patient); + } catch (InternalErrorException | ConversionException e) { + assertEquals("Exception while Mapping with GITB elements !", e.getMessage()); + } + } + + @Test + @Description("Test on update, exception thrown when no Patient is given") + @Severity(SeverityLevel.CRITICAL) + @Story("update") + public void TestFeedUpdateNullPatient() throws PreferenceException, NamespaceException, PatientFeedException { + + String uuid = TEST_UUID; + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + assertThrows(InvalidRequestException.class, () -> patientRegistryFeedClient.updatePatient(null, null)); + + } + + + @Test + @Description("Test on update, exception thrown from PatReg") + @Severity(SeverityLevel.CRITICAL) + @Story("update") + public void TestFeedUpdatePatientFeedInvalidOperationThrown() throws PreferenceException, NamespaceException, PatientFeedException { + + String uuid = TEST_UUID; + Patient patient = createPatient("", "", LocalDate.of(1990, 06, 19), GenderCode.MALE); + + PatientFeedException embedException = new PatientFeedException("Invalid operation used on distant PatientFeedProcessingService !"); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doThrow(embedException).when(patientFeedClientMock).updatePatient(any(), any()); + + assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.updatePatient(patient, null)); + try { + patientRegistryFeedClient.updatePatient(patient, null); + } catch (InternalErrorException | ConversionException e) { + assertEquals("Invalid operation used on distant PatientFeedProcessingService !", e.getMessage()); + } + } + + @Test + @Description("Test on update, exception thrown from PatReg") + @Severity(SeverityLevel.CRITICAL) + @Story("update") + public void TestFeedUpdatePatientInvalidRequest() throws PatientFeedException { + + String uuid = TEST_UUID; + Patient patient = createPatient("", "", LocalDate.of(1990, 06, 19), GenderCode.MALE); + + PatientFeedException embedException = new PatientFeedException("Invalid Request sent to distant PatientFeedProcessingService !"); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doThrow(embedException).when(patientFeedClientMock).updatePatient(any(), any()); + + assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.updatePatient(patient, null)); + try { + patientRegistryFeedClient.updatePatient(patient, null); + } catch (InternalErrorException e) { + assertEquals("Invalid Request sent to distant PatientFeedProcessingService !", e.getMessage()); + } catch (ConversionException e) { + throw new RuntimeException(e); + } + } + + @Test + @Description("Test on update, exception thrown from PatReg") + @Severity(SeverityLevel.CRITICAL) + @Story("update") + public void TestFeedUpdatePatientUnhandled() throws PatientFeedException { + + String uuid = TEST_UUID; + Patient patient = createPatient("", "", LocalDate.of(1990, 06, 19), GenderCode.MALE); + + PatientFeedException embedException = new PatientFeedException(""); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doThrow(embedException).when(patientFeedClientMock).updatePatient(any(), any()); + + assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.updatePatient(patient, null)); + try { + patientRegistryFeedClient.updatePatient(patient, null); + } catch (InternalErrorException | ConversionException e) { + assertEquals("An unhandled error was thrown.", e.getMessage()); + } + } + + @Test + @Description("Test on merge, when feeding basic request") + @Severity(SeverityLevel.CRITICAL) + @Story("merge") + public void TestFeedMergeNominalCase() throws PatientFeedException { + + EntityIdentifier identifier1 = new EntityIdentifier(); + EntityIdentifier identifier2 = new EntityIdentifier(); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doReturn(TEST_UUID).when(patientFeedClientMock).mergePatient(any(), any()); + //TODO change expected value when feature will be done + assertEquals(TEST_UUID, patientRegistryFeedClient.mergePatient("identifier1", "identifier2").getId()); + + } + + @Test + @Description("Test on merge, exception thrown when no Patient is given") + @Severity(SeverityLevel.CRITICAL) + @Story("merge") + public void TestFeedMergeNullPatient() { + + EntityIdentifier identifier2 = new EntityIdentifier(); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + assertThrows(InvalidRequestException.class, () -> patientRegistryFeedClient.mergePatient(null, null)); + + } + + @Test + @Description("Test on merge, exception thrown when no Uuid is given") + @Severity(SeverityLevel.CRITICAL) + @Story("merge") + public void TestFeedMergeNullUuid() { + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + assertThrows(InvalidRequestException.class, () -> patientRegistryFeedClient.mergePatient(null, null)); + + } + + @Test + @Description("Test on create, for particular exceptions returned from PatientFeedApplication") + @Severity(SeverityLevel.CRITICAL) + @Story("delete") + public void TestFeedDelete() throws PatientFeedException { + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doReturn(true).when(patientFeedClientMock).deletePatient(TEST_UUID); + + try { + assertTrue(patientRegistryFeedClient.delete(TEST_UUID)); + } catch (InternalErrorException e) { + fail("Deletion should not have thrown an error"); + } + } + + @Test + @Description("Test on create, for particular exceptions returned from PatientFeedApplication") + @Severity(SeverityLevel.CRITICAL) + @Story("delete") + public void TestFeedDeleteBlankUUID() { + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + assertThrows(InvalidRequestException.class, () -> patientRegistryFeedClient.delete("")); + + try { + patientRegistryFeedClient.delete(""); + } catch (InvalidRequestException e) { + assertEquals("Invalid parameter", e.getMessage()); + } catch (Exception e) { + fail("Test failed, the expected result has not happend"); + } + } + + @Test + @Description("Test on create, for particular exceptions returned from PatientFeedApplication") + @Severity(SeverityLevel.CRITICAL) + @Story("delete") + public void TestFeedDeleteStatusGone() throws PatientFeedException { + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doReturn(false).when(patientFeedClientMock).deletePatient(TEST_UUID); + assertThrows(ResourceGoneException.class, () -> patientRegistryFeedClient.delete(TEST_UUID)); + + try { + patientRegistryFeedClient.delete(TEST_UUID); + } catch (ResourceGoneException e) { + assertEquals("Patient with UUID " + TEST_UUID, e.getMessage()); + } catch (Exception e) { + fail("Test failed, the expected result has not happend"); + } + } + + @Test + @Description("Test on delete, for a particular exception returned from PatientFeedApplication") + @Severity(SeverityLevel.CRITICAL) + @Story("delete") + public void TestFeedDeleteNoUuid() throws PatientFeedException { + + PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("The uuid cannot be null or empty"); + PatientFeedException firstException = new PatientFeedException(embedException); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doThrow(firstException).when(patientFeedClientMock).deletePatient(TEST_UUID); + assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.delete(TEST_UUID)); + + try { + patientRegistryFeedClient.delete(TEST_UUID); + } catch (InternalErrorException e) { + assertEquals("The uuid cannot be null or empty", e.getMessage()); + } catch (Exception e) { + fail("Test failed, the expected result has not happend"); + } + } + + @Test + @Description("Test on delete, for a particular exception returned from PatientFeedApplication") + @Severity(SeverityLevel.CRITICAL) + @Story("delete") + public void TestFeedDeleteDeleteCannotOperate() throws PatientFeedException { + + PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("Cannot proceed to delete"); + PatientFeedException firstException = new PatientFeedException(embedException); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doThrow(firstException).when(patientFeedClientMock).deletePatient(TEST_UUID); + assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.delete(TEST_UUID)); + + try { + patientRegistryFeedClient.delete(TEST_UUID); + } catch (InternalErrorException e) { + assertEquals("Cannot proceed to delete", e.getMessage()); + } catch (Exception e) { + fail("Test failed, the expected result has not happend"); + } + } + + @Test + @Description("Test on delete, for a particular exception returned from PatientFeedApplication") + @Severity(SeverityLevel.CRITICAL) + @Story("delete") + public void TestFeedDeleteDeleteInvalidResponse() throws PatientFeedException { + + PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("other error"); + PatientFeedException firstException = new PatientFeedException("Invalid Response from distant PatientFeedProcessingService !", embedException); + + patientRegistryFeedClient = new PatientRegistryFeedClient(); + patientRegistryFeedClient.setClient(patientFeedClientMock); + + Mockito.doThrow(firstException).when(patientFeedClientMock).deletePatient(TEST_UUID); + assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.delete(TEST_UUID)); + + try { + patientRegistryFeedClient.delete(TEST_UUID); + } catch (InternalErrorException e) { + assertEquals("Invalid Response from distant PatientFeedProcessingService !", e.getMessage()); + } catch (Exception e) { + fail("Test failed, the expected result has not happend"); + } + } + + private Patient createPatient(String familyName, String givenName, LocalDate birthDate, GenderCode gender) { + + Patient patient = new Patient(); + + PersonName name = new PersonName(); + name.addGiven(givenName); + name.setFamily(familyName); + patient.addName(name); + patient.setDateOfBirth(Date.from(birthDate.atStartOfDay(ZoneId.systemDefault()).toInstant())); + patient.setGender(gender); + return patient; + + } + +} diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/application/PatientRegistrySearchClientTest.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/PatientRegistrySearchClientTest.java new file mode 100644 index 0000000..43be469 --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/PatientRegistrySearchClientTest.java @@ -0,0 +1,317 @@ +package net.ihe.gazelle.application; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import io.qameta.allure.*; +import jakarta.xml.ws.WebServiceException; +import net.ihe.gazelle.app.patientregistryapi.business.Patient; +import net.ihe.gazelle.app.patientregistryapi.business.PatientSearchCriterionKey; +import net.ihe.gazelle.app.patientregistryapi.business.PersonName; +import net.ihe.gazelle.app.patientregistrysearchclient.adapter.PatientSearchClient; +import net.ihe.gazelle.framework.preferencesmodelapi.application.NamespaceException; +import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; +import net.ihe.gazelle.framework.preferencesmodelapi.application.PreferenceException; +import net.ihe.gazelle.lib.searchmodelapi.business.SearchCriteria; +import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; +import net.ihe.gazelle.lib.searchmodelapi.business.searchcriterion.SearchCriterion; +import net.ihe.gazelle.lib.searchmodelapi.business.searchcriterion.StringSearchCriterion; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; + + +@Feature("PatientSearchClientTest") +@ExtendWith(MockitoExtension.class) +public class PatientRegistrySearchClientTest { + + private static final String TEST_UUID = "123e4567-e89b-12d3-a456-426614174000"; + private static final String MALFORMED_UUID = "123e4567-e89b-12d3-a456-42661417400000000000000000000000000"; + + @Mock + static private PatientSearchClient patientSearchClientMock; + @Mock + static private OperationalPreferencesService operationalPreferencesService; + @Mock + private PatientRegistrySearchClient patientRegistrySearchClient; + + @BeforeAll + static void initialize() { + patientSearchClientMock = Mockito.mock(PatientSearchClient.class); + operationalPreferencesService = Mockito.mock(OperationalPreferencesService.class); + } + + @Test //TODO correct this test + @Disabled + @Description("Test on initialization, when a namespace exception is thrown") + @Severity(SeverityLevel.CRITICAL) + @Story("initialization") + public void TestInitializeNameSpaceException() throws PreferenceException, NamespaceException { + Mockito.doThrow(NamespaceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + + "/operational" + + "-preferences", "patientregistry.url"); + patientRegistrySearchClient = new PatientRegistrySearchClient(operationalPreferencesService); + + SearchCriteria searchCriteria = new SearchCriteria(); + SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); + searchCriterion.setValue(TEST_UUID); + searchCriteria.addSearchCriterion(searchCriterion); + + assertThrows(SearchException.class, + () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); + } + + @Test //TODO correct this test + @Disabled + @Description("Test on initialization, when a preference exception is thrown") + @Severity(SeverityLevel.CRITICAL) + @Story("initialization") + public void TestInitializePreferenceException() throws PreferenceException, NamespaceException { + Mockito.doThrow(PreferenceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + + "/operational" + + "-preferences", "patientregistry.url"); + patientRegistrySearchClient = new PatientRegistrySearchClient(operationalPreferencesService); + + SearchCriteria searchCriteria = new SearchCriteria(); + SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); + searchCriterion.setValue(TEST_UUID); + searchCriteria.addSearchCriterion(searchCriterion); + + assertThrows(SearchException.class, + () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); + } + + @Test //TODO correct this test + @Disabled + @Description("Test on initialization, when the url of the server is malformed") + @Severity(SeverityLevel.CRITICAL) + @Story("initialization") + public void TestInitializeMalformedURLException() throws PreferenceException, NamespaceException { + Mockito.doThrow(PreferenceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + + "/operational" + + "-preferences", "patientregistry.url"); + patientRegistrySearchClient = new PatientRegistrySearchClient(operationalPreferencesService); + + SearchCriteria searchCriteria = new SearchCriteria(); + SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); + searchCriterion.setValue(MALFORMED_UUID); + searchCriteria.addSearchCriterion(searchCriterion); + + assertThrows(SearchException.class, + () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); + } + + @Test //TODO correct this test + @Disabled + @Description("Test on initialization, when a web service exception is thrown") + @Severity(SeverityLevel.CRITICAL) + @Story("initialization") + public void TestInitializeWebServiceException() throws PreferenceException, NamespaceException { + Mockito.doThrow(WebServiceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + + "/operational" + + "-preferences", "patientregistry.url"); + patientRegistrySearchClient = new PatientRegistrySearchClient(operationalPreferencesService); + + SearchCriteria searchCriteria = new SearchCriteria(); + SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); + searchCriterion.setValue(TEST_UUID); + searchCriteria.addSearchCriterion(searchCriterion); + + assertThrows(SearchException.class, + () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); + } + + @Test + @Description("Test on read, a specific excpetion is thrown") + @Severity(SeverityLevel.CRITICAL) + @Story("read") + public void TestSearchExceptionMapping() throws SearchException { + + SearchCriteria searchCriteria = new SearchCriteria(); + SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); + searchCriterion.setValue(TEST_UUID); + searchCriteria.addSearchCriterion(searchCriterion); + + SearchException exception = new SearchException("Exception while Mapping with GITB elements !"); + patientRegistrySearchClient = new PatientRegistrySearchClient(); + patientRegistrySearchClient.setClient(patientSearchClientMock); + Mockito.doThrow(exception).when(patientSearchClientMock).search(any()); + assertThrows(InternalErrorException.class, + () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); + } + + @Test + @Description("Test on read, a specific excpetion is thrown") + @Severity(SeverityLevel.CRITICAL) + @Story("read") + public void TestSearchExceptionInvalidResponse() throws SearchException { + + SearchCriteria searchCriteria = new SearchCriteria(); + SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); + searchCriterion.setValue(TEST_UUID); + searchCriteria.addSearchCriterion(searchCriterion); + + SearchException exception = new SearchException("Invalid Response from distant PatientFeedProcessingService !"); + Mockito.doThrow(exception).when(patientSearchClientMock).search(any()); + patientRegistrySearchClient = new PatientRegistrySearchClient(); + patientRegistrySearchClient.setClient(patientSearchClientMock); + assertThrows(ResourceNotFoundException.class, + () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); + } + + @Test + @Description("Test on read, a specific exception is thrown") + @Severity(SeverityLevel.CRITICAL) + @Story("read") + public void TestSearchExceptionInvalidOperation() throws SearchException { + + SearchCriteria searchCriteria = new SearchCriteria(); + SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); + searchCriterion.setValue(TEST_UUID); + searchCriteria.addSearchCriterion(searchCriterion); + + SearchException exception = new SearchException("Invalid operation used on distant PatientFeedProcessingService !"); + Mockito.doThrow(exception).when(patientSearchClientMock).search(any()); + patientRegistrySearchClient = new PatientRegistrySearchClient(); + patientRegistrySearchClient.setClient(patientSearchClientMock); + assertThrows(InvalidRequestException.class, + () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); + } + + @Test + @Description("Test on read, a specific exception is thrown") + @Severity(SeverityLevel.CRITICAL) + @Story("read") + public void TestSearchExceptionInvalidRequest() throws SearchException { + + SearchCriteria searchCriteria = new SearchCriteria(); + SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); + searchCriterion.setValue(TEST_UUID); + searchCriteria.addSearchCriterion(searchCriterion); + + SearchException exception = new SearchException("Invalid Request sent to distant PatientFeedProcessingService !"); + Mockito.doThrow(exception).when(patientSearchClientMock).search(any()); + patientRegistrySearchClient = new PatientRegistrySearchClient(); + patientRegistrySearchClient.setClient(patientSearchClientMock); + assertThrows(InvalidRequestException.class, + () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); + } + + @Test + @Description("Test on read, when no patient is returned") + @Severity(SeverityLevel.CRITICAL) + @Story("read") + public void TestNoPatientReturned() throws SearchException { + + List<Patient> resources = new ArrayList<>(); + + SearchCriteria searchCriteria = new SearchCriteria(); + SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); + searchCriterion.setValue(TEST_UUID); + searchCriteria.addSearchCriterion(searchCriterion); + + doAnswer(invocation -> resources).when(patientSearchClientMock).search(any()); + + patientRegistrySearchClient = new PatientRegistrySearchClient(); + patientRegistrySearchClient.setClient(patientSearchClientMock); + assertThrows(ResourceNotFoundException.class, + () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); + + + } + + @Test + @Description("Test on read, when exactly one patient is returned") + @Severity(SeverityLevel.CRITICAL) + @Story("read") + public void TestOnePatientReturned() throws SearchException { + + org.hl7.fhir.r4.model.Patient arthur = new org.hl7.fhir.r4.model.Patient(); + arthur.addName().addGiven("Arthur"); + + List<Patient> resources = new ArrayList<>(); + Patient pat = new Patient(); + PersonName ps = new PersonName(); + ps.addGiven("Arthur"); + pat.addName(ps); + resources.add(pat); + + SearchCriteria searchCriteria = new SearchCriteria(); + SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); + searchCriterion.setValue(TEST_UUID); + searchCriteria.addSearchCriterion(searchCriterion); + + doAnswer(invocation -> resources).when(patientSearchClientMock).search(any()); + + patientRegistrySearchClient = new PatientRegistrySearchClient(); + patientRegistrySearchClient.setClient(patientSearchClientMock); + assertEquals(arthur.getName().get(0).getGiven().toString(), patientRegistrySearchClient.searchPatient(searchCriterion.getValue()).getName().get(0).getGiven().toString()); + + } + + @Test + @Description("Test on read, when more than one patient is returned") + @Severity(SeverityLevel.CRITICAL) + @Story("read") + public void TestManyPatientsReturned() throws SearchException { + + List<Patient> resources = new ArrayList<>(); + resources.add(new Patient()); + resources.add(new Patient()); + + + doAnswer(invocation -> resources).when(patientSearchClientMock).search(any()); + + patientRegistrySearchClient = new PatientRegistrySearchClient(); + patientRegistrySearchClient.setClient(patientSearchClientMock); + assertThrows(ResourceNotFoundException.class, + () -> patientRegistrySearchClient.searchPatient(TEST_UUID)); + + + } + + //@Test + @Description("Test on read, when a conversion exception happens") + @Severity(SeverityLevel.CRITICAL) + @Story("read") + public void TestConversionException() throws SearchException { + + List<Patient> resources = new ArrayList<>(); + resources.add(new Patient()); + resources.add(new Patient()); + patientRegistrySearchClient = new PatientRegistrySearchClient(); + patientRegistrySearchClient.setClient(patientSearchClientMock); + assertThrows(ResourceNotFoundException.class, + () -> patientRegistrySearchClient.searchPatient(TEST_UUID)); + + + } + + + @Test + @Description("Test on read, when parameter is null") + @Severity(SeverityLevel.CRITICAL) + @Story("read") + public void TestNoEntry() throws SearchException { + + patientRegistrySearchClient = new PatientRegistrySearchClient(); + patientRegistrySearchClient.setClient(patientSearchClientMock); + + assertThrows(InvalidRequestException.class, + () -> patientRegistrySearchClient.searchPatient(null)); + + + } +} diff --git a/src/test/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClientTest.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClientTest.java similarity index 92% rename from src/test/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClientTest.java rename to pixm-connector-service/src/test/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClientTest.java index 7b804ff..635710c 100644 --- a/src/test/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClientTest.java +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/PatientRegistryXRefSearchClientTest.java @@ -18,23 +18,23 @@ import net.ihe.gazelle.framework.preferencesmodelapi.application.NamespaceExcept import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; import net.ihe.gazelle.framework.preferencesmodelapi.application.PreferenceException; import org.hl7.fhir.r4.model.Parameters; -import org.junit.Assert; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.jupiter.MockitoExtension; -import javax.xml.ws.WebServiceException; -import java.net.MalformedURLException; +import jakarta.xml.ws.WebServiceException; import java.util.ArrayList; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -@RunWith(MockitoJUnitRunner.class) +@ExtendWith(MockitoExtension.class) class PatientRegistryXRefSearchClientTest { @Mock static private XRefSearchClient xRefSearchClientMock; @@ -49,10 +49,27 @@ class PatientRegistryXRefSearchClientTest { operationalPreferencesService = Mockito.mock(OperationalPreferencesService.class); } + @NotNull + private static PatientAliases getPatientAliases() { + PatientAliases patientAliases = new PatientAliases(); + Patient patient = new Patient(); + patient.setUuid("test1"); + PersonName personName = new PersonName(); + personName.setFamily("Test"); + patient.addName(personName); + EntityIdentifier sourceIdentifier2 = new EntityIdentifier(); + sourceIdentifier2.setSystemIdentifier("domain1"); + sourceIdentifier2.setValue("1"); + patient.addIdentifier(sourceIdentifier2); + patientAliases.addMember(patient); + return patientAliases; + } + @Test @Description("Test on initialization, when a namespace exception is thrown") @Severity(SeverityLevel.CRITICAL) @Story("initialization") + @Disabled void TestInitializeNameSpaceException() throws PreferenceException, NamespaceException { Mockito.doThrow(NamespaceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + "/operational" + @@ -71,6 +88,7 @@ class PatientRegistryXRefSearchClientTest { @Description("Test on initialization, when a preference exception is thrown") @Severity(SeverityLevel.CRITICAL) @Story("initialization") + @Disabled void TestInitializePreferenceException() throws PreferenceException, NamespaceException { Mockito.doThrow(PreferenceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + "/operational" + @@ -89,8 +107,9 @@ class PatientRegistryXRefSearchClientTest { @Description("Test on initialization, when a malformed url exception is thrown") @Severity(SeverityLevel.CRITICAL) @Story("initialization") + @Disabled void TestInitializeMalformedURLException() throws PreferenceException, NamespaceException { - Mockito.doThrow(MalformedURLException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + + Mockito.doThrow(PreferenceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + "/operational" + "-preferences", "xrefpatientregistry.url"); patientRegistryXRefSearchClient = new PatientRegistryXRefSearchClient(operationalPreferencesService); @@ -107,6 +126,7 @@ class PatientRegistryXRefSearchClientTest { @Description("Test on initialization, when a web service exception is thrown") @Severity(SeverityLevel.CRITICAL) @Story("initialization") + @Disabled void TestInitializeWebServiceException() throws PreferenceException, NamespaceException { Mockito.doThrow(WebServiceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + "/operational" + @@ -121,7 +141,6 @@ class PatientRegistryXRefSearchClientTest { () -> patientRegistryXRefSearchClient.process(sourceIdentifier, targetDomains)); } - @Test @Description("Test on ihe_pix, a specific excpetion is thrown") @Severity(SeverityLevel.CRITICAL) @@ -207,7 +226,6 @@ class PatientRegistryXRefSearchClientTest { () -> patientRegistryXRefSearchClient.process(sourceIdentifier, targetDomains)); } - @Test @Description("Test on ihe_pix, when one Patient is returned") @Severity(SeverityLevel.CRITICAL) @@ -218,22 +236,12 @@ class PatientRegistryXRefSearchClientTest { sourceIdentifier.setValue("1"); List<String> targetDomains = new ArrayList<>(); targetDomains.add("domain1"); - PatientAliases patientAliases = new PatientAliases(); - Patient patient = new Patient(); - patient.setUuid("test1"); - PersonName personName = new PersonName(); - personName.setFamily("Test"); - patient.addName(personName); - EntityIdentifier sourceIdentifier2 = new EntityIdentifier(); - sourceIdentifier2.setSystemIdentifier("domain1"); - sourceIdentifier2.setValue("1"); - patient.addIdentifier(sourceIdentifier2); - patientAliases.addMember(patient); + PatientAliases patientAliases = getPatientAliases(); Mockito.doReturn(patientAliases).when(xRefSearchClientMock).search(sourceIdentifier, targetDomains); patientRegistryXRefSearchClient = new PatientRegistryXRefSearchClient(); patientRegistryXRefSearchClient.setClient(xRefSearchClientMock); Parameters parameters = patientRegistryXRefSearchClient.process(sourceIdentifier, targetDomains); - Assert.assertEquals(2, parameters.getParameter().size()); + assertEquals(2, parameters.getParameter().size()); } @Test @@ -246,22 +254,12 @@ class PatientRegistryXRefSearchClientTest { sourceIdentifier.setValue("1"); List<String> targetDomains = new ArrayList<>(); targetDomains.add("domain3"); - PatientAliases patientAliases = new PatientAliases(); - Patient patient = new Patient(); - patient.setUuid("test1"); - PersonName personName = new PersonName(); - personName.setFamily("Test"); - patient.addName(personName); - EntityIdentifier sourceIdentifier2 = new EntityIdentifier(); - sourceIdentifier2.setSystemIdentifier("domain1"); - sourceIdentifier2.setValue("1"); - patient.addIdentifier(sourceIdentifier2); - patientAliases.addMember(patient); + PatientAliases patientAliases = getPatientAliases(); Mockito.doReturn(patientAliases).when(xRefSearchClientMock).search(sourceIdentifier, targetDomains); patientRegistryXRefSearchClient = new PatientRegistryXRefSearchClient(); patientRegistryXRefSearchClient.setClient(xRefSearchClientMock); Parameters parameters = patientRegistryXRefSearchClient.process(sourceIdentifier, targetDomains); - Assert.assertEquals(1, parameters.getParameter().size()); + assertEquals(1, parameters.getParameter().size()); } @Description("Test on ihe_pix, when two target domain was given as parameter") @@ -292,6 +290,6 @@ class PatientRegistryXRefSearchClientTest { patientRegistryXRefSearchClient = new PatientRegistryXRefSearchClient(); patientRegistryXRefSearchClient.setClient(xRefSearchClientMock); Parameters parameters = patientRegistryXRefSearchClient.process(sourceIdentifier, targetDomains); - Assert.assertEquals(3, parameters.getParameter().size()); + assertEquals(3, parameters.getParameter().size()); } } \ No newline at end of file diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock/PatientFeedClientMock.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock/PatientFeedClientMock.java new file mode 100644 index 0000000..01792a7 --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock/PatientFeedClientMock.java @@ -0,0 +1,27 @@ +package net.ihe.gazelle.application.mock; + +import com.gitb.ps.ProcessingService; +import net.ihe.gazelle.app.patientregistryapi.business.Patient; +import net.ihe.gazelle.app.patientregistryfeedclient.adapter.PatientFeedClient; + +public class PatientFeedClientMock extends PatientFeedClient { + + public PatientFeedClientMock(ProcessingService processingService) { + super(processingService); + } + + @Override + public String createPatient(Patient patient) { + + return null; + + } + + @Override + public boolean deletePatient(String uuid) { + + return false; + + } + +} diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock/SearchClientMock.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock/SearchClientMock.java new file mode 100644 index 0000000..953da28 --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock/SearchClientMock.java @@ -0,0 +1,33 @@ +package net.ihe.gazelle.application.mock; + +import com.gitb.ps.ProcessingService; +import net.ihe.gazelle.app.patientregistryapi.business.Patient; +import net.ihe.gazelle.app.patientregistryapi.business.PatientAliases; +import net.ihe.gazelle.app.patientregistrysearchclient.adapter.PatientSearchClient; +import net.ihe.gazelle.lib.searchmodelapi.business.SearchCriteria; +import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; +import net.ihe.gazelle.lib.searchmodelapi.business.searchcriterion.SearchCriterion; + +import java.util.List; + +public class SearchClientMock extends PatientSearchClient { + + public SearchClientMock(ProcessingService processingService) { + super(processingService); + } + + @Override + public List<Patient> search(SearchCriteria searchCriteria) throws SearchException { + PatientAliases patientAliases = new PatientAliases(); + for (SearchCriterion criterion : searchCriteria.getSearchCriterions()) { + switch (criterion.toString()) { + case "": + return null; + default: + return null; + } + } + return null; + } + +} diff --git a/src/test/java/net/ihe/gazelle/application/XRefSearchClientMock.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock/XRefSearchClientMock.java similarity index 95% rename from src/test/java/net/ihe/gazelle/application/XRefSearchClientMock.java rename to pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock/XRefSearchClientMock.java index 4c4d72b..a297e46 100644 --- a/src/test/java/net/ihe/gazelle/application/XRefSearchClientMock.java +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/application/mock/XRefSearchClientMock.java @@ -1,4 +1,4 @@ -package net.ihe.gazelle.application; +package net.ihe.gazelle.application.mock; import com.gitb.ps.ProcessingService; import net.ihe.gazelle.app.patientregistryapi.application.SearchCrossReferenceException; diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/IhePatientResourceProviderTest.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/IhePatientResourceProviderTest.java new file mode 100644 index 0000000..43f7bc1 --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/IhePatientResourceProviderTest.java @@ -0,0 +1,479 @@ +package net.ihe.gazelle.business.provider; + +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import jakarta.servlet.http.HttpServletRequest; +import net.ihe.gazelle.adapter.connector.FhirToGazelleRegistryConverter; +import net.ihe.gazelle.application.ConfigurationAdapter; +import net.ihe.gazelle.application.PatientRegistryFeedClient; +import net.ihe.gazelle.application.PatientRegistrySearchClient; +import net.ihe.gazelle.application.PatientRegistryXRefSearchClient; +import net.ihe.gazelle.business.provider.mock.*; +import net.ihe.gazelle.business.service.RequestValidatorService; +import org.hl7.fhir.r4.model.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Collectors; + +import static net.ihe.gazelle.business.provider.IhePatientResourceProvider.NO_IDENTIFIER_PROVIDED; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class IhePatientResourceProviderTest { + + public static final String UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN = "Unprocessable Entity Exception has to be thrown"; + public static final String INVALID_REQUEST_EXCEPTION_HAS_TO_BE_THROWN = "InvalideRequestException has to be thrown"; + public static final String ERROR_IN_DELETION = "Error in deletion"; + private static PatientRegistryXRefSearchClient xRefSearchClientMock; + private static PatientRegistrySearchClient searchClientMock; + private static RequestValidatorService rvs; + private static PatientRegistryFeedClient patientRegistryFeedClient; + private static ConfigurationAdapter configuration; + private static IhePatientResourceProvider provider; + private static final HttpServletRequest iti104Request = createHttpServletRequest("src/test/resources/http_request_iti104.http"); + static final String patientString = convertReaderIntoString(iti104Request); + private static final HttpServletRequest iti83GetRequest = createHttpServletRequest("src/test/resources/http_request_get_iti83.http"); + + + @BeforeAll + static void initialized() { + xRefSearchClientMock = new PatientRegistryXRefSearchClientMock(); + searchClientMock = new PatientRegistrySearchClientMock(); + rvs = new RequestValidatorServiceMock(); + configuration = new ConfigurationAdapterMock(); + patientRegistryFeedClient = new PatientRegistryFeedClientMock(); + provider = new IhePatientResourceProvider(xRefSearchClientMock, searchClientMock, patientRegistryFeedClient, rvs, configuration); + } + + @Test + void testCreatePatient() { + //Given + Patient patient = provider.createFhirResourceFromString(Patient.class, patientString); + //When + MethodOutcome mo = provider.create(patient, iti104Request); + //Then + assertTrue(mo.getCreated()); + } + + @Test + void testCreatePatientWithFailedValidationReport() { + //Given + Patient patient = provider.createFhirResourceFromString(Patient.class, patientString); + patient.setId("FAILED_VALIDATION"); + try { + //When + provider.create(patient, iti104Request); + } catch (InvalidRequestException e) { + //Then + OperationOutcome oo = (OperationOutcome) e.getOperationOutcome(); + assertTrue(oo.getIssue().get(0).getDiagnostics().contains("HAPI-0450")); + } catch (Exception e) { + fail(INVALID_REQUEST_EXCEPTION_HAS_TO_BE_THROWN, e); + } + } + + @Test + void testCreatePatientWithNull() { + //Given + Patient patient = provider.createFhirResourceFromString(Patient.class, patientString); + patient.setId("PatientFeedException"); + try { + //When + provider.create(null, iti104Request); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(IhePatientResourceProvider.NO_PATIENT_PROVIDED, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + void testCreatePatientWithPatientFeedException() { + //Given + Patient patient = provider.createFhirResourceFromString(Patient.class, patientString); + patient.setId("PatientFeedException"); + try { + //When + provider.create(patient, iti104Request); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(IhePatientResourceProvider.PATIENT_FEED_CLIENT_IS_NOT_SET, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + void testCreatePatientWithConversionException() { + //Given + Patient patient = provider.createFhirResourceFromString(Patient.class, patientString); + patient.setId("ConversionException"); + try { + //When + provider.create(patient, iti104Request); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(IhePatientResourceProvider.FHIR_PATIENT_COULD_NOT_BE_CONVERTED_TO_REGISTRY_PATIENT, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + + } + + @Test + void testCreatePatientWithNullCreatedPatientFromRegistry() { + //Given + Patient patient = provider.createFhirResourceFromString(Patient.class, patientString); + patient.setId("null"); + try { + //When + provider.create(patient, iti104Request); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(IhePatientResourceProvider.PROBLEM_DURING_CREATING_PATIENT_IN_PATIENT_REGISTRY, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + void testUpdatePatient() { + //Given + Patient patient = provider.createFhirResourceFromString(Patient.class, patientString); + String theConditional = "Patient?identifier=system%7C00001"; + //When + MethodOutcome mo = provider.update(null, theConditional, patient, iti104Request); + //Then + Patient patientReturned = (Patient) mo.getResource(); + assertEquals("PatientIsUpdated", patientReturned.getId()); + } + + @Test + void testUpdatePatientWithPatientFeedException() { + //Given + Patient patient = provider.createFhirResourceFromString(Patient.class, patientString); + String theConditional = "Patient?identifier=system%7C00001"; + + //When + try { + //When + provider.update(null, theConditional, patient, iti104Request); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(IhePatientResourceProvider.PATIENT_FEED_CLIENT_IS_NOT_SET, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + void testUpdatePatientWithConversionException() { + //Given + Patient patient = provider.createFhirResourceFromString(Patient.class, patientString); + String theConditional = "Patient?identifier=system%7CConversionException"; + try { + //When + provider.update(null, theConditional, patient, iti104Request); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(IhePatientResourceProvider.FHIR_PATIENT_COULD_NOT_BE_CONVERTED_TO_REGISTRY_PATIENT, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + void testUpdatePatientWithNullConditional() { + //Given + Patient patient = provider.createFhirResourceFromString(Patient.class, patientString); + try { + //When + provider.update(null, null, patient, iti104Request); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(NO_IDENTIFIER_PROVIDED, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + void testReadOk() { + //Given + IdType id = new IdType(); + id.setValue("3"); + //When + Patient p = provider.read(id); + //Then + assertEquals(PatientRegistrySearchClientMock.GIVEN_NAME, p.getName().get(0).getGivenAsSingleString()); + } + + @Test + void testReadThrows() { + //Given + IdType id = new IdType(); + id.setValue("1"); + try { + //When + provider.read(id); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(IhePatientResourceProvider.PATIENT_COULD_NOT_BE_RETRIEVED, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + void testReadNull() { + IdType id = new IdType(); + try { + //When + provider.read(id); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(NO_IDENTIFIER_PROVIDED, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + void testGetResource() { + assertEquals(Patient.class, provider.getResourceType()); + } + + @Test + void testDelete() { + //Given + IdType id = new IdType(40); + String theConditional = "Patient?identifier=system%7C00001"; + + //When + MethodOutcome mo = provider.delete(id, theConditional, iti104Request); + //Then + assertEquals(200, mo.getResponseStatusCode()); + } + + @Test + void testDeleteWithNoSystemValueForIdentifier() { + //Given + IdType id = new IdType(); + String theConditional = "Patient?identifier="; + try { + //when + Assertions.assertThrows(UnprocessableEntityException.class, () -> provider.delete(id, theConditional, iti104Request)); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(ERROR_IN_DELETION, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + void testDeleteWithErrorDuringDeletion() { + //Given + IdType id = new IdType(42); + String theConditional = "Patient?identifier=system%7CFAILED"; + + try { + //When + provider.delete(id, theConditional, iti104Request); + } catch (UnprocessableEntityException e) { + //Then + assertTrue(e.getCause().getMessage().contains(PatientRegistryFeedClientMock.CANNOT_DELETE_PATIENT_WITH_IDENTIFIER)); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + void testDeleteWithNullReturned() { + //Given + IdType id = new IdType(42); + String theConditional = "Patient?identifier=system%7CNULL"; + + //When + MethodOutcome mo = provider.delete(id, theConditional, iti104Request); + //Then + assertEquals(204, mo.getResponseStatusCode()); + } + + @Test + void testDeleteWithNullIdentifier() { + //Given + IdType id = new IdType(42); + String theConditional = ""; + try { + //When + provider.delete(id, theConditional, iti104Request); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(NO_IDENTIFIER_PROVIDED, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + + @Test + void findPatientsByIdentifierUrn() { + //Given + String system = "urn:oid:1"; + String value = "69420"; + TokenAndListParam tokenAndListParam = createTokenAndListParam(system, value); + String value2 = "urn:oid:2"; + StringAndListParam targetSystem = createTokenAndListParam(value2); + //When + Parameters parameters = provider.findCorrespondingIdentifiersWithGet(tokenAndListParam, targetSystem, iti83GetRequest); + + //Then + Parameters.ParametersParameterComponent singleResponse = parameters.getParameter().get(0); + Identifier r4 = (Identifier) singleResponse.getValue(); + assertEquals(PatientRegistryXRefSearchClientMock.URN_OK, r4.getValue()); + assertEquals(PatientRegistryXRefSearchClientMock.URN_OK, r4.getSystem()); + } + + @Test + public void findPatientsByIdentifierHttp() { + //Given + String system = "http://1"; + String value = "69420"; + TokenAndListParam tokenAndListParam = createTokenAndListParam(system, value); + String value2 = "http://2"; + StringAndListParam targetSystem = createTokenAndListParam(value2); + //When + Parameters response = provider.findCorrespondingIdentifiersWithGet(tokenAndListParam, targetSystem, iti83GetRequest); + + //Then + Parameters.ParametersParameterComponent singleResponse = response.getParameter().get(0); + Identifier r4 = (Identifier) singleResponse.getValue(); + assertEquals(PatientRegistryXRefSearchClientMock.HTTP_OK, r4.getValue()); + assertEquals(PatientRegistryXRefSearchClientMock.HTTP_OK, r4.getSystem()); + + } + + @Test + void findPatientsByIdentifierNull() { + //Given + String value2 = "http://2"; + StringAndListParam targetSystem = createTokenAndListParam(value2); + try { + //When + provider.findCorrespondingIdentifiersWithGet(null, targetSystem, iti83GetRequest); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(IhePatientResourceProvider.SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + void findPatientsByNullTarget() { + //Given + String system = "http://1"; + String value = "69420"; + TokenAndListParam tokenAndListParam = createTokenAndListParam(system, value); + try { + //When + provider.findCorrespondingIdentifiersWithGet(tokenAndListParam, null, iti83GetRequest); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(PatientRegistryXRefSearchClient.TARGET_SYSTEM_NOT_FOUND, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + } + + @Test + public void findPatientsByIdentifierWithoutHttpNorUrnOid() { + //Given + String system = "0"; + String value = "69420"; + TokenAndListParam tokenAndListParam = createTokenAndListParam(system, value); + String value2 = "http://2"; + StringAndListParam targetSystem = createTokenAndListParam(value2); + + try { + //When + provider.findCorrespondingIdentifiersWithGet(tokenAndListParam, targetSystem, iti83GetRequest); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(FhirToGazelleRegistryConverter.SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_HTTP_NOR_URN_OID, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + + } + + @Test + public void findPatientsByIdentifierWithSearchCrossReferenceException() { + //Given + String system = "urn:oid:0"; + String value = "69420"; + TokenAndListParam tokenAndListParam = createTokenAndListParam(system, value); + String value2 = "http://2"; + StringAndListParam targetSystem = createTokenAndListParam(value2); + + try { + //When + provider.findCorrespondingIdentifiersWithGet(tokenAndListParam, targetSystem, iti83GetRequest); + } catch (UnprocessableEntityException e) { + //Then + assertEquals(PatientRegistryXRefSearchClientMock.ERROR_DURING_SEARCH_CROSS_REFERENCE, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + + } + + + //////////////////////////////////////////////////////////////// + // Methods used privately in the exclusive usage of Test Class // + //////////////////////////////////////////////////////////////// + + + private TokenAndListParam createTokenAndListParam(String system, String value) { + return new TokenAndListParam().addAnd(new TokenParam(system, value)); + } + + private StringAndListParam createTokenAndListParam(String value) { + StringAndListParam targetDomains = new StringAndListParam(); + StringOrListParam stringParam = new StringOrListParam(); + stringParam.add(new StringParam(value)); + targetDomains.addAnd(stringParam); + return targetDomains; + } + + private static String convertReaderIntoString(HttpServletRequest request) { + try { + return request.getReader().lines().collect(Collectors.joining(System.lineSeparator())); + } catch (IOException e) { + throw new IllegalStateException(); + } + } + + private static HttpServletRequest createHttpServletRequest(String path) { + HttpServletRequest request; + try { + String content = Files.readString(Path.of(path), StandardCharsets.UTF_8); + request = new HttpServletRequestMock(content); + } catch (HttpFormatException | IOException e) { + throw new RuntimeException(e); + } + return request; + + } +} + diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/ConfigurationAdapterMock.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/ConfigurationAdapterMock.java new file mode 100644 index 0000000..a51ce46 --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/ConfigurationAdapterMock.java @@ -0,0 +1,20 @@ +package net.ihe.gazelle.business.provider.mock; + +import net.ihe.gazelle.application.ConfigurationAdapter; + +public class ConfigurationAdapterMock extends ConfigurationAdapter { + + + @Override + public String getProfileIdCreateUpdateDeleteIti104() { + return "IHE_ITI-104-PatientFeed_Query"; + } + + public String getProfileIdPostIti83() { + return "IHE_ITI-83_GET_PIXm_Query"; + } + + public String getProfileIdGetIti83() { + return "IHE_ITI-83_GET_PIXm_Query"; + } +} diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpFormatException.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpFormatException.java new file mode 100644 index 0000000..c4b6b96 --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpFormatException.java @@ -0,0 +1,7 @@ +package net.ihe.gazelle.business.provider.mock; + +public class HttpFormatException extends Exception { + public HttpFormatException(String message) { + super(message); + } +} diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpRequestParser.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpRequestParser.java new file mode 100644 index 0000000..ddbd321 --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpRequestParser.java @@ -0,0 +1,118 @@ +package net.ihe.gazelle.business.provider.mock; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.Enumeration; +import java.util.Hashtable; + +/** + * Class for HTTP request parsing as defined by RFC 2612: + * <p> + * Request = Request-Line ; Section 5.1 (( general-header ; Section 4.5 | + * request-header ; Section 5.3 | entity-header ) CRLF) ; Section 7.1 CRLF [ + * message-body ] ; Section 4.3 + * + * @author izelaya + */ +public class HttpRequestParser { + + private final Hashtable<String, String> _requestHeaders; + private final StringBuffer _messageBody; + private String _requestLine; + + public HttpRequestParser() { + _requestHeaders = new Hashtable<>(); + _messageBody = new StringBuffer(); + } + + /** + * Parse and HTTP request. + * + * @param request String holding http request. + * @throws IOException If an I/O error occurs reading the input stream. + * @throws HttpFormatException If HTTP Request is malformed + */ + public void parseRequest(String request) throws IOException, HttpFormatException { + BufferedReader reader = new BufferedReader(new StringReader(request)); + + setRequestLine(reader.readLine()); // Request-Line ; Section 5.1 + + String header = reader.readLine(); + while (header != null && !header.isEmpty()) { + appendHeaderParameter(header); + header = reader.readLine(); + } + + String bodyLine = reader.readLine(); + while (bodyLine != null) { + appendMessageBody(bodyLine); + bodyLine = reader.readLine(); + } + reader.close(); + } + + /** + * 5.1 Request-Line The Request-Line begins with a method token, followed by + * the Request-URI and the protocol version, and ending with CRLF. The + * elements are separated by SP characters. No CR or LF is allowed except in + * the final CRLF sequence. + * + * @return String with Request-Line + */ + public String getRequestLine() { + return _requestLine; + } + + private void setRequestLine(String requestLine) throws HttpFormatException { + if (requestLine == null || requestLine.isEmpty()) { + throw new HttpFormatException("Invalid Request-Line: " + requestLine); + } + _requestLine = requestLine; + } + + private void appendHeaderParameter(String header) throws HttpFormatException { + int idx = header.indexOf(":"); + if (idx == -1) { + throw new HttpFormatException("Invalid Header Parameter: " + header); + } + _requestHeaders.put(header.substring(0, idx), header.substring(idx + 1)); + } + + /** + * The message-body (if any) of an HTTP message is used to carry the + * entity-body associated with the request or response. The message-body + * differs from the entity-body only when a transfer-coding has been + * applied, as indicated by the Transfer-Encoding header field (section + * 14.41). + * + * @return String with message-body + */ + public String getMessageBody() { + return _messageBody.toString(); + } + + private void appendMessageBody(String bodyLine) { + _messageBody.append(bodyLine).append("\r\n"); + } + + /** + * For list of available headers refer to sections: 4.5, 5.3, 7.1 of RFC 2616 + * + * @param headerName Name of header + * @return String with the value of the header or null if not found. + */ + public String getHeaderParam(String headerName) { + return _requestHeaders.get(headerName); + } + + public String getMethod() { + return getRequestLine().substring(0, getRequestLine().indexOf(" ")); + } + + public Enumeration<String> getHeaderNames() { + return _requestHeaders.keys(); + } + + +} \ No newline at end of file diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpServletRequestMock.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpServletRequestMock.java new file mode 100644 index 0000000..5eb572c --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/HttpServletRequestMock.java @@ -0,0 +1,377 @@ +package net.ihe.gazelle.business.provider.mock; + +import jakarta.servlet.*; +import jakarta.servlet.http.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.security.Principal; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; + +public class HttpServletRequestMock implements HttpServletRequest { + + static HttpRequestParser parser; + + public HttpServletRequestMock(String content) throws HttpFormatException, IOException { + parser = new HttpRequestParser(); + parser.parseRequest(content); + + } + + @Override + public String getAuthType() { + return null; + } + + @Override + public Cookie[] getCookies() { + return new Cookie[0]; + } + + @Override + public long getDateHeader(String s) { + return 0; + } + + @Override + public String getHeader(String s) { + return parser.getHeaderParam(s); + } + + @Override + public Enumeration<String> getHeaders(String s) { + return null; + } + + @Override + public Enumeration<String> getHeaderNames() { + return parser.getHeaderNames(); + } + + @Override + public int getIntHeader(String s) { + return 0; + } + + @Override + public String getMethod() { + return parser.getMethod(); + } + + @Override + public String getPathInfo() { + return null; + } + + @Override + public String getPathTranslated() { + return null; + } + + @Override + public String getContextPath() { + return null; + } + + @Override + public String getQueryString() { + return null; + } + + @Override + public String getRemoteUser() { + return null; + } + + @Override + public boolean isUserInRole(String s) { + return false; + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + @Override + public String getRequestedSessionId() { + return null; + } + + @Override + public String getRequestURI() { + return null; + } + + @Override + public StringBuffer getRequestURL() { + return null; + } + + @Override + public String getServletPath() { + return null; + } + + @Override + public HttpSession getSession(boolean b) { + return null; + } + + @Override + public HttpSession getSession() { + return null; + } + + @Override + public String changeSessionId() { + return null; + } + + @Override + public boolean isRequestedSessionIdValid() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + return false; + } + + @Override + public boolean isRequestedSessionIdFromURL() { + return false; + } + + @Override + public boolean authenticate(HttpServletResponse httpServletResponse) { + return false; + } + + @Override + public void login(String s, String s1) { + + } + + @Override + public void logout() { + + } + + @Override + public Collection<Part> getParts() { + return null; + } + + @Override + public Part getPart(String s) { + return null; + } + + @Override + public <T extends HttpUpgradeHandler> T upgrade(Class<T> aClass) { + return null; + } + + @Override + public Object getAttribute(String s) { + return null; + } + + @Override + public Enumeration<String> getAttributeNames() { + return null; + } + + @Override + public String getCharacterEncoding() { + return null; + } + + @Override + public void setCharacterEncoding(String s) { + + } + + @Override + public int getContentLength() { + return 0; + } + + @Override + public long getContentLengthLong() { + return 0; + } + + @Override + public String getContentType() { + return null; + } + + @Override + public ServletInputStream getInputStream() { + return null; + } + + @Override + public String getParameter(String s) { + return null; + } + + @Override + public Enumeration<String> getParameterNames() { + return null; + } + + @Override + public String[] getParameterValues(String s) { + return new String[0]; + } + + @Override + public Map<String, String[]> getParameterMap() { + return null; + } + + @Override + public String getProtocol() { + return null; + } + + @Override + public String getScheme() { + return null; + } + + @Override + public String getServerName() { + return null; + } + + @Override + public int getServerPort() { + return 0; + } + + @Override + public BufferedReader getReader() { + String inputStringAsReader = parser.getMessageBody(); + Reader inputString = new StringReader(inputStringAsReader); + return new BufferedReader(inputString); + } + + @Override + public String getRemoteAddr() { + return null; + } + + @Override + public String getRemoteHost() { + return null; + } + + @Override + public void setAttribute(String s, Object o) { + + } + + @Override + public void removeAttribute(String s) { + + } + + @Override + public Locale getLocale() { + return null; + } + + @Override + public Enumeration<Locale> getLocales() { + return null; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public RequestDispatcher getRequestDispatcher(String s) { + return null; + } + + @Override + public int getRemotePort() { + return 0; + } + + @Override + public String getLocalName() { + return null; + } + + @Override + public String getLocalAddr() { + return null; + } + + @Override + public int getLocalPort() { + return 0; + } + + @Override + public ServletContext getServletContext() { + return null; + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + return null; + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException { + return null; + } + + @Override + public boolean isAsyncStarted() { + return false; + } + + @Override + public boolean isAsyncSupported() { + return false; + } + + @Override + public AsyncContext getAsyncContext() { + return null; + } + + @Override + public DispatcherType getDispatcherType() { + return null; + } + + @Override + public String getRequestId() { + return null; + } + + @Override + public String getProtocolRequestId() { + return null; + } + + @Override + public ServletConnection getServletConnection() { + return null; + } +} diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistryFeedClientMock.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistryFeedClientMock.java new file mode 100644 index 0000000..bed3623 --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistryFeedClientMock.java @@ -0,0 +1,77 @@ +package net.ihe.gazelle.business.provider.mock; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import net.ihe.gazelle.adapter.connector.ConversionException; +import net.ihe.gazelle.adapter.connector.GazelleRegistryToFhirConverter; +import net.ihe.gazelle.app.patientregistryapi.application.PatientFeedException; +import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; +import net.ihe.gazelle.app.patientregistryfeedclient.adapter.PatientFeedClient; +import net.ihe.gazelle.application.PatientRegistryFeedClient; +import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; +import org.hl7.fhir.r4.model.Patient; + +public class PatientRegistryFeedClientMock extends PatientRegistryFeedClient { + + public static final String HTTP_OK = "HTTP_OK"; + public static final String URN_OK = "URN_OK"; + private static final String CROSS_REFERENCE = "Cross Reference"; + + public static final String CANNOT_DELETE_PATIENT_WITH_IDENTIFIER = "Cannot delete patient with identifier "; + + public PatientRegistryFeedClientMock() { + } + + public PatientRegistryFeedClientMock(OperationalPreferencesService operationalPreferencesService) { + super(operationalPreferencesService); + } + + public PatientRegistryFeedClientMock(PatientFeedClient searchClient) { + this.setClient(searchClient); + } + + @Override + public Patient createPatient(net.ihe.gazelle.app.patientregistryapi.business.Patient patient) throws PatientFeedException, ConversionException { + + String uuid = patient.getUuid(); + return switch (uuid) { + case "PatientFeedException" -> throw new PatientFeedException(); + case "ConversionException" -> throw new ConversionException(); + default -> (Patient) new Patient().setId("1"); + }; + } + + @Override + public Patient updatePatient(net.ihe.gazelle.app.patientregistryapi.business.Patient patientToUpdate, EntityIdentifier identifier) throws PatientFeedException, ConversionException { + return switch (identifier.getValue()) { + case "PatientFeedException" -> throw new PatientFeedException(); + case "ConversionException" -> throw new ConversionException(); + default -> { + Patient patient = GazelleRegistryToFhirConverter.convertPatient(patientToUpdate); + patient.setId("PatientIsUpdated"); + yield patient; + } + }; + } + + @Override + public EntityIdentifier delete(EntityIdentifier identifier){ + + if (identifier == null || identifier.getValue() == null) { + throw new InvalidRequestException("Invalid identifier"); + } + + if ("FAILED".equals(identifier.getValue())) { + throw new ResourceGoneException(CANNOT_DELETE_PATIENT_WITH_IDENTIFIER + identifier); + } + + if ("NULL".equals(identifier.getValue())) { + identifier = null; + } + + + return identifier; + } + +} diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistrySearchClientMock.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistrySearchClientMock.java new file mode 100644 index 0000000..cd063f5 --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistrySearchClientMock.java @@ -0,0 +1,25 @@ +package net.ihe.gazelle.business.provider.mock; + +import net.ihe.gazelle.application.PatientRegistrySearchClient; +import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; +import org.hl7.fhir.r4.model.Patient; + +public class PatientRegistrySearchClientMock extends PatientRegistrySearchClient { + public static final String GIVEN_NAME = "Arthur"; + + public PatientRegistrySearchClientMock() { + } + + @Override + public Patient searchPatient(String uuid) throws SearchException { + + Patient arthur = new Patient(); + arthur.addName().addGiven(GIVEN_NAME); + + return switch (uuid) { + case "1" -> throw new SearchException("Test exception"); + case "3" -> arthur; + default -> null; + }; + } +} diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistryXRefSearchClientMock.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistryXRefSearchClientMock.java new file mode 100644 index 0000000..45b4ebc --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/PatientRegistryXRefSearchClientMock.java @@ -0,0 +1,64 @@ +package net.ihe.gazelle.business.provider.mock; + +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import net.ihe.gazelle.app.patientregistryapi.application.SearchCrossReferenceException; +import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; +import net.ihe.gazelle.app.patientregistryxrefsearchclient.adapter.XRefSearchClient; +import net.ihe.gazelle.application.PatientRegistryXRefSearchClient; +import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; + +import java.util.List; + +public class PatientRegistryXRefSearchClientMock extends PatientRegistryXRefSearchClient { + public static final String HTTP_OK = "HTTP_OK"; + public static final String URN_OK = "URN_OK"; + public static final String ERROR_DURING_SEARCH_CROSS_REFERENCE = "Error during Search cross reference"; + public static final String IS_NULL = "is null"; + private static final String CROSS_REFERENCE = "Cross Reference"; + + public PatientRegistryXRefSearchClientMock() { + } + + public PatientRegistryXRefSearchClientMock(OperationalPreferencesService operationalPreferencesService) { + super(operationalPreferencesService); + } + + public PatientRegistryXRefSearchClientMock(XRefSearchClient xRefSearchClient) { + setClient(xRefSearchClient); + } + + @Override + public Parameters process(EntityIdentifier sourceIdentifier, List<String> targetSystemList) throws SearchCrossReferenceException { + Parameters response = new Parameters(); + ParametersParameterComponent alias = new ParametersParameterComponent(); + Identifier r4Identifier = new Identifier(); + if (sourceIdentifier == null) { + throw new UnprocessableEntityException(IS_NULL); + } + switch (sourceIdentifier.getSystemIdentifier()) { + case "http://1" -> { + r4Identifier.setSystem(HTTP_OK); + r4Identifier.setValue(HTTP_OK); + alias.setValue(r4Identifier); + alias.setName(CROSS_REFERENCE); + response.addParameter(alias); + return response; + } + case "1" -> { + r4Identifier.setSystem(URN_OK); + r4Identifier.setValue(URN_OK); + alias.setValue(r4Identifier); + alias.setName(CROSS_REFERENCE); + response.addParameter(alias); + return response; + } + case "0" -> throw new SearchCrossReferenceException(ERROR_DURING_SEARCH_CROSS_REFERENCE); + default -> { + throw new UnprocessableEntityException(IS_NULL); + } + } + } +} diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/RequestValidatorServiceMock.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/RequestValidatorServiceMock.java new file mode 100644 index 0000000..0b220ff --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/business/provider/mock/RequestValidatorServiceMock.java @@ -0,0 +1,73 @@ +package net.ihe.gazelle.business.provider.mock; + + +import com.fasterxml.jackson.databind.json.JsonMapper; +import jakarta.servlet.http.HttpServletRequest; +import net.ihe.gazelle.business.service.RequestValidatorService; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationReport; +import net.ihe.gazelle.validation.api.domain.report.structure.ValidationTestResult; +import net.ihe.gazelle.validation.api.domain.request.structure.ValidationItem; +import net.ihe.gazelle.validation.interlay.dto.report.ValidationReportDTO; +import org.hl7.fhir.r4.model.Resource; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static net.ihe.gazelle.matchbox.client.interlay.validation.CustomPatientValidationService.MATCHBOX_VALIDATION; + +public class RequestValidatorServiceMock extends RequestValidatorService { + + String pathToReport = "src/test/resources/validationReport.json"; + String operationOutcomePath = "src/test/resources/operationOutcome.json"; + List<ValidationItem> validationItemList = new ArrayList<>(); + + public List<ValidationReport> validateRequest(HttpServletRequest request, Resource resource, String profileId) { + ValidationReport vr = getValidationReport(); + List<ValidationReport> lvr = new ArrayList<>(); + if (resource != null && "FAILED_VALIDATION".equals(resource.getId())) { + vr.setOverallResult(ValidationTestResult.FAILED); + addMessageToValidationItem(getOperationOutcomeAsString()); + vr.setValidationItems(validationItemList); + lvr.add(vr); + } + return lvr; + } + + + + private ValidationReport getValidationReport() { + ValidationReport vr = new ValidationReport(); + try { + JsonMapper jsonMapper = new JsonMapper(); + String content = Files.readString(Path.of(pathToReport), StandardCharsets.UTF_8); + vr = jsonMapper.readValue(content, ValidationReportDTO.class); + } catch (Exception e) { + System.out.println("Problem while getting file content: " + e.getMessage()); + } + return vr; + } + + private void addMessageToValidationItem(String message) { + ValidationItem vi = new ValidationItem(); + vi.setItemId(MATCHBOX_VALIDATION); + vi.setContent(message.getBytes(StandardCharsets.UTF_8)); + validationItemList.add(vi); + } + + + private String getOperationOutcomeAsString() { + String s = ""; + try { + s = Files.readString(Path.of(operationOutcomePath), StandardCharsets.UTF_8); + } catch (Exception e) { + System.out.println("Problem while getting file content: " + e.getMessage()); + } + return s; + + } + + +} diff --git a/pixm-connector-service/src/test/java/net/ihe/gazelle/interlay/profiles/ProfilesValidatorsFactoryProviderTest.java b/pixm-connector-service/src/test/java/net/ihe/gazelle/interlay/profiles/ProfilesValidatorsFactoryProviderTest.java new file mode 100644 index 0000000..08665d1 --- /dev/null +++ b/pixm-connector-service/src/test/java/net/ihe/gazelle/interlay/profiles/ProfilesValidatorsFactoryProviderTest.java @@ -0,0 +1,73 @@ +package net.ihe.gazelle.interlay.profiles; + +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import net.ihe.gazelle.application.ProfilesValidatorsFactory; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; +import uk.org.webcompere.systemstubs.jupiter.SystemStub; +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SystemStubsExtension.class) +class ProfilesValidatorsFactoryProviderTest { + + public static final String UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN = "Unprocessable Entity Exception has to be thrown"; + + ProfilesValidatorsFactoryProvider factoryProvider = new ProfilesValidatorsFactoryProvider(); + Map<String, String> mapOfEnv = Map.of("PROFILE_ID_POST_ITI_83", "IHE_ITI-83_POST_PIXm_Query", + "PROFILE_ID_GET_ITI_83", "IHE_ITI-83_GET_PIXm_Query", + "PROFILE_ID_CREATE_UPDATE_DELETE_ITI_104", "IHE_ITI-104-PatientFeed_Query"); + @SystemStub + private EnvironmentVariables environmentVariables = new EnvironmentVariables(mapOfEnv); + + + @Test + void testCreateProfileValidatorFactoryIti104() throws Exception { + //Given + String profile = "IHE_ITI-104-PatientFeed_Query"; + //When + ProfilesValidatorsFactory profilesValidatorsFactory = environmentVariables.execute(() -> factoryProvider.createProfileValidatorFactory(profile)); + //Then + assertInstanceOf(ITI104PatientFeedQueryProfile.class, profilesValidatorsFactory.createProfileValidator()); + } + + @Test + void testCreateProfileValidatorFactoryIti83Get() throws Exception { + //Given + String profile = "IHE_ITI-83_GET_PIXm_Query"; + //When + ProfilesValidatorsFactory profilesValidatorsFactory = environmentVariables.execute(() -> factoryProvider.createProfileValidatorFactory(profile)); + //Then + assertInstanceOf(ITI83GetPIXmQueryProfile.class, profilesValidatorsFactory.createProfileValidator()); + } + + @Test + void testCreateProfileValidatorFactoryIti83Post() throws Exception { + //Given + String profile = "IHE_ITI-83_POST_PIXm_Query"; + //When + ProfilesValidatorsFactory profilesValidatorsFactory = environmentVariables.execute(() -> factoryProvider.createProfileValidatorFactory(profile)); + //Then + assertInstanceOf(ITI83PostPIXmQueryProfile.class, profilesValidatorsFactory.createProfileValidator()); + } + + @Test + void testCreateProfileValidatorFactoryUnknownProfile() throws Exception { + //Given + String profile = "unknown"; + //When + try { + environmentVariables.execute(() -> factoryProvider.createProfileValidatorFactory(profile)); + } catch (UnprocessableEntityException e) { + assertEquals(ProfilesValidatorsFactoryProvider.NO_VALIDATOR_FOUND_FOR_THIS_PROFIL_ID + profile, e.getMessage()); + } catch (Exception e) { + fail(UNPROCESSABLE_ENTITY_EXCEPTION_HAS_TO_BE_THROWN); + } + + + } +} \ No newline at end of file diff --git a/pixm-connector-service/src/test/resources/META-INF/services/net.ihe.gazelle.application.ProfilesValidatorsFactory b/pixm-connector-service/src/test/resources/META-INF/services/net.ihe.gazelle.application.ProfilesValidatorsFactory new file mode 100644 index 0000000..a89b120 --- /dev/null +++ b/pixm-connector-service/src/test/resources/META-INF/services/net.ihe.gazelle.application.ProfilesValidatorsFactory @@ -0,0 +1,3 @@ +net.ihe.gazelle.interlay.profiles.ITI104PatientFeedQueryProfileFactory +net.ihe.gazelle.interlay.profiles.ITI83GetPIXmQueryProfileFactory +net.ihe.gazelle.interlay.profiles.ITI83PostPIXmQueryProfileFactory \ No newline at end of file diff --git a/pixm-connector-service/src/test/resources/archives/post_request_1_entry.json b/pixm-connector-service/src/test/resources/archives/post_request_1_entry.json new file mode 100644 index 0000000..32f78cb --- /dev/null +++ b/pixm-connector-service/src/test/resources/archives/post_request_1_entry.json @@ -0,0 +1,37 @@ +{ + "resourceType": "Bundle", + "id": "BundlePIXmFeed", + "meta": { + "profile": [ + "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle" + ] + }, + "type": "message", + "entry": [ + { + "fullUrl": "http://example.com/fhir/MessageHeader/1", + "resource": { + "resourceType": "MessageHeader", + "id": "1", + "text": { + "status": "generated", + "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative</b></p><p><b>event</b>: <code>urn:ihe:iti:pmir:2019:patient-feed</code></p><h3>Destinations</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientEndpoint\">http://example.com/patientEndpoint</a></td></tr></table><h3>Sources</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientSource\">http://example.com/patientSource</a></td></tr></table><p><b>focus</b>: <a href=\"#Bundle_abc\">See above (Bundle/abc)</a></p></div>" + }, + "eventUri": "urn:ihe:iti:pmir:2019:patient-feed", + "destination": [ + { + "endpoint": "http://example.com/patientEndpoint" + } + ], + "source": { + "endpoint": "http://example.com/patientSource" + }, + "focus": [ + { + "reference": "Bundle/abc" + } + ] + } + } + ] +} diff --git a/pixm-connector-service/src/test/resources/archives/post_request_NO_PATIENT.json b/pixm-connector-service/src/test/resources/archives/post_request_NO_PATIENT.json new file mode 100644 index 0000000..b52d6b6 --- /dev/null +++ b/pixm-connector-service/src/test/resources/archives/post_request_NO_PATIENT.json @@ -0,0 +1,47 @@ +{ + "resourceType": "Bundle", + "id": "BundlePIXmFeed", + "meta": { + "profile": [ + "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle" + ] + }, + "type": "message", + "entry": [ + { + "fullUrl": "http://example.com/fhir/MessageHeader/1", + "resource": { + "resourceType": "MessageHeader", + "id": "1", + "text": { + "status": "generated", + "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative</b></p><p><b>event</b>: <code>urn:ihe:iti:pmir:2019:patient-feed</code></p><h3>Destinations</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientEndpoint\">http://example.com/patientEndpoint</a></td></tr></table><h3>Sources</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientSource\">http://example.com/patientSource</a></td></tr></table><p><b>focus</b>: <a href=\"#Bundle_abc\">See above (Bundle/abc)</a></p></div>" + }, + "eventUri": "urn:ihe:iti:pmir:2019:patient-feed", + "destination": [ + { + "endpoint": "http://example.com/patientEndpoint" + } + ], + "source": { + "endpoint": "http://example.com/patientSource" + }, + "focus": [ + { + "reference": "Bundle/abc" + } + ] + } + }, + { + "fullUrl": "http://example.com/fhir/Bundle/abc", + "resource": { + "resourceType": "Bundle", + "id": "abc", + "type": "history", + "entry": [ + ] + } + } + ] +} diff --git a/pixm-connector-service/src/test/resources/archives/post_response.json b/pixm-connector-service/src/test/resources/archives/post_response.json new file mode 100644 index 0000000..f116110 --- /dev/null +++ b/pixm-connector-service/src/test/resources/archives/post_response.json @@ -0,0 +1,36 @@ +{ + "resourceType": "Bundle", + "id": "BundlePIXmResponse", + "meta": { + "profile": [ + "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle-response" + ] + }, + "type": "message", + "entry": [ + { + "fullUrl": "http://example.com/fhir/MessageHeader/1", + "resource": { + "resourceType": "MessageHeader", + "id": "1", + "text": { + "status": "generated", + "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative</b></p><p><b>event</b>: <code>urn:ihe:iti:pmir:2019:patient-feed</code></p><h3>Destinations</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientEndpoint\">http://example.com/patientEndpoint</a></td></tr></table><h3>Sources</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientSource\">http://example.com/patientSource</a></td></tr></table><h3>Responses</h3><table class=\"grid\"><tr><td>-</td><td><b>Identifier</b></td><td><b>Code</b></td></tr><tr><td>*</td><td>1</td><td>ok</td></tr></table></div>" + }, + "eventUri": "urn:ihe:iti:pmir:2019:patient-feed", + "destination": [ + { + "endpoint": "http://example.com/patientEndpoint" + } + ], + "source": { + "endpoint": "http://example.com/patientSource" + }, + "response": { + "identifier": "1", + "code": "ok" + } + } + } + ] +} \ No newline at end of file diff --git a/pixm-connector-service/src/test/resources/http_request_get_iti83.http b/pixm-connector-service/src/test/resources/http_request_get_iti83.http new file mode 100644 index 0000000..d7e097e --- /dev/null +++ b/pixm-connector-service/src/test/resources/http_request_get_iti83.http @@ -0,0 +1,5 @@ +GET https://hapi.fhir.org/fhir/Patient/$ihe-pix?sourceIdentifier=urn:oid:1|69420&targetSystem=urn:oid:2 HTTP/1.1 +Content-Type: application/fhir+json; charset=utf-8 +Accept: application/fhir+json; charset=utf-8 +Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW +Host: hapi.fhir.org diff --git a/pixm-connector-service/src/test/resources/http_request_iti104.http b/pixm-connector-service/src/test/resources/http_request_iti104.http new file mode 100644 index 0000000..d69bb76 --- /dev/null +++ b/pixm-connector-service/src/test/resources/http_request_iti104.http @@ -0,0 +1,27 @@ +POST https://hapi.fhir.org/fhir/Patient/$ihe_pix?identifier=urn:oid:1.3.6.1.4.1.21367.13.20.1000|IHERED-994 HTTP/1.1 +Accept: application/fhir+json +Content-Type: application/fhir+json +Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW +Host: hapi.fhir.org + +{ + "resourceType": "Patient", + "id": "PatientPixMFeed", + "identifier": [ + { + "system": "urn:oid:1.3.6.1.4.1.21367.13.20.1000", + "value": "IHERED-994" + } + ], + "active": true, + "name": [ + { + "family": "MOHR", + "given": [ + "ALISSA" + ] + } + ], + "gender": "female", + "birthDate": "1958-01-30" +} \ No newline at end of file diff --git a/pixm-connector-service/src/test/resources/http_request_post_urn_iti83.http b/pixm-connector-service/src/test/resources/http_request_post_urn_iti83.http new file mode 100644 index 0000000..e098171 --- /dev/null +++ b/pixm-connector-service/src/test/resources/http_request_post_urn_iti83.http @@ -0,0 +1,28 @@ +POST https://hapi.fhir.org/fhir/Patient/$ihe_pix HTTP/1.1 +Accept: application/fhir+json +Content-Type: application/fhir+json +Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW +Host: hapi.fhir.org + +{ + "resourceType": "Parameters", + "id": "ParametersPIXmInput", + "meta": { + "profile": [ + "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-in-parameters" + ] + }, + "parameter": [ + { + "name": "sourceIdentifier", + "valueIdentifier": { + "system": "urn:oid:1", + "value": "69420" + } + }, + { + "name": "targetSystem", + "valueUri": "urn:oid:2" + } + ] +} \ No newline at end of file diff --git a/pixm-connector-service/src/test/resources/operationOutcome.json b/pixm-connector-service/src/test/resources/operationOutcome.json new file mode 100644 index 0000000..e767cee --- /dev/null +++ b/pixm-connector-service/src/test/resources/operationOutcome.json @@ -0,0 +1,14 @@ +{ + "resourceType": "OperationOutcome", + "text": { + "status": "generated", + "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\"><h1>Operation Outcome</h1><table border=\"0\"><tr><td style=\"font-weight: bold;\">ERROR</td><td>[]</td><td>HAPI-0450: Failed to parse request body as JSON resource. Error was: HAPI-1861: Failed to parse JSON encoded FHIR content: HAPI-1857: Did not find any content to parse</td></tr></table></div>" + }, + "issue": [ + { + "severity": "error", + "code": "processing", + "diagnostics": "HAPI-0450: Failed to parse request body as JSON resource. Error was: HAPI-1861: Failed to parse JSON encoded FHIR content: HAPI-1857: Did not find any content to parse" + } + ] +} \ No newline at end of file diff --git a/pixm-connector-service/src/test/resources/validationReport.json b/pixm-connector-service/src/test/resources/validationReport.json new file mode 100644 index 0000000..9cb6298 --- /dev/null +++ b/pixm-connector-service/src/test/resources/validationReport.json @@ -0,0 +1,54 @@ +{ + "modelVersion": "0.1", + "uuid": "053f4d20-274c-4dc1-a277-415693309f7a", + "dateTime": "2023-12-15T13:41:48.117+0000", + "disclaimer": "This report is generated by HTTP Validator Web Service", + "overallResult": "PASSED", + "validationMethod": { + "validationServiceName": "http-validator", + "validationServiceVersion": "0.3.0", + "validationProfileID": "IHE_ITI-104-PatientFeed_Query", + "validationProfileVersion": "1.0" + }, + "validationItems": null, + "reports": [ + { + "name": "Well Formed Validation", + "standards": [ + "HTTP" + ], + "subReportResult": "PASSED", + "subReports": null, + "assertionReports": [ + { + "assertionID": "Well Formed Validation", + "assertionType": null, + "description": "Success: The document you have validated is supposed to be a well-formed document.", + "subjectLocation": null, + "subjectValue": null, + "requirementIDs": null, + "severity": "INFO", + "priority": "MANDATORY", + "result": "PASSED", + "unexpectedErrors": null + } + ], + "subCounters": { + "numberOfFailedWithInfos": 0, + "numberOfFailedWithWarnings": 0, + "numberOfFailedWithErrors": 0, + "numberOfUnexpectedErrors": 0, + "numberOfAssertions": 1 + }, + "unexpectedErrors": null + } + ], + "counters": { + "numberOfFailedWithInfos": 0, + "numberOfFailedWithWarnings": 0, + "numberOfFailedWithErrors": 0, + "numberOfUnexpectedErrors": 0, + "numberOfAssertions": 1 + }, + "additionalMetadata": null +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index b9858e8..0234c46 100644 --- a/pom.xml +++ b/pom.xml @@ -1,430 +1,249 @@ -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> - <modelVersion>4.0.0</modelVersion> - <groupId>net.ihe.gazelle</groupId> - <artifactId>pixm-connector</artifactId> - <packaging>war</packaging> - <version>2.0.1-SNAPSHOT</version> - <name>Pixm Connector</name> +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>net.ihe.gazelle</groupId> + <artifactId>pixm-connector</artifactId> + <packaging>pom</packaging> + <modules> + <module>pixm-connector-service</module> + <module>matchbox-client</module> + <module>http-validator-client</module> + </modules> + <version>3.0.0-SNAPSHOT</version> + <name>Pixm Connector</name> + + <properties> + <allure.junit5.version>2.25.0</allure.junit5.version> + <allure.maven.version>2.12.0</allure.maven.version> + <aspectj.version>1.9.7</aspectj.version> + <caffeine.version>3.1.8</caffeine.version> + <cors.filter.version>1.0.1</cors.filter.version> + <cvss.score.level.tolerate>8</cvss.score.level.tolerate> + <dependency.check.version>5.2.4</dependency.check.version> + <dependency.check.skip>true</dependency.check.skip> + <maven-javadoc-plugin.version>3.1.1</maven-javadoc-plugin.version> + <nexus.url>https://gazelle.ihe.net/nexus</nexus.url> + <nexus.path>/content/groups/public/</nexus.path> + <git.user.name>gitlab-ci</git.user.name> + <git.user.token>changeit</git.user.token> + <git.project.url> + https://${git.user.name}:${git.user.token}@gitlab.inria.fr/gazelle/applications/test-execution/simulator/pixm-connector.git + </git.project.url> + <hapi.fhir.version>6.11.9-SNAPSHOT</hapi.fhir.version> + <maven.release.plugin.version>2.5.3</maven.release.plugin.version> + <nexus.staging.maven.plugin.version>1.6.8</nexus.staging.maven.plugin.version> + <sonar.maven.plugin>3.9.0.2155</sonar.maven.plugin> + <java.version>17</java.version> + <junit.jupiter.version>5.10.1</junit.jupiter.version> + <jacoco.maven.plugin.version>0.8.11</jacoco.maven.plugin.version> + <junit.platform.commons.version>1.7.2</junit.platform.commons.version> + <junit.platform.launcher.version>1.7.2</junit.platform.launcher.version> + <maven.compiler.plugin.version>3.8.1</maven.compiler.plugin.version> + <maven.surefire.plugin.version>3.2.3</maven.surefire.plugin.version> + <mockito.core.version>5.8.0</mockito.core.version> + <mockito.junit.jupiter.version>5.8.0</mockito.junit.jupiter.version> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <phloc.schematron.version>2.7.1</phloc.schematron.version> + <java.string.similarity.version>2.0.0</java.string.similarity.version> + <logback.classic.version>1.4.11</logback.classic.version> + <javaee.api.version>8.0.1</javaee.api.version> + <thymeleaf.version>3.1.2.RELEASE</thymeleaf.version> + <ucum.version>1.0.8</ucum.version> + <system.stubs.jupiter.version>2.1.5</system.stubs.jupiter.version> + <jakarta.servlet.api.version>6.1.0-M1</jakarta.servlet.api.version> + <jakarta.jakartaee.api.version>10.0.0</jakarta.jakartaee.api.version> + <jacoco-maven-plugin.version>0.8.11</jacoco-maven-plugin.version> + <maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version> + </properties> + + <scm> + <connection>scm:git:${git.project.url}</connection> + <url>scm:git:${git.project.url}</url> + <developerConnection>scm:git:${git.project.url}</developerConnection> + <tag>HEAD</tag> + </scm> + + <repositories> + <repository> + <releases> + <enabled>true</enabled> + <updatePolicy>never</updatePolicy> + </releases> + <snapshots> + <enabled>true</enabled> + </snapshots> + <id>IHE</id> + <name>IHE Public Maven Repository Group</name> + <url>https://gazelle.ihe.net/nexus/content/groups/public/</url> + <layout>default</layout> + </repository> + </repositories> + <distributionManagement> + <repository> + <id>nexus-releases</id> + <url>https://gazelle.ihe.net/nexus/content/repositories/releases</url> + </repository> + </distributionManagement> + + <build> + <plugins> + <plugin> + <groupId>org.jacoco</groupId> + <artifactId>jacoco-maven-plugin</artifactId> + <version>${jacoco-maven-plugin.version}</version> + <executions> + <execution> + <id>pre-unit-test</id> + <goals> + <goal>prepare-agent</goal> + </goals> + </execution> + <execution> + <id>post-unit-test</id> + <phase>test</phase> + <goals> + <goal>report</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <artifactId>maven-surefire-plugin</artifactId> + <version>${maven-surefire-plugin.version}</version> + <executions> + <execution> + <id>default-test</id> + <phase>test</phase> + <goals> + <goal>test</goal> + </goals> + </execution> + </executions> + </plugin> + + </plugins> + </build> + + <profiles> + <profile> + <id>dev</id> + <activation> + <activeByDefault>true</activeByDefault> + </activation> + </profile> + <profile> + <id>sonar</id> + <build> + <plugins> + <plugin> + <groupId>org.sonarsource.scanner.maven</groupId> + <artifactId>sonar-maven-plugin</artifactId> + <version>${sonar.maven.plugin}</version> + <executions> + <execution> + <phase>verify</phase> + <goals> + <goal>sonar</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>release</id> + <build> + <plugins> + <plugin> + <groupId>org.sonatype.plugins</groupId> + <artifactId>nexus-staging-maven-plugin</artifactId> + <version>${nexus.staging.maven.plugin.version}</version> + <executions> + <execution> + <id>default-deploy</id> + <phase>deploy</phase> + <goals> + <goal>deploy</goal> + </goals> + </execution> + </executions> + <configuration> + <serverId>nexus-releases</serverId> + <nexusUrl>https://gazelle.ihe.net/nexus</nexusUrl> + <skipStaging>true</skipStaging> + </configuration> + </plugin> + </plugins> + </build> + </profile> + </profiles> + + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>${mockito.core.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-junit-jupiter</artifactId> + <version>${mockito.junit.jupiter.version}</version> + </dependency> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>validation-jaxrs-api</artifactId> + <version>1.0.0</version> + </dependency> + <dependency> + <groupId>net.ihe.gazelle</groupId> + <artifactId>validation-api</artifactId> + <version>1.0.0</version> + </dependency> + <!-- This dependency includes the core HAPI-FHIR classes --> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-base</artifactId> + <version>${hapi.fhir.version}</version> + </dependency> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-server</artifactId> + <version>${hapi.fhir.version}</version> + </dependency> + + <!-- At least one "structures" JAR must also be included --> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-structures-r4</artifactId> + <version>${hapi.fhir.version}</version> + </dependency> + + <!-- Used for validation --> + <dependency> + <groupId>ca.uhn.hapi.fhir</groupId> + <artifactId>hapi-fhir-validation-resources-r4</artifactId> + <version>${hapi.fhir.version}</version> + </dependency> + <dependency> + <groupId>jakarta.platform</groupId> + <artifactId>jakarta.jakartaee-api</artifactId> + <version>${jakarta.jakartaee.api.version}</version> + </dependency> + <dependency> + <groupId>jakarta.servlet</groupId> + <artifactId>jakarta.servlet-api</artifactId> + <version>${jakarta.servlet.api.version}</version> + </dependency> + <dependency> + <groupId>org.fhir</groupId> + <artifactId>ucum</artifactId> + <version>${ucum.version}</version> + </dependency> + </dependencies> + </dependencyManagement> - <properties> - <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - <dependency.check.version>5.2.4</dependency.check.version> - <dependency.check.skip>true</dependency.check.skip> - <cvss.score.level.tolerate>8</cvss.score.level.tolerate> - <nexus.url>https://gazelle.ihe.net/nexus</nexus.url> - <nexus.path>/content/groups/public/</nexus.path> - <java.version>11</java.version> - <aspectj.version>1.9.7</aspectj.version> - <junit.jupiter.version>5.8.0-M1</junit.jupiter.version> - <maven-javadoc-plugin.version>3.1.1</maven-javadoc-plugin.version> - <git.user.name>gitlab-ci</git.user.name> - <git.user.token>changeit</git.user.token> - <git.project.url> - https://${git.user.name}:${git.user.token}@gitlab.inria.fr/gazelle/applications/test-execution/simulator/pixm-connector.git - </git.project.url> - <hapifhir_version>5.4.0</hapifhir_version> - <maven.release.plugin.version>2.5.3</maven.release.plugin.version> - <nexus.staging.maven.plugin.version>1.6.8</nexus.staging.maven.plugin.version> - <sonar.maven.plugin>3.9.0.2155</sonar.maven.plugin> - </properties> - - <scm> - <connection>scm:git:${git.project.url}</connection> - <url>scm:git:${git.project.url}</url> - <developerConnection>scm:git:${git.project.url}</developerConnection> - <tag>HEAD</tag> - </scm> - - <repositories> - <repository> - <releases> - <enabled>true</enabled> - <updatePolicy>never</updatePolicy> - </releases> - <snapshots> - <enabled>true</enabled> - </snapshots> - <id>IHE</id> - <name>IHE Public Maven Repository Group</name> - <url>https://gazelle.ihe.net/nexus/content/groups/public/</url> - <layout>default</layout> - </repository> - </repositories> - <distributionManagement> - <repository> - <id>nexus-releases</id> - <url>https://gazelle.ihe.net/nexus/content/repositories/releases</url> - </repository> - </distributionManagement> - <profiles> - <profile> - <id>dev</id> - <activation> - <activeByDefault>true</activeByDefault> - </activation> - </profile> - <profile> - <id>sonar</id> - <build> - <plugins> - <plugin> - <groupId>org.sonarsource.scanner.maven</groupId> - <artifactId>sonar-maven-plugin</artifactId> - <version>${sonar.maven.plugin}</version> - <executions> - <execution> - <phase>verify</phase> - <goals> - <goal>sonar</goal> - </goals> - </execution> - </executions> - </plugin> - </plugins> - </build> - </profile> - <profile> - <id>release</id> - <build> - <plugins> - <plugin> - <groupId>org.sonatype.plugins</groupId> - <artifactId>nexus-staging-maven-plugin</artifactId> - <version>${nexus.staging.maven.plugin.version}</version> - <executions> - <execution> - <id>default-deploy</id> - <phase>deploy</phase> - <goals> - <goal>deploy</goal> - </goals> - </execution> - </executions> - <configuration> - <serverId>nexus-releases</serverId> - <nexusUrl>https://gazelle.ihe.net/nexus</nexusUrl> - <skipStaging>true</skipStaging> - </configuration> - </plugin> - </plugins> - </build> - </profile> - </profiles> - - <build> - <finalName>pixm_fhir_server</finalName> - - <plugins> - - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-release-plugin</artifactId> - <version>${maven.release.plugin.version}</version> - <configuration> - <tagNameFormat>@{project.version}</tagNameFormat> - <autoVersionSubmodules>true</autoVersionSubmodules> - <releaseProfiles>release</releaseProfiles> - </configuration> - </plugin> - <!-- Tell Maven which Java source version you want to use --> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <version>3.8.1</version> - <configuration> - <source>${java.version}</source> - <target>${java.version}</target> - </configuration> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-surefire-plugin</artifactId> - <version>3.0.0-M3</version> - <configuration> - <properties> - <property> - <name>listener</name> - <value>io.qameta.allure.junit5.AllureJunit5</value> - </property> - </properties> - <dependenciesToScan> - <dependency>net.ihe.gazelle:lib.unit-test</dependency> - </dependenciesToScan> - <testFailureIgnore>false</testFailureIgnore> - <argLine> - -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" - </argLine> - <systemProperties> - <property> - <name>junit.jupiter.extensions.autodetection.enabled</name> - <value>true</value> - </property> - <property> - <name>allure.results.directory</name> - <value>${project.basedir}/target/allure-results</value> - </property> - </systemProperties> - <argLine>${argLine}</argLine> - </configuration> - <dependencies> - <dependency> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-jupiter-api</artifactId> - <version>${junit.jupiter.version}</version> - </dependency> - <dependency> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-jupiter-engine</artifactId> - <version>${junit.jupiter.version}</version> - </dependency> - <dependency> - <groupId>org.aspectj</groupId> - <artifactId>aspectjweaver</artifactId> - <version>${aspectj.version}</version> - </dependency> - </dependencies> - </plugin> - <plugin> - <groupId>io.qameta.allure</groupId> - <artifactId>allure-maven</artifactId> - <version>2.10.0</version> - <configuration> - <allureDownloadUrl>https://github.com/allure-framework/allure-maven/archive/refs/tags/2.10.0.zip</allureDownloadUrl> - </configuration> - </plugin> - - <!-- The configuration here tells the WAR plugin to include the FHIR Tester - overlay. You can omit it if you are not using that feature. --> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-war-plugin</artifactId> - <configuration> - <overlays> - <overlay> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-testpage-overlay</artifactId> - </overlay> - </overlays> - </configuration> - </plugin> - <plugin> - <groupId>org.jacoco</groupId> - <artifactId>jacoco-maven-plugin</artifactId> - <version>0.8.6</version> - <executions> - <execution> - <id>pre-unit-test</id> - <goals> - <goal>prepare-agent</goal> - </goals> - </execution> - <execution> - <id>default-report</id> - <phase>package</phase> - <goals> - <goal>report</goal> - </goals> - <configuration> - <dataFile>${project.build.directory}/target/jacoco.exec</dataFile> - </configuration> - </execution> - </executions> - </plugin> - - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-javadoc-plugin</artifactId> - <configuration> - <additionalparam>-Xdoclint:none</additionalparam> - </configuration> - </plugin> - </plugins> - </build> - <dependencies> - <dependency> - <groupId>net.ihe.gazelle</groupId> - <artifactId>framework.preferences-model-api</artifactId> - <version>1.0.0</version> - </dependency> - <dependency> - <groupId>net.ihe.gazelle</groupId> - <artifactId>framework.operational-preferences-service</artifactId> - <version>1.0.0</version> - </dependency> - <dependency> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-jupiter</artifactId> - <version>5.6.0</version> - <scope>test</scope> - </dependency> - <!-- https://mvnrepository.com/artifact/org.junit.platform/junit-platform-commons --> - <dependency> - <groupId>org.junit.platform</groupId> - <artifactId>junit-platform-commons</artifactId> - <version>1.7.2</version> - </dependency> - <dependency> - <groupId>org.junit.platform</groupId> - <artifactId>junit-platform-launcher</artifactId> - <version>1.7.2</version> - </dependency> - - <dependency> - <groupId>net.ihe.gazelle</groupId> - <artifactId>framework.preferences-model-api</artifactId> - <version>1.0.0</version> - </dependency> - - <!-- This dependency includes the core HAPI-FHIR classes --> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-base</artifactId> - <version>${hapifhir_version}</version> - </dependency> - - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-server</artifactId> - <version>${hapifhir_version}</version> - </dependency> - - <!-- At least one "structures" JAR must also be included --> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-structures-r4</artifactId> - <version>${hapifhir_version}</version> - </dependency> - - <!-- Used for validation --> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-validation-resources-r4</artifactId> - <version>${hapifhir_version}</version> - </dependency> - <dependency> - <groupId>com.phloc</groupId> - <artifactId>phloc-schematron</artifactId> - <version>2.7.0</version> - </dependency> - - <!-- This dependency is used for the "FHIR Tester" web app overlay --> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-testpage-overlay</artifactId> - <version>${hapifhir_version}</version> - <type>war</type> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>ca.uhn.hapi.fhir</groupId> - <artifactId>hapi-fhir-testpage-overlay</artifactId> - <version>${hapifhir_version}</version> - <classifier>classes</classifier> - <scope>provided</scope> - </dependency> - - <!-- HAPI-FHIR uses Logback for logging support. The logback library is - included automatically by Maven as a part of the hapi-fhir-base dependency, - but you also need to include a logging library. Logback is used here, but - log4j would also be fine. --> - <dependency> - <groupId>ch.qos.logback</groupId> - <artifactId>logback-classic</artifactId> - <version>1.1.7</version> - </dependency> - - <dependency> - <groupId>info.debatty</groupId> - <artifactId>java-string-similarity</artifactId> - <version>RELEASE</version> - </dependency> - - <!-- Needed for JEE/Servlet support --> - <dependency> - <groupId>javax</groupId> - <artifactId>javaee-api</artifactId> - <version>8.0.1</version> - </dependency> - <dependency> - <groupId>javax.servlet</groupId> - <artifactId>javax.servlet-api</artifactId> - <version>4.0.1</version> - </dependency> - - <!-- If you are using HAPI narrative generation, you will need to include - Thymeleaf as well. Otherwise the following can be omitted. --> - <dependency> - <groupId>org.thymeleaf</groupId> - <artifactId>thymeleaf</artifactId> - <version>3.0.2.RELEASE</version> - </dependency> - - <dependency> - <groupId>org.fhir</groupId> - <artifactId>ucum</artifactId> - <version>1.0.3</version> - </dependency> - - <dependency> - <groupId>com.github.ben-manes.caffeine</groupId> - <artifactId>caffeine</artifactId> - <version>3.0.2</version> - </dependency> - - <dependency> - <groupId>net.ihe.gazelle</groupId> - <artifactId>app.patient-registry-xref-search-client</artifactId> - <version>2.1.0</version> - </dependency> - - <dependency> - <groupId>net.ihe.gazelle</groupId> - <artifactId>app.patient-registry-search-client</artifactId> - <version>2.1.0</version> - </dependency> - - <dependency> - <groupId>net.ihe.gazelle</groupId> - <artifactId>app.patient-registry-feed-client</artifactId> - <version>2.1.0</version> - </dependency> - - <!-- Used for CORS support --> - <dependency> - <groupId>org.ebaysf.web</groupId> - <artifactId>cors-filter</artifactId> - <version>1.0.1</version> - <exclusions> - <exclusion> - <artifactId>servlet-api</artifactId> - <groupId>javax.servlet</groupId> - </exclusion> - </exclusions> - </dependency> - - <dependency> - <groupId>org.mockito</groupId> - <artifactId>mockito-all</artifactId> - <version>1.10.19</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.powermock</groupId> - <artifactId>powermock-module-junit4</artifactId> - <version>2.0.9</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>io.qameta.allure</groupId> - <artifactId>allure-junit5</artifactId> - <version>2.14.0</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>com.googlecode.json-simple</groupId> - <artifactId>json-simple</artifactId> - <version>1.1.1</version> - </dependency> - - </dependencies> </project> diff --git a/settings.xml b/settings.xml index 41fe8d8..2821c11 100644 --- a/settings.xml +++ b/settings.xml @@ -1,11 +1,25 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> -<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> +<settings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/SETTINGS/1.0.0" + xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <servers> + <!-- server configuration for releases --> <server> <id>nexus-releases</id> <username>${ARTIFACT_RELEASE_REPOSITORY_USER}</username> <password>${ARTIFACT_RELEASE_REPOSITORY_PASS}</password> </server> + <!-- server configuration for snapshot publishes --> + <server> + <id>nexus-snapshots</id> + <username>${ARTIFACT_RELEASE_REPOSITORY_USER}</username> + <password>${ARTIFACT_RELEASE_REPOSITORY_PASS}</password> + </server> + <!-- server configuration for docker pushes --> + <server> + <id>rg.fr-par.scw.cloud</id> + <username>${CS_SVC_REGISTRY_IMAGE_USER}</username> + <password>${CS_SVC_REGISTRY_IMAGE_PASS}</password> + </server> </servers> <mirrors> <mirror> diff --git a/src/main/java/net/ihe/gazelle/adapter/connector/BundleToPatientRegistryConverter.java b/src/main/java/net/ihe/gazelle/adapter/connector/BundleToPatientRegistryConverter.java deleted file mode 100644 index 6e8c702..0000000 --- a/src/main/java/net/ihe/gazelle/adapter/connector/BundleToPatientRegistryConverter.java +++ /dev/null @@ -1,226 +0,0 @@ -package net.ihe.gazelle.adapter.connector; - -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; -import net.ihe.gazelle.app.patientregistryapi.business.GenderCode; -import net.ihe.gazelle.app.patientregistryapi.business.Person; -import net.ihe.gazelle.app.patientregistryapi.business.PersonName; -import org.hl7.fhir.r4.model.*; -import org.hl7.fhir.r4.model.Address.AddressUse; -import org.hl7.fhir.r4.model.Patient.ContactComponent; - -/** - * Class containing static methods to convert - * - * @author pvm - */ -public class BundleToPatientRegistryConverter { - - /** - * private constructor to override the public implicit one. - */ - private BundleToPatientRegistryConverter() { - } - - - - public static net.ihe.gazelle.app.patientregistryapi.business.Patient fhirPatientToGazellePatient(Patient patient) throws ConversionException { - net.ihe.gazelle.app.patientregistryapi.business.Patient convertedPatient = new net.ihe.gazelle.app.patientregistryapi.business.Patient(); - if (patient.hasActive()) { - convertedPatient.setActive(patient.getActive()); - } - - if (patient.hasBirthDate()) { - convertedPatient.setDateOfBirth(patient.getBirthDate()); - } - - if (patient.hasDeceased()) { - convertedPatient.setDateOfDeath(patient.getDeceasedDateTimeType().getValue()); - } - - if (patient.hasGender()) { - convertedPatient.setGender(convertGender(patient.getGender())); - } - - if (patient.hasMultipleBirth()) { - convertedPatient.setMultipleBirthOrder(patient.getMultipleBirthIntegerType().getValue()); - } - - if (patient.hasId()) { - convertedPatient.setUuid(patient.getId()); - } - - convertingLists(patient, convertedPatient); - - return convertedPatient; - } - - private static void convertingLists(Patient patient, net.ihe.gazelle.app.patientregistryapi.business.Patient convertedPatient) throws ConversionException { - if (patient.hasAddress()) { - for (Address address : patient.getAddress()) { - convertedPatient.addAddress(convertAddress(address)); - } - } - - if (patient.hasContact()) { - for (ContactComponent contact : patient.getContact()) { - convertedPatient.addContact(convertContact(contact)); - } - } - - if (patient.hasName()) { - for (HumanName name : patient.getName()) { - convertedPatient.addName(convertName(name)); - } - } - - if (patient.hasIdentifier()) { - for (Identifier id : patient.getIdentifier()) { - addEntity(convertedPatient, id); - } - } - } - - private static void addEntity(net.ihe.gazelle.app.patientregistryapi.business.Patient convertedPatient, Identifier id) { - EntityIdentifier entityIdentifier = new EntityIdentifier(); - - if (id.hasSystem()) { - String systemID = id.getSystem(); - systemID = systemID.replace("urn:oid:", ""); - entityIdentifier.setSystemIdentifier(systemID); - entityIdentifier.setType("ISO"); - } - - if (id.hasValue()) { - entityIdentifier.setValue(id.getValue()); - } - else { - throw new InvalidRequestException("Cannot create Patient without any Identifier"); - } - - convertedPatient.addIdentifier(entityIdentifier); - } - - private static GenderCode convertGender(Enumerations.AdministrativeGender gender) throws ConversionException { - if (gender == null) { - throw new ConversionException("No gender present"); - } - - switch (gender) { - case MALE: - return GenderCode.MALE; - case FEMALE: - return GenderCode.FEMALE; - case UNKNOWN: - return GenderCode.UNDEFINED; - case OTHER: - return GenderCode.OTHER; - default: - throw new ConversionException(String.format("Cannot map GenderCode : %s", gender)); - - } - - - } - - private static net.ihe.gazelle.app.patientregistryapi.business.Address convertAddress(Address address) throws ConversionException { - net.ihe.gazelle.app.patientregistryapi.business.Address convertedAddress = new net.ihe.gazelle.app.patientregistryapi.business.Address(); - - if (address.hasCity()) { - convertedAddress.setCity(address.getCity()); - } - - if (address.hasCountry()) { - convertedAddress.setCountryIso3(address.getCountry()); - } - - if (address.hasPostalCode()) { - convertedAddress.setPostalCode(address.getPostalCode()); - } - - if (address.hasState()) { - convertedAddress.setState(address.getState()); - } - - if (address.hasUse()) { - convertedAddress.setUse(convertAddressUse(address.getUse())); - } - - if (address.hasLine()) { - for (StringType addressLine : address.getLine()) { - convertedAddress.addLine(addressLine.getValue()); - } - } - return convertedAddress; - } - - private static net.ihe.gazelle.app.patientregistryapi.business.AddressUse convertAddressUse(AddressUse addressUse) throws ConversionException { - if (addressUse != null) { - switch (addressUse) { - case BILLING: - return net.ihe.gazelle.app.patientregistryapi.business.AddressUse.BILLING; - case HOME: - return net.ihe.gazelle.app.patientregistryapi.business.AddressUse.HOME; - case OLD: - return net.ihe.gazelle.app.patientregistryapi.business.AddressUse.BAD; - case TEMP: - return net.ihe.gazelle.app.patientregistryapi.business.AddressUse.TEMPORARY; - case WORK: - return net.ihe.gazelle.app.patientregistryapi.business.AddressUse.WORK; - default: - throw new ConversionException(String.format("Cannot map address use : %s", addressUse)); - } - } else { - throw new ConversionException("No address use present"); - } - } - - private static Person convertContact(ContactComponent contact) throws ConversionException { - Person convertedContact = new Person(); - - if (contact.hasGender()) { - convertedContact.setGender(convertGender(contact.getGender())); - } - - if (contact.hasAddress()) { - convertedContact.addAddress(convertAddress(contact.getAddress())); - } - - if (contact.hasName()) { - convertedContact.addName(convertName(contact.getName())); - } - return convertedContact; - } - - private static PersonName convertName(HumanName name) { - PersonName convertedName = new PersonName(); - - if (name.hasUse()) { - convertedName.setUse(name.getUse().toString()); - } - - if (name.hasFamily()) { - convertedName.setFamily(name.getFamily()); - } - - if (name.hasGiven()) { - for (StringType givenName : name.getGiven()) { - convertedName.addGiven(givenName.getValue()); - } - } - - if (name.hasPrefix()) { - for (StringType prefixName : name.getPrefix()) { - convertedName.setPrefix(prefixName.getValue()); - } - } - - if (name.hasSuffix()) { - for (StringType suffixName : name.getSuffix()) { - convertedName.setSuffix(suffixName.getValue()); - } - } - - return convertedName; - } -} diff --git a/src/main/java/net/ihe/gazelle/adapter/connector/BusinessToFhirConverter.java b/src/main/java/net/ihe/gazelle/adapter/connector/BusinessToFhirConverter.java deleted file mode 100644 index 5bac0bd..0000000 --- a/src/main/java/net/ihe/gazelle/adapter/connector/BusinessToFhirConverter.java +++ /dev/null @@ -1,251 +0,0 @@ -package net.ihe.gazelle.adapter.connector; - -import net.ihe.gazelle.app.patientregistryapi.business.Address; -import net.ihe.gazelle.app.patientregistryapi.business.ContactPoint; -import net.ihe.gazelle.app.patientregistryapi.business.Patient; -import net.ihe.gazelle.app.patientregistryapi.business.*; -import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.r4.model.*; -import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.r4.model.Bundle.BundleType; -import org.hl7.fhir.r4.model.MessageHeader.MessageDestinationComponent; -import org.hl7.fhir.r4.model.MessageHeader.MessageHeaderResponseComponent; -import org.hl7.fhir.r4.model.MessageHeader.MessageSourceComponent; -import org.hl7.fhir.r4.model.MessageHeader.ResponseType; -import org.hl7.fhir.r4.model.Narrative.NarrativeStatus; - -import java.util.List; - -/** - * Converter to transform Patient Registry's {@link Patient} to {@link org.hl7.fhir.r4.model.Patient}. - * - * @author pvm - */ -public class BusinessToFhirConverter { - - private static final String URN_PREFIX = "urn:oid:"; - - /** - * private constructor to override the implicit one. - */ - private BusinessToFhirConverter() { - } - - public static Bundle uuidToBundle(String uuid, String serverUrl) { - - Bundle response = new Bundle(); - response.setId("BundlePIXmResponse"); - Meta metadata = new Meta(); - metadata.addProfile("http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle-response"); - response.setType(BundleType.MESSAGE); - BundleEntryComponent entry = new BundleEntryComponent(); - entry.setFullUrl(serverUrl + "/pixm-connector/fhir_ch/Patient/" + uuid); - MessageHeader messageHeader = new MessageHeader(); - messageHeader.setId(uuid); - Narrative narrative = new Narrative(); - narrative.setStatus(NarrativeStatus.GENERATED); - messageHeader.setText(narrative); - MessageDestinationComponent mdc = new MessageDestinationComponent(); - mdc.setEndpoint(serverUrl + "/pixm-connector/fhir_ch/Patient/" + uuid); - messageHeader.addDestination(mdc); - MessageSourceComponent msc = new MessageSourceComponent(); - msc.setEndpoint(serverUrl + "/pixm-connector/fhir_ch/Patient/" + uuid); - messageHeader.setSource(msc); - MessageHeaderResponseComponent mhrc = new MessageHeaderResponseComponent(); - mhrc.setIdentifier(uuid); - mhrc.setCode(ResponseType.OK); - messageHeader.setResponse(mhrc); - entry.setResource(messageHeader); - response.addEntry(entry); - response.setMeta(metadata); - - return response; - - } - - public static org.hl7.fhir.r4.model.Patient patientToFhirPatient(Patient patient) throws ConversionException { - org.hl7.fhir.r4.model.Patient fhirPatient = new org.hl7.fhir.r4.model.Patient(); - fhirPatient.setId(patient.getUuid()); - setNames(fhirPatient, patient); - fhirPatient.setGender(getGenderCode(patient)); - setCrossIdentifier(fhirPatient, patient); - setBirthDate(fhirPatient, patient); - setAddresses(fhirPatient, patient); - setTelecom(fhirPatient, patient); - fhirPatient.setActive(patient.isActive()); - return fhirPatient; - - } - - private static void setNames(org.hl7.fhir.r4.model.Patient fhirPatient, Patient patient) throws ConversionException { - List<PersonName> patientNames = patient.getNames(); - if (patientNames != null) { - for (PersonName personName : patientNames) { - if (personName != null) { - HumanName name = new HumanName(); - name.setFamily(personName.getFamily()); - for (String given : personName.getGivens()) { - name.addGiven(given); - } - try { - name.setUse(HumanName.NameUse.fromCode(personName.getUse())); - } catch (FHIRException e) { - throw new ConversionException(String.format("Cannot convert PersonName use : %s", personName.getUse()), e); - } - name.addPrefix(personName.getPrefix()); - name.addSuffix(personName.getSuffix()); - fhirPatient.addName(name); - } - } - } - } - - private static Enumerations.AdministrativeGender getGenderCode(Patient patient) throws ConversionException { - if (patient.getGender() != null) { - switch (patient.getGender()) { - case MALE: - return Enumerations.AdministrativeGender.MALE; - case FEMALE: - return Enumerations.AdministrativeGender.FEMALE; - case UNDEFINED: - return Enumerations.AdministrativeGender.UNKNOWN; - case OTHER: - return Enumerations.AdministrativeGender.OTHER; - default: - throw new ConversionException(String.format("Cannot map GenderCode : %s", patient.getGender())); - } - } - return null; - - } - - private static void setCrossIdentifier(org.hl7.fhir.r4.model.Patient fhirPatient, Patient patient) { - if (patient.getIdentifiers() != null) { - for (EntityIdentifier currentPatientIdentifier : patient.getIdentifiers()) { - if (currentPatientIdentifier.getSystemIdentifier() != null) { - String fhirSystem = getUniversalIDAsUrn(currentPatientIdentifier.getSystemIdentifier()); - fhirPatient.addIdentifier().setSystem(fhirSystem).setValue(currentPatientIdentifier.getValue()); - } - } - } - } - - private static void setBirthDate(org.hl7.fhir.r4.model.Patient fhirPatient, Patient patient) { - if (patient.getDateOfBirth() != null) { - fhirPatient.setBirthDate(patient.getDateOfBirth()); - } - } - - private static void setAddresses(org.hl7.fhir.r4.model.Patient fhirPatient, Patient patient) throws ConversionException { - List<Address> addressList = patient.getAddresses(); - if (addressList != null) { - for (Address address : addressList) { - org.hl7.fhir.r4.model.Address fhirAddress = new org.hl7.fhir.r4.model.Address(); - for (String line : address.getLines()) { - fhirAddress.addLine(line); - } - fhirAddress.setCity(address.getCity()); - fhirAddress.setCountry(address.getCountryIso3()); - fhirAddress.setPostalCode(address.getPostalCode()); - fhirAddress.setState(address.getState()); - fhirAddress.setUse(getAddressUse(address.getUse())); - fhirPatient.addAddress(fhirAddress); - } - } - } - - private static void setTelecom(org.hl7.fhir.r4.model.Patient fhirPatient, Patient patient) throws ConversionException { - List<ContactPoint> contactPoints = patient.getContactPoints(); - if (contactPoints != null) { - for (ContactPoint contactPoint : contactPoints) { - if (contactPoint != null) { - org.hl7.fhir.r4.model.ContactPoint fhirContactPoint = new org.hl7.fhir.r4.model.ContactPoint(); - fhirContactPoint.setSystem(getContactPointSystem(contactPoint.getType())); - fhirContactPoint.setValue(contactPoint.getValue()); - fhirContactPoint.setUse(getContactPointUse(contactPoint.getUse())); - fhirPatient.addTelecom(fhirContactPoint); - } - } - } - } - - private static String getUniversalIDAsUrn(String universalID) { - if (universalID == null) { - return null; - } else if (universalID.startsWith(URN_PREFIX)) { - return universalID; - } else { - return URN_PREFIX + universalID; - } - } - - private static org.hl7.fhir.r4.model.Address.AddressUse getAddressUse(AddressUse addressUse) throws ConversionException { - if (addressUse == null) { - return null; - } - - switch (addressUse) { - case HOME: - case PRIMARY_HOME: - return org.hl7.fhir.r4.model.Address.AddressUse.HOME; - case WORK: - return org.hl7.fhir.r4.model.Address.AddressUse.WORK; - case VACATION_HOME: - case TEMPORARY: - return org.hl7.fhir.r4.model.Address.AddressUse.TEMP; - case BAD: - return org.hl7.fhir.r4.model.Address.AddressUse.OLD; - case BILLING: - return org.hl7.fhir.r4.model.Address.AddressUse.BILLING; - default: - throw new ConversionException(String.format("Cannot map AddressUse : %s", addressUse)); - } - - } - - private static org.hl7.fhir.r4.model.ContactPoint.ContactPointUse getContactPointUse(ContactPointUse contactPointUse) throws ConversionException { - if (contactPointUse == null) { - return null; - } - switch (contactPointUse) { - case HOME: - case PRIMARY_HOME: - return org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.HOME; - case MOBILE: - return org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.MOBILE; - case WORK: - return org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.WORK; - case TEMPORARY: - return org.hl7.fhir.r4.model.ContactPoint.ContactPointUse.TEMP; - default: - throw new ConversionException(String.format("Cannot map ContactPointUse : %s", contactPointUse.value())); - } - } - - private static org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem getContactPointSystem(ContactPointType contactPointType) - throws ConversionException { - if (contactPointType == null) { - return null; - } - switch (contactPointType) { - case BEEPER: - return org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.PAGER; - case PHONE: - return org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.PHONE; - case FAX: - return org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.FAX; - case URL: - return org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.URL; - case EMAIL: - return org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.EMAIL; - case SMS: - return org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.SMS; - case OTHER: - return org.hl7.fhir.r4.model.ContactPoint.ContactPointSystem.OTHER; - default: - throw new ConversionException(String.format("Cannot map ContactPointType : %s", contactPointType)); - } - - } - -} diff --git a/src/main/java/net/ihe/gazelle/adapter/connector/DeletionBundle.java b/src/main/java/net/ihe/gazelle/adapter/connector/DeletionBundle.java deleted file mode 100644 index 4f072f9..0000000 --- a/src/main/java/net/ihe/gazelle/adapter/connector/DeletionBundle.java +++ /dev/null @@ -1,52 +0,0 @@ -package net.ihe.gazelle.adapter.connector; - -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.MessageHeader; -import org.hl7.fhir.r4.model.Meta; -import org.hl7.fhir.r4.model.Narrative; - -public class DeletionBundle { - /** - * private constructor to override the implicit one. - */ - private DeletionBundle() { - } - - public static Bundle prepareBundleAfterDeletionOperation(String uuid, String url) { - Bundle response = new Bundle(); - response.setId("BundlePIXmResponse"); - - Meta metadata = new Meta(); - metadata.addProfile("http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle-response"); - response.setType(Bundle.BundleType.MESSAGE); - response.setMeta(metadata); - - - Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent(); - entry.setFullUrl(url + "/pixm-connector/fhir_ch/Patient/" + uuid); - - MessageHeader messageHeader = new MessageHeader(); - messageHeader.setId(uuid); - Narrative narrative = new Narrative(); - narrative.setStatus(Narrative.NarrativeStatus.GENERATED); - messageHeader.setText(narrative); - - MessageHeader.MessageHeaderResponseComponent mhrc = new MessageHeader.MessageHeaderResponseComponent(); - mhrc.setIdentifier(uuid); - mhrc.setCode(MessageHeader.ResponseType.OK); - - messageHeader.setResponse(mhrc); - entry.setResource(messageHeader); - - entry.getRequest() - .setUrl("Delete") - .setMethod(Bundle.HTTPVerb.DELETE); - entry.getResponse() - .setStatus("DONE"); - response.addEntry(entry); - - - return response; - - } -} diff --git a/src/main/java/net/ihe/gazelle/adapter/connector/UpdateBundle.java b/src/main/java/net/ihe/gazelle/adapter/connector/UpdateBundle.java deleted file mode 100644 index 6052574..0000000 --- a/src/main/java/net/ihe/gazelle/adapter/connector/UpdateBundle.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.ihe.gazelle.adapter.connector; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.MessageHeader; -import org.hl7.fhir.r4.model.Meta; -import org.hl7.fhir.r4.model.Narrative; - -public class UpdateBundle { - - /** - * private constructor to override the implicit one. - */ - private UpdateBundle() { - } - public static Bundle prepareBundleAfterUpdateOperation(String uuid, String url) { - Bundle response = new Bundle(); - response.setId("BundlePIXmResponse"); - Meta metadata = new Meta(); - metadata.addProfile("http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle-response"); - response.setType(Bundle.BundleType.MESSAGE); - response.setMeta(metadata); - - Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent(); - entry.setFullUrl(url + "/pixm-connector/fhir_ch/Patient/" + uuid); - MessageHeader messageHeader = new MessageHeader(); - messageHeader.setId(uuid); - Narrative narrative = new Narrative(); - narrative.setStatus(Narrative.NarrativeStatus.GENERATED); - messageHeader.setText(narrative); - MessageHeader.MessageHeaderResponseComponent mhrc = new MessageHeader.MessageHeaderResponseComponent(); - mhrc.setIdentifier(uuid); - mhrc.setCode(MessageHeader.ResponseType.OK); - messageHeader.setResponse(mhrc); - entry.setResource(messageHeader); - response.addEntry(entry); - - response.addEntry() - .getRequest() - .setUrl("PUT") - .setMethod(Bundle.HTTPVerb.PUT); - return response; - - } -} diff --git a/src/main/java/net/ihe/gazelle/adapter/servlet/ChHapiFhirServer.java b/src/main/java/net/ihe/gazelle/adapter/servlet/ChHapiFhirServer.java deleted file mode 100644 index de34314..0000000 --- a/src/main/java/net/ihe/gazelle/adapter/servlet/ChHapiFhirServer.java +++ /dev/null @@ -1,78 +0,0 @@ -package net.ihe.gazelle.adapter.servlet; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; -import ca.uhn.fhir.narrative.INarrativeGenerator; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor; -import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; -import net.ihe.gazelle.business.provider.CHBundleProvider; -import net.ihe.gazelle.business.provider.ChPatientResourceProvider; - -import javax.inject.Inject; -import javax.servlet.annotation.WebServlet; -import java.util.ArrayList; -import java.util.List; - -/** - * This servlet is the actual FHIR server itself - */ -@WebServlet(urlPatterns = {"/fhir_ch/*"}, displayName = "FHIR Server CH") -public class ChHapiFhirServer extends RestfulServer { - private static final long serialVersionUID = 1L; - - @Inject - private ChPatientResourceProvider chPatientResourceProvider; - - @Inject - private CHBundleProvider chBundleProvider; - - /** - * Constructor - */ - public ChHapiFhirServer() { - super(FhirContext.forR4()); // This is an R4 server - } - - /** - * This method is called automatically when the - * servlet is initializing. - */ - @Override - public void initialize() { - /* - * Two resource providers are defined. Each one handles a specific - * type of resource. - */ - List<IResourceProvider> providers = new ArrayList<>(); - providers.add(chPatientResourceProvider); - providers.add(chBundleProvider); - setResourceProviders(providers); - - //creating an interceptor to log in console an error occurring - LoggingInterceptor loggingInterceptor = new LoggingInterceptor(); - loggingInterceptor.setLoggerName("test.accesslog"); - loggingInterceptor.setMessageFormat("Source[$remoteAddr] Operation[${operationType}" - + "${idOrResourceName}] UA[${requestHeader.user-agent}] Params[${requestParameters}]"); - - registerInterceptor(loggingInterceptor); - - //creating an interceptor for special exception to dump the stack trace in the logs. - ExceptionHandlingInterceptor exceptionInterceptor = new ExceptionHandlingInterceptor(); - exceptionInterceptor.setReturnStackTracesForExceptionTypes(InternalErrorException.class, NullPointerException.class); - registerInterceptor(exceptionInterceptor); - - /* - * Use a narrative generator. This is a completely optional step, - * but can be useful as it causes HAPI to generate narratives for - * resources which don't otherwise have one. - */ - INarrativeGenerator narrativeGen = new DefaultThymeleafNarrativeGenerator(); - getFhirContext().setNarrativeGenerator(narrativeGen); - - - } - -} diff --git a/src/main/java/net/ihe/gazelle/adapter/servlet/IheHapiFhirServer.java b/src/main/java/net/ihe/gazelle/adapter/servlet/IheHapiFhirServer.java deleted file mode 100644 index 72494be..0000000 --- a/src/main/java/net/ihe/gazelle/adapter/servlet/IheHapiFhirServer.java +++ /dev/null @@ -1,69 +0,0 @@ -package net.ihe.gazelle.adapter.servlet; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; -import ca.uhn.fhir.narrative.INarrativeGenerator; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor; -import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; -import net.ihe.gazelle.business.provider.IhePatientResourceProvider; - -import javax.inject.Inject; -import javax.servlet.annotation.WebServlet; -import java.util.ArrayList; -import java.util.List; - - -@WebServlet(urlPatterns = {"/fhir_ihe/*"}, displayName = "FHIR Server IHE") -public class IheHapiFhirServer extends RestfulServer { - private static final long serialVersionUID = 1L; - @Inject - private IhePatientResourceProvider ihePatientResourceProvider; - - /** - * Constructor - */ - public IheHapiFhirServer() { - super(FhirContext.forR4()); - } - - /** - * This method is called automatically when the - * servlet is initializing. - */ - @Override - public void initialize() { - /* - * Two resource providers are defined. Each one handles a specific - * type of resource. - */ - List<IResourceProvider> providers = new ArrayList<>(); - providers.add(ihePatientResourceProvider); - setResourceProviders(providers); - - //creating an interceptor to log in console an error occurring - LoggingInterceptor loggingInterceptor = new LoggingInterceptor(); - loggingInterceptor.setLoggerName("test.accesslog"); - loggingInterceptor.setMessageFormat("Source[$remoteAddr] Operation[${operationType}" - + "${idOrResourceName}] UA[${requestHeader.user-agent}] Params[${requestParameters}]"); - - registerInterceptor(loggingInterceptor); - - //creating an interceptor for special exception to dump the stack trace in the logs. - ExceptionHandlingInterceptor exceptionInterceptor = new ExceptionHandlingInterceptor(); - exceptionInterceptor.setReturnStackTracesForExceptionTypes(InternalErrorException.class, NullPointerException.class); - registerInterceptor(exceptionInterceptor); - - /* - * Use a narrative generator. This is a completely optional step, - * but can be useful as it causes HAPI to generate narratives for - * resources which don't otherwise have one. - */ - INarrativeGenerator narrativeGen = new DefaultThymeleafNarrativeGenerator(); - getFhirContext().setNarrativeGenerator(narrativeGen); - - } - -} diff --git a/src/main/java/net/ihe/gazelle/application/PatientRegistryFeedClient.java b/src/main/java/net/ihe/gazelle/application/PatientRegistryFeedClient.java deleted file mode 100644 index f93c2da..0000000 --- a/src/main/java/net/ihe/gazelle/application/PatientRegistryFeedClient.java +++ /dev/null @@ -1,263 +0,0 @@ -package net.ihe.gazelle.application; - -import ca.uhn.fhir.rest.server.exceptions.*; -import net.ihe.gazelle.adapter.connector.BusinessToFhirConverter; -import net.ihe.gazelle.adapter.connector.DeletionBundle; -import net.ihe.gazelle.adapter.connector.UpdateBundle; -import net.ihe.gazelle.adapter.preferences.Preferences; -import net.ihe.gazelle.app.patientregistryapi.application.PatientFeedException; -import net.ihe.gazelle.app.patientregistryapi.business.PersonName; -import net.ihe.gazelle.app.patientregistryfeedclient.adapter.PatientFeedClient; -import net.ihe.gazelle.framework.loggerservice.application.GazelleLogger; -import net.ihe.gazelle.framework.loggerservice.application.GazelleLoggerFactory; -import net.ihe.gazelle.framework.preferencesmodelapi.application.NamespaceException; -import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; -import net.ihe.gazelle.framework.preferencesmodelapi.application.PreferenceException; -import net.ihe.gazelle.lib.annotations.Package; -import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.r4.model.Bundle; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.xml.ws.WebServiceException; -import java.net.InetAddress; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.UnknownHostException; - -@Named("PatientRegistryFeedClient") -public class PatientRegistryFeedClient { - - public static final String NO_UUID = "No UUID was retrieved from the created Patient"; - public static final String NO_PATIENT_PARAMETER = "No Patient were given to create"; - public static final String CLIENT_NOT_SET = "client not set"; - public static final String INVALID_PARAMETERS = "Invalid parameters"; - private String serverUrl; - private static final GazelleLogger logger = GazelleLoggerFactory.getInstance().getLogger(PatientRegistryFeedClient.class); - private OperationalPreferencesService operationalPreferencesService; - private PatientFeedClient client; - - public PatientRegistryFeedClient() { - } - - /** - * Default constructor, used for injection. - * - * @param operationalPreferencesService {@link OperationalPreferencesService} used to retrieve Patient Registry's URL. - */ - @Inject - public PatientRegistryFeedClient(OperationalPreferencesService operationalPreferencesService) { - - this.operationalPreferencesService = operationalPreferencesService; - } - - /** - * Package-private constructor used for test purposes. - * - * @param url {@link URL} to be used by this to instantiate the processing service to retrieve patients. - */ - @Package - PatientRegistryFeedClient(URL url) { - this.client = new PatientFeedClient(url); - } - - @Package - public void setClient(PatientFeedClient client) { - this.client = client; - } - - /** - * Initialize the Search Client using the Operational Preferences Service. - * - * @throws PatientFeedException if the service cannot be instantiated. - */ - private void initializeClient() throws PatientFeedException { - String patientRegistryUrl = null; - getUrlOfServer(); - try { - patientRegistryUrl = this.operationalPreferencesService. - getStringValue(Preferences.PATIENT_REGISTRY_URL.getNamespace().getValue(), Preferences.PATIENT_REGISTRY_URL.getKey()); - this.client = new PatientFeedClient(new URL(patientRegistryUrl)); - } catch (NamespaceException | PreferenceException e) { - throw new PatientFeedException(String.format("Unable to retrieve [%s] Preference !", Preferences.PATIENT_REGISTRY_URL.getKey())); - } catch (MalformedURLException e) { - throw new PatientFeedException(String.format("Preference [%s] with value [%s] is not a valid URL !", - Preferences.PATIENT_REGISTRY_URL.getKey(), - patientRegistryUrl)); - } catch (WebServiceException e) { - logger.warn(e.getMessage()); - throw new PatientFeedException(String.format("Can't connect to patient registry ! at address [%s]", patientRegistryUrl)); - } - } - - /** - * Method called to create a Patient in the Patient Registry Database - * - * @param patient : the patient to add in database, represented in patientRegistry model - * @return a string corresponding to the uuid of the newly created patient in the database. - */ - public Bundle createPatient(net.ihe.gazelle.app.patientregistryapi.business.Patient patient) throws PatientFeedException { - if (client == null) { - logger.info(CLIENT_NOT_SET); - initializeClient(); - } - if (patient == null) { - throw new InvalidRequestException(NO_PATIENT_PARAMETER); - } - - PersonName name = patient.getNames().get(0); - if (StringUtils.isBlank(name.getFamily()) || name.getGivens().isEmpty()) { - throw new InvalidRequestException("Mandatory fields are missing"); - } - if (patient.getDateOfBirth() == null || patient.getGender() == null) { - throw new InvalidRequestException("Mandatory fields are missing"); - } - String uuid = ""; - try { - patient.setUuid("PatientPIXmFeed"); - patient.setActive(true); - uuid = client.createPatient(patient); - - if (StringUtils.isBlank(uuid)) { - throw new InternalErrorException(NO_UUID); - } - - - } catch (PatientFeedException e) { - switch (e.getCause().getMessage()) { - case "Impossible to cross reference the patient (not saved)": - throw new InternalErrorException("Impossible to cross reference the patient, it will not be saved !", e); - case "Unexpected Exception persisting Patient !": - throw new InternalErrorException("Unexpected Exception persisting Patient !", e); - - default: - treatClientBaseErrors(e); - } - } - return BusinessToFhirConverter.uuidToBundle(uuid, serverUrl); - } - - /** - * Method called to update a Patient in the PAtient Registry Database. - * - * @param patient : the Patient object we want to insert in DB - * @param uuid : The uuid of the patient corresponding to it in DB - * @return a String corresponding to the uuid confirming the transaction has been successful. - */ - public Bundle updatePatient(net.ihe.gazelle.app.patientregistryapi.business.Patient patient, String uuid) throws PatientFeedException { - if (client == null) { - logger.info(CLIENT_NOT_SET); - initializeClient(); - } - if (patient == null || StringUtils.isBlank(uuid)) { - throw new InvalidRequestException(INVALID_PARAMETERS); - } - - try { - patient.setUuid(uuid); - client.updatePatient(patient); - } catch (PatientFeedException e) { - treatClientBaseErrors(e); - } - return UpdateBundle.prepareBundleAfterUpdateOperation(uuid,serverUrl); - - } - - /** - * Method called to merge two Patients in the Patient Registry Database. - * - * @param uuidOriginal : the Patient object we want to insert in DB - * @param uuidDuplicated : The uuid of the patient corresponding to it in DB - * @return a String corresponding to the uuid confirming the transaction has been successful. - */ - public Bundle mergePatient(String uuidOriginal, String uuidDuplicated) throws PatientFeedException { - if (client == null) { - logger.info(CLIENT_NOT_SET); - initializeClient(); - } - if (StringUtils.isBlank(uuidOriginal)) { - throw new InvalidRequestException(INVALID_PARAMETERS); - } - if (StringUtils.isBlank(uuidDuplicated)) { - throw new InvalidRequestException(INVALID_PARAMETERS); - } - Bundle response = new Bundle(); - try { - - response.setId(client.mergePatient(uuidOriginal, uuidDuplicated)); - - } catch (PatientFeedException e) { - treatClientBaseErrors(e); - } - return response; - } - - /** - * Methode to delete a Patient with its uuid - * @param uuid of the patient to delete - * @return Result of the Deletion - * @throws PatientFeedException If deletion cannot be performed. - */ - public Bundle delete(String uuid) throws PatientFeedException { - if (client == null) { - logger.info(CLIENT_NOT_SET); - initializeClient(); - } - if (StringUtils.isBlank(uuid)) { - throw new InvalidRequestException("Invalid parameter"); - } - try { - boolean deletionStatus = client.deletePatient(uuid); - if (!deletionStatus) { - throw new ResourceGoneException("Patient with UUID " + uuid); - } - } catch (PatientFeedException exception) { - switch (exception.getCause().getMessage()) { - case "The uuid cannot be null or empty": - throw new InternalErrorException("The uuid cannot be null or empty", exception); - case "Cannot proceed to delete": - throw new InternalErrorException("Cannot proceed to delete", exception); - default: - treatClientBaseErrors(exception); - } - } - return DeletionBundle.prepareBundleAfterDeletionOperation(uuid, serverUrl); - } - - /** - * The method is used to retrieve the url of the current server. - */ - private void getUrlOfServer() { - InetAddress ip; - try { - ip = InetAddress.getLocalHost(); - serverUrl = ip.getCanonicalHostName(); - logger.info(String.format("Your current Hostname : %s", serverUrl)); - - } catch (UnknownHostException exception) { - logger.error("Unable to find serverUrl"); - serverUrl = "ReplaceByTheRightEndpoint"; - } - } - - /** - * Private Method which goal is to process the first levels exceptions thrown by the Patient feed client. - * - * @param e the exception thrown - * @throws InternalErrorException throws back the exception with the right http code and the stack trace. - */ - private void treatClientBaseErrors (Exception e) { - switch(e.getMessage()) { - case "Exception while Mapping with GITB elements !": - throw new InternalErrorException("Exception while Mapping with GITB elements !", e); - case "Invalid Response from distant PatientFeedProcessingService !": - throw new InternalErrorException("Invalid Response from distant PatientFeedProcessingService !", e); - case "Invalid operation used on distant PatientFeedProcessingService !": - throw new InternalErrorException("Invalid operation used on distant PatientFeedProcessingService !", e); - case "Invalid Request sent to distant PatientFeedProcessingService !": - throw new InternalErrorException("Invalid Request sent to distant PatientFeedProcessingService !", e); - default: - throw new InternalErrorException("An unhandled error was thrown.", e); - } - } -} diff --git a/src/main/java/net/ihe/gazelle/application/PatientRegistrySearchClient.java b/src/main/java/net/ihe/gazelle/application/PatientRegistrySearchClient.java deleted file mode 100644 index 8fab2cf..0000000 --- a/src/main/java/net/ihe/gazelle/application/PatientRegistrySearchClient.java +++ /dev/null @@ -1,208 +0,0 @@ -package net.ihe.gazelle.application; - -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import com.gitb.ps.ProcessingService; -import net.ihe.gazelle.adapter.connector.BusinessToFhirConverter; -import net.ihe.gazelle.adapter.connector.ConversionException; -import net.ihe.gazelle.adapter.preferences.Preferences; -import net.ihe.gazelle.app.patientregistryapi.business.Patient; -import net.ihe.gazelle.app.patientregistryapi.business.PatientSearchCriterionKey; -import net.ihe.gazelle.app.patientregistrysearchclient.adapter.PatientSearchClient; -import net.ihe.gazelle.framework.loggerservice.application.GazelleLogger; -import net.ihe.gazelle.framework.loggerservice.application.GazelleLoggerFactory; -import net.ihe.gazelle.framework.preferencesmodelapi.application.NamespaceException; -import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; -import net.ihe.gazelle.framework.preferencesmodelapi.application.PreferenceException; -import net.ihe.gazelle.lib.annotations.Package; -import net.ihe.gazelle.lib.searchmodelapi.business.SearchCriteria; -import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; -import net.ihe.gazelle.lib.searchmodelapi.business.searchcriterion.SearchCriterion; -import net.ihe.gazelle.lib.searchmodelapi.business.searchcriterion.StringSearchCriterion; -import org.hl7.fhir.r4.model.OperationOutcome; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.xml.ws.WebServiceException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -@Named("patientRegistrySearchClient") -public class PatientRegistrySearchClient { - - private static final String INVALID_REQUEST = "Invalid Request sent to distant PatientFeedProcessingService !"; - private static final String INVALID_OPERATION = "Invalid operation used on distant PatientFeedProcessingService !"; - private static final String INVALID_RESPONSE = "Invalid Response from distant PatientFeedProcessingService !"; - private static final String INVALID_MAPPING = "Exception while Mapping with GITB elements !"; - public static final String PROCESSING_UUID_OF_PATIENT = "Processing uuid of Patient"; - public static final String NOT_FOUND = "not-found"; - - - private static final String CONVERSION_ERROR = "A problem occured when Converting Patient from Patient Registry model to hl7 v4"; - private static final String NO_UUID = "No patient Uuid given in parameter"; - private static final String NO_PATIENT_FOUND = "No Patient were found"; - private static final String MORE_THAN_ONE_PATIENT_FOUND = "More than one Patient was found for this ID"; - - private static final GazelleLogger logger = GazelleLoggerFactory.getInstance().getLogger(PatientRegistrySearchClient.class); - private OperationalPreferencesService operationalPreferencesService; - private PatientSearchClient client; - - /** - * Default constructor, used for injection. - * - * @param operationalPreferencesService {@link OperationalPreferencesService} used to retrieve Patient Registry's URL. - */ - @Inject - public PatientRegistrySearchClient(OperationalPreferencesService operationalPreferencesService) { - - this.operationalPreferencesService = operationalPreferencesService; - } - - public PatientRegistrySearchClient() { - - } - - /** - * Package-private constructor used for test purposes. - * - * @param url {@link URL} to be used by this to instantiate the processing service to retrieve patients. - */ - @Package - PatientRegistrySearchClient(URL url) { - this.client = new PatientSearchClient(url); - } - - - /** - * Package-private constructor used for test purposes. - * - * @param service {@link ProcessingService} to be used by this to retrieve Patients. - */ - @Package - PatientRegistrySearchClient(ProcessingService service) { - this.client = new PatientSearchClient(service); - } - - @Package - public void setClient(PatientSearchClient client) { - this.client = client; - } - - /** - * Method To call the Search Client from Patient Registry with the uuid of a Patient. - * - * @param patientUuid the uuid of the Patient as a String - * - * @return a single Patient (in hl7 v4 model) corresponding to the uuid given - * - * @throws SearchException - */ - public org.hl7.fhir.r4.model.Patient searchPatient(String patientUuid) throws SearchException { - if(patientUuid == null || patientUuid.equals("")) { - logger.error(NO_UUID); - throw new InvalidRequestException(NO_UUID); - - } - - if (client == null) { - logger.info("client not set"); - initializeSearchClient(); - } - - logger.info(PROCESSING_UUID_OF_PATIENT); - - - SearchCriteria searchCriteria = new SearchCriteria(); - SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); - searchCriterion.setValue(patientUuid); - searchCriteria.addSearchCriterion(searchCriterion); - - List<org.hl7.fhir.r4.model.Patient> resources = new ArrayList<>(); - - try { - - logger.info("Sending request to patient registry"); - List<Patient> patients = client.search(searchCriteria); - - if (patients.isEmpty()) { - logger.error("No Patient found"); - throw new ResourceNotFoundException(NO_PATIENT_FOUND, generateOperationOutcome(NOT_FOUND, NO_PATIENT_FOUND)); - } - - if (patients.size() > 1) { - logger.error("More than one Patient were found"); - throw new ResourceNotFoundException(MORE_THAN_ONE_PATIENT_FOUND, generateOperationOutcome(NOT_FOUND, MORE_THAN_ONE_PATIENT_FOUND)); - } - - logger.info("Search complete"); - for (Patient pat : patients) { - logger.info("converting patient to right model"); - resources.add(BusinessToFhirConverter.patientToFhirPatient(pat)); - } - } catch (ConversionException e) { - logger.error(CONVERSION_ERROR); - throw new InternalErrorException(CONVERSION_ERROR, e); - } catch (SearchException e) { - logger.warn(e.getMessage()); - switch (e.getMessage()) { - case INVALID_REQUEST: - throw new InvalidRequestException(INVALID_REQUEST, generateOperationOutcome("code-invalid", INVALID_REQUEST)); - case INVALID_OPERATION: - throw new InvalidRequestException(INVALID_OPERATION, generateOperationOutcome("code-invalid", INVALID_OPERATION)); - case INVALID_RESPONSE: - throw new ResourceNotFoundException(INVALID_RESPONSE, generateOperationOutcome(NOT_FOUND, INVALID_RESPONSE)); - case INVALID_MAPPING: - throw new InternalErrorException(INVALID_MAPPING); - default: - throw new InternalErrorException(e); - } - } - - - return resources.get(0); - } - - /** - * Initialize the Search Client using the Operational Preferences Service. - * - * @throws SearchException if the service cannot be instantiated. - */ - private void initializeSearchClient() throws SearchException { - String patientRegistryUrl = null; - try { - patientRegistryUrl = this.operationalPreferencesService. - getStringValue(Preferences.PATIENT_REGISTRY_URL.getNamespace().getValue(), Preferences.PATIENT_REGISTRY_URL.getKey()); - this.client = new PatientSearchClient(new URL(patientRegistryUrl)); - } catch (NamespaceException | PreferenceException e) { - throw new SearchException(String.format("Unable to retrieve [%s] Preference !", Preferences.PATIENT_REGISTRY_URL.getKey())); - } catch (MalformedURLException e) { - throw new SearchException(String.format("Preference [%s] with value [%s] is not a valid URL !", Preferences.PATIENT_REGISTRY_URL.getKey(), - patientRegistryUrl)); - } catch (WebServiceException e) { - logger.warn(e.getMessage()); - throw new SearchException(String.format("Can't connect to patient registry ! at address [%s]", patientRegistryUrl)); - } - - } - - - /** - * Method to generate the Error outcome in the response - * - * @param codeString Code of the error catch - * @param diagnostics origin of the issue - * @return generated Operation outcome for the Fhir response - */ - private OperationOutcome generateOperationOutcome(String codeString, String diagnostics) { - OperationOutcome code = new OperationOutcome(); - OperationOutcome.OperationOutcomeIssueComponent issue = new OperationOutcome.OperationOutcomeIssueComponent(); - issue.setSeverity(OperationOutcome.IssueSeverity.ERROR); - issue.setCode(OperationOutcome.IssueType.fromCode(codeString)); - issue.setDiagnostics(diagnostics); - code.addIssue(issue); - return code; - } -} diff --git a/src/main/java/net/ihe/gazelle/business/provider/CHBundleProvider.java b/src/main/java/net/ihe/gazelle/business/provider/CHBundleProvider.java deleted file mode 100644 index 8bb75cc..0000000 --- a/src/main/java/net/ihe/gazelle/business/provider/CHBundleProvider.java +++ /dev/null @@ -1,148 +0,0 @@ -package net.ihe.gazelle.business.provider; - -import ca.uhn.fhir.rest.annotation.*; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import net.ihe.gazelle.application.PatientRegistryFeedClient; -import net.ihe.gazelle.lib.annotations.Package; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.ResourceType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import javax.inject.Named; -import java.util.List; - -@Named("chBundleProvider") -public class CHBundleProvider implements IResourceProvider { - - private static final Logger patientLogger = LoggerFactory.getLogger(CHBundleProvider.class); - - private CHBundleProvider() { - } - - @Package - public CHBundleProvider(ChPatientResourceProvider chPatientResourceProvider) { - this.chPatientResourceProvider = chPatientResourceProvider; - } - - @Inject - private ChPatientResourceProvider chPatientResourceProvider; - - @Inject - private PatientRegistryFeedClient patientRegistryFeedClient; - - @Package - public CHBundleProvider(PatientRegistryFeedClient client) { - this.patientRegistryFeedClient = client; - } - - @Override - public Class<? extends IBaseResource> getResourceType() { - return Bundle.class; - } - - /** - * Method called to create a Patient contained in an iti 93 bundle - * - * @param iti93Bundle : the Bundle containing the patient to add to Patient Registry. - * @return a Method Outcome containing - */ - @Create - public MethodOutcome create(@ResourceParam Bundle iti93Bundle) { - if (iti93Bundle == null || iti93Bundle.getEntry().isEmpty()) { - patientLogger.error("Bundle is null or Empty"); - throw new InvalidRequestException("Bundle is null or Empty"); - } - Patient patientBundle = iti93BundleToPatient(iti93Bundle); - return chPatientResourceProvider.create(patientBundle); - } - - /** - * Method to delete a Patient - represented by its uuid - and a bundle representing the Patientas well. - * - * @param theId : the uuid of the patient to delete in database - * @param iti93Bundle : the bundle containing the patient to delete. - * - * @return a method outcome containing a response Bundle with the patient and the http response. - */ - @Delete - public MethodOutcome delete(@IdParam IdType theId, @ResourceParam Bundle iti93Bundle) { - - if (theId == null || theId.isEmpty()) { - throw new InvalidRequestException("Invalid ID Parameter, either null or empty."); - } - - if (iti93Bundle == null || iti93Bundle.getEntry().isEmpty()) { - throw new InvalidRequestException("Invalid bundle provided, either null or empty."); - } - try { - iti93BundleToPatient(iti93Bundle); - return chPatientResourceProvider.delete(theId); - } catch (InvalidRequestException e) { - throw new InvalidRequestException("Bundle Could not be converted to HL7 Patient"); - } - } - - /** - * Method called to update a Patient - represented by its uuid - with another Patient included in an iti 93 Bundle. - * - * @param theId : the id of the Patient to update in the database - * @param iti93Bundle : the new value of the patient to be saved. - * @return a Method Outcome containing the response bundle with a representation of the patient, and the http response - */ - @Update - public MethodOutcome updatePatient(@IdParam IdType theId, @ResourceParam Bundle iti93Bundle) { - - if (theId == null || theId.isEmpty()) { - throw new InvalidRequestException("Invalid ID Parameter, either null or empty."); - } - - if (iti93Bundle == null || iti93Bundle.getEntry().isEmpty()) { - throw new InvalidRequestException("Invalid bundle provided, either null or empty."); - } - - try { - Patient updatedPatient = iti93BundleToPatient(iti93Bundle); - return chPatientResourceProvider.update(theId, updatedPatient); - } catch (InvalidRequestException e) { - if (e.getMessage().equals("Cannot find resource Type Patient in Bundle")) { - throw e; - } else { - throw new InvalidRequestException("Patient Resource Provider could not take request.", e); - } - } catch (InternalErrorException e) { - throw new InternalErrorException("Patient Resource Provider could not take request.", e); - } - - } - - /** - * Private method to get back the Patient contained in an iti 93 Bundle. - * @param pixmFeed : the bundle in which to recover the Patient. - * @return the patient contained in the bundle for further checks and use. - */ - private Patient iti93BundleToPatient(Bundle pixmFeed) { - List<Bundle.BundleEntryComponent> listOfEntries = pixmFeed.getEntry(); - if (listOfEntries.size() != 2) { - throw new InvalidRequestException("PixM Feed should have 2 entries."); - } - for (Bundle.BundleEntryComponent entry : listOfEntries) { - if (entry.getResource().getResourceType() == ResourceType.Bundle) { - Bundle historyBundle = (Bundle) entry.getResource(); - for (Bundle.BundleEntryComponent resource : historyBundle.getEntry()) { - if (resource.getResource().getResourceType() == ResourceType.Patient) { - return (Patient) resource.getResource(); - } - } - } - } - throw new InvalidRequestException("Cannot find resource Type Patient in Bundle"); - } -} \ No newline at end of file diff --git a/src/main/java/net/ihe/gazelle/business/provider/ChPatientResourceProvider.java b/src/main/java/net/ihe/gazelle/business/provider/ChPatientResourceProvider.java deleted file mode 100644 index 8d6a2e3..0000000 --- a/src/main/java/net/ihe/gazelle/business/provider/ChPatientResourceProvider.java +++ /dev/null @@ -1,303 +0,0 @@ -package net.ihe.gazelle.business.provider; - -import ca.uhn.fhir.rest.annotation.*; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import net.ihe.gazelle.adapter.connector.BundleToPatientRegistryConverter; -import net.ihe.gazelle.adapter.connector.ConversionException; -import net.ihe.gazelle.app.patientregistryapi.application.PatientFeedException; -import net.ihe.gazelle.app.patientregistryapi.application.SearchCrossReferenceException; -import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; -import net.ihe.gazelle.application.PatientRegistryFeedClient; -import net.ihe.gazelle.application.PatientRegistrySearchClient; -import net.ihe.gazelle.application.PatientRegistryXRefSearchClient; -import net.ihe.gazelle.lib.annotations.Package; -import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; -import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Patient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import javax.inject.Named; -import java.util.ArrayList; -import java.util.List; - -/** - * This is a resource provider which stores Patient resources in memory using a HashMap. This is obviously not a production-ready solution for many - * reasons, - * but it is useful to help illustrate how to build a fully-functional server. - */ -@Named("chPatientResourceProvider") -public class ChPatientResourceProvider implements IResourceProvider { - - public static final String URN_OID = "urn:oid:"; - public static final String INVALID_REQUEST_BAD_SOURCE_IDENTIFIER = "Invalid request : bad sourceIdentifier"; - public static final String INVALID_REQUEST_TARGET_DOMAIN_CAN_NOT_BE_EMPTY = "Invalid request : targetDomain can not be empty"; - public static final String INVALID_REQUEST_THE_REQUEST_SHALL_CONTAIN_ONE_OR_TWO_TARGET_DOMAIN = "Invalid request : The request shall contain " + - "one or two targetDomain"; - public static final String SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND = "sourceIdentifier Patient Identifier not found"; - public static final String SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND = "sourceIdentifier Assigning Authority not found"; - public static final String TARGET_SYSTEM_NOT_FOUND = "targetSystem not found"; - private static final Logger patientLogger = LoggerFactory.getLogger(ChPatientResourceProvider.class); - public static final String NO_ID_PROVIDED = "No ID provided"; - public static final String NO_BUNDLE_PROVIDED = "No Bundle provided"; - public static final String NO_PATIENT_PROVIDED = "No Patient Provided"; - - @Inject - private PatientRegistryXRefSearchClient patientRegistryXRefSearchClient; - - @Inject - private PatientRegistrySearchClient patientRegistrySearchClient; - - @Inject - private PatientRegistryFeedClient patientRegistryFeedClient; - - private ChPatientResourceProvider() { - } - - @Package - public ChPatientResourceProvider(PatientRegistryXRefSearchClient client) { - this.patientRegistryXRefSearchClient = client; - } - - @Package - public ChPatientResourceProvider(PatientRegistryFeedClient client) { - this.patientRegistryFeedClient = client; - } - - @Package - public ChPatientResourceProvider(PatientRegistrySearchClient client) { - this.patientRegistrySearchClient = client; - } - - - /** - * The getResourceType method comes from IResourceProvider, and must be overridden to indicate what type of resource this provider supplies. - */ - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } - - /** - * Method to read a Patient through its uuid in the patient Manager database - * - * @param theId : Id Type given in the url. represents the uuid of the searched patient - * @return One patient corresponding to the uuid given in entry. If many Patients are found, we consider it as an error and won't return ANY - * patient. - */ - @Read - public Patient read(@IdParam IdType theId) { - String uuid = theId.getIdPart(); - if (StringUtils.isBlank(uuid)) { - patientLogger.error(NO_ID_PROVIDED); - throw new InvalidRequestException(NO_ID_PROVIDED); - } - - try { - Patient retrievedPatient = patientRegistrySearchClient.searchPatient(uuid); - patientLogger.info("Patient Successfully found"); - return retrievedPatient; - } catch (SearchException e) { - throw new InternalErrorException("Patient could not be retrieved", e); - } - - } - - /** - * Method called to create a Patient on Patient Registry - * - * @param iti93Patient : the Patient to create. - * @return a MethodOutcome containing a representation of the patient created. - */ - @Create - public MethodOutcome create(@ResourceParam Patient iti93Patient) { - if (iti93Patient == null) { - patientLogger.error(NO_PATIENT_PROVIDED); - throw new InvalidRequestException(NO_PATIENT_PROVIDED); - } - try { - net.ihe.gazelle.app.patientregistryapi.business.Patient patient = - BundleToPatientRegistryConverter.fhirPatientToGazellePatient(iti93Patient); - MethodOutcome methodOutcome = new MethodOutcome(); - methodOutcome.setResource(patientRegistryFeedClient.createPatient(patient)); - if (methodOutcome.getResource() != null) { - return methodOutcome; - } else { - throw new ResourceNotFoundException("MethodOutcome was not created"); - } - - } catch (ConversionException e) { - throw new InvalidRequestException("Bundle Could not be converted to HL7 Patient"); - } catch (PatientFeedException e) { - throw new InternalErrorException("Patient Feed client is not set"); - } - } - - /** - * Method to Update a patient related to PIXm Delete Method - * - * @param theId the UUID of the Patient we want to update - * @param hl7Patient The Bundle content of the patient - * @return FhirBundle that contains the updated patient - */ - @Update - public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient hl7Patient) { - String uuid = theId.getIdPart(); - if (StringUtils.isBlank(uuid)) { - patientLogger.error(NO_ID_PROVIDED); - throw new InvalidRequestException(NO_ID_PROVIDED); - } - if (hl7Patient == null || !hl7Patient.hasId()) { - throw new InvalidRequestException(NO_BUNDLE_PROVIDED); - } - try { - net.ihe.gazelle.app.patientregistryapi.business.Patient patientToUpdate = - BundleToPatientRegistryConverter.fhirPatientToGazellePatient(hl7Patient); - MethodOutcome methodOutcome = new MethodOutcome(); - methodOutcome.setResource(patientRegistryFeedClient.updatePatient(patientToUpdate, uuid)); - return methodOutcome; - } catch (ConversionException e) { - throw new InvalidRequestException("Bundle Could not be converted to HL7 Patient", e); - } catch (PatientFeedException e) { - throw new InternalErrorException("Patient Feed client is not set"); - } - } - - /** - * Method to delete a Patient related to PIXm Delete Method - * - * @param theId of the patient to delete - * @return FhirBundle that contains the deletion status - */ - @Delete - public MethodOutcome delete(@IdParam IdType theId) { - - if (theId == null || theId.isEmpty()) { - throw new InvalidRequestException(NO_ID_PROVIDED); - } - - String uuid = theId.getIdPart(); - try { - MethodOutcome methodOutcome = new MethodOutcome(); - methodOutcome.setResource(patientRegistryFeedClient.delete(uuid)); - return methodOutcome; - } catch (PatientFeedException e) { - patientLogger.error("Error in deletion :", e); - throw new InternalErrorException("Internal Error :", e); - } - } - - /** - * Search method for a Patient using the source identifier required parameter - * and an optional list of target system - * - * @param sourceIdentifierParam : the source identifier of the patient, should be formatted "urn:oid:x.x.x.x.x.x.x.x.x.x|value" - * @param targetSystemParam: the target System(s) we want to find the Patient on, should be formatted "urn:oid:x.x.x.x.x.x.x.x.x.x,urn:oid:x - * .x.x.x.x.x.x.x.x.x," - * @return a Parameter element composed of a list of target identifier for every Patient found, and an url to the Patient in the server. - */ - @Operation(name = "$ihe-pix", idempotent = true) - public Parameters findPatientsByIdentifier(@OperationParam(name = "sourceIdentifier", min = 1, max = 1) TokenAndListParam sourceIdentifierParam, - @OperationParam(name = "targetSystem", min = 1, max = 2) StringAndListParam targetSystemParam) { - - EntityIdentifier sourceIdentifier = createEntityIdentifierFromSourceIdentifier(sourceIdentifierParam); - List<String> targetSystemList = createTargetSystemListFromParam(targetSystemParam); - - Parameters parametersResults; - try { - parametersResults = patientRegistryXRefSearchClient.process(sourceIdentifier, targetSystemList); - } catch (SearchCrossReferenceException e) { - throw new InternalErrorException(e); - } - return parametersResults; - } - - /** - * Private method used to transform a Source Identifier into an Entity Identifier for Patient Registry database - * @param sourceIdentifier : the source Identifier to convert - * @return An Entity Identifier understood by Patient Registry. - */ - private EntityIdentifier createEntityIdentifierFromSourceIdentifier(TokenAndListParam sourceIdentifier) { - if (sourceIdentifier == null || sourceIdentifier.size()==0 || sourceIdentifier.size()>1) { - throw new InvalidRequestException(SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND); - } - TokenOrListParam tokenOrListParams = sourceIdentifier.getValuesAsQueryTokens().get(0); - TokenParam source = tokenOrListParams.getValuesAsQueryTokens().get(0); - - String sourceIdentifierSystem = source.getSystem(); - String sourceIdentifierValue = source.getValue(); - if (sourceIdentifierSystem == null) { - patientLogger.error(INVALID_REQUEST_BAD_SOURCE_IDENTIFIER + " null system"); - throw new InvalidRequestException(SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND); - } - if (sourceIdentifierValue == null) { - patientLogger.error(INVALID_REQUEST_BAD_SOURCE_IDENTIFIER + " null value"); - throw new InvalidRequestException(SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND); - } - if (StringUtils.isBlank(sourceIdentifierSystem) || StringUtils.isBlank(sourceIdentifierValue)) { - patientLogger.error(INVALID_REQUEST_BAD_SOURCE_IDENTIFIER + " empty system or value"); - throw new InvalidRequestException(SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND); - } - patientLogger.info("Searching Patient with given sourceIdentifier"); - - EntityIdentifier wellFormedEntityIdentifier = new EntityIdentifier(); - if (sourceIdentifierSystem.contains(URN_OID)) { - wellFormedEntityIdentifier.setSystemIdentifier(sourceIdentifierSystem.replace(URN_OID, "")); - wellFormedEntityIdentifier.setType("ISO"); - wellFormedEntityIdentifier.setValue(sourceIdentifierValue); - } else if (sourceIdentifierSystem.contains("http")) { - wellFormedEntityIdentifier.setSystemIdentifier(sourceIdentifierSystem); - wellFormedEntityIdentifier.setType("ISO"); - wellFormedEntityIdentifier.setValue(sourceIdentifierValue); - } else { - throw new ResourceNotFoundException(SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND); - } - return wellFormedEntityIdentifier; - } - - /** - * Create the targetSystem list for CH Implementation - * - * @param targetSystemParam Param from the query - * @return List of targetSystem - */ - private List<String> createTargetSystemListFromParam(StringAndListParam targetSystemParam) { - if (targetSystemParam == null) { - throw new ForbiddenOperationException(TARGET_SYSTEM_NOT_FOUND); - } - List<String> targetSystemList = new ArrayList<>(); - for (StringOrListParam listParam : targetSystemParam.getValuesAsQueryTokens()) { - List<StringParam> queryStrings = listParam.getValuesAsQueryTokens(); - if (queryStrings.isEmpty() || queryStrings.size() > 2) { - throw new ForbiddenOperationException(TARGET_SYSTEM_NOT_FOUND); - } - for (StringParam singleParam : queryStrings) { - String singleParamValue = singleParam.getValue(); - if (singleParamValue.contains(URN_OID)) { - singleParamValue = singleParamValue.replace(URN_OID, ""); - } - if (singleParamValue.equals("")) { - throw new ForbiddenOperationException(TARGET_SYSTEM_NOT_FOUND); - } - targetSystemList.add(singleParamValue); - } - } - if (targetSystemList.size()>2){ - throw new ForbiddenOperationException(TARGET_SYSTEM_NOT_FOUND); - } - return targetSystemList; - } - -} - - diff --git a/src/main/java/net/ihe/gazelle/business/provider/IhePatientResourceProvider.java b/src/main/java/net/ihe/gazelle/business/provider/IhePatientResourceProvider.java deleted file mode 100644 index 554e006..0000000 --- a/src/main/java/net/ihe/gazelle/business/provider/IhePatientResourceProvider.java +++ /dev/null @@ -1,182 +0,0 @@ -package net.ihe.gazelle.business.provider; - -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationParam; -import ca.uhn.fhir.rest.annotation.Read; -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import net.ihe.gazelle.app.patientregistryapi.application.SearchCrossReferenceException; -import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; -import net.ihe.gazelle.application.PatientRegistrySearchClient; -import net.ihe.gazelle.application.PatientRegistryXRefSearchClient; -import net.ihe.gazelle.lib.annotations.Package; -import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; -import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Patient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import javax.inject.Named; -import java.util.ArrayList; -import java.util.List; - -/** - * This is a resource provider which stores Patient resources in memory using a HashMap. This is obviously not a production-ready solution for many - * reasons, - * but it is useful to help illustrate how to build a fully-functional server. - */ -@Named("ihePatientResourceProvider") -public class IhePatientResourceProvider implements IResourceProvider { - private static final String URN_OID = "urn:oid:"; - public static final String INVALID_REQUEST_BAD_SOURCE_IDENTIFIER = "Invalid request : bad sourceIdentifier"; - private static final Logger patientLogger = LoggerFactory.getLogger(IhePatientResourceProvider.class); - public static final String SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND = "sourceIdentifier Patient Identifier not found"; - public static final String SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND = "sourceIdentifier Assigning Authority not found"; - public static final String TARGET_SYSTEM_NOT_FOUND = "targetSystem not found"; - public static final String NO_ID_PROVIDED = "No ID provided"; - - @Inject - private PatientRegistryXRefSearchClient patientRegistryXRefSearchClient; - - @Inject - private PatientRegistrySearchClient patientRegistrySearchClient; - - /** - * The getResourceType method comes from IResourceProvider, and must be overridden to indicate what type of resource this provider supplies. - */ - @Override - public Class<? extends IBaseResource> getResourceType() { - return Patient.class; - } - - public IhePatientResourceProvider() { - } - - @Package - public IhePatientResourceProvider(PatientRegistryXRefSearchClient client) { - this.patientRegistryXRefSearchClient = client; - } - - @Package - public IhePatientResourceProvider(PatientRegistrySearchClient client) { - this.patientRegistrySearchClient = client; - } - - /** - * Method to read a Patient through its uuid in the patient Manager database - * - * @param theId : Id Type given in the url. represents the uuid of the searched patient - * @return One patient corresponding to the uuid given in entry. If many Patients are found, we consider it as an error and won't return ANY - * patient. - */ - @Read - public Patient read(@IdParam IdType theId) { - if (StringUtils.isBlank(theId.getIdPart())) { - patientLogger.error(NO_ID_PROVIDED); - throw new InvalidRequestException(NO_ID_PROVIDED); - } - String uuid = theId.getIdPart(); - try { - Patient retrievedPatient = patientRegistrySearchClient.searchPatient(uuid); - patientLogger.info("Patient Successfully found"); - return retrievedPatient; - } catch (SearchException e) { - throw new InternalErrorException("Patient could not be retrieved", e); - } - - } - - /** - * Search method for a Patient using the source identifier required parameter - * and an optional list of target system - * - * @param sourceIdentifierParam : the source identifier of the patient, should be formatted "urn:oid:x.x.x.x.x.x.x.x.x.x|value" - * @param targetSystemParam: the target System(s) we want to find the Patient on, should be formatted "urn:oid:x.x.x.x.x.x.x.x.x.x, - * urn:oid:x.x.x.x.x.x.x.x.x.x" - * @return a Parameters element composed of a list of target identifier for every Patient found, and an url to the Patient in the server. - */ - @Operation(name = "$ihe-pix", idempotent = true) - public Parameters findPatientsByIdentifier(@OperationParam(name = "sourceIdentifier", min = 1, max = 1) TokenAndListParam sourceIdentifierParam, - @OperationParam(name = "targetSystem") StringAndListParam targetSystemParam) { - - EntityIdentifier sourceIdentifier = createEntityIdentifierFromSourceIdentifier(sourceIdentifierParam); - List<String> targetSystemList = createTargetSystemListFromParam(targetSystemParam); - - Parameters parametersResults; - try { - parametersResults = patientRegistryXRefSearchClient.process(sourceIdentifier, targetSystemList); - } catch (SearchCrossReferenceException e) { - throw new InternalErrorException(e); - } - - return parametersResults; - } - - private EntityIdentifier createEntityIdentifierFromSourceIdentifier(TokenAndListParam sourceIdentifier) { - if (sourceIdentifier == null || sourceIdentifier.size() == 0 || sourceIdentifier.size() > 1) { - throw new InvalidRequestException(SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND); - } - TokenOrListParam tokenOrListParams = sourceIdentifier.getValuesAsQueryTokens().get(0); - TokenParam source = tokenOrListParams.getValuesAsQueryTokens().get(0); - - String sourceIdentifierSystem = source.getSystem(); - String sourceIdentifierValue = source.getValue(); - if (sourceIdentifierSystem == null) { - patientLogger.error(INVALID_REQUEST_BAD_SOURCE_IDENTIFIER + " null system"); - throw new InvalidRequestException(SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND); - } - if (sourceIdentifierValue == null) { - patientLogger.error(INVALID_REQUEST_BAD_SOURCE_IDENTIFIER + " null value"); - throw new InvalidRequestException(SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND); - } - if (StringUtils.isBlank(sourceIdentifierSystem) || StringUtils.isBlank(sourceIdentifierValue)) { - patientLogger.error(INVALID_REQUEST_BAD_SOURCE_IDENTIFIER + " empty system or value"); - throw new InvalidRequestException(SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND); - } - patientLogger.info("Searching Patient with given sourceIdentifier"); - - EntityIdentifier wellFormedEntityIdentifier = new EntityIdentifier(); - if (sourceIdentifierSystem.contains(URN_OID)) { - wellFormedEntityIdentifier.setSystemIdentifier(sourceIdentifierSystem.replace(URN_OID, "")); - wellFormedEntityIdentifier.setType("ISO"); - wellFormedEntityIdentifier.setValue(sourceIdentifierValue); - } else if (sourceIdentifierSystem.contains("http")) { - wellFormedEntityIdentifier.setSystemIdentifier(sourceIdentifierSystem); - wellFormedEntityIdentifier.setType("ISO"); - wellFormedEntityIdentifier.setValue(sourceIdentifierValue); - } else { - throw new ResourceNotFoundException(SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND); - } - return wellFormedEntityIdentifier; - } - - private List<String> createTargetSystemListFromParam(StringAndListParam targetSystemParam) { - List<String> targetSystemList = new ArrayList<>(); - if (targetSystemParam != null) { - for (StringOrListParam listParam : targetSystemParam.getValuesAsQueryTokens()) { - List<StringParam> queryStrings = listParam.getValuesAsQueryTokens(); - for (StringParam singleParam : queryStrings) { - String singleParamValue = singleParam.getValue(); - if (singleParamValue.contains(URN_OID)) { - singleParamValue = singleParamValue.replace(URN_OID, ""); - } - if (singleParamValue.equals("")) { - throw new ForbiddenOperationException(TARGET_SYSTEM_NOT_FOUND); - } - targetSystemList.add(singleParamValue); - } - } - } - return targetSystemList; - } - -} diff --git a/src/main/java/net/ihe/gazelle/business/provider/PatientResourceProviderException.java b/src/main/java/net/ihe/gazelle/business/provider/PatientResourceProviderException.java deleted file mode 100644 index 242b39b..0000000 --- a/src/main/java/net/ihe/gazelle/business/provider/PatientResourceProviderException.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.ihe.gazelle.business.provider; - -public class PatientResourceProviderException extends Exception { - - public PatientResourceProviderException() { - super(); - } - - public PatientResourceProviderException(String message) { - super(message); - } - - public PatientResourceProviderException(String message, Throwable cause) { - super(message, cause); - } - - public PatientResourceProviderException(Throwable cause) { - super(cause); - } - - public PatientResourceProviderException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } - -} diff --git a/src/main/resources/SoapUI/IHE-Pixm-soapui-project.xml b/src/main/resources/SoapUI/IHE-Pixm-soapui-project.xml deleted file mode 100644 index dfd9227..0000000 --- a/src/main/resources/SoapUI/IHE-Pixm-soapui-project.xml +++ /dev/null @@ -1,78 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<con:soapui-project id="0f6914ec-af8e-40a6-9910-e95e833c773b" activeEnvironment="Default" name="IHE:Pixm" resourceRoot="" soapui-version="5.6.0" abortOnError="false" runType="SEQUENTIAL" xmlns:con="http://eviware.com/soapui/config"><con:settings><con:setting id="com.eviware.soapui.impl.wsdl.actions.iface.tools.soapui.TestRunnerAction@values-local"><![CDATA[<xml-fragment xmlns:con="http://eviware.com/soapui/config"> - <con:entry key="Report Format(s)" value=""/> - <con:entry key="Host:Port" value=""/> - <con:entry key="Export JUnit Results" value="false"/> - <con:entry key="Export All" value="false"/> - <con:entry key="Save After" value="false"/> - <con:entry key="Add Settings" value="false"/> - <con:entry key="WSS Password Type" value=""/> - <con:entry key="TestSuite" value="PixM - Code 200"/> - <con:entry key="Endpoint" value=""/> - <con:entry key="Select Report Type" value=""/> - <con:entry key="System Properties" value=""/> - <con:entry key="Password" value=""/> - <con:entry key="Print Report" value="false"/> - <con:entry key="Open Report" value="false"/> - <con:entry key="Export JUnit Results with test properties" value="false"/> - <con:entry key="Global Properties" value=""/> - <con:entry key="Project Properties" value=""/> - <con:entry key="Project Password" value=""/> - <con:entry key="TestCase" value="<all>"/> - <con:entry key="Username" value=""/> - <con:entry key="user-settings.xml Password" value=""/> - <con:entry key="TestRunner Path" value=""/> - <con:entry key="Environment" value="Default"/> - <con:entry key="Coverage Report" value="false"/> - <con:entry key="Enable UI" value="false"/> - <con:entry key="Root Folder" value=""/> - <con:entry key="Ignore Errors" value="false"/> - <con:entry key="Domain" value=""/> - <con:entry key="Tool Args" value=""/> - <con:entry key="Save Project" value="false"/> -</xml-fragment>]]></con:setting></con:settings><con:interface xsi:type="con:RestService" id="3aa45915-2866-4224-a6c0-61c79628c944" wadlVersion="http://wadl.dev.java.net/2009/02" name="http://qualification.ihe-europe.net" type="rest" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><con:settings/><con:definitionCache type="TEXT" rootPart=""/><con:endpoints><con:endpoint>http://localhost:8580/pixm_fhir_server/fhir_ihe</con:endpoint><con:endpoint>http://qualification.ihe-europe.net/pixm_fhir_server/fhir_ihe</con:endpoint></con:endpoints><con:resource name="" path="/{resource}/{operation}" id="cf9419c5-50ad-4552-915e-bcb5492a80db"><con:settings/><con:parameters><con:parameter><con:name>resource</con:name><con:value/><con:style>TEMPLATE</con:style><con:default/><con:description xsi:nil="true"/></con:parameter><con:parameter><con:name>operation</con:name><con:value/><con:style>TEMPLATE</con:style><con:default/><con:description xsi:nil="true"/></con:parameter><con:parameter><con:name>sourceIdentifier</con:name><con:value/><con:style>QUERY</con:style><con:default/><con:description xsi:nil="true"/></con:parameter><con:parameter><con:name>targetSystem</con:name><con:value/><con:style>QUERY</con:style><con:default/><con:description xsi:nil="true"/></con:parameter><con:parameter><con:name>_format</con:name><con:value/><con:style>QUERY</con:style><con:default/><con:description xsi:nil="true"/></con:parameter></con:parameters><con:method name="GET from sourceIdentifier" id="a06fed5a-ee7b-4b4b-be29-9368040008a4" method="GET"><con:settings/><con:parameters/><con:representation type="FAULT"><con:mediaType>text/html; charset=iso-8859-1</con:mediaType><con:status>404</con:status><con:params/><con:element>html</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>404</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>404</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>404</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>404</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>404</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>404</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType>text/html; charset=iso-8859-1</con:mediaType><con:status>200</con:status><con:params/><con:element>html</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>404</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType>application/json</con:mediaType><con:status>200</con:status><con:params/><con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Response</con:element></con:representation><con:representation type="FAULT"><con:mediaType>application/json</con:mediaType><con:status>404 400 403 401</con:status><con:params/><con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Fault</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>500</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>500</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>500</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>500</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType>fhir+json</con:mediaType><con:status>200</con:status><con:params/><con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Response</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType>fhir+xml</con:mediaType><con:status>200</con:status><con:params/><con:element>id</con:element></con:representation><con:representation type="FAULT"><con:mediaType>application/fhir+xml</con:mediaType><con:status>403</con:status><con:params/><con:element>id</con:element></con:representation><con:representation type="FAULT"><con:mediaType/><con:status>404</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>500</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>500</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>500</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>500</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>500</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>500</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>404</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType xsi:nil="true"/><con:status>500</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType>application/fhir+json</con:mediaType><con:status>404</con:status><con:params/><con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Fault</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType xsi:nil="true"/><con:status>0</con:status><con:params/><con:element>data</con:element></con:representation><con:representation type="FAULT"><con:mediaType>application/fhir+json; charset=UTF-8</con:mediaType><con:status>404 500 400</con:status><con:params/><con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Fault</con:element></con:representation><con:representation type="RESPONSE"><con:mediaType>application/fhir+json; charset=UTF-8</con:mediaType><con:status>200</con:status><con:params/><con:element xmlns:pat="http://qualification.ihe-europe.net/Patient%2F%24ihe-pix">pat:Response</con:element></con:representation><con:representation type="FAULT"><con:mediaType>application/fhir+xml; charset=UTF-8</con:mediaType><con:status>500 404</con:status><con:params/><con:element xmlns:fhir="http://hl7.org/fhir">fhir:OperationOutcome</con:element></con:representation><con:request name="code 200" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" multiValueDelimiter=","><con:settings><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false</con:setting></con:settings><con:endpoint>http://localhost:8089/</con:endpoint><con:request/><con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri><con:credentials><con:authType>No Authorization</con:authType></con:credentials><con:jmsConfig JMSDeliveryMode="PERSISTENT"/><con:jmsPropertyConfig/><con:parameters> - <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404"/> - <con:entry key="resource" value="Patient"/> - <con:entry key="operation" value="/$ihe-pix"/> -</con:parameters><con:parameterOrder><con:entry>resource</con:entry><con:entry>operation</con:entry><con:entry>sourceIdentifier</con:entry><con:entry>targetSystem</con:entry><con:entry>_format</con:entry></con:parameterOrder></con:request><con:request name="code 400" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" multiValueDelimiter=","><con:settings><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false</con:setting></con:settings><con:endpoint>http://localhost:8089/</con:endpoint><con:request/><con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri><con:credentials><con:authType>No Authorization</con:authType></con:credentials><con:jmsConfig JMSDeliveryMode="PERSISTENT"/><con:jmsPropertyConfig/><con:parameters> - <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2|NA5404 "/> - <con:entry key="resource" value="Patient"/> - <con:entry key="operation" value="/$ihe-pix"/> -</con:parameters><con:parameterOrder><con:entry>resource</con:entry><con:entry>operation</con:entry><con:entry>sourceIdentifier</con:entry><con:entry>targetSystem</con:entry><con:entry>_format</con:entry></con:parameterOrder></con:request><con:request name="code 403" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" multiValueDelimiter=","><con:settings><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false</con:setting></con:settings><con:endpoint>http://qualification.ihe-europe.net</con:endpoint><con:request/><con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri><con:credentials><con:authType>No Authorization</con:authType></con:credentials><con:jmsConfig JMSDeliveryMode="PERSISTENT"/><con:jmsPropertyConfig/><con:parameters> - <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404 "/> - <con:entry key="resource" value="Patient"/> - <con:entry key="operation" value="/$ihe-pix"/> -</con:parameters><con:parameterOrder><con:entry>resource</con:entry><con:entry>operation</con:entry><con:entry>sourceIdentifier</con:entry><con:entry>targetSystem</con:entry><con:entry>_format</con:entry></con:parameterOrder></con:request><con:request name="code 404" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" multiValueDelimiter=","><con:settings><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false</con:setting></con:settings><con:endpoint>http://qualification.ihe-europe.net</con:endpoint><con:request/><con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri><con:credentials><con:authType>No Authorization</con:authType></con:credentials><con:jmsConfig JMSDeliveryMode="PERSISTENT"/><con:jmsPropertyConfig/><con:parameters> - <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404 "/> - <con:entry key="resource" value="Patient"/> - <con:entry key="operation" value="/$ihe-pix"/> -</con:parameters><con:parameterOrder><con:entry>resource</con:entry><con:entry>operation</con:entry><con:entry>sourceIdentifier</con:entry><con:entry>targetSystem</con:entry><con:entry>_format</con:entry></con:parameterOrder></con:request></con:method></con:resource></con:interface><con:testSuite id="4008d084-2b9b-4853-90b1-75ae50eda188" name="PixM - Code 200"><con:description>TestSuite generated for REST Service [http://qualification.ihe-europe.net]</con:description><con:settings/><con:runType>SEQUENTIAL</con:runType><con:testCase id="c9b20a8c-efd3-4613-8b00-ed4cbbf5a582" failOnError="true" failTestCaseOnErrors="true" keepSession="false" maxResults="0" name="TestCase" searchProperties="true"><con:description>TestCase generated for REST Resource [] located at [/{resource}{operation}]</con:description><con:settings/><con:testStep type="transfer" name="Property Transfer" id="2bbdf78b-4de4-4c6b-b73d-e88fca4f3bd0"><con:settings/><con:config xsi:type="con:PropertyTransfersStep" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name>sourceIdentifier</con:name><con:sourceType>sourceIdentifier</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>sourceIdentifier</con:targetType><con:targetStep>code 200</con:targetStep><con:upgraded>true</con:upgraded></con:transfers><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name> targetSystem</con:name><con:sourceType>targetSystem</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>targetSystem</con:targetType><con:targetStep>code 200</con:targetStep><con:upgraded>true</con:upgraded></con:transfers><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name>_format</con:name><con:sourceType>_format</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>_format</con:targetType><con:targetStep>code 200</con:targetStep><con:upgraded>true</con:upgraded></con:transfers></con:config></con:testStep><con:testStep type="restrequest" name="code 200" id="92a9dcef-29ec-4d16-a50b-8379860b7234"><con:settings/><con:config service="http://qualification.ihe-europe.net" resourcePath="/{resource}/{operation}" methodName="GET from sourceIdentifier" xsi:type="con:RestRequestStep" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><con:restRequest name="code 200" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" multiValueDelimiter=","><con:settings><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false</con:setting></con:settings><con:endpoint>http://qualification.ihe-europe.net/pixm-connector/fhir_ihe</con:endpoint><con:request/><con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri><con:assertion type="Valid HTTP Status Codes" id="19207097-8fec-4017-89c0-f4ba61af51e4" name="Valid HTTP Status Codes"><con:configuration><codes>200</codes></con:configuration></con:assertion><con:assertion type="GroovyScriptAssertion" id="22513178-52bd-4e7a-9cf0-0e6d2eb7649d" name="Script Assertion"><con:configuration><scriptText>String sourceIdentifier = context.testCase.testSteps['code 200'].getPropertyValue( "sourceIdentifier" ) -log.info sourceIdentifier -assert (sourceIdentifier ==~ /urn:oid:([0-9]*)(\.[0-9]*){10}\|([0-9A-Z])*/)</scriptText></con:configuration></con:assertion><con:credentials><con:authType>No Authorization</con:authType></con:credentials><con:jmsConfig JMSDeliveryMode="PERSISTENT"/><con:jmsPropertyConfig/><con:parameters> - <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404"/> - <con:entry key="resource" value="Patient"/> - <con:entry key="_format" value="json"/> - <con:entry key="operation" value="$ihe-pix"/> - <con:entry key="targetSystem" value="domain1"/> -</con:parameters><con:parameterOrder><con:entry>resource</con:entry><con:entry>operation</con:entry><con:entry>sourceIdentifier</con:entry><con:entry>targetSystem</con:entry><con:entry>_format</con:entry></con:parameterOrder></con:restRequest></con:config></con:testStep><con:properties/></con:testCase><con:properties><con:property><con:name>Endpoint</con:name><con:value>http://localhost:8580/pixm_fhir_server/fhir_ihe</con:value></con:property><con:property><con:name>resource</con:name><con:value>Patient</con:value></con:property><con:property><con:name>operation</con:name><con:value>/$ihe-pix</con:value></con:property><con:property><con:name>sourceIdentifier</con:name><con:value>urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404</con:value></con:property><con:property><con:name>targetSystem</con:name><con:value>domain1</con:value></con:property><con:property><con:name>_format</con:name><con:value>json</con:value></con:property></con:properties></con:testSuite><con:testSuite id="6de478cb-4400-4297-ae33-55cbde01bbea" name="PixM - Code 400"><con:description>TestSuite generated for REST Service [http://qualification.ihe-europe.net]</con:description><con:settings/><con:runType>SEQUENTIAL</con:runType><con:testCase id="6cb2bee4-3d20-40f6-9b61-c58440ba620c" failOnError="true" failTestCaseOnErrors="true" keepSession="false" maxResults="0" name="TestCase" searchProperties="true"><con:description>TestCase generated for REST Resource [] located at [/{resource}{operation}]</con:description><con:settings/><con:testStep type="transfer" name="Property Transfer 400" id="b1f85dac-008d-477c-a678-e7e5b2dd3ff6"><con:settings/><con:config xsi:type="con:PropertyTransfersStep" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name>sourceIdentifier</con:name><con:sourceType>sourceIdentifier</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>sourceIdentifier</con:targetType><con:targetStep>code 400</con:targetStep><con:upgraded>true</con:upgraded></con:transfers><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name>targetSystem</con:name><con:sourceType>targetSystem</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>targetSystem</con:targetType><con:targetStep>code 400</con:targetStep><con:upgraded>true</con:upgraded></con:transfers><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name>_format</con:name><con:sourceType>_format</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>_format</con:targetType><con:targetStep>code 400</con:targetStep><con:upgraded>true</con:upgraded></con:transfers></con:config></con:testStep><con:testStep type="restrequest" name="code 400" id="ef0eb5bd-cd75-4804-b094-600c16de753f"><con:settings/><con:config service="http://qualification.ihe-europe.net" resourcePath="/{resource}/{operation}" methodName="GET from sourceIdentifier" xsi:type="con:RestRequestStep" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><con:restRequest name="code 400" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" multiValueDelimiter=","><con:settings><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false</con:setting></con:settings><con:endpoint>http://localhost:8580/pixm_fhir_server/fhir_ihe</con:endpoint><con:request/><con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri><con:assertion type="GroovyScriptAssertion" id="92a5155f-cd74-43fb-bc66-9402e5fb602c" name="Script Assertion"><con:configuration><scriptText>String sourceIdentifier = context.testCase.testSteps['code 400'].getPropertyValue('sourceIdentifier') -assert !(sourceIdentifier ==~ /urn:oid:([0-9]*)(\.[0-9]*){10}\|([0-9A-Z])*/)</scriptText></con:configuration></con:assertion><con:assertion type="Valid HTTP Status Codes" id="1661d742-bcb2-4e22-ae22-deb68f175e4b" name="Valid HTTP Status Codes"><con:configuration><codes>400</codes></con:configuration></con:assertion><con:credentials><con:authType>No Authorization</con:authType></con:credentials><con:jmsConfig JMSDeliveryMode="PERSISTENT"/><con:jmsPropertyConfig/><con:parameters> - <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.13.20.3000%7CIHEBLUE-998"/> - <con:entry key="resource" value="Patient"/> - <con:entry key="_format" value="json"/> - <con:entry key="operation" value="/$ihe-pix"/> - <con:entry key="targetSystem" value="domain1"/> -</con:parameters><con:parameterOrder><con:entry>resource</con:entry><con:entry>operation</con:entry><con:entry>sourceIdentifier</con:entry><con:entry>targetSystem</con:entry><con:entry>_format</con:entry></con:parameterOrder></con:restRequest></con:config></con:testStep><con:properties/></con:testCase><con:properties><con:property><con:name>Endpoint</con:name><con:value>http://localhost:8089</con:value></con:property><con:property><con:name>resource</con:name><con:value>Patient</con:value></con:property><con:property><con:name>operation</con:name><con:value>/$ihe-pix</con:value></con:property><con:property><con:name>sourceIdentifier</con:name><con:value>urn:oid:1.3.6.1.4.1.21367.2010.1.2|NA5404</con:value></con:property><con:property><con:name>targetSystem</con:name><con:value>domain1</con:value></con:property><con:property><con:name>_format</con:name><con:value>json</con:value></con:property></con:properties></con:testSuite><con:testSuite id="d5f31f17-d59f-4cbd-9107-44cc8015c0e0" name="PixM - Code 403"><con:description>TestSuite generated for REST Service [http://qualification.ihe-europe.net]</con:description><con:settings/><con:runType>SEQUENTIAL</con:runType><con:testCase id="206c3c04-8afe-4947-83f6-24ad0976216d" failOnError="true" failTestCaseOnErrors="true" keepSession="false" maxResults="0" name="TestCase" searchProperties="true"><con:description>TestCase generated for REST Resource [] located at [/{resource}{operation}]</con:description><con:settings/><con:testStep type="transfer" name="Property Transfer 403" id="f7a14909-71cf-4e8c-a2c9-6cf571a0b533"><con:settings/><con:config xsi:type="con:PropertyTransfersStep" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name>sourceIdentifier</con:name><con:sourceType>sourceIdentifier</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>sourceIdentifier</con:targetType><con:targetStep>code 403</con:targetStep><con:upgraded>true</con:upgraded></con:transfers><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name>targetSystem</con:name><con:sourceType>targetSystem</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>targetSystem</con:targetType><con:targetStep>code 403</con:targetStep><con:upgraded>true</con:upgraded></con:transfers><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name>_format</con:name><con:sourceType>_format</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>_format</con:targetType><con:targetStep>code 403</con:targetStep><con:upgraded>true</con:upgraded></con:transfers></con:config></con:testStep><con:testStep type="restrequest" name="code 403" id="b2d62938-5129-4372-ba8a-75d5e518f30f"><con:settings/><con:config service="http://qualification.ihe-europe.net" resourcePath="/{resource}/{operation}" methodName="GET from sourceIdentifier" xsi:type="con:RestRequestStep" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><con:restRequest name="code 403" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" multiValueDelimiter=","><con:settings><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false</con:setting></con:settings><con:endpoint>http://localhost:8089/</con:endpoint><con:request/><con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri><con:assertion type="GroovyScriptAssertion" id="9b29c838-e6b9-4627-adb1-5c5f7fb1adae" name="Script Assertion"><con:configuration><scriptText>String sourceIdentifier = context.testCase.testSteps['code 403'].getPropertyValue('sourceIdentifier') -assert (sourceIdentifier ==~ /urn:oid:([0-9]*)(\.[0-9]*){10}\|([0-9A-Z])*/)</scriptText></con:configuration></con:assertion><con:assertion type="Valid HTTP Status Codes" id="c04ae33e-63c1-47e3-aae2-a8bf46f30048" name="Valid HTTP Status Codes"><con:configuration><codes>403</codes></con:configuration></con:assertion><con:credentials><con:authType>No Authorization</con:authType></con:credentials><con:jmsConfig JMSDeliveryMode="PERSISTENT"/><con:jmsPropertyConfig/><con:parameters> - <con:entry key="sourceIdentifier" value="urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404"/> - <con:entry key="resource" value="Patient"/> - <con:entry key="_format" value="json"/> - <con:entry key="operation" value="/$ihe-pix"/> - <con:entry key="targetSystem" value="domain4"/> -</con:parameters><con:parameterOrder><con:entry>resource</con:entry><con:entry>operation</con:entry><con:entry>sourceIdentifier</con:entry><con:entry>targetSystem</con:entry><con:entry>_format</con:entry></con:parameterOrder></con:restRequest></con:config></con:testStep><con:properties/></con:testCase><con:properties><con:property><con:name>Endpoint</con:name><con:value>http://localhost:8089</con:value></con:property><con:property><con:name>resource</con:name><con:value>Patient</con:value></con:property><con:property><con:name>operation</con:name><con:value>/$ihe-pix</con:value></con:property><con:property><con:name>sourceIdentifier</con:name><con:value>urn:oid:1.3.6.1.4.1.21367.2010.1.2.300|NA5404</con:value></con:property><con:property><con:name>targetSystem</con:name><con:value>domain4</con:value></con:property><con:property><con:name>_format</con:name><con:value>json</con:value></con:property></con:properties></con:testSuite><con:testSuite id="b6325bf6-8648-4734-be8b-d30dfd6319c7" name="PixM - Code 404"><con:description>TestSuite generated for REST Service [http://qualification.ihe-europe.net]</con:description><con:settings/><con:runType>SEQUENTIAL</con:runType><con:testCase id="8c2a2ef4-00d7-4f85-8138-93863d113c1e" failOnError="true" failTestCaseOnErrors="true" keepSession="false" maxResults="0" name="TestCase" searchProperties="true"><con:description>TestCase generated for REST Resource [] located at [/{resource}{operation}]</con:description><con:settings/><con:testStep type="transfer" name="Property Transfer 404" id="3939b84b-910b-4e9a-9632-ff0e1474a2f4"><con:settings/><con:config xsi:type="con:PropertyTransfersStep" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name>sourceIdentifier</con:name><con:sourceType>sourceIdentifier</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>sourceIdentifier</con:targetType><con:targetStep>code 404</con:targetStep><con:upgraded>true</con:upgraded></con:transfers><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name>targetSystem</con:name><con:sourceType>targetSystem</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>targetSystem</con:targetType><con:targetStep>code 404</con:targetStep><con:upgraded>true</con:upgraded></con:transfers><con:transfers setNullOnMissingSource="true" transferTextContent="true" failOnError="true" ignoreEmpty="false" transferToAll="false" entitize="false" transferChildNodes="false"><con:name>_format</con:name><con:sourceType>_format</con:sourceType><con:sourceStep>#TestSuite#</con:sourceStep><con:targetType>_format</con:targetType><con:targetStep>code 404</con:targetStep><con:upgraded>true</con:upgraded></con:transfers></con:config></con:testStep><con:testStep type="restrequest" name="code 404" id="e13bc6cd-1799-4521-8fe1-639565d1affe"><con:settings/><con:config service="http://qualification.ihe-europe.net" resourcePath="/{resource}/{operation}" methodName="GET from sourceIdentifier" xsi:type="con:RestRequestStep" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><con:restRequest name="code 404" id="79fc74bf-5277-4aef-8f00-744072264e5e" mediaType="application/json" multiValueDelimiter=","><con:settings><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@request-headers"><xml-fragment/></con:setting><con:setting id="com.eviware.soapui.impl.wsdl.WsdlRequest@remove_empty_content">false</con:setting></con:settings><con:endpoint>http://qualification.ihe-europe.net/pixm_fhir_server/fhir_ihe</con:endpoint><con:request/><con:originalUri>http://qualification.ihe-europe.net/Patient%2F%24ihe-pix</con:originalUri><con:assertion type="Valid HTTP Status Codes" id="5e3da6e1-9f94-48f1-af24-f9ee0ca899cc" name="Valid HTTP Status Codes"><con:configuration><codes>404</codes></con:configuration></con:assertion><con:assertion type="GroovyScriptAssertion" id="42c5b179-978b-4d0d-a279-fbb44cd81f41" name="Script Assertion"><con:configuration><scriptText>String sourceIdentifier = context.testCase.testSteps['code 404'].getPropertyValue('sourceIdentifier') -assert (sourceIdentifier ==~ /urn:oid:([0-9]*)(\.[0-9]*){10}\|([0-9A-Z])*/)</scriptText></con:configuration></con:assertion><con:credentials><con:authType>No Authorization</con:authType></con:credentials><con:jmsConfig JMSDeliveryMode="PERSISTENT"/><con:jmsPropertyConfig/><con:parameters> - <con:entry key="sourceIdentifier" value=" "/> - <con:entry key="resource" value="Patient"/> - <con:entry key="_format" value="json"/> - <con:entry key="operation" value="$ihe-pix"/> - <con:entry key="targetSystem" value="domain1"/> -</con:parameters><con:parameterOrder><con:entry>resource</con:entry><con:entry>operation</con:entry><con:entry>sourceIdentifier</con:entry><con:entry>targetSystem</con:entry><con:entry>_format</con:entry></con:parameterOrder></con:restRequest></con:config></con:testStep><con:properties/></con:testCase><con:properties><con:property><con:name>Endpoint</con:name><con:value>http://localhost:8089</con:value></con:property><con:property><con:name>resource</con:name><con:value>Patient</con:value></con:property><con:property><con:name>operation</con:name><con:value>/$ihe-pix</con:value></con:property><con:property><con:name>sourceIdentifier</con:name><con:value>urn:oid:1.3.6.1.4.1.21367.2010.1.2.301|NA5404</con:value></con:property><con:property><con:name>targetSystem</con:name><con:value>domain1</con:value></con:property><con:property><con:name>_format</con:name><con:value>json</con:value></con:property></con:properties></con:testSuite><con:properties><con:property><con:name>resource</con:name><con:value>Patient</con:value></con:property><con:property><con:name>operation</con:name><con:value>/$ihe-pix</con:value></con:property></con:properties><con:wssContainer/><con:oAuth2ProfileContainer/><con:oAuth1ProfileContainer/><con:sensitiveInformation/></con:soapui-project> \ No newline at end of file diff --git a/src/main/resources/deployment.properties b/src/main/resources/deployment.properties deleted file mode 100644 index f0d9283..0000000 --- a/src/main/resources/deployment.properties +++ /dev/null @@ -1,3 +0,0 @@ -#Â Defines the URL of the PatientRegistry X-Ref Processing Service. -xrefpatientregistry.url = https://qualification.ihe-europe.net/patient-registry/CrossReferenceService/xref-processing-service?wsdl -patientregistry.url = https://qualification.ihe-europe.net/patient-registry/PatientProcessingService/patient-processing-service?wsdl \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jboss-web.xml b/src/main/webapp/WEB-INF/jboss-web.xml deleted file mode 100644 index 0219759..0000000 --- a/src/main/webapp/WEB-INF/jboss-web.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<jboss-web> - <context-root>pixm-connector</context-root> -</jboss-web> \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 99ee6fe..0000000 --- a/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,6 +0,0 @@ -<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee - http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" - version="3.1"> -</web-app> \ No newline at end of file diff --git a/src/test/ResourcesToTestPurpose/post_request.json b/src/test/ResourcesToTestPurpose/post_request.json deleted file mode 100644 index a2f3449..0000000 --- a/src/test/ResourcesToTestPurpose/post_request.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "resourceType" : "Bundle", - "id" : "BundlePIXmFeed", - "meta" : { - "profile" : [ - "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle" - ] - }, - "type" : "message", - "entry" : [ - { - "fullUrl" : "http://example.com/fhir/MessageHeader/1", - "resource" : { - "resourceType" : "MessageHeader", - "id" : "1", - "text" : { - "status" : "generated", - "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative</b></p><p><b>event</b>: <code>urn:ihe:iti:pmir:2019:patient-feed</code></p><h3>Destinations</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientEndpoint\">http://example.com/patientEndpoint</a></td></tr></table><h3>Sources</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientSource\">http://example.com/patientSource</a></td></tr></table><p><b>focus</b>: <a href=\"#Bundle_abc\">See above (Bundle/abc)</a></p></div>" - }, - "eventUri" : "urn:ihe:iti:pmir:2019:patient-feed", - "destination" : [ - { - "endpoint" : "http://example.com/patientEndpoint" - } - ], - "source" : { - "endpoint" : "http://example.com/patientSource" - }, - "focus" : [ - { - "reference" : "Bundle/abc" - } - ] - } - }, - { - "fullUrl" : "http://example.com/fhir/Bundle/abc", - "resource" : { - "resourceType" : "Bundle", - "id" : "abc", - "type" : "history", - "entry" : [ - { - "fullUrl" : "http://example.com/fhir/Patient/PatientPIXmFeed", - "resource" : { - "resourceType" : "Patient", - "id" : "PatientPIXmFeed", - "text" : { - "status" : "generated", - "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative</b></p><p><b>id</b>: PatientPIXmFeed</p><p><b>meta</b>: </p><p><b>identifier</b>: Medical record number = 8734</p><p><b>name</b>: Franz Muster , Muster </p><p><b>gender</b>: male</p><p><b>birthDate</b>: 1995-01-27</p></div>" - }, - "contained" : [ - { - "resourceType" : "Organization", - "id" : "org1", - "identifier" : [ - { - "system" : "urn:oid:2.51.1.3", - "value" : "7601000201041" - } - ], - "address" : [ - { - "use" : "work", - "line" : [ - "Doktorgasse", - "2" - ], - "city" : "Musterhausen", - "postalCode" : "8888", - "country" : "CH" - } - ] - } - ], - "identifier" : [ - { - "type" : { - "coding" : [ - { - "system" : "http://terminology.hl7.org/CodeSystem/v2-0203", - "code" : "MR" - } - ] - }, - "system" : "urn:oid:2.16.756.888888.3.1", - "value" : "8734" - } - ], - "name" : [ - { - "family" : "Muster", - "given" : [ - "Franz" - ] - }, - { - "family" : "Muster", - "_family" : { - "extension" : [ - { - "url" : "http://hl7.org/fhir/StructureDefinition/iso21090-EN-qualifier", - "valueCode" : "BR" - } - ] - } - } - ], - "gender" : "male", - "birthDate" : "1995-01-27", - "managingOrganization" : { - "reference" : "#org1" - } - }, - "request" : { - "method" : "POST", - "url" : "Patient" - }, - "response" : { - "status" : "200" - } - } - ] - } - } - ] -} diff --git a/src/test/ResourcesToTestPurpose/post_request_1_entry.json b/src/test/ResourcesToTestPurpose/post_request_1_entry.json deleted file mode 100644 index 2272eb1..0000000 --- a/src/test/ResourcesToTestPurpose/post_request_1_entry.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "resourceType" : "Bundle", - "id" : "BundlePIXmFeed", - "meta" : { - "profile" : [ - "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle" - ] - }, - "type" : "message", - "entry" : [ - { - "fullUrl" : "http://example.com/fhir/MessageHeader/1", - "resource" : { - "resourceType" : "MessageHeader", - "id" : "1", - "text" : { - "status" : "generated", - "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative</b></p><p><b>event</b>: <code>urn:ihe:iti:pmir:2019:patient-feed</code></p><h3>Destinations</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientEndpoint\">http://example.com/patientEndpoint</a></td></tr></table><h3>Sources</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientSource\">http://example.com/patientSource</a></td></tr></table><p><b>focus</b>: <a href=\"#Bundle_abc\">See above (Bundle/abc)</a></p></div>" - }, - "eventUri" : "urn:ihe:iti:pmir:2019:patient-feed", - "destination" : [ - { - "endpoint" : "http://example.com/patientEndpoint" - } - ], - "source" : { - "endpoint" : "http://example.com/patientSource" - }, - "focus" : [ - { - "reference" : "Bundle/abc" - } - ] - } - } - ] -} diff --git a/src/test/ResourcesToTestPurpose/post_request_NO_PATIENT.json b/src/test/ResourcesToTestPurpose/post_request_NO_PATIENT.json deleted file mode 100644 index cf37919..0000000 --- a/src/test/ResourcesToTestPurpose/post_request_NO_PATIENT.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "resourceType" : "Bundle", - "id" : "BundlePIXmFeed", - "meta" : { - "profile" : [ - "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle" - ] - }, - "type" : "message", - "entry" : [ - { - "fullUrl" : "http://example.com/fhir/MessageHeader/1", - "resource" : { - "resourceType" : "MessageHeader", - "id" : "1", - "text" : { - "status" : "generated", - "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative</b></p><p><b>event</b>: <code>urn:ihe:iti:pmir:2019:patient-feed</code></p><h3>Destinations</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientEndpoint\">http://example.com/patientEndpoint</a></td></tr></table><h3>Sources</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientSource\">http://example.com/patientSource</a></td></tr></table><p><b>focus</b>: <a href=\"#Bundle_abc\">See above (Bundle/abc)</a></p></div>" - }, - "eventUri" : "urn:ihe:iti:pmir:2019:patient-feed", - "destination" : [ - { - "endpoint" : "http://example.com/patientEndpoint" - } - ], - "source" : { - "endpoint" : "http://example.com/patientSource" - }, - "focus" : [ - { - "reference" : "Bundle/abc" - } - ] - } - }, - { - "fullUrl" : "http://example.com/fhir/Bundle/abc", - "resource" : { - "resourceType" : "Bundle", - "id" : "abc", - "type" : "history", - "entry" : [ - - ] - } - } - ] -} diff --git a/src/test/ResourcesToTestPurpose/post_response.json b/src/test/ResourcesToTestPurpose/post_response.json deleted file mode 100644 index 213578d..0000000 --- a/src/test/ResourcesToTestPurpose/post_response.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "resourceType" : "Bundle", - "id" : "BundlePIXmResponse", - "meta" : { - "profile" : [ - "http://fhir.ch/ig/ch-epr-mhealth/StructureDefinition/ch-pixm-bundle-response" - ] - }, - "type" : "message", - "entry" : [ - { - "fullUrl" : "http://example.com/fhir/MessageHeader/1", - "resource" : { - "resourceType" : "MessageHeader", - "id" : "1", - "text" : { - "status" : "generated", - "div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><p><b>Generated Narrative</b></p><p><b>event</b>: <code>urn:ihe:iti:pmir:2019:patient-feed</code></p><h3>Destinations</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientEndpoint\">http://example.com/patientEndpoint</a></td></tr></table><h3>Sources</h3><table class=\"grid\"><tr><td>-</td><td><b>Endpoint</b></td></tr><tr><td>*</td><td><a href=\"http://example.com/patientSource\">http://example.com/patientSource</a></td></tr></table><h3>Responses</h3><table class=\"grid\"><tr><td>-</td><td><b>Identifier</b></td><td><b>Code</b></td></tr><tr><td>*</td><td>1</td><td>ok</td></tr></table></div>" - }, - "eventUri" : "urn:ihe:iti:pmir:2019:patient-feed", - "destination" : [ - { - "endpoint" : "http://example.com/patientEndpoint" - } - ], - "source" : { - "endpoint" : "http://example.com/patientSource" - }, - "response" : { - "identifier" : "1", - "code" : "ok" - } - } - } - ] -} \ No newline at end of file diff --git a/src/test/java/net/ihe/gazelle/application/PatientFeedClientMock.java b/src/test/java/net/ihe/gazelle/application/PatientFeedClientMock.java deleted file mode 100644 index 4f8c525..0000000 --- a/src/test/java/net/ihe/gazelle/application/PatientFeedClientMock.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.ihe.gazelle.application; - -import com.gitb.ps.ProcessingService; - -import net.ihe.gazelle.app.patientregistryapi.business.Patient; -import net.ihe.gazelle.app.patientregistryfeedclient.adapter.PatientFeedClient; - -public class PatientFeedClientMock extends PatientFeedClient{ - - public PatientFeedClientMock(ProcessingService processingService) { - super(processingService); - } - - @Override - public String createPatient(Patient patient) { - - return null; - - } - - @Override - public boolean deletePatient(String uuid) { - - return false; - - } - -} diff --git a/src/test/java/net/ihe/gazelle/application/PatientFeedClientTest.java b/src/test/java/net/ihe/gazelle/application/PatientFeedClientTest.java deleted file mode 100644 index 6467fe8..0000000 --- a/src/test/java/net/ihe/gazelle/application/PatientFeedClientTest.java +++ /dev/null @@ -1,568 +0,0 @@ -package net.ihe.gazelle.application; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Matchers.anyObject; - -import java.net.MalformedURLException; -import java.time.LocalDate; -import java.time.ZoneId; -import java.util.Date; - -import javax.xml.ws.WebServiceException; - -import ca.uhn.fhir.rest.server.exceptions.*; -import org.hl7.fhir.r4.model.Bundle; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.Mockito; - -import io.qameta.allure.Description; -import io.qameta.allure.Feature; -import io.qameta.allure.Severity; -import io.qameta.allure.SeverityLevel; -import io.qameta.allure.Story; -import net.ihe.gazelle.app.patientregistryapi.application.PatientFeedException; -import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; -import net.ihe.gazelle.app.patientregistryapi.business.GenderCode; -import net.ihe.gazelle.app.patientregistryapi.business.Patient; -import net.ihe.gazelle.app.patientregistryapi.business.PersonName; -import net.ihe.gazelle.app.patientregistryfeedclient.adapter.PatientFeedClient; -import net.ihe.gazelle.app.patientregistryfeedclient.adapter.PatientFeedProcessResponseException; -import net.ihe.gazelle.framework.preferencesmodelapi.application.NamespaceException; -import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; -import net.ihe.gazelle.framework.preferencesmodelapi.application.PreferenceException; - -@Feature("PatientFeedClient") -public class PatientFeedClientTest { - - private static final String TEST_UUID = "123e4567-e89b-12d3-a456-426614174000"; - private static final String MALFORMED_UUID = "123e4567-e89b-12d3-a456-42661417400000000000000000000000000"; - - @Mock - static private PatientFeedClient patientFeedClientMock; - @Mock - static private OperationalPreferencesService operationalPreferencesService; - @Mock - private PatientRegistryFeedClient patientRegistryFeedClient; - - @BeforeAll - static void initialize() { - patientFeedClientMock = Mockito.mock(PatientFeedClient.class); - operationalPreferencesService = Mockito.mock(OperationalPreferencesService.class); - } - - @Test - @Description("Test on initialization, when a namespace exception is thrown") - @Severity(SeverityLevel.CRITICAL) - @Story("initialization") - void TestInitializeNameSpaceException() throws PreferenceException, NamespaceException { - - patientRegistryFeedClient = new PatientRegistryFeedClient(operationalPreferencesService); - - Mockito.doThrow(NamespaceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + - "/operational" + - "-preferences", "patientregistry.url"); - - Patient patient = new Patient(); - - assertThrows(PatientFeedException.class, - () -> patientRegistryFeedClient.createPatient(patient)); - } - - @Test - @Description("Test on initialization, when a preference exception is thrown") - @Severity(SeverityLevel.CRITICAL) - @Story("initialization") - void TestInitializePreferenceException() throws PreferenceException, NamespaceException { - Mockito.doThrow(PreferenceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + - "/operational" + - "-preferences", "patientregistry.url"); - patientRegistryFeedClient = new PatientRegistryFeedClient(operationalPreferencesService); - - Patient patient = new Patient(); - - assertThrows(PatientFeedException.class, - () -> patientRegistryFeedClient.updatePatient(patient,"")); - } - - @Test - @Description("Test on initialization, when the url of the server is malformed") - @Severity(SeverityLevel.CRITICAL) - @Story("initialization") - void TestInitializeMalformedURLException() throws PreferenceException, NamespaceException { - Mockito.doThrow(MalformedURLException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + - "/operational" + - "-preferences", "patientregistry.url"); - patientRegistryFeedClient = new PatientRegistryFeedClient(operationalPreferencesService); - - Patient patient = new Patient(); - - assertThrows(PatientFeedException.class, - () -> patientRegistryFeedClient.delete("")); - } - - @Test - @Description("Test on initialization, when a web service exception is thrown") - @Severity(SeverityLevel.CRITICAL) - @Story("initialization") - void TestInitializeWebServiceException() throws PreferenceException, NamespaceException { - Mockito.doThrow(WebServiceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + - "/operational" + - "-preferences", "patientregistry.url"); - patientRegistryFeedClient = new PatientRegistryFeedClient(operationalPreferencesService); - - Patient patient = new Patient(); - - assertThrows(PatientFeedException.class, - () -> patientRegistryFeedClient.mergePatient("","")); - } - - @Test - @Description("Test on create, nominal case") - @Severity(SeverityLevel.CRITICAL) - @Story("create") - void TestNominalCreation() throws PreferenceException, NamespaceException, PatientFeedException { - - Patient patient = createPatient("name","name",LocalDate.of(1990, 06, 19), GenderCode.MALE); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doReturn(TEST_UUID).when(patientFeedClientMock).createPatient(anyObject()); - assertEquals(TEST_UUID, patientRegistryFeedClient.createPatient(patient).getEntry().get(0).getResource().getId()); - } - - @Test - @Description("Test on create, null Patient") - @Severity(SeverityLevel.CRITICAL) - @Story("create") - void TestNullPatientException() throws PreferenceException, NamespaceException, PatientFeedException { - - Patient patient = null; - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - assertThrows(InvalidRequestException.class, () -> patientRegistryFeedClient.createPatient(patient)); - try { - patientRegistryFeedClient.createPatient(patient); - } catch (InvalidRequestException e) { - assertEquals(PatientRegistryFeedClient.NO_PATIENT_PARAMETER, e.getMessage()); - } - } - - @Test - @Description("Test on create, when a blank uuid is returned") - @Severity(SeverityLevel.CRITICAL) - @Story("create") - void TestBlankUuidReturnedException() throws PreferenceException, NamespaceException, PatientFeedException { - - Patient patient = createPatient("name","name",LocalDate.of(1990, 06, 19), GenderCode.MALE); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doReturn("").when(patientFeedClientMock).createPatient(anyObject()); - assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.createPatient(patient)); - try { - patientRegistryFeedClient.createPatient(patient); - } catch (InternalErrorException e) { - assertEquals(PatientRegistryFeedClient.NO_UUID, e.getMessage()); - } - } - - @Test - @Description("Test on create, when a Malformed UUID is returned") - @Severity(SeverityLevel.CRITICAL) - @Story("create") - void TestMalformedUuidReturnedException() throws PreferenceException, NamespaceException, PatientFeedException { - - Patient patient = createPatient("name","name",LocalDate.of(1990, 06, 19), GenderCode.MALE); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doReturn(MALFORMED_UUID).when(patientFeedClientMock).createPatient(anyObject()); - try { - assertEquals(MALFORMED_UUID, patientRegistryFeedClient.createPatient(patient).getEntry().get(0).getResource().getId()); - } catch (Exception e) { - fail(); - } - } - - @Test - @Description("Test on create, for particular exceptions returned from PatientFeedApplication") - @Severity(SeverityLevel.CRITICAL) - @Story("create") - void TestFeedThrowsPatientFeedCannotCrossRef() throws PreferenceException, NamespaceException, PatientFeedException { - - Patient patient = createPatient("name","name",LocalDate.of(1990, 06, 19), GenderCode.MALE); - - PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("Impossible to cross reference the patient (not saved)"); - PatientFeedException firstException = new PatientFeedException(embedException); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doThrow(firstException).when(patientFeedClientMock).createPatient(anyObject()); - - assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.createPatient(patient)); - try { - patientRegistryFeedClient.createPatient(patient); - } catch (InternalErrorException e) { - assertEquals("Impossible to cross reference the patient, it will not be saved !", e.getMessage()); - } - } - - @Test - @Description("Test on create, for particular exceptions returned from PatientFeedApplication") - @Severity(SeverityLevel.CRITICAL) - @Story("create") - void TestFeedThrowsPatientFeedPersistingPatient() throws PreferenceException, NamespaceException, PatientFeedException { - - Patient patient = createPatient("name","name",LocalDate.of(1990, 06, 19), GenderCode.MALE); - - PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("Unexpected Exception persisting Patient !"); - PatientFeedException firstException = new PatientFeedException(embedException); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doThrow(firstException).when(patientFeedClientMock).createPatient(anyObject()); - - assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.createPatient(patient)); - try { - patientRegistryFeedClient.createPatient(patient); - } catch (InternalErrorException e) { - assertEquals("Unexpected Exception persisting Patient !", e.getMessage()); - } - } - - @Test - @Description("Test on create, for particular exceptions returned from PatientFeedApplication") - @Severity(SeverityLevel.CRITICAL) - @Story("create") - void TestFeedThrowsPatientFeedExceptionSystemNotFound() throws PreferenceException, NamespaceException, PatientFeedException { - - Patient patient = createPatient("name","name",LocalDate.of(1990, 06, 19), GenderCode.MALE); - - PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("System not found"); - PatientFeedException firstException = new PatientFeedException("Exception while Mapping with GITB elements !", embedException); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doThrow(firstException).when(patientFeedClientMock).createPatient(anyObject()); - - assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.createPatient(patient)); - try { - patientRegistryFeedClient.createPatient(patient); - } catch (InternalErrorException e) { - assertEquals("Exception while Mapping with GITB elements !", e.getMessage()); - } - } - - - - - - - @Test - @Description("Test on update, exception thrown when no Patient is given") - @Severity(SeverityLevel.CRITICAL) - @Story("update") - void TestFeedUpdateNullPatient() throws PreferenceException, NamespaceException, PatientFeedException { - - String uuid = TEST_UUID; - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - assertThrows(InvalidRequestException.class, () -> patientRegistryFeedClient.updatePatient(null, uuid)); - - } - - - @Test - @Description("Test on update, exception thrown from PatReg") - @Severity(SeverityLevel.CRITICAL) - @Story("update") - void TestFeedUpdatePatientFeedInvalidOperationThrown() throws PreferenceException, NamespaceException, PatientFeedException { - - String uuid = TEST_UUID; - Patient patient = createPatient("","",LocalDate.of(1990, 06, 19), GenderCode.MALE); - - PatientFeedException embedException = new PatientFeedException("Invalid operation used on distant PatientFeedProcessingService !"); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doThrow(embedException).when(patientFeedClientMock).updatePatient(anyObject()); - - assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.updatePatient(patient, uuid)); - try { - patientRegistryFeedClient.updatePatient(patient, uuid); - } - catch (InternalErrorException e) { - assertEquals("Invalid operation used on distant PatientFeedProcessingService !", e.getMessage()); - } - } - - @Test - @Description("Test on update, exception thrown from PatReg") - @Severity(SeverityLevel.CRITICAL) - @Story("update") - void TestFeedUpdatePatientInvalidRequest() throws PreferenceException, NamespaceException, PatientFeedException { - - String uuid = TEST_UUID; - Patient patient = createPatient("","",LocalDate.of(1990, 06, 19), GenderCode.MALE); - - PatientFeedException embedException = new PatientFeedException("Invalid Request sent to distant PatientFeedProcessingService !"); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doThrow(embedException).when(patientFeedClientMock).updatePatient(anyObject()); - - assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.updatePatient(patient, uuid)); - try { - patientRegistryFeedClient.updatePatient(patient, uuid); - } - catch (InternalErrorException e) { - assertEquals("Invalid Request sent to distant PatientFeedProcessingService !", e.getMessage()); - } - } - - @Test - @Description("Test on update, exception thrown from PatReg") - @Severity(SeverityLevel.CRITICAL) - @Story("update") - void TestFeedUpdatePatientUnhandled() throws PreferenceException, NamespaceException, PatientFeedException { - - String uuid = TEST_UUID; - Patient patient = createPatient("","",LocalDate.of(1990, 06, 19), GenderCode.MALE); - - PatientFeedException embedException = new PatientFeedException(""); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doThrow(embedException).when(patientFeedClientMock).updatePatient(anyObject()); - - assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.updatePatient(patient, uuid)); - try { - patientRegistryFeedClient.updatePatient(patient, uuid); - } - catch (InternalErrorException e) { - assertEquals("An unhandled error was thrown.", e.getMessage()); - } - } - - @Test - @Description("Test on update, exception thrown when no Uuid is given") - @Severity(SeverityLevel.CRITICAL) - @Story("update") - void TestFeedUpdateNullUuid() throws PreferenceException, NamespaceException, PatientFeedException { - - Patient patient = createPatient("","",LocalDate.of(1990, 06, 19), GenderCode.MALE); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - assertThrows(InvalidRequestException.class, () -> patientRegistryFeedClient.updatePatient(patient, null)); - - } - - @Test - @Description("Test on merge, when feeding basic request") - @Severity(SeverityLevel.CRITICAL) - @Story("merge") - void TestFeedMergeNominalCase() throws PreferenceException, NamespaceException, PatientFeedException { - - EntityIdentifier identifier1 = new EntityIdentifier(); - EntityIdentifier identifier2 = new EntityIdentifier(); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doReturn(TEST_UUID).when(patientFeedClientMock).mergePatient(anyObject(), anyObject()); - //TODO change expected value when feature will be done - assertEquals(TEST_UUID, patientRegistryFeedClient.mergePatient("identifier1", "identifier2").getId()); - - } - - @Test - @Description("Test on merge, exception thrown when no Patient is given") - @Severity(SeverityLevel.CRITICAL) - @Story("merge") - void TestFeedMergeNullPatient() throws PreferenceException, NamespaceException, PatientFeedException { - - EntityIdentifier identifier2 = new EntityIdentifier(); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - assertThrows(InvalidRequestException.class, () -> patientRegistryFeedClient.mergePatient(null, null)); - - } - - @Test - @Description("Test on merge, exception thrown when no Uuid is given") - @Severity(SeverityLevel.CRITICAL) - @Story("merge") - void TestFeedMergeNullUuid() { - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - assertThrows(InvalidRequestException.class, () -> patientRegistryFeedClient.mergePatient(null, null)); - - } - - @Test - @Description("Test on create, for particular exceptions returned from PatientFeedApplication") - @Severity(SeverityLevel.CRITICAL) - @Story("delete") - void TestFeedDelete() throws PreferenceException, NamespaceException, PatientFeedException { - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doReturn(true).when(patientFeedClientMock).deletePatient(TEST_UUID); - - try { - Bundle response = patientRegistryFeedClient.delete(TEST_UUID); - assertEquals(TEST_UUID, response.getEntry().get(0).getResource().getId()); - } catch (InternalErrorException e) { - fail("Deletion should not have thrown an error"); - } - } - - @Test - @Description("Test on create, for particular exceptions returned from PatientFeedApplication") - @Severity(SeverityLevel.CRITICAL) - @Story("delete") - void TestFeedDeleteBlankUUID() throws PreferenceException, NamespaceException, PatientFeedException { - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - assertThrows(InvalidRequestException.class, () -> patientRegistryFeedClient.delete("")); - - try { - patientRegistryFeedClient.delete(""); - } catch (InvalidRequestException e) { - assertEquals("Invalid parameter", e.getMessage()); - } catch (Exception e) { - fail("Test failed, the expected result has not happend"); - } - } - - @Test - @Description("Test on create, for particular exceptions returned from PatientFeedApplication") - @Severity(SeverityLevel.CRITICAL) - @Story("delete") - void TestFeedDeleteStatusGone() throws PreferenceException, NamespaceException, PatientFeedException { - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doReturn(false).when(patientFeedClientMock).deletePatient(TEST_UUID); - assertThrows(ResourceGoneException.class, () -> patientRegistryFeedClient.delete(TEST_UUID)); - - try { - patientRegistryFeedClient.delete(TEST_UUID); - } catch (ResourceGoneException e) { - assertEquals("Patient with UUID " + TEST_UUID, e.getMessage()); - } catch (Exception e) { - fail("Test failed, the expected result has not happend"); - } - } - - @Test - @Description("Test on delete, for a particular exception returned from PatientFeedApplication") - @Severity(SeverityLevel.CRITICAL) - @Story("delete") - void TestFeedDeleteNoUuid() throws PreferenceException, NamespaceException, PatientFeedException { - - PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("The uuid cannot be null or empty"); - PatientFeedException firstException = new PatientFeedException(embedException); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doThrow(firstException).when(patientFeedClientMock).deletePatient(TEST_UUID); - assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.delete(TEST_UUID)); - - try { - patientRegistryFeedClient.delete(TEST_UUID); - } catch (InternalErrorException e) { - assertEquals("The uuid cannot be null or empty", e.getMessage()); - } catch (Exception e) { - fail("Test failed, the expected result has not happend"); - } - } - - @Test - @Description("Test on delete, for a particular exception returned from PatientFeedApplication") - @Severity(SeverityLevel.CRITICAL) - @Story("delete") - void TestFeedDeleteDeleteCannotOperate() throws PreferenceException, NamespaceException, PatientFeedException { - - PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("Cannot proceed to delete"); - PatientFeedException firstException = new PatientFeedException(embedException); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doThrow(firstException).when(patientFeedClientMock).deletePatient(TEST_UUID); - assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.delete(TEST_UUID)); - - try { - patientRegistryFeedClient.delete(TEST_UUID); - } catch (InternalErrorException e) { - assertEquals("Cannot proceed to delete", e.getMessage()); - } catch (Exception e) { - fail("Test failed, the expected result has not happend"); - } - } - - @Test - @Description("Test on delete, for a particular exception returned from PatientFeedApplication") - @Severity(SeverityLevel.CRITICAL) - @Story("delete") - void TestFeedDeleteDeleteInvalidResponse() throws PreferenceException, NamespaceException, PatientFeedException { - - PatientFeedProcessResponseException embedException = new PatientFeedProcessResponseException("other error"); - PatientFeedException firstException = new PatientFeedException("Invalid Response from distant PatientFeedProcessingService !", embedException); - - patientRegistryFeedClient = new PatientRegistryFeedClient(); - patientRegistryFeedClient.setClient(patientFeedClientMock); - - Mockito.doThrow(firstException).when(patientFeedClientMock).deletePatient(TEST_UUID); - assertThrows(InternalErrorException.class, () -> patientRegistryFeedClient.delete(TEST_UUID)); - - try { - patientRegistryFeedClient.delete(TEST_UUID); - } catch (InternalErrorException e) { - assertEquals("Invalid Response from distant PatientFeedProcessingService !", e.getMessage()); - } catch (Exception e) { - fail("Test failed, the expected result has not happend"); - } - } - - private Patient createPatient(String familyName, String givenName, LocalDate birthDate, GenderCode gender) { - - Patient patient = new Patient(); - - PersonName name = new PersonName(); - name.addGiven(givenName); - name.setFamily(familyName); - patient.addName(name); - patient.setDateOfBirth(Date.from(birthDate.atStartOfDay(ZoneId.systemDefault()).toInstant())); - patient.setGender(gender); - return patient; - - } - -} diff --git a/src/test/java/net/ihe/gazelle/application/PatientRegistrySearchClientTest.java b/src/test/java/net/ihe/gazelle/application/PatientRegistrySearchClientTest.java deleted file mode 100644 index 899534e..0000000 --- a/src/test/java/net/ihe/gazelle/application/PatientRegistrySearchClientTest.java +++ /dev/null @@ -1,321 +0,0 @@ -package net.ihe.gazelle.application; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Mockito.doAnswer; - -import java.net.MalformedURLException; -import java.util.ArrayList; -import java.util.List; - -import javax.xml.ws.WebServiceException; - -import net.ihe.gazelle.adapter.connector.BusinessToFhirConverter; -import net.ihe.gazelle.adapter.connector.ConversionException; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; - -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import io.qameta.allure.Description; -import io.qameta.allure.Feature; -import io.qameta.allure.Severity; -import io.qameta.allure.SeverityLevel; -import io.qameta.allure.Story; -import net.ihe.gazelle.app.patientregistryapi.business.Patient; -import net.ihe.gazelle.app.patientregistryapi.business.PatientSearchCriterionKey; -import net.ihe.gazelle.app.patientregistryapi.business.PersonName; -import net.ihe.gazelle.app.patientregistrysearchclient.adapter.PatientSearchClient; -import net.ihe.gazelle.framework.preferencesmodelapi.application.NamespaceException; -import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; -import net.ihe.gazelle.framework.preferencesmodelapi.application.PreferenceException; -import net.ihe.gazelle.lib.searchmodelapi.business.SearchCriteria; -import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; -import net.ihe.gazelle.lib.searchmodelapi.business.searchcriterion.SearchCriterion; -import net.ihe.gazelle.lib.searchmodelapi.business.searchcriterion.StringSearchCriterion; - - -@Feature("PatientSearchClientTest") -@RunWith(MockitoJUnitRunner.class) -public class PatientRegistrySearchClientTest { - - private static final String TEST_UUID = "123e4567-e89b-12d3-a456-426614174000"; - private static final String MALFORMED_UUID = "123e4567-e89b-12d3-a456-42661417400000000000000000000000000"; - - @Mock - static private PatientSearchClient patientSearchClientMock; - @Mock - static private OperationalPreferencesService operationalPreferencesService; - @Mock - private PatientRegistrySearchClient patientRegistrySearchClient; - - @BeforeAll - static void initialize() { - patientSearchClientMock = Mockito.mock(PatientSearchClient.class); - operationalPreferencesService = Mockito.mock(OperationalPreferencesService.class); - } - - @Test - @Description("Test on initialization, when a namespace exception is thrown") - @Severity(SeverityLevel.CRITICAL) - @Story("initialization") - public void TestInitializeNameSpaceException() throws PreferenceException, NamespaceException { - Mockito.doThrow(NamespaceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + - "/operational" + - "-preferences", "patientregistry.url"); - patientRegistrySearchClient = new PatientRegistrySearchClient(operationalPreferencesService); - - SearchCriteria searchCriteria = new SearchCriteria(); - SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); - searchCriterion.setValue(TEST_UUID); - searchCriteria.addSearchCriterion(searchCriterion); - - assertThrows(SearchException.class, - () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); - } - - @Test - @Description("Test on initialization, when a preference exception is thrown") - @Severity(SeverityLevel.CRITICAL) - @Story("initialization") - public void TestInitializePreferenceException() throws PreferenceException, NamespaceException { - Mockito.doThrow(PreferenceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + - "/operational" + - "-preferences", "patientregistry.url"); - patientRegistrySearchClient = new PatientRegistrySearchClient(operationalPreferencesService); - - SearchCriteria searchCriteria = new SearchCriteria(); - SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); - searchCriterion.setValue(TEST_UUID); - searchCriteria.addSearchCriterion(searchCriterion); - - assertThrows(SearchException.class, - () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); - } - - @Test - @Description("Test on initialization, when the url of the server is malformed") - @Severity(SeverityLevel.CRITICAL) - @Story("initialization") - public void TestInitializeMalformedURLException() throws PreferenceException, NamespaceException { - Mockito.doThrow(MalformedURLException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + - "/operational" + - "-preferences", "patientregistry.url"); - patientRegistrySearchClient = new PatientRegistrySearchClient(operationalPreferencesService); - - SearchCriteria searchCriteria = new SearchCriteria(); - SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); - searchCriterion.setValue(TEST_UUID); - searchCriteria.addSearchCriterion(searchCriterion); - - assertThrows(SearchException.class, - () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); - } - - @Test - @Description("Test on initialization, when a web service exception is thrown") - @Severity(SeverityLevel.CRITICAL) - @Story("initialization") - public void TestInitializeWebServiceException() throws PreferenceException, NamespaceException { - Mockito.doThrow(WebServiceException.class).when(operationalPreferencesService).getStringValue("java:/app/gazelle/pixm-connector" + - "/operational" + - "-preferences", "patientregistry.url"); - patientRegistrySearchClient = new PatientRegistrySearchClient(operationalPreferencesService); - - SearchCriteria searchCriteria = new SearchCriteria(); - SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); - searchCriterion.setValue(TEST_UUID); - searchCriteria.addSearchCriterion(searchCriterion); - - assertThrows(SearchException.class, - () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); - } - - @Test - @Description("Test on read, a specific excpetion is thrown") - @Severity(SeverityLevel.CRITICAL) - @Story("read") - public void TestSearchExceptionMapping() throws SearchException { - - SearchCriteria searchCriteria = new SearchCriteria(); - SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); - searchCriterion.setValue(TEST_UUID); - searchCriteria.addSearchCriterion(searchCriterion); - - SearchException exception = new SearchException("Exception while Mapping with GITB elements !"); - patientRegistrySearchClient = new PatientRegistrySearchClient(); - patientRegistrySearchClient.setClient(patientSearchClientMock); - Mockito.doThrow(exception).when(patientSearchClientMock).search(anyObject()); - assertThrows(InternalErrorException.class, - () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); - } - - @Test - @Description("Test on read, a specific excpetion is thrown") - @Severity(SeverityLevel.CRITICAL) - @Story("read") - public void TestSearchExceptionInvalidResponse() throws SearchException { - - SearchCriteria searchCriteria = new SearchCriteria(); - SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); - searchCriterion.setValue(TEST_UUID); - searchCriteria.addSearchCriterion(searchCriterion); - - SearchException exception = new SearchException("Invalid Response from distant PatientFeedProcessingService !"); - Mockito.doThrow(exception).when(patientSearchClientMock).search(anyObject()); - patientRegistrySearchClient = new PatientRegistrySearchClient(); - patientRegistrySearchClient.setClient(patientSearchClientMock); - assertThrows(ResourceNotFoundException.class, - () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); - } - - @Test - @Description("Test on read, a specific exception is thrown") - @Severity(SeverityLevel.CRITICAL) - @Story("read") - public void TestSearchExceptionInvalidOperation() throws SearchException { - - SearchCriteria searchCriteria = new SearchCriteria(); - SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); - searchCriterion.setValue(TEST_UUID); - searchCriteria.addSearchCriterion(searchCriterion); - - SearchException exception = new SearchException("Invalid operation used on distant PatientFeedProcessingService !"); - Mockito.doThrow(exception).when(patientSearchClientMock).search(anyObject()); - patientRegistrySearchClient = new PatientRegistrySearchClient(); - patientRegistrySearchClient.setClient(patientSearchClientMock); - assertThrows(InvalidRequestException.class, - () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); - } - - @Test - @Description("Test on read, a specific exception is thrown") - @Severity(SeverityLevel.CRITICAL) - @Story("read") - public void TestSearchExceptionInvalidRequest() throws SearchException { - - SearchCriteria searchCriteria = new SearchCriteria(); - SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); - searchCriterion.setValue(TEST_UUID); - searchCriteria.addSearchCriterion(searchCriterion); - - SearchException exception = new SearchException("Invalid Request sent to distant PatientFeedProcessingService !"); - Mockito.doThrow(exception).when(patientSearchClientMock).search(anyObject()); - patientRegistrySearchClient = new PatientRegistrySearchClient(); - patientRegistrySearchClient.setClient(patientSearchClientMock); - assertThrows(InvalidRequestException.class, - () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); - } - - @Test - @Description("Test on read, when no patient is returned") - @Severity(SeverityLevel.CRITICAL) - @Story("read") - public void TestNoPatientReturned() throws SearchException { - - List <Patient> resources = new ArrayList<>(); - - SearchCriteria searchCriteria = new SearchCriteria(); - SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); - searchCriterion.setValue(TEST_UUID); - searchCriteria.addSearchCriterion(searchCriterion); - - doAnswer(invocation -> resources).when(patientSearchClientMock).search(anyObject()); - - patientRegistrySearchClient = new PatientRegistrySearchClient(); - patientRegistrySearchClient.setClient(patientSearchClientMock); - assertThrows(ResourceNotFoundException.class, - () -> patientRegistrySearchClient.searchPatient(searchCriterion.getValue())); - - - } - - @Test - @Description("Test on read, when exactly one patient is returned") - @Severity(SeverityLevel.CRITICAL) - @Story("read") - public void TestOnePatientReturned() throws SearchException { - - org.hl7.fhir.r4.model.Patient arthur = new org.hl7.fhir.r4.model.Patient(); - arthur.addName().addGiven("Arthur"); - - List <Patient> resources = new ArrayList<>(); - Patient pat = new Patient(); - PersonName ps = new PersonName(); - ps.addGiven("Arthur"); - pat.addName(ps); - resources.add(pat); - - SearchCriteria searchCriteria = new SearchCriteria(); - SearchCriterion<String> searchCriterion = new StringSearchCriterion(PatientSearchCriterionKey.UUID); - searchCriterion.setValue(TEST_UUID); - searchCriteria.addSearchCriterion(searchCriterion); - - doAnswer(invocation -> resources).when(patientSearchClientMock).search(anyObject()); - - patientRegistrySearchClient = new PatientRegistrySearchClient(); - patientRegistrySearchClient.setClient(patientSearchClientMock); - assertEquals(arthur.getName().get(0).getGiven().toString(), patientRegistrySearchClient.searchPatient(searchCriterion.getValue()).getName().get(0).getGiven().toString()); - - } - - @Test - @Description("Test on read, when more than one patient is returned") - @Severity(SeverityLevel.CRITICAL) - @Story("read") - public void TestManyPatientsReturned() throws SearchException { - - List <Patient> resources = new ArrayList<>(); - resources.add(new Patient()); - resources.add(new Patient()); - - - doAnswer(invocation -> resources).when(patientSearchClientMock).search(anyObject()); - - patientRegistrySearchClient = new PatientRegistrySearchClient(); - patientRegistrySearchClient.setClient(patientSearchClientMock); - assertThrows(ResourceNotFoundException.class, - () -> patientRegistrySearchClient.searchPatient(TEST_UUID)); - - - } - - //@Test - @Description("Test on read, when a conversion exception happens") - @Severity(SeverityLevel.CRITICAL) - @Story("read") - public void TestConversionException() throws SearchException { - - List <Patient> resources = new ArrayList<>(); - resources.add(new Patient()); - resources.add(new Patient()); - patientRegistrySearchClient = new PatientRegistrySearchClient(); - patientRegistrySearchClient.setClient(patientSearchClientMock); - assertThrows(ResourceNotFoundException.class, - () -> patientRegistrySearchClient.searchPatient(TEST_UUID)); - - - } - - - @Test - @Description("Test on read, when parameter is null") - @Severity(SeverityLevel.CRITICAL) - @Story("read") - public void TestNoEntry() throws SearchException { - - patientRegistrySearchClient = new PatientRegistrySearchClient(); - patientRegistrySearchClient.setClient(patientSearchClientMock); - - assertThrows(InvalidRequestException.class, - () -> patientRegistrySearchClient.searchPatient(null)); - - - } -} diff --git a/src/test/java/net/ihe/gazelle/application/SearchClientMock.java b/src/test/java/net/ihe/gazelle/application/SearchClientMock.java deleted file mode 100644 index dd88d6a..0000000 --- a/src/test/java/net/ihe/gazelle/application/SearchClientMock.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.ihe.gazelle.application; - -import java.util.List; - -import com.gitb.ps.ProcessingService; - -import net.ihe.gazelle.app.patientregistryapi.business.Patient; -import net.ihe.gazelle.app.patientregistryapi.business.PatientAliases; -import net.ihe.gazelle.app.patientregistrysearchclient.adapter.PatientSearchClient; -import net.ihe.gazelle.lib.searchmodelapi.business.SearchCriteria; -import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; -import net.ihe.gazelle.lib.searchmodelapi.business.searchcriterion.SearchCriterion; - -public class SearchClientMock extends PatientSearchClient { - - public SearchClientMock(ProcessingService processingService) { - super(processingService); - } - - @Override - public List<Patient> search(SearchCriteria searchCriteria) throws SearchException { - PatientAliases patientAliases = new PatientAliases(); - for (SearchCriterion criterion : searchCriteria.getSearchCriterions()) { - switch (criterion.toString()) { - case "": - return null; - default: - return null; - } - } - return null; - } - -} diff --git a/src/test/java/net/ihe/gazelle/business/provider/CHBundleProviderMock.java b/src/test/java/net/ihe/gazelle/business/provider/CHBundleProviderMock.java deleted file mode 100644 index fdebe3b..0000000 --- a/src/test/java/net/ihe/gazelle/business/provider/CHBundleProviderMock.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.ihe.gazelle.business.provider; - - -import ca.uhn.fhir.context.FhirContext; -import org.hl7.fhir.r4.model.Bundle; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; - -public class CHBundleProviderMock { - - public final static String ID_TO_RETURN= "OK"; - public final static String PATH_TO_RESOURCE="src/test/ResourcesToTestPurpose/"; - - - - protected Bundle returnBundleFromResource(String nameOfFile){ - try{ - - if ("null.json".equals(nameOfFile)){ - return null; - } - Bundle bundleResquest = (Bundle) FhirContext.forR4().newJsonParser().parseResource(new FileReader(PATH_TO_RESOURCE+nameOfFile)); - return bundleResquest; - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return null; - } - - -}// end class \ No newline at end of file diff --git a/src/test/java/net/ihe/gazelle/business/provider/CHBundleProviderTest.java b/src/test/java/net/ihe/gazelle/business/provider/CHBundleProviderTest.java deleted file mode 100644 index a1cb969..0000000 --- a/src/test/java/net/ihe/gazelle/business/provider/CHBundleProviderTest.java +++ /dev/null @@ -1,164 +0,0 @@ -package net.ihe.gazelle.business.provider; - -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import io.qameta.allure.*; -import net.ihe.gazelle.application.PatientRegistryFeedClient; -import org.hl7.fhir.r4.model.IdType; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@Feature("CHBundleProvider Test") -class CHBundleProviderTest { - - @BeforeAll - static void initialized() { - - } - - private static PatientRegistryFeedClient feedClientMock = new PatientRegistryFeedClientMock(); - - private static final CHBundleProvider bundle_feed_provider = new CHBundleProvider(new CHPatientProviderMock(feedClientMock)); - public static final String GOOD_UUID = "123e4567-e89b-12d3-a456-426614174000"; - public static final String Throw_two_id = "LOLILO"; - public static final String Throw_ID = "MDR"; - - CHBundleProviderMock cbpm = new CHBundleProviderMock(); - - - @Test - @Description("Test on the Create Method for pixm-connector") - void testCreatePatient() { - String fileName = "post_request.json"; - MethodOutcome mo = bundle_feed_provider.create(cbpm.returnBundleFromResource(fileName)); - assertEquals(cbpm.ID_TO_RETURN,mo.getId().getValue()); - } - - @Test - @Description("Test on the Create Method for pixm-connector with a null Bundle") - void testCreateNullBundle() { - String fileName = "null.json"; - assertThrows(InvalidRequestException.class, ()-> bundle_feed_provider.create(cbpm.returnBundleFromResource(fileName)),"Bundle is null or Empty"); - } - - @Test - @Description("Test on the Create Method with a Bundle with No Patient inside") - void testCreateNoPatientInBundle() { - String fileName = "post_request_NO_PATIENT.json"; - assertThrows(InvalidRequestException.class, ()-> bundle_feed_provider.create(cbpm.returnBundleFromResource(fileName)),"Missing Patient in Bundle"); - } - - @Test - @Description("Test on the Create Method with a Bundle with 3 entries") - void testCreatePatientBundleWith1Entry() { - String fileName = "post_request_1_entry.json"; - assertThrows(InvalidRequestException.class, ()-> bundle_feed_provider.create(cbpm.returnBundleFromResource(fileName))); - } - - @Test - @Description("Test on the delete operation, clean delete without issue") - @Severity(SeverityLevel.CRITICAL) - @Story("delete operation") - void testDeletePatient() { - String fileName = "post_request.json"; - IdType id = new IdType(GOOD_UUID); - MethodOutcome mo = bundle_feed_provider.delete(id, cbpm.returnBundleFromResource(fileName)); - assertEquals(cbpm.ID_TO_RETURN,mo.getId().getValue()); - } - - @Test - @Description("Test on the delete operation, deleting a Patient but with a null bundle") - @Severity(SeverityLevel.CRITICAL) - @Story("delete operation") - void testDeletePatientNullBundle() { - String fileName = "null.json"; - IdType id = new IdType(GOOD_UUID); - assertThrows(InvalidRequestException.class, ()-> bundle_feed_provider.delete(id, cbpm.returnBundleFromResource(fileName)),"Bundle is null or Empty"); - } - - @Test - @Description("Test on the delete operation, deleting a Patient but there is no Patient in the bundle") - @Severity(SeverityLevel.CRITICAL) - @Story("delete operation") - void testDeletePatientBlank() { - String fileName = "post_request_NO_PATIENT.json"; - IdType id = new IdType(GOOD_UUID); - assertThrows(InvalidRequestException.class, ()-> bundle_feed_provider.delete(id, cbpm.returnBundleFromResource(fileName)),"Bundle Could not be converted to HL7 Patient"); - } - - @Test - @Description("Test on the delete operation, deleting a Patient but with a null bundle") - @Severity(SeverityLevel.CRITICAL) - @Story("delete operation") - void testDeleteBlankId() { - String fileName = "post_request.json"; - IdType id = new IdType(""); - assertThrows(InvalidRequestException.class, ()-> bundle_feed_provider.delete(id, cbpm.returnBundleFromResource(fileName)),"Bundle is null or Empty"); - } - - @Test - @Description("Test on the update operation, clean update without issue") - @Severity(SeverityLevel.CRITICAL) - @Story("update operation") - void testUpdatePatient() { - String fileName = "post_request.json"; - IdType id = new IdType(GOOD_UUID); - MethodOutcome mo = bundle_feed_provider.updatePatient(id, cbpm.returnBundleFromResource(fileName)); - assertEquals(cbpm.ID_TO_RETURN,mo.getId().getValue()); - } - - @Test - @Description("Test on the update operation, updating a Patient but with a null bundle") - @Severity(SeverityLevel.CRITICAL) - @Story("update operation") - void testUpdatePatientNullBundle() { - String fileName = "null.json"; - IdType id = new IdType(GOOD_UUID); - assertThrows(InvalidRequestException.class, ()-> bundle_feed_provider.updatePatient(id, cbpm.returnBundleFromResource(fileName)),"Bundle is null or Empty"); - } - - @Test - @Description("Test on the update operation, updating a Patient but there is no Patient in the bundle") - @Severity(SeverityLevel.CRITICAL) - @Story("update operation") - void testUpdatePatientBlank() { - String fileName = "post_request_NO_PATIENT.json"; - IdType id = new IdType(GOOD_UUID); - assertThrows(InvalidRequestException.class, ()-> bundle_feed_provider.updatePatient(id, cbpm.returnBundleFromResource(fileName)),"bizu"); - } - - @Test - @Description("Test on the update operation, updating a Patient but there is no Patient in the bundle") - @Severity(SeverityLevel.CRITICAL) - @Story("update operation") - void testUpdateEmptyId() { - String fileName = "post_request_NO_PATIENT.json"; - IdType id = new IdType(""); - assertThrows(InvalidRequestException.class, ()-> bundle_feed_provider.updatePatient(id, cbpm.returnBundleFromResource(fileName)),"Invalid ID Parameter, either null or empty."); - } - - @Test - @Description("Test on the update operation, updating a Patient but there is no Patient in the bundle") - @Severity(SeverityLevel.CRITICAL) - @Story("update operation") - void testUpdateInvalidRequest() { - String fileName = "post_request_NO_PATIENT.json"; - IdType id = new IdType(Throw_ID); - assertThrows(InvalidRequestException.class, ()-> bundle_feed_provider.updatePatient(id, cbpm.returnBundleFromResource(fileName)),"Invalid ID Parameter, either null or empty."); - } - - @Test - @Description("Test on the update operation, updating a Patient but there is no Patient in the bundle") - @Severity(SeverityLevel.CRITICAL) - @Story("update operation") - void testUpdateInternalError() { - String fileName = "post_request_NO_PATIENT.json"; - IdType id = new IdType(Throw_two_id); - assertThrows(InvalidRequestException.class, ()-> bundle_feed_provider.updatePatient(id, cbpm.returnBundleFromResource(fileName)),"Invalid ID Parameter, either null or empty."); - } - - -} \ No newline at end of file diff --git a/src/test/java/net/ihe/gazelle/business/provider/CHPatientProviderMock.java b/src/test/java/net/ihe/gazelle/business/provider/CHPatientProviderMock.java deleted file mode 100644 index e4ced87..0000000 --- a/src/test/java/net/ihe/gazelle/business/provider/CHPatientProviderMock.java +++ /dev/null @@ -1,71 +0,0 @@ -package net.ihe.gazelle.business.provider; - -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.ResourceParam; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import net.ihe.gazelle.application.PatientRegistryFeedClient; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Patient; - -public class CHPatientProviderMock extends ChPatientResourceProvider { - - - public CHPatientProviderMock(PatientRegistryFeedClient patientRegistryFeedClient) { - super(patientRegistryFeedClient); - - } - - @Override - public MethodOutcome create(@ResourceParam Patient iti93Patient) { - MethodOutcome mo = new MethodOutcome(); - IIdType iIdType = new IdType(); - - //Case OK - if (iti93Patient.getId().equals("Patient/PatientPIXmFeed")) { - return mo.setId(iIdType.setValue("OK")); - } - //Case KO - if (iti93Patient.getId().equals("Bundle/PatientPIXmFeed")) { - return null; - } - //Case NULL - if (iti93Patient == null) { - return null; - } - return null; - } - - @Override - public MethodOutcome delete(@ResourceParam IdType id) { - MethodOutcome mo = new MethodOutcome(); - IdType iIdType = new IdType(); - - //Case OK - if (id.equals(CHBundleProviderTest.GOOD_UUID)) { - return mo.setId(iIdType.setValue("OK")); - } - - return null; - } - - @Override - public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient hl7Patient) { - MethodOutcome mo = new MethodOutcome(); - IdType iIdType = new IdType(); - - //Case OK - - if (theId.equals(CHBundleProviderTest.GOOD_UUID)) { - return mo.setId(iIdType.setValue("OK")); - } else if (theId.equals(CHBundleProviderTest.Throw_ID)) { - throw new InvalidRequestException(""); - } else if (theId.equals(CHBundleProviderTest.Throw_two_id)) { - throw new InternalErrorException(""); - } - - return null; - } -} diff --git a/src/test/java/net/ihe/gazelle/business/provider/ChPatientResourceProviderTest.java b/src/test/java/net/ihe/gazelle/business/provider/ChPatientResourceProviderTest.java deleted file mode 100644 index 9ff5dee..0000000 --- a/src/test/java/net/ihe/gazelle/business/provider/ChPatientResourceProviderTest.java +++ /dev/null @@ -1,424 +0,0 @@ -package net.ihe.gazelle.business.provider; - -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import io.qameta.allure.*; -import net.ihe.gazelle.application.PatientRegistryFeedClient; -import net.ihe.gazelle.application.PatientRegistrySearchClient; -import net.ihe.gazelle.application.PatientRegistryXRefSearchClient; -import org.hl7.fhir.r4.model.*; -import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -@Feature("ChaPatientProvider") -class ChPatientResourceProviderTest { - private static PatientRegistryXRefSearchClient xRefSearchClientMock = new PatientRegistryXRefSearchClientMock(); - private static PatientRegistrySearchClient searchClientMock = new PatientRegistrySearchClientMock(); - private static PatientRegistryFeedClient feedClientMock = new PatientRegistryFeedClientMock(); - private static final ChPatientResourceProvider provider = new ChPatientResourceProvider(xRefSearchClientMock); - private static final ChPatientResourceProvider read_provider = new ChPatientResourceProvider(searchClientMock); - private static final ChPatientResourceProvider feed_provider = new ChPatientResourceProvider(feedClientMock); - - @BeforeAll - static void initialized() { - xRefSearchClientMock = new PatientRegistryXRefSearchClientMock(); - } - - @Test - @Description("Test on ihe_pix operation, when aiming for a patient on 1 target System, with unr:oid: type system identifiers") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testOkPatient() { - TokenParam sourceId = new TokenParam("urn:oid:1", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("urn:oid:2")); - targetDomains.addAnd(stringParam); - - Parameters response = provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - ParametersParameterComponent singleResponse = response.getParameter().get(0); - Identifier r4 = (Identifier) singleResponse.getValue(); - assertEquals(PatientRegistryXRefSearchClientMock.URN_OK, r4.getValue()); - assertEquals(PatientRegistryXRefSearchClientMock.URN_OK, r4.getSystem()); - - } - - @Test - @Description("Test on ihe_pix operation, when aiming for a patient on 1 target System, with http system identifiers") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testOkPatientHttp() { - TokenParam sourceId = new TokenParam("http://1", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - Parameters response = provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - ParametersParameterComponent singleResponse = response.getParameter().get(0); - Identifier r4 = (Identifier) singleResponse.getValue(); - assertEquals(PatientRegistryXRefSearchClientMock.HTTP_OK, r4.getValue()); - assertEquals(PatientRegistryXRefSearchClientMock.HTTP_OK, r4.getSystem()); - - } - - @Test - @Description("Test on ihe_pix operation, when aiming for a patient on 2 target System") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testOkPatientTwoTargetSystem() { - TokenParam sourceId = new TokenParam("http://1", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - stringParam.add(new StringParam("http://3")); - targetDomains.addAnd(stringParam); - - Parameters response = provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - ParametersParameterComponent singleResponse = response.getParameter().get(0); - Identifier r4 = (Identifier) singleResponse.getValue(); - assertEquals(PatientRegistryXRefSearchClientMock.HTTP_OK, r4.getValue()); - assertEquals(PatientRegistryXRefSearchClientMock.HTTP_OK, r4.getSystem()); - - } - - @Test - @Description("Test on ihe_pix operation, when the patient source identifiers is null") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testSourceIdentifierNull() { - TokenParam sourceId = new TokenParam(); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (InvalidRequestException e) { - assertEquals(ChPatientResourceProvider.SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when the patient system identifiers is null") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testSourceIdentifierSystemNull() { - TokenParam sourceId = new TokenParam(); - sourceId.setValue("69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (InvalidRequestException e) { - assertEquals(ChPatientResourceProvider.SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when the patient source identifiers is null") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testSourceIdentifierValueNull() { - TokenParam sourceId = new TokenParam(); - sourceId.setSystem("http://1"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (InvalidRequestException e) { - assertEquals(ChPatientResourceProvider.SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when the patient system identifiers is blank") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testSourceIdentifierSystemBlank() { - TokenParam sourceId = new TokenParam("", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (InvalidRequestException e) { - assertEquals(ChPatientResourceProvider.SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when the patient system identifier hasn't been recognized") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testMalformedSystemIdentifier() { - TokenParam sourceId = new TokenParam("1:2:3", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (ResourceNotFoundException e) { - assertEquals(ChPatientResourceProvider.SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when target identifiers is blank") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testTargetIdentifierBlank() { - TokenParam sourceId = new TokenParam("http::/1", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (ForbiddenOperationException e) { - assertEquals(ChPatientResourceProvider.TARGET_SYSTEM_NOT_FOUND, e.getMessage()); - } - } - - @Test - @Description("Test on ihe_pix operation, when no target identifiers is given") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testTargetIdentifierNull() { - TokenParam sourceId = new TokenParam("http::/1", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (ForbiddenOperationException e) { - assertEquals(ChPatientResourceProvider.TARGET_SYSTEM_NOT_FOUND, e.getMessage()); - } - } - - @Test - @Description("Test on ihe_pix operation, when target identifiers don't have valid cardinality") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testTargetIdentifierMoreThanTwo() { - TokenParam sourceId = new TokenParam("http::/1", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - stringParam.add(new StringParam("http://3")); - stringParam.add(new StringParam("http://4")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (ForbiddenOperationException e) { - assertEquals(ChPatientResourceProvider.TARGET_SYSTEM_NOT_FOUND, e.getMessage()); - } - } - - @Test - @Description("Test on read method, but resource is not found") - @Severity(SeverityLevel.CRITICAL) - @Story("ReadMethod") - void testReadResourceNotFound() { - IdType id = new IdType(); - id.setValue("1"); - assertThrows(InternalErrorException.class, - () -> read_provider.read(id)); - } - - - - @Test - @Description("Test on read method, but no parameter is given") - @Severity(SeverityLevel.CRITICAL) - @Story("ReadMethod") - void testReadNoIdGiven() { - IdType id = new IdType(); - id.setValue(""); - try { - read_provider.read(id); - } catch (InvalidRequestException e) { - assertEquals(ChPatientResourceProvider.NO_ID_PROVIDED, e.getMessage()); - } catch (Exception e) { - fail(); - } - } - - @Test - @Description("Test on read method, returns ok") - @Severity(SeverityLevel.CRITICAL) - @Story("ReadMethod") - void testReadFine() { - IdType id = new IdType(); - id.setValue("3"); - assertEquals("[Arthur]", read_provider.read(id).getName().get(0).getGiven().toString()); - } - - @Test - @Description("Test on Update method, returns ok") - @Severity(SeverityLevel.CRITICAL) - @Story("UpdateMethod") - void testUpdateFine() { - IdType id = new IdType(); - Patient iti93Bundle = new Patient(); - iti93Bundle.setId("Plop"); - id.setValue("3"); - Bundle returnedValue = (Bundle) feed_provider.update(id, iti93Bundle).getResource(); - assertEquals("Plop", returnedValue.getEntry().get(0).getResource().getId()); - } - - @Test - @Description("Test on Update method, returns ok") - @Severity(SeverityLevel.CRITICAL) - @Story("UpdateMethod") - void testUpdateUuidBlank() { - IdType id = new IdType(); - Patient iti93Bundle = new Patient(); - iti93Bundle.setId("Plop"); - id.setValue(""); - assertThrows(InvalidRequestException.class, () -> feed_provider.update(id, iti93Bundle)); - } - - @Test - @Description("Test on Update method, returns ok") - @Severity(SeverityLevel.CRITICAL) - @Story("UpdateMethod") - void testUpdatePatientBlank() { - IdType id = new IdType(); - Patient iti93Bundle = new Patient(); - id.setValue("3"); - assertThrows(InvalidRequestException.class, () -> feed_provider.update(id, iti93Bundle)); - } - - @Test - @Description("Test on Update method, returns ok") - @Severity(SeverityLevel.CRITICAL) - @Story("UpdateMethod") - void testUpdatePatientFeedThrown() { - IdType id = new IdType(); - Patient iti93Bundle = new Patient(); - iti93Bundle.setId("Plop"); - id.setValue("42"); - assertThrows(InternalErrorException.class, () -> feed_provider.update(id, iti93Bundle)); - } - - @Test - @Description("Test on Create method, returns ok") - @Severity(SeverityLevel.CRITICAL) - @Story("CreateMethod") - void testCreateFine() { - Patient iti93Bundle = new Patient(); - iti93Bundle.setId("PatientPixMFeed"); - Bundle returnedValue = (Bundle) feed_provider.create(iti93Bundle).getResource(); - assertEquals("1", returnedValue.getId()); - } - - @Test - @Description("Test on Create method, returns ok") - @Severity(SeverityLevel.CRITICAL) - @Story("CreateMethod") - void testCreatePatientBlank() { - assertThrows(InvalidRequestException.class, () -> feed_provider.create(null)); - } - - @Test - @Description("Test on Create method, returns nothing") - @Severity(SeverityLevel.CRITICAL) - @Story("CreateMethod") - void testCreatePatientNPE() { - Patient iti93Bundle = new Patient(); - iti93Bundle.setId("Plop"); - assertThrows(ResourceNotFoundException.class, () -> feed_provider.create(iti93Bundle)); - } - - @Test - @Description("Test on Create method, returns nothing") - @Severity(SeverityLevel.CRITICAL) - @Story("CreateMethod") - void testCreatePatientFeedThrown() { - Patient iti93Bundle = new Patient(); - iti93Bundle.setId("throw_me_something"); - assertThrows(InternalErrorException.class, () -> feed_provider.create(iti93Bundle)); - } - - @Test - @Description("Test on Delete method, returns ok") - @Severity(SeverityLevel.CRITICAL) - @Story("DeleteMethod") - void testDeleteFine() { - IdType theId = new IdType(); - theId.setValue("PatientPixMFeed"); - Bundle returnedValue = (Bundle) feed_provider.delete(theId).getResource(); - assertEquals("PatientPixMFeed", returnedValue.getId()); - } - - @Test - @Description("Test on Delete method, returns ok") - @Severity(SeverityLevel.CRITICAL) - @Story("DeleteMethod") - void testDeleteUuidEmpty() { - IdType theId = new IdType(); - theId.setValue(""); - assertThrows(InvalidRequestException.class, () -> feed_provider.delete(theId)); - } - - - @Test - @Description("Test on Delete method, returns ok") - @Severity(SeverityLevel.CRITICAL) - @Story("DeleteMethod") - void testDeletePatientFeedThrown() { - IdType theId = new IdType(); - theId.setValue("42"); - assertThrows(InternalErrorException.class, () -> feed_provider.delete(theId)); - } - -} \ No newline at end of file diff --git a/src/test/java/net/ihe/gazelle/business/provider/IhePatientResourceProviderTest.java b/src/test/java/net/ihe/gazelle/business/provider/IhePatientResourceProviderTest.java deleted file mode 100644 index ad50c47..0000000 --- a/src/test/java/net/ihe/gazelle/business/provider/IhePatientResourceProviderTest.java +++ /dev/null @@ -1,242 +0,0 @@ -package net.ihe.gazelle.business.provider; - -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import io.qameta.allure.Description; -import io.qameta.allure.Severity; -import io.qameta.allure.SeverityLevel; -import io.qameta.allure.Story; -import net.ihe.gazelle.app.patientregistryapi.application.SearchCrossReferenceException; -import net.ihe.gazelle.application.PatientRegistryXRefSearchClient; -import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@Story("IHE Patient Provider") -class IhePatientResourceProviderTest { - private static PatientRegistryXRefSearchClient xRefSearchClientMock = new PatientRegistryXRefSearchClientMock(); - private static final IhePatientResourceProvider provider = new IhePatientResourceProvider(xRefSearchClientMock); - - @BeforeAll - static void initialized() { - xRefSearchClientMock = new PatientRegistryXRefSearchClientMock(); - } - - @Test - @Description("Test on ihe_pix operation, when aiming for a patient on 1 target System, with urn:oid: type system identifiers") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testOkPatient() { - TokenParam sourceId = new TokenParam("urn:oid:1", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("urn:oid:2")); - targetDomains.addAnd(stringParam); - - Parameters response = provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - ParametersParameterComponent singleResponse = response.getParameter().get(0); - Identifier r4 = (Identifier) singleResponse.getValue(); - assertEquals(PatientRegistryXRefSearchClientMock.URN_OK, r4.getValue()); - assertEquals(PatientRegistryXRefSearchClientMock.URN_OK, r4.getSystem()); - - } - - @Test - @Description("Test on ihe_pix operation, when aiming for a patient on 1 target System, with http system identifiers") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testOkPatientHttp() { - TokenParam sourceId = new TokenParam("http://1", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - Parameters response = provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - ParametersParameterComponent singleResponse = response.getParameter().get(0); - Identifier r4 = (Identifier) singleResponse.getValue(); - assertEquals(PatientRegistryXRefSearchClientMock.HTTP_OK, r4.getValue()); - assertEquals(PatientRegistryXRefSearchClientMock.HTTP_OK, r4.getSystem()); - - } - - @Test - @Description("Test on ihe_pix operation, when the whole patient source identifier is null") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testSourceIdentifierNull() { - TokenParam sourceId = new TokenParam(); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (InvalidRequestException e) { - assertEquals(IhePatientResourceProvider.SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when the patient system identifiers is null") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testSourceIdentifierSystemNull() { - TokenParam sourceId = new TokenParam(); - sourceId.setValue("69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (InvalidRequestException e) { - assertEquals(IhePatientResourceProvider.SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when the patient source identifiers is null") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testSourceIdentifierValueNull() { - TokenParam sourceId = new TokenParam(); - sourceId.setSystem("http://1"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (InvalidRequestException e) { - assertEquals(IhePatientResourceProvider.SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when the patient system identifiers is blank") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testSourceIdentifierSystemBlank() { - TokenParam sourceId = new TokenParam("", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (InvalidRequestException e) { - assertEquals(IhePatientResourceProvider.SOURCE_IDENTIFIER_ASSIGNING_AUTHORITY_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when the target system is malformed and not recognized") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testMalformedSystemIdentifier() { - TokenParam sourceId = new TokenParam("1:2:3", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("http://2")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (ResourceNotFoundException e) { - assertEquals(IhePatientResourceProvider.SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when the target system is blank") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testTargetIdentifierBlank() { - TokenParam sourceId = new TokenParam("http::/1", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (ForbiddenOperationException e) { - assertEquals(IhePatientResourceProvider.TARGET_SYSTEM_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when the target system is blank") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testSourceIdentifierBlank() { - TokenParam sourceId = new TokenParam("http::/1", "69420"); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(null, targetDomains); - } catch (InvalidRequestException e) { - assertEquals(IhePatientResourceProvider.SOURCE_IDENTIFIER_PATIENT_IDENTIFIER_NOT_FOUND, e.getMessage()); - } - - } - - @Test - @Description("Test on ihe_pix operation, when the target system is blank") - @Severity(SeverityLevel.CRITICAL) - @Story("ihe_pix operation") - void testCrossRefException() { - TokenParam sourceId = new TokenParam("urn:oid:3", "69420"); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - tokenAndListParam.addAnd(sourceId); - StringAndListParam targetDomains = new StringAndListParam(); - StringOrListParam stringParam = new StringOrListParam(); - stringParam.add(new StringParam("urn:oid:3")); - targetDomains.addAnd(stringParam); - - try { - provider.findPatientsByIdentifier(tokenAndListParam, targetDomains); - } catch (InternalErrorException e) { - assertEquals("net.ihe.gazelle.business.provider.PatientResourceProviderException: One of the target domain does not exist", e.getMessage()); - } - - } - -} \ No newline at end of file diff --git a/src/test/java/net/ihe/gazelle/business/provider/PatientRegistryFeedClientMock.java b/src/test/java/net/ihe/gazelle/business/provider/PatientRegistryFeedClientMock.java deleted file mode 100644 index 669f20d..0000000 --- a/src/test/java/net/ihe/gazelle/business/provider/PatientRegistryFeedClientMock.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.ihe.gazelle.business.provider; - -import net.ihe.gazelle.adapter.connector.BusinessToFhirConverter; -import net.ihe.gazelle.adapter.connector.ConversionException; -import net.ihe.gazelle.app.patientregistryapi.application.PatientFeedException; -import net.ihe.gazelle.app.patientregistryapi.business.Patient; -import net.ihe.gazelle.app.patientregistryfeedclient.adapter.PatientFeedClient; -import net.ihe.gazelle.application.PatientRegistryFeedClient; -import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; -import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; -import org.hl7.fhir.r4.model.Bundle; - -public class PatientRegistryFeedClientMock extends PatientRegistryFeedClient { - - public static final String HTTP_OK = "HTTP_OK"; - public static final String URN_OK = "URN_OK"; - private static final String CROSS_REFERENCE = "Cross Reference"; - - public PatientRegistryFeedClientMock() { - } - - public PatientRegistryFeedClientMock(OperationalPreferencesService operationalPreferencesService) { - super(operationalPreferencesService); - } - - public PatientRegistryFeedClientMock(PatientFeedClient searchClient) { - this.setClient(searchClient); - } - - @Override - public Bundle createPatient(net.ihe.gazelle.app.patientregistryapi.business.Patient patient) throws PatientFeedException { - - String uuid = patient.getUuid(); - Bundle bundle = new Bundle(); - bundle.setId("1"); - - switch (uuid){ - case "PatientPixMFeed": - return bundle; - case "throw_me_something": - throw new PatientFeedException(); - default: - return null; - } - } - - @Override - public Bundle updatePatient(net.ihe.gazelle.app.patientregistryapi.business.Patient patientToUpdate, String uuid) throws PatientFeedException { - if (uuid.equals("42")) { - throw new PatientFeedException(); - } - Bundle response = new Bundle(); - Bundle.BundleEntryComponent component = new Bundle.BundleEntryComponent(); - try { - component.setResource(BusinessToFhirConverter.patientToFhirPatient(patientToUpdate)); - } - catch (ConversionException e) { - throw new PatientFeedException(e); - } - response.addEntry(component); - return response; - } - - @Override - public Bundle delete(String uuid) throws PatientFeedException{ - - if (uuid.equals("42")) { - throw new PatientFeedException(); - } - - Bundle response = new Bundle(); - response.setId(uuid); - - return response; - } - -} diff --git a/src/test/java/net/ihe/gazelle/business/provider/PatientRegistrySearchClientMock.java b/src/test/java/net/ihe/gazelle/business/provider/PatientRegistrySearchClientMock.java deleted file mode 100644 index 67d932c..0000000 --- a/src/test/java/net/ihe/gazelle/business/provider/PatientRegistrySearchClientMock.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.ihe.gazelle.business.provider; - -import java.util.ArrayList; -import java.util.List; - -import org.hl7.fhir.r4.model.Patient; - -import net.ihe.gazelle.app.patientregistrysearchclient.adapter.PatientSearchClient; -import net.ihe.gazelle.application.PatientRegistrySearchClient; -import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; -import net.ihe.gazelle.lib.searchmodelapi.business.exception.SearchException; - -public class PatientRegistrySearchClientMock extends PatientRegistrySearchClient { - public static final String HTTP_OK = "HTTP_OK"; - public static final String URN_OK = "URN_OK"; - private static final String CROSS_REFERENCE = "Cross Reference"; - - public PatientRegistrySearchClientMock() { - } - - public PatientRegistrySearchClientMock(OperationalPreferencesService operationalPreferencesService) { - super(operationalPreferencesService); - } - - public PatientRegistrySearchClientMock(PatientSearchClient searchClient) { - setClient(searchClient); - } - @Override - public Patient searchPatient(String uuid) throws SearchException { - - Patient arthur = new Patient(); - arthur.addName().addGiven("Arthur"); - - switch (uuid){ - case "1": - throw new SearchException("test exception"); - case "3": - return arthur; - default: - return null; - } - } -} diff --git a/src/test/java/net/ihe/gazelle/business/provider/PatientRegistryXRefSearchClientMock.java b/src/test/java/net/ihe/gazelle/business/provider/PatientRegistryXRefSearchClientMock.java deleted file mode 100644 index da57eab..0000000 --- a/src/test/java/net/ihe/gazelle/business/provider/PatientRegistryXRefSearchClientMock.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.ihe.gazelle.business.provider; - -import java.util.List; - -import net.ihe.gazelle.app.patientregistryxrefsearchclient.adapter.XRefSearchClient; -import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; - -import net.ihe.gazelle.app.patientregistryapi.application.SearchCrossReferenceException; -import net.ihe.gazelle.app.patientregistryapi.business.EntityIdentifier; -import net.ihe.gazelle.application.PatientRegistryXRefSearchClient; -import net.ihe.gazelle.framework.preferencesmodelapi.application.OperationalPreferencesService; - -public class PatientRegistryXRefSearchClientMock extends PatientRegistryXRefSearchClient { - public static final String HTTP_OK = "HTTP_OK"; - public static final String URN_OK = "URN_OK"; - private static final String CROSS_REFERENCE = "Cross Reference"; - - public PatientRegistryXRefSearchClientMock() { - } - - public PatientRegistryXRefSearchClientMock(OperationalPreferencesService operationalPreferencesService) { - super(operationalPreferencesService); - } - - public PatientRegistryXRefSearchClientMock(XRefSearchClient xRefSearchClient) { - setClient(xRefSearchClient); - } - @Override - public Parameters process(EntityIdentifier sourceIdentifier, List<String> targetSystemList) throws SearchCrossReferenceException { - Parameters response = new Parameters(); - ParametersParameterComponent alias = new ParametersParameterComponent(); - Identifier r4Identifier = new Identifier(); - switch (sourceIdentifier.getSystemIdentifier()){ - case "http://1": - r4Identifier.setSystem(HTTP_OK); - r4Identifier.setValue(HTTP_OK); - alias.setValue(r4Identifier); - alias.setName(CROSS_REFERENCE); - response.addParameter(alias); - return response; - case "1": - r4Identifier.setSystem(URN_OK); - r4Identifier.setValue(URN_OK); - alias.setValue(r4Identifier); - alias.setName(CROSS_REFERENCE); - response.addParameter(alias); - return response; - case "0": - throw new SearchCrossReferenceException(new PatientResourceProviderException("Error in the sourceIdentifier : System does not exit")); - case "3": - throw new SearchCrossReferenceException(new PatientResourceProviderException("One of the target domain does not exist")); - case "4": - throw new SearchCrossReferenceException(new PatientResourceProviderException("Error in the sourceIdentifier : it does not match " + - "any Patient")); - default: - return null; - } - } -} diff --git a/src/test/java/net/ihe/gazelle/business/provider/PatientResourceProviderExceptionTest.java b/src/test/java/net/ihe/gazelle/business/provider/PatientResourceProviderExceptionTest.java deleted file mode 100644 index c814956..0000000 --- a/src/test/java/net/ihe/gazelle/business/provider/PatientResourceProviderExceptionTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.ihe.gazelle.business.provider; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -class PatientResourceProviderExceptionTest { - /** - * Dummy Test for coverage - * - * @throws PatientResourceProviderException testException - */ - @Test - void generateException() { - assertThrows(PatientResourceProviderException.class, () -> { - throw new PatientResourceProviderException("Test"); - }); - } - - /** - * Dummy Test for coverage - * - * @throws PatientResourceProviderException testException - */ - @Test - void generateException1() { - assertThrows(PatientResourceProviderException.class, () -> { - throw new PatientResourceProviderException("Test", new IllegalArgumentException("test")); - }); - } - - /** - * Dummy Test for coverage - * - * @throws PatientResourceProviderException testException - */ - @Test - void generateException2() { - assertThrows(PatientResourceProviderException.class, () -> { - throw new PatientResourceProviderException(new IllegalArgumentException("test")); - }); - } - - /** - * Dummy Test for coverage - * - * @throws PatientResourceProviderException testException - */ - @Test - void generateException4() { - assertThrows(PatientResourceProviderException.class, () -> { - throw new PatientResourceProviderException("Test", new IllegalArgumentException("test"), false, true); - }); - } - - /** - * Dummy Test for coverage - * - * @throws PatientResourceProviderException testException - */ - @Test - void generateException3() { - assertThrows(PatientResourceProviderException.class, () -> { - throw new PatientResourceProviderException(); - }); - } -} \ No newline at end of file -- GitLab