From 4f6432b241e9c143e0d139fea9d7895ceeb31928 Mon Sep 17 00:00:00 2001 From: Liu Date: Mon, 4 Oct 2021 11:07:37 -0400 Subject: [PATCH 01/30] add endpoint for next question --- .../org/cdshooks/CoverageRequirements.java | 9 +++++ .../cdshooks/services/crd/CdsService.java | 9 ++++- .../services/crd/r4/OrderSignService.java | 8 ++++ .../controllers/QuestionnaireController.java | 40 +++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java diff --git a/resources/src/main/java/org/cdshooks/CoverageRequirements.java b/resources/src/main/java/org/cdshooks/CoverageRequirements.java index 28be93d9b..135e3b5b6 100644 --- a/resources/src/main/java/org/cdshooks/CoverageRequirements.java +++ b/resources/src/main/java/org/cdshooks/CoverageRequirements.java @@ -12,6 +12,7 @@ public class CoverageRequirements { private String questionnairePARequestUri; private String questionnairePlanOfCareUri; private String questionnaireDispenseUri; + private String questionnaireAdditionalUri; private String requestId; private boolean priorAuthRequired; private boolean documentationRequired; @@ -137,4 +138,12 @@ public CoverageRequirements setDocumentationRequired(boolean documentationRequir this.documentationRequired = documentationRequired; return this; } + + public String getQuestionnaireAdditionalUri() { + return questionnaireAdditionalUri; + } + + public void setQuestionnaireAdditionalUri(String questionnaireAdditionalUri) { + this.questionnaireAdditionalUri = questionnaireAdditionalUri; + } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java index c3439d028..8d5badbb5 100755 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java @@ -177,7 +177,8 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireProgressNoteUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePARequestUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePlanOfCareUri()) - || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireDispenseUri()))) { + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireDispenseUri()) + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireAdditionalUri()))) { List smartAppLinks = createQuestionnaireLinks(request, applicationBaseUrl, lookupResult, results); response.addCard(CardBuilder.transform(results, smartAppLinks)); @@ -265,6 +266,12 @@ private List createQuestionnaireLinks(requestTypeT request, URL applicatio coverageRequirements.getQuestionnaireDispenseUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Dispense Form")); } + + if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireAdditionalUri())) { + listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, + coverageRequirements.getQuestionnaireAdditionalUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), + coverageRequirements.isPriorAuthRequired(), "Additional Form")); + } return listOfLinks; } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java index 304a4e663..5c26c4fe7 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java @@ -151,6 +151,14 @@ protected CqlResultsForCard executeCqlAndGetRelevantResults(Context context, Str logger.info("-- No PA Request questionnaire defined"); } + try { + if (evaluateStatement("RESULT_QuestionnaireAdditionalUri", context) != null) { + coverageRequirements.setQuestionnaireAdditionalUri(evaluateStatement("RESULT_QuestionnaireAdditionalUri", context).toString()); + } + } catch (Exception e) { + logger.info("-- No additional questionnaire defined"); + } + // process the alternative therapies try { if (evaluateStatement("ALTERNATIVE_THERAPY", context) != null) { diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java new file mode 100644 index 000000000..650ceb131 --- /dev/null +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -0,0 +1,40 @@ +package org.hl7.davinci.endpoint.controllers; + +import org.hl7.davinci.FhirResourceInfo; +import org.hl7.davinci.endpoint.Application; +import org.hl7.davinci.endpoint.Utils; +import org.hl7.davinci.endpoint.database.FhirResource; +import org.hl7.davinci.endpoint.database.FhirResourceRepository; +import org.hl7.davinci.endpoint.files.FileResource; +import org.hl7.davinci.endpoint.files.FileStore; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.logging.Logger; +import javax.servlet.http.HttpServletRequest; + +@CrossOrigin +@RestController +@RequestMapping("/Questionnaire") +public class QuestionnaireController { + private static Logger logger = Logger.getLogger(Application.class.getName()); + + @PostMapping(value = "/$next-question", consumes = { MediaType.APPLICATION_JSON_VALUE, "application/fhir+json" }) + public ResponseEntity retrieveNextQuestion(HttpServletRequest request, HttpEntity entity) { + return getNextQuestionOperation(entity.getBody(), request); + } + + private ResponseEntity getNextQuestionOperation(String body, HttpServletRequest request) { + logger.info("POST /Questionnaire/$next-question fhir+" ); + return ResponseEntity.status(HttpStatus.ACCEPTED).contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.CONTENT_TYPE, "application/fhir+json" + "; charset=utf-8") + .body("Request received"); + } +} From 8c4651ab1ff4b3eb3f1feeb1580cc3ff9c2450d5 Mon Sep 17 00:00:00 2001 From: Liu Date: Thu, 7 Oct 2021 11:43:02 -0400 Subject: [PATCH 02/30] add item to questionnaire --- .../controllers/QuestionnaireController.java | 86 +++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index 650ceb131..d00ff015a 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -7,8 +7,9 @@ import org.hl7.davinci.endpoint.database.FhirResourceRepository; import org.hl7.davinci.endpoint.files.FileResource; import org.hl7.davinci.endpoint.files.FileStore; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IDomainResource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.Resource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -16,10 +17,26 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import ca.uhn.fhir.context.FhirContext; +import org.hl7.davinci.r4.FhirComponents; +import ca.uhn.fhir.parser.IParser; + import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemAnswerOptionComponent; +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType; + @CrossOrigin @RestController @RequestMapping("/Questionnaire") @@ -31,10 +48,67 @@ public ResponseEntity retrieveNextQuestion(HttpServletRequest request, H return getNextQuestionOperation(entity.getBody(), request); } - private ResponseEntity getNextQuestionOperation(String body, HttpServletRequest request) { - logger.info("POST /Questionnaire/$next-question fhir+" ); - return ResponseEntity.status(HttpStatus.ACCEPTED).contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.CONTENT_TYPE, "application/fhir+json" + "; charset=utf-8") - .body("Request received"); + private ResponseEntity getNextQuestionOperation(String body, HttpServletRequest request) { + logger.info("POST /Questionnaire/$next-question fhir+"); + + FhirContext ctx = new FhirComponents().getFhirContext(); + IParser parser = ctx.newJsonParser(); + + IDomainResource domainResource = (IDomainResource) parser.parseResource(QuestionnaireResponse.class, body); + if (!domainResource.fhirType().equalsIgnoreCase("QuestionnaireResponse")) { + logger.warning("unsupported resource type: "); + HttpStatus status = HttpStatus.BAD_REQUEST; + MediaType contentType = MediaType.TEXT_PLAIN; + return ResponseEntity.status(status).contentType(contentType).body("Bad Request"); + } else { + logger.info(" ---- Resource received " + domainResource.toString()); + QuestionnaireResponse inputResource = (QuestionnaireResponse) domainResource; + String fragmentId = inputResource.getQuestionnaire(); + List containedResource = inputResource.getContained(); + Questionnaire inQuestionnaire = null; + for (int i = 0; i < containedResource.size(); i++) { + Resource item = containedResource.get(i); + if (item.getResourceType().equals(ResourceType.Questionnaire)) { + Questionnaire inputQuestionnaire = (Questionnaire) item; + if (inputQuestionnaire.getId().equals(fragmentId)) { + inQuestionnaire = inputQuestionnaire; + break; + } + } + } + + if (inQuestionnaire != null) { + // TODO retrieve the questions and set it in the contained Questionnaire + logger.info("--- Get next question for questionnaire " + inQuestionnaire.getId()); + logger.info("---- Get meta profile " + inQuestionnaire.getMeta().getProfile().get(0).getValue()); + + Questionnaire.QuestionnaireItemComponent orderReason = inQuestionnaire.addItem().setLinkId("1"); + orderReason.setText("order Reason"); + orderReason.setType(QuestionnaireItemType.CHOICE); + orderReason.addAnswerOption(new QuestionnaireItemAnswerOptionComponent() + .setValue(new Coding().setCode("Initial or original order for certification"))); + orderReason.addAnswerOption(new QuestionnaireItemAnswerOptionComponent() + .setValue(new Coding().setCode("Change in statue"))); + orderReason.addAnswerOption(new QuestionnaireItemAnswerOptionComponent() + .setValue(new Coding().setCode("Revision or change in equipment"))); + orderReason.addAnswerOption( + new QuestionnaireItemAnswerOptionComponent().setValue(new Coding().setCode("Replacement"))); + + List newContainedList = new ArrayList<>(); + newContainedList.add(inQuestionnaire); + // inputResource.setContained(newContainedList); + inputResource.addContained(inQuestionnaire); + String formattedResourceString = ctx.newJsonParser().encodeResourceToString(inputResource); + + return ResponseEntity.status(HttpStatus.ACCEPTED).contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.CONTENT_TYPE, "application/fhir+json" + "; charset=utf-8") + .body(formattedResourceString); + } else { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.CONTENT_TYPE, "application/fhir+json" + "; charset=utf-8") + .body("Invalid input questionnaire"); + } + + } } } From a17a1cc4ee1c206d1b548e76009771e14320db11 Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Thu, 14 Oct 2021 17:11:54 -0400 Subject: [PATCH 03/30] Update gradle to newest version --- gradle/wrapper/gradle-wrapper.properties | 2 +- operations/build.gradle | 8 ++-- resources/build.gradle | 26 +++++------ server/build.gradle | 56 ++++++++++++------------ 4 files changed, 47 insertions(+), 45 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4b442974..ffed3a254 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/operations/build.gradle b/operations/build.gradle index 7f536fdf0..600a57e9e 100644 --- a/operations/build.gradle +++ b/operations/build.gradle @@ -1,7 +1,7 @@ -apply plugin: 'maven' +apply plugin: 'maven-publish' dependencies { - compile project(':resources') - compile 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' - compile 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' + implementation project(':resources') + implementation 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' } diff --git a/resources/build.gradle b/resources/build.gradle index c43254ba9..692387e0c 100644 --- a/resources/build.gradle +++ b/resources/build.gradle @@ -1,22 +1,22 @@ -apply plugin: 'maven' +apply plugin: 'maven-publish' dependencies { - compile group: 'com.helger', name: 'ph-schematron', version: '4.1.0' - compile 'com.googlecode.json-simple:json-simple:1.1.1' + implementation group: 'com.helger', name: 'ph-schematron', version: '4.1.0' + implementation 'com.googlecode.json-simple:json-simple:1.1.1' - compile 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' - compile 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' - compile group: 'ca.uhn.hapi.fhir', name: 'hapi-fhir-validation-resources-r4', version: '5.3.0' - compile 'ca.uhn.hapi.fhir:hapi-fhir-validation:5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' + implementation group: 'ca.uhn.hapi.fhir', name: 'hapi-fhir-validation-resources-r4', version: '5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-validation:5.3.0' - compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.12.1' - compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.12.1' - compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.12.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.12.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.12.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.12.1' - compile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final' - compile group: 'org.springframework', name: 'spring-context', version: '5.0.8.RELEASE' + implementation group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final' + implementation group: 'org.springframework', name: 'spring-context', version: '5.0.8.RELEASE' - compile('ca.uhn.hapi.fhir:hapi-fhir-jpaserver-base:3.6.0'){ + implementation('ca.uhn.hapi.fhir:hapi-fhir-jpaserver-base:3.6.0'){ exclude group: 'org.thymeleaf' } } diff --git a/server/build.gradle b/server/build.gradle index a4ba77c2f..72e0916d0 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -5,7 +5,6 @@ buildscript { repositories { mavenCentral() maven { url 'https://repo.spring.io/snapshot' } - maven { url 'http://repo.jenkins-ci.org/releases/' } } dependencies { @@ -13,7 +12,7 @@ buildscript { } } -apply plugin: 'maven' +apply plugin: 'maven-publish' //apply plugin: 'war' apply plugin: 'java' apply plugin: 'org.springframework.boot' @@ -23,48 +22,51 @@ apply plugin: 'io.spring.dependency-management' dependencies { - compile project(':resources') - compile project(':operations') + implementation project(':resources') + implementation project(':operations') + implementation 'com.googlecode.json-simple:json-simple:1.1.1' + implementation 'com.google.code.gson:gson:2.8.8' - compile('org.springframework.boot:spring-boot-starter-actuator') - compile('org.springframework.boot:spring-boot-starter-data-jpa') - compile('org.springframework.boot:spring-boot-starter-data-rest') - compile('org.springframework.boot:spring-boot-starter-thymeleaf') - compile('org.springframework.boot:spring-boot-starter-security') + implementation('org.springframework.boot:spring-boot-starter-actuator') + implementation('org.springframework.boot:spring-boot-starter-data-jpa') + implementation('org.springframework.boot:spring-boot-starter-data-rest') + implementation('org.springframework.boot:spring-boot-starter-thymeleaf') + implementation('org.springframework.boot:spring-boot-starter-security') + implementation('org.springframework.boot:spring-boot-starter-validation') - compile('com.h2database:h2') + implementation('com.h2database:h2') - compile("io.jsonwebtoken:jjwt:0.7.0") + implementation("io.jsonwebtoken:jjwt:0.7.0") -// compile 'javax.servlet:javax.servlet-api:3.1.0' +// implementation 'javax.servlet:javax.servlet-api:3.1.0' - compile 'commons-beanutils:commons-beanutils:1.9.3' + implementation 'commons-beanutils:commons-beanutils:1.9.3' - testCompile('org.springframework.boot:spring-boot-starter-test') - testCompile "com.github.tomakehurst:wiremock-standalone:2.18.0" - testCompile('org.springframework.boot:spring-boot-starter-test') + testImplementation('org.springframework.boot:spring-boot-starter-test') + testImplementation "com.github.tomakehurst:wiremock-standalone:2.18.0" + testImplementation('org.springframework.boot:spring-boot-starter-test') - compile 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' - compile 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-base:5.3.0' + implementation 'ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.3.0' - compile 'com.jayway.jsonpath:json-path:2.4.0' - compile 'joda-time:joda-time:2.10.5' + implementation 'com.jayway.jsonpath:json-path:2.4.0' + implementation 'joda-time:joda-time:2.10.5' //cql stuff - compile (group: 'org.opencds.cqf.cql', name: 'engine', version: '1.5.1') { + implementation (group: 'org.opencds.cqf.cql', name: 'engine', version: '1.5.1') { exclude group: 'org.slf4j', module: 'slf4j-log4j12' } - compile (group: 'org.opencds.cqf.cql', name: 'engine.fhir', version: '1.5.1') { + implementation (group: 'org.opencds.cqf.cql', name: 'engine.fhir', version: '1.5.1') { exclude group: 'org.slf4j', module: 'slf4j-log4j12' } - //Use locally compiled cql libs (engine and fhir) - compile fileTree(dir: 'libs', include: ['*.jar']) - compile group: 'info.cqframework', name: 'cql-to-elm', version: '1.5.1' + //Use locally implementationd cql libs (engine and fhir) + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation group: 'info.cqframework', name: 'cql-to-elm', version: '1.5.1' - compile 'org.zeroturnaround:zt-zip:1.13' + implementation 'org.zeroturnaround:zt-zip:1.13' - compile group: 'org.kohsuke', name:'github-api', version:'1.77' + implementation group: 'org.kohsuke', name:'github-api', version:'1.77' } task buildReact(type:Exec) { From 608741ca524e59788782097bad9c9cf27d7a0be1 Mon Sep 17 00:00:00 2001 From: Liu Date: Mon, 18 Oct 2021 16:57:28 -0400 Subject: [PATCH 04/30] fix merge issue --- .../cdshooks/services/crd/CdsService.java | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java index 17c0e9a0d..f86fc096f 100755 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java @@ -170,25 +170,26 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a if (results.getCoverageRequirements().getApplies()) { - if ((coverageRequirements.isDocumentationRequired() || coverageRequirements.isPriorAuthRequired()) - && (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireOrderUri()) - || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireFaceToFaceUri()) - || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireLabUri()) - || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireProgressNoteUri()) - || StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePARequestUri()) - || StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePlanOfCareUri()) - || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireDispenseUri()) - || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireAdditionalUri()))) { - List smartAppLinks = createQuestionnaireLinks(request, applicationBaseUrl, lookupResult, results); - response.addCard(CardBuilder.transform(results, smartAppLinks)); - - // add a card for an alternative therapy if there is one - if (results.getAlternativeTherapy().getApplies()) { - try { - response.addCard(CardBuilder.alternativeTherapyCard(results.getAlternativeTherapy(), - results.getRequest(), fhirComponents)); - } catch (RuntimeException e) { - logger.warn("Failed to process alternative therapy: " + e.getMessage()); + if (coverageRequirements.isDocumentationRequired() || coverageRequirements.isPriorAuthRequired()) { + if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireOrderUri()) + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireFaceToFaceUri()) + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireLabUri()) + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireProgressNoteUri()) + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePARequestUri()) + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePlanOfCareUri()) + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireDispenseUri()) + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireAdditionalUri())) { + List smartAppLinks = createQuestionnaireLinks(request, applicationBaseUrl, lookupResult, results); + response.addCard(CardBuilder.transform(results, smartAppLinks)); + + // add a card for an alternative therapy if there is one + if (results.getAlternativeTherapy().getApplies()) { + try { + response.addCard(CardBuilder.alternativeTherapyCard(results.getAlternativeTherapy(), + results.getRequest(), fhirComponents)); + } catch (RuntimeException e) { + logger.warn("Failed to process alternative therapy: " + e.getMessage()); + } } } else { logger.warn("Unspecified Questionnaire URI; summary card sent to client"); From eb61b07de99d6ae67205602f7cc1ac212e9a50c9 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Fri, 22 Oct 2021 13:35:17 -0400 Subject: [PATCH 05/30] Working version of next-question (assuming there is only one parent question) --- .../controllers/QuestionnaireController.java | 211 ++++++++++++++---- ...Questions-HomeOxygenTherapyAdditional.json | 89 ++++++++ 2 files changed, 257 insertions(+), 43 deletions(-) create mode 100644 server/src/main/java/org/hl7/davinci/endpoint/controllers/Questions-HomeOxygenTherapyAdditional.json diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index d00ff015a..f8e600bfa 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -1,15 +1,7 @@ package org.hl7.davinci.endpoint.controllers; -import org.hl7.davinci.FhirResourceInfo; import org.hl7.davinci.endpoint.Application; -import org.hl7.davinci.endpoint.Utils; -import org.hl7.davinci.endpoint.database.FhirResource; -import org.hl7.davinci.endpoint.database.FhirResourceRepository; -import org.hl7.davinci.endpoint.files.FileResource; -import org.hl7.davinci.endpoint.files.FileStore; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IDomainResource; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -19,41 +11,146 @@ import ca.uhn.fhir.context.FhirContext; import org.hl7.davinci.r4.FhirComponents; + +import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; -import java.io.IOException; -import java.util.ArrayList; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.util.HashMap; import java.util.List; -import java.util.Optional; +import java.util.Map; import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; -import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.ResourceType; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemAnswerOptionComponent; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; -import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; + +// --- ORDER OF RESPONSE-REQUEST OPERATIONS +// (REQUEST) External user sends the initial QuestionnaireResponse JSON that contains which questionnaire it would like to trigger as n element the "contained" field. +// (RESPONSE) QuestionnaireController adds the first question with its answerResponse options (with its linkId and text) to the JSON in QuestionnaireResponse.contained.item[] and sends it back. +// (REQUEST) External user adds their answer to the question to the JSON in QuestionnaireResponse.item[] and sends it back. +// (RESPONSE) QuestionnaireController takes that response and adds the next indicated question to the JSON in QuestionnaireResponse.contained.item[] and sends it back. +// Repeat intil QuestionnaireController reaches a leaf-node, then it sets the status to "completed" from "in-progress" +// Ultimately, The QuestionnaireController responses add ONLY to the QuestionnaireResponse.contained.item[]. The external requester adds answers to QuestionnaireResponse.item[] and includes the associated linkid and text. @CrossOrigin @RestController @RequestMapping("/Questionnaire") public class QuestionnaireController { + + /** + * A class that demos a tree to define next questions based on responses. + */ + private class AdaptiveQuestionnaireTree { + private NextQuestionNode root; + + /** + * Constructor. + * @param inputQuestionnaire The input questionnaire from the CDS-Library. + */ + public AdaptiveQuestionnaireTree(Questionnaire inputQuestionnaire) { + // Build child nodes. (Does not yet include building out children's child nodes. Assumes only one question with followup questions.) + Map childMap = new HashMap(); + // Map of child question IDs for the first question that map to their associated possible response. + Map childIdsToResponses = new HashMap(); + // This loop iterates over the possible answer options of the first item in the inputQuestionnaire, which is assumed to be the only parent question. + for(QuestionnaireItemAnswerOptionComponent answerOption : inputQuestionnaire.getItemFirstRep().getAnswerOption()) { + // The Id of this answer response's next question. + String answerNextQuestionId = answerOption.getModifierExtensionFirstRep().getUrl(); + // The response that indicates this answer to the question. + String possibleAnswerResponse = answerOption.getValueCoding().getCode(); + System.out.println("LLLL:" + answerNextQuestionId); + System.out.println("AAAA:" + possibleAnswerResponse); + // Add the key-value pair of next question id to its assocated answer response. + childIdsToResponses.put(answerNextQuestionId, possibleAnswerResponse); + } + + // Extract the children and add them to their parent question nodes. + for(QuestionnaireItemComponent childQuestion : inputQuestionnaire.getItem()){ + String linkId = childQuestion.getLinkId(); + System.out.println("SSSSS:" + linkId); + System.out.println("QQQQQ:" + childIdsToResponses.keySet()); + if(childIdsToResponses.containsKey(linkId)){ + // This question is a child of the parent question. Add it to the parent's map of children as a key-value pair of response-childQuestion. + childMap.put(childIdsToResponses.get(linkId), childQuestion); + } + } + // Create the root node with its question and children. + root = new NextQuestionNode(inputQuestionnaire, childMap); + } + + /** + * Returns the next question based on the response to the current question. + * @param response The response given to this question. + * @return + */ + public QuestionnaireItemComponent getNextQuestionForResponse(String response){ + if(!root.children.containsKey(response)){ + throw new NullPointerException("Not a valid response for this question: \'" + response + "\''. Possible responses for this question: \'" + root.children.keySet() + "\''."); + } + return root.children.get(response);//.data; + } + + /** + * Returns whether this is a leaf node (needs work, but is fine for demo purposes). + * @param response + * @return + */ + public boolean isLeafNode(String response) { + return !this.getNextQuestionForResponse(response).hasAnswerOption(); + } + + /** + * Inner class that describes a node of the tree. + */ + private class NextQuestionNode { + private Questionnaire data; // To be used to contain future answer options for multiple subquestions? + private Map children; + + public NextQuestionNode(Questionnaire data, Map children) { + this.data = data; + this.children = children; + } + } + } + + // Logger. private static Logger logger = Logger.getLogger(Application.class.getName()); + // Tree that tracks the questions. + private AdaptiveQuestionnaireTree questionnaireTree; + /** + * + * @param request + * @param entity + * @return + */ @PostMapping(value = "/$next-question", consumes = { MediaType.APPLICATION_JSON_VALUE, "application/fhir+json" }) public ResponseEntity retrieveNextQuestion(HttpServletRequest request, HttpEntity entity) { return getNextQuestionOperation(entity.getBody(), request); } + /** + * + * @param body + * @param request + * @return + */ private ResponseEntity getNextQuestionOperation(String body, HttpServletRequest request) { logger.info("POST /Questionnaire/$next-question fhir+"); FhirContext ctx = new FhirComponents().getFhirContext(); IParser parser = ctx.newJsonParser(); + // Parses the body. IDomainResource domainResource = (IDomainResource) parser.parseResource(QuestionnaireResponse.class, body); if (!domainResource.fhirType().equalsIgnoreCase("QuestionnaireResponse")) { logger.warning("unsupported resource type: "); @@ -62,44 +159,72 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet return ResponseEntity.status(status).contentType(contentType).body("Bad Request"); } else { logger.info(" ---- Resource received " + domainResource.toString()); - QuestionnaireResponse inputResource = (QuestionnaireResponse) domainResource; - String fragmentId = inputResource.getQuestionnaire(); - List containedResource = inputResource.getContained(); - Questionnaire inQuestionnaire = null; + QuestionnaireResponse inputQuestionnaireResponse = (QuestionnaireResponse) domainResource; + String fragmentId = inputQuestionnaireResponse.getQuestionnaire(); + List containedResource = inputQuestionnaireResponse.getContained(); + Questionnaire inputQuestionnaireFromRequest = null; for (int i = 0; i < containedResource.size(); i++) { Resource item = containedResource.get(i); if (item.getResourceType().equals(ResourceType.Questionnaire)) { - Questionnaire inputQuestionnaire = (Questionnaire) item; - if (inputQuestionnaire.getId().equals(fragmentId)) { - inQuestionnaire = inputQuestionnaire; + Questionnaire checkInputQuestionnaire = (Questionnaire) item; + if (checkInputQuestionnaire.getId().equals(fragmentId)) { + inputQuestionnaireFromRequest = checkInputQuestionnaire; break; } } } - if (inQuestionnaire != null) { - // TODO retrieve the questions and set it in the contained Questionnaire - logger.info("--- Get next question for questionnaire " + inQuestionnaire.getId()); - logger.info("---- Get meta profile " + inQuestionnaire.getMeta().getProfile().get(0).getValue()); - - Questionnaire.QuestionnaireItemComponent orderReason = inQuestionnaire.addItem().setLinkId("1"); - orderReason.setText("order Reason"); - orderReason.setType(QuestionnaireItemType.CHOICE); - orderReason.addAnswerOption(new QuestionnaireItemAnswerOptionComponent() - .setValue(new Coding().setCode("Initial or original order for certification"))); - orderReason.addAnswerOption(new QuestionnaireItemAnswerOptionComponent() - .setValue(new Coding().setCode("Change in statue"))); - orderReason.addAnswerOption(new QuestionnaireItemAnswerOptionComponent() - .setValue(new Coding().setCode("Revision or change in equipment"))); - orderReason.addAnswerOption( - new QuestionnaireItemAnswerOptionComponent().setValue(new Coding().setCode("Replacement"))); - - List newContainedList = new ArrayList<>(); - newContainedList.add(inQuestionnaire); - // inputResource.setContained(newContainedList); - inputResource.addContained(inQuestionnaire); - String formattedResourceString = ctx.newJsonParser().encodeResourceToString(inputResource); + if (inputQuestionnaireFromRequest != null) { + + if(questionnaireTree == null){ + + // Import the requested CDS-Library Questionnaire (Couldn't get CDS to work with it, just reading it in it locally for now. In future will need to be pulled from CDS.) + Questionnaire cdsQuestionnaire = null; + try { + cdsQuestionnaire = (Questionnaire) parser.parseResource(Questionnaire.class, new FileReader(new File("/Users/rscalfani/Documents/code/drlsroot/CRD/server/src/main/java/org/hl7/davinci/endpoint/controllers/Questions-HomeOxygenTherapyAdditional.json"))); + logger.info("--- Imported Questionnaire " + cdsQuestionnaire.getId()); + } catch (DataFormatException e) { + e.printStackTrace(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + if(cdsQuestionnaire == null) { + throw new RuntimeException("Requested CDS Questionnaire XXX was not imported and may not exist."); + } + // Pull the first question from the CDS Questionnaire (because we're assuming that there is only one parent question). + QuestionnaireItemComponent currentQuestionItem = cdsQuestionnaire.getItemFirstRep(); + + // Add the first Question item to the contained Questionnaire in the response/request QuestionnaireResponse JSON as part of the response. + inputQuestionnaireFromRequest.addItem(currentQuestionItem); + + // Build the tree and don't expect any answers since we only just received the required questions. + questionnaireTree = new AdaptiveQuestionnaireTree(cdsQuestionnaire); + System.out.println(inputQuestionnaireFromRequest.getItem()); + logger.info("--- Built Questionnaire Tree for " + inputQuestionnaireFromRequest.getId()); + + } else { + // Get the first answer component object from the recieved resource. + QuestionnaireResponseItemAnswerComponent answerComponent = inputQuestionnaireResponse.getItem().get(0).getAnswer().get(0); + // Pull the string response the person gave. + String response = answerComponent.getValueCoding().getCode(); + // Pull the resulting next question that the recieved response points to from the tree. + QuestionnaireItemComponent result = questionnaireTree.getNextQuestionForResponse(response); + // Add the next question to the QuestionnaireResponse.contained[0].item[]. + Questionnaire containedQuestionnaire = (Questionnaire) inputQuestionnaireResponse.getContained().get(0); + containedQuestionnaire.addItem(result); + logger.info("--- Added next question for questionnaire " + inputQuestionnaireFromRequest.getId() + " for response " + response); + // If this question is a leaf node and is the final question, set status to "completed" + if(this.questionnaireTree.isLeafNode(response)){ + inputQuestionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); + logger.info("--- Question leaf node reached, setting status to \"completed\"."); + } + } + logger.info("--- Get next question for questionnaire " + inputQuestionnaireFromRequest.getId()); + logger.info("---- Get meta profile " + inputQuestionnaireFromRequest.getMeta().getProfile().get(0).getValue()); + + // Build and send the response. + String formattedResourceString = ctx.newJsonParser().encodeResourceToString(inputQuestionnaireResponse); return ResponseEntity.status(HttpStatus.ACCEPTED).contentType(MediaType.APPLICATION_JSON) .header(HttpHeaders.CONTENT_TYPE, "application/fhir+json" + "; charset=utf-8") .body(formattedResourceString); @@ -111,4 +236,4 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } } -} +} \ No newline at end of file diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/Questions-HomeOxygenTherapyAdditional.json b/server/src/main/java/org/hl7/davinci/endpoint/controllers/Questions-HomeOxygenTherapyAdditional.json new file mode 100644 index 000000000..6c206db7c --- /dev/null +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/Questions-HomeOxygenTherapyAdditional.json @@ -0,0 +1,89 @@ +{ + "resourceType": "Questionnaire", + "id": "HomeOxygenTherapyAdditional", + "name": "HomeOxygenTherapyAdditional", + "description": "Questions for Home Oxygen Therapy Order Reason", + "meta": { + "profile": [ + "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", + "http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-questionnaire-r4" + ] + }, + "title": "Home Oxygen Therapy Additional Adaptive Form Questionnaire", + "status": "draft", + "subjectType": [ + "Patient" + ], + "date": "2019-03-26", + "publisher": "Da Vinci DTR", + "item": [ + { + "linkId": "1", + "text": "Order Reason", + "type": "choice", + "required": true, + "answerOption": [ + { + "modifierExtension": [ + { + "url": "1.1" + } + ], + "valueCoding": { + "code": "Initial or original order for certification" + } + }, + { + "modifierExtension": [ + { + "url": "1.2" + } + ], + "valueCoding": { + "code": "Change in status" + } + }, + { + "modifierExtension": [ + { + "url": "1.3" + } + ], + "valueCoding": { + "code": "Revision or change in equipment" + } + }, + { + "modifierExtension": [ + { + "url": "1.4" + } + ], + "valueCoding": { + "code": "Replacement" + } + } + ] + }, + { + "linkId": "1.1", + "type": "display", + "text": "Submit the order form" + }, + { + "linkId": "1.4", + "type": "display", + "text": "Replacement Reasoning, etc." + }, + { + "linkId": "1.2", + "type": "display", + "text": "Status Reasoning, Patient relocated, etc." + }, + { + "linkId": "1.3", + "type": "display", + "text": "Revision/Equipment change Reasoning, etc." + } + ] +} \ No newline at end of file From d94c7cfc955cb7c7b99c0414020fac9d1328c907 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Fri, 22 Oct 2021 17:24:07 -0400 Subject: [PATCH 06/30] next-question tree can now have infiinite depth of questions and answer options. --- .../controllers/QuestionnaireController.java | 163 ++++++++++++------ ...Questions-HomeOxygenTherapyAdditional.json | 128 +++++++++++--- 2 files changed, 222 insertions(+), 69 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index f8e600bfa..f9795e339 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; import java.util.logging.Logger; +import java.util.stream.Collectors; + import javax.servlet.http.HttpServletRequest; import org.hl7.fhir.r4.model.Questionnaire; @@ -31,6 +33,7 @@ import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemAnswerOptionComponent; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; // --- ORDER OF RESPONSE-REQUEST OPERATIONS @@ -50,74 +53,132 @@ public class QuestionnaireController { * A class that demos a tree to define next questions based on responses. */ private class AdaptiveQuestionnaireTree { - private NextQuestionNode root; + + // The current question (defiend within the node). + private AdaptiveQuestionnaireNode root; /** - * Constructor. + * Initial constructor that generates the beginning of the tree. * @param inputQuestionnaire The input questionnaire from the CDS-Library. */ public AdaptiveQuestionnaireTree(Questionnaire inputQuestionnaire) { - // Build child nodes. (Does not yet include building out children's child nodes. Assumes only one question with followup questions.) - Map childMap = new HashMap(); - // Map of child question IDs for the first question that map to their associated possible response. - Map childIdsToResponses = new HashMap(); - // This loop iterates over the possible answer options of the first item in the inputQuestionnaire, which is assumed to be the only parent question. - for(QuestionnaireItemAnswerOptionComponent answerOption : inputQuestionnaire.getItemFirstRep().getAnswerOption()) { - // The Id of this answer response's next question. - String answerNextQuestionId = answerOption.getModifierExtensionFirstRep().getUrl(); - // The response that indicates this answer to the question. - String possibleAnswerResponse = answerOption.getValueCoding().getCode(); - System.out.println("LLLL:" + answerNextQuestionId); - System.out.println("AAAA:" + possibleAnswerResponse); - // Add the key-value pair of next question id to its assocated answer response. - childIdsToResponses.put(answerNextQuestionId, possibleAnswerResponse); - } - // Extract the children and add them to their parent question nodes. - for(QuestionnaireItemComponent childQuestion : inputQuestionnaire.getItem()){ - String linkId = childQuestion.getLinkId(); - System.out.println("SSSSS:" + linkId); - System.out.println("QQQQQ:" + childIdsToResponses.keySet()); - if(childIdsToResponses.containsKey(linkId)){ - // This question is a child of the parent question. Add it to the parent's map of children as a key-value pair of response-childQuestion. - childMap.put(childIdsToResponses.get(linkId), childQuestion); - } + // Because of the nested structure of the input JSON, there can be only one super-parent questionitem in the inputQuestionnaire. + if(inputQuestionnaire.getItem().size() != 1){ + throw new RuntimeException("An input Adaptive next-question questionnaire can have only one super-parent question.item, found " + inputQuestionnaire.getItem().size() + "."); } - // Create the root node with its question and children. - root = new NextQuestionNode(inputQuestionnaire, childMap); + + // Top level parent question item. This is also the first question. + QuestionnaireItemComponent topQuestion = inputQuestionnaire.getItemFirstRep(); + + // Start the root building. + this.root = new AdaptiveQuestionnaireNode(topQuestion); } /** - * Returns the next question based on the response to the current question. + * Returns the next question based on the response to the current question. Also sets the next question based on that response. * @param response The response given to this question. * @return */ public QuestionnaireItemComponent getNextQuestionForResponse(String response){ if(!root.children.containsKey(response)){ - throw new NullPointerException("Not a valid response for this question: \'" + response + "\''. Possible responses for this question: \'" + root.children.keySet() + "\''."); + throw new RuntimeException("Not a valid response for question: \'" + this.root.questionItem.getText() + "\' with response \'" + response + "\'. Possible responses for this question: \'" + root.children.keySet() + "\'."); } - return root.children.get(response);//.data; + // Pull the current question. + QuestionnaireItemComponent currentQuestionnaireItem = this.root.getChildForResponse(response).questionItem; + // Set the new next question. + this.root = this.root.getChildForResponse(response); + // Return the prior current question. + return currentQuestionnaireItem; } /** - * Returns whether this is a leaf node (needs work, but is fine for demo purposes). + * Returns whether this has reached a leaf node. * @param response * @return */ - public boolean isLeafNode(String response) { - return !this.getNextQuestionForResponse(response).hasAnswerOption(); + public boolean reachedLeafNode() { + return this.root.isLeafNode(); + } + + /** + * Returns the linkid of the current question. + * @return + */ + public String getCurrentQuestionId() { + return this.root.getQuestionId(); } /** * Inner class that describes a node of the tree. */ - private class NextQuestionNode { - private Questionnaire data; // To be used to contain future answer options for multiple subquestions? - private Map children; + private class AdaptiveQuestionnaireNode { + // Contains the current question of the node. + private QuestionnaireItemComponent questionItem; + // Map of (answerResponse->childQuestionItemNode) (The child could have answer options within it or be a leaf node. It does have a question item component though). + private Map children; + + public AdaptiveQuestionnaireNode(QuestionnaireItemComponent questionItem) { + this.questionItem = questionItem; + + // The number of answer options should always equal the number of subquestion items. + if((this.questionItem.getAnswerOption().size() != this.questionItem.getItem().size())){ + throw new RuntimeException("There should be the same number of answer options as sub-items. Answer options: " + this.questionItem.getAnswerOption().size() + ", Sub-tems: " + this.questionItem.getItem().size()); + } + + Map childIdsToResponses = new HashMap(); + // This loop iterates over the possible answer options of this questionitem and links the linkId to its possible responses. + for(QuestionnaireItemAnswerOptionComponent answerOption : questionItem.getAnswerOption()) { + // The Id of this answer response's next question. + String answerNextQuestionId = answerOption.getModifierExtensionFirstRep().getUrl(); + // The response that indicates this answer to the question. + String possibleAnswerResponse = answerOption.getValueCoding().getCode(); + // Check for issues. + if(answerNextQuestionId == null || possibleAnswerResponse == null){ + throw new RuntimeException("Malformed Adaptive Questionnaire. Missing a questionID or answer response."); + } + // Add the key-value pair of next question id to its assocated answer response. + childIdsToResponses.put(answerNextQuestionId, possibleAnswerResponse); + } + + // Create the map of answerResponses->subQuestionItems + this.children = new HashMap(); + List subQuestionItems = questionItem.getItem(); + for(QuestionnaireItemComponent subQuestionItem : subQuestionItems){ + // SubQuestion linkId. + String subQuestionLinkId = subQuestionItem.getLinkId(); + // SubQuestion's associated response. + String subQuestionResponse = childIdsToResponses.get(subQuestionLinkId); + // Create a new node for this subQuestion. + AdaptiveQuestionnaireNode subQuestionNode = new AdaptiveQuestionnaireNode(subQuestionItem); + this.children.put(subQuestionResponse, subQuestionNode); // Should not be ID, should be response. + } + } + + /** + * Returns whether this questionniare is a leaf node. + * @return + */ + private boolean isLeafNode() { + return this.children.size() < 1; + // return !this.questionItem.hasAnswerOption(); + } + + /** + * Returns the child associated with this node for the given response. + * @param response + * @return + */ + private AdaptiveQuestionnaireNode getChildForResponse(String response) { + return this.children.get(response); + } - public NextQuestionNode(Questionnaire data, Map children) { - this.data = data; - this.children = children; + /** + * Returns the question linkid for this node question. + * @return + */ + public String getQuestionId() { + return this.questionItem.getLinkId(); } } } @@ -191,11 +252,11 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet if(cdsQuestionnaire == null) { throw new RuntimeException("Requested CDS Questionnaire XXX was not imported and may not exist."); } - // Pull the first question from the CDS Questionnaire (because we're assuming that there is only one parent question). - QuestionnaireItemComponent currentQuestionItem = cdsQuestionnaire.getItemFirstRep(); + // Pull the first question from the CDS Questionnaire because it should be the top-level question. + QuestionnaireItemComponent topQuestionItem = cdsQuestionnaire.getItemFirstRep(); // Add the first Question item to the contained Questionnaire in the response/request QuestionnaireResponse JSON as part of the response. - inputQuestionnaireFromRequest.addItem(currentQuestionItem); + inputQuestionnaireFromRequest.addItem(topQuestionItem); // Build the tree and don't expect any answers since we only just received the required questions. questionnaireTree = new AdaptiveQuestionnaireTree(cdsQuestionnaire); @@ -203,18 +264,22 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet logger.info("--- Built Questionnaire Tree for " + inputQuestionnaireFromRequest.getId()); } else { - // Get the first answer component object from the recieved resource. - QuestionnaireResponseItemAnswerComponent answerComponent = inputQuestionnaireResponse.getItem().get(0).getAnswer().get(0); + // Previous question Id + String previousQuestionId = this.questionnaireTree.getCurrentQuestionId(); + // Get the answer component of the item with the previous question id from the recieved resource. + List allQuestions = inputQuestionnaireResponse.getItem(); + allQuestions = allQuestions.stream().filter((QuestionnaireResponseItemComponent item) -> item.getLinkId().equals(previousQuestionId)).collect(Collectors.toList()); + QuestionnaireResponseItemAnswerComponent answerComponent = allQuestions.get(0).getAnswerFirstRep(); // Pull the string response the person gave. String response = answerComponent.getValueCoding().getCode(); // Pull the resulting next question that the recieved response points to from the tree. - QuestionnaireItemComponent result = questionnaireTree.getNextQuestionForResponse(response); + QuestionnaireItemComponent nextQuestionResult = questionnaireTree.getNextQuestionForResponse(response); // Add the next question to the QuestionnaireResponse.contained[0].item[]. Questionnaire containedQuestionnaire = (Questionnaire) inputQuestionnaireResponse.getContained().get(0); - containedQuestionnaire.addItem(result); - logger.info("--- Added next question for questionnaire " + inputQuestionnaireFromRequest.getId() + " for response " + response); + containedQuestionnaire.addItem(nextQuestionResult); + logger.info("--- Added next question for questionnaire \'" + inputQuestionnaireFromRequest.getId() + "\' for response \'" + response + "\''."); // If this question is a leaf node and is the final question, set status to "completed" - if(this.questionnaireTree.isLeafNode(response)){ + if(this.questionnaireTree.reachedLeafNode()){ inputQuestionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); logger.info("--- Question leaf node reached, setting status to \"completed\"."); } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/Questions-HomeOxygenTherapyAdditional.json b/server/src/main/java/org/hl7/davinci/endpoint/controllers/Questions-HomeOxygenTherapyAdditional.json index 6c206db7c..a954b82b5 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/Questions-HomeOxygenTherapyAdditional.json +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/Questions-HomeOxygenTherapyAdditional.json @@ -63,27 +63,115 @@ "code": "Replacement" } } + ], + "item": [ + { + "linkId": "1.1", + "type": "display", + "text": "Submit the order form" + }, + + { + "linkId": "1.2", + "type": "display", + "text": "Status Reasoning, Patient relocated, etc." + }, + { + "linkId": "1.3", + "type": "display", + "text": "Revision/Equipment change Reasoning, etc." + }, + { + "linkId": "1.4", + "text": "Replacement Reason", + "type": "choice", + "required": true, + "answerOption": [ + { + "valueCoding": { + "code": "Lost or stolen" + }, + "modifierExtension": [ + { + "url": "1.4.1" + } + ] + }, + { + "valueCoding": { + "code": "End of lifetime" + }, + "modifierExtension": [ + { + "url": "1.4.2" + } + ] + }, + { + "valueCoding": { + "code": "Repair exceeds 60% of cost" + }, + "modifierExtension": [ + { + "url": "1.4.3" + } + ] + } + ], + "item": [ + { + "linkId": "1.4.1", + "type": "display", + "text": "Your prior auth number is A1234 (lost/stolen response)" + }, + { + "linkId": "1.4.3", + "text": "Your prior auth number is B1234 (repair cost response). How much will it cost?", + "type": "choice", + "required": true, + "answerOption": [ + { + "valueCoding": { + "code": "Greater than $500" + }, + "modifierExtension": [ + { + "url": "1.4.3.1" + } + ] + }, + { + "valueCoding": { + "code": "Less than $500" + }, + "modifierExtension": [ + { + "url": "1.4.3.2" + } + ] + } + ], + "item": [ + { + "linkId": "1.4.3.1", + "type": "display", + "text": "Your response idicated that it will cost more than $500." + }, + { + "linkId": "1.4.3.2", + "type": "display", + "text": "Your response indicated that it will cost less than $500." + } + ] + }, + { + "linkId": "1.4.2", + "type": "display", + "text": "Your prior auth number is B1234 (EOL response)" + } + ] + } ] - }, - { - "linkId": "1.1", - "type": "display", - "text": "Submit the order form" - }, - { - "linkId": "1.4", - "type": "display", - "text": "Replacement Reasoning, etc." - }, - { - "linkId": "1.2", - "type": "display", - "text": "Status Reasoning, Patient relocated, etc." - }, - { - "linkId": "1.3", - "type": "display", - "text": "Revision/Equipment change Reasoning, etc." } ] } \ No newline at end of file From c17006820a5cce757066cef6bd6286bc24832c53 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Mon, 25 Oct 2021 10:26:47 -0400 Subject: [PATCH 07/30] Changed the questionnaire tree to use a static map to track multuple questionnaire options. --- .../controllers/QuestionnaireController.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index f9795e339..308beb0c7 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -50,7 +50,7 @@ public class QuestionnaireController { /** - * A class that demos a tree to define next questions based on responses. + * An inner class that demos a tree to define next questions based on responses. */ private class AdaptiveQuestionnaireTree { @@ -113,6 +113,7 @@ public String getCurrentQuestionId() { * Inner class that describes a node of the tree. */ private class AdaptiveQuestionnaireNode { + // Contains the current question of the node. private QuestionnaireItemComponent questionItem; // Map of (answerResponse->childQuestionItemNode) (The child could have answer options within it or be a leaf node. It does have a question item component though). @@ -185,8 +186,8 @@ public String getQuestionId() { // Logger. private static Logger logger = Logger.getLogger(Application.class.getName()); - // Tree that tracks the questions. - private AdaptiveQuestionnaireTree questionnaireTree; + // Trees that track the current and next questions. Is key-value mappng of: Map AdaptiveQuestionnaireTree> + private static final Map questionnaireTrees = new HashMap(); /** * @@ -237,7 +238,7 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet if (inputQuestionnaireFromRequest != null) { - if(questionnaireTree == null){ + if(!questionnaireTrees.containsKey(inputQuestionnaireFromRequest.getId())){ // Import the requested CDS-Library Questionnaire (Couldn't get CDS to work with it, just reading it in it locally for now. In future will need to be pulled from CDS.) Questionnaire cdsQuestionnaire = null; @@ -259,13 +260,15 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet inputQuestionnaireFromRequest.addItem(topQuestionItem); // Build the tree and don't expect any answers since we only just received the required questions. - questionnaireTree = new AdaptiveQuestionnaireTree(cdsQuestionnaire); - System.out.println(inputQuestionnaireFromRequest.getItem()); - logger.info("--- Built Questionnaire Tree for " + inputQuestionnaireFromRequest.getId()); + AdaptiveQuestionnaireTree newTree = new AdaptiveQuestionnaireTree(cdsQuestionnaire); + questionnaireTrees.put(inputQuestionnaireFromRequest.getId(), newTree); + logger.info("--- Built Questionnaire Tree for " + inputQuestionnaireFromRequest.getId()); } else { + // Pull in the current tree for the requested questionnaire id. + AdaptiveQuestionnaireTree currentTree = questionnaireTrees.get(inputQuestionnaireFromRequest.getId()); // Previous question Id - String previousQuestionId = this.questionnaireTree.getCurrentQuestionId(); + String previousQuestionId = currentTree.getCurrentQuestionId(); // Get the answer component of the item with the previous question id from the recieved resource. List allQuestions = inputQuestionnaireResponse.getItem(); allQuestions = allQuestions.stream().filter((QuestionnaireResponseItemComponent item) -> item.getLinkId().equals(previousQuestionId)).collect(Collectors.toList()); @@ -273,15 +276,15 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet // Pull the string response the person gave. String response = answerComponent.getValueCoding().getCode(); // Pull the resulting next question that the recieved response points to from the tree. - QuestionnaireItemComponent nextQuestionResult = questionnaireTree.getNextQuestionForResponse(response); + QuestionnaireItemComponent nextQuestionResult = currentTree.getNextQuestionForResponse(response); // Add the next question to the QuestionnaireResponse.contained[0].item[]. Questionnaire containedQuestionnaire = (Questionnaire) inputQuestionnaireResponse.getContained().get(0); containedQuestionnaire.addItem(nextQuestionResult); logger.info("--- Added next question for questionnaire \'" + inputQuestionnaireFromRequest.getId() + "\' for response \'" + response + "\''."); // If this question is a leaf node and is the final question, set status to "completed" - if(this.questionnaireTree.reachedLeafNode()){ + if (currentTree.reachedLeafNode()) { inputQuestionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); - logger.info("--- Question leaf node reached, setting status to \"completed\"."); + logger.info("--- Questionnaire leaf node reached, setting status to \"completed\"."); } } From 763ec63d357ee79c6204061f77748214060e1176 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Mon, 25 Oct 2021 11:09:35 -0400 Subject: [PATCH 08/30] Removed children display in response JSON for question items --- .../controllers/QuestionnaireController.java | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index 308beb0c7..b55888d58 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -114,7 +114,7 @@ public String getCurrentQuestionId() { */ private class AdaptiveQuestionnaireNode { - // Contains the current question of the node. + // Contains the current question item of the node. private QuestionnaireItemComponent questionItem; // Map of (answerResponse->childQuestionItemNode) (The child could have answer options within it or be a leaf node. It does have a question item component though). private Map children; @@ -152,7 +152,7 @@ public AdaptiveQuestionnaireNode(QuestionnaireItemComponent questionItem) { String subQuestionResponse = childIdsToResponses.get(subQuestionLinkId); // Create a new node for this subQuestion. AdaptiveQuestionnaireNode subQuestionNode = new AdaptiveQuestionnaireNode(subQuestionItem); - this.children.put(subQuestionResponse, subQuestionNode); // Should not be ID, should be response. + this.children.put(subQuestionResponse, subQuestionNode); } } @@ -238,7 +238,7 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet if (inputQuestionnaireFromRequest != null) { - if(!questionnaireTrees.containsKey(inputQuestionnaireFromRequest.getId())){ + if(!questionnaireTrees.containsKey(inputQuestionnaireFromRequest.getId() /** || item.isempty() */)){ // Import the requested CDS-Library Questionnaire (Couldn't get CDS to work with it, just reading it in it locally for now. In future will need to be pulled from CDS.) Questionnaire cdsQuestionnaire = null; @@ -253,9 +253,9 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet if(cdsQuestionnaire == null) { throw new RuntimeException("Requested CDS Questionnaire XXX was not imported and may not exist."); } - // Pull the first question from the CDS Questionnaire because it should be the top-level question. + // Pull the first question from the CDS Questionnaire because it should be the top-level question (and the only item in the list). QuestionnaireItemComponent topQuestionItem = cdsQuestionnaire.getItemFirstRep(); - + topQuestionItem = removeChildrenFromQuestionItem(topQuestionItem); // Add the first Question item to the contained Questionnaire in the response/request QuestionnaireResponse JSON as part of the response. inputQuestionnaireFromRequest.addItem(topQuestionItem); @@ -275,11 +275,13 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet QuestionnaireResponseItemAnswerComponent answerComponent = allQuestions.get(0).getAnswerFirstRep(); // Pull the string response the person gave. String response = answerComponent.getValueCoding().getCode(); - // Pull the resulting next question that the recieved response points to from the tree. - QuestionnaireItemComponent nextQuestionResult = currentTree.getNextQuestionForResponse(response); + // Pull the resulting next question that the recieved response points to from the tree without including its children. + QuestionnaireItemComponent nextQuestionItemResult = currentTree.getNextQuestionForResponse(response); + nextQuestionItemResult = removeChildrenFromQuestionItem(nextQuestionItemResult); + // Add the next question to the QuestionnaireResponse.contained[0].item[]. Questionnaire containedQuestionnaire = (Questionnaire) inputQuestionnaireResponse.getContained().get(0); - containedQuestionnaire.addItem(nextQuestionResult); + containedQuestionnaire.addItem(nextQuestionItemResult); logger.info("--- Added next question for questionnaire \'" + inputQuestionnaireFromRequest.getId() + "\' for response \'" + response + "\''."); // If this question is a leaf node and is the final question, set status to "completed" if (currentTree.reachedLeafNode()) { @@ -299,9 +301,28 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } else { return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON) .header(HttpHeaders.CONTENT_TYPE, "application/fhir+json" + "; charset=utf-8") - .body("Invalid input questionnaire"); + .body("Invalid input questionnaire does not exist"); } } } + + /** + * Returns a new question item that is indentical to the input qusetion item except without the children. + * @param inputQuestionItem + * @return + */ + private static QuestionnaireItemComponent removeChildrenFromQuestionItem(QuestionnaireItemComponent inputQuestionItem){ + QuestionnaireItemComponent questionItemNoChildren = new QuestionnaireItemComponent(); + questionItemNoChildren.setLinkId(inputQuestionItem.getLinkId()); + questionItemNoChildren.setText(inputQuestionItem.getText()); + questionItemNoChildren.setType(inputQuestionItem.getType()); + questionItemNoChildren.setRequired(inputQuestionItem.getRequired()); + questionItemNoChildren.setAnswerOption(inputQuestionItem.getAnswerOption()); + + // Can't just remove the children because then that would alter the original object in the tree. + // questionItemNoChildren.setItem(null); + + return questionItemNoChildren; + } } \ No newline at end of file From c033a5e8bbcda4ef91064242a019d1a3d82208e5 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Mon, 25 Oct 2021 11:20:12 -0400 Subject: [PATCH 09/30] Made it so that sending a response with no answer items will reset that questionnaire's next-question path to the initial form. --- .../controllers/QuestionnaireController.java | 77 ++++++++++++++++--- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index b55888d58..ead2570f1 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -236,9 +236,16 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } } + String questionnaireId = inputQuestionnaireFromRequest.getId(); + if (inputQuestionnaireFromRequest != null) { - if(!questionnaireTrees.containsKey(inputQuestionnaireFromRequest.getId() /** || item.isempty() */)){ + // If there are no item answer responses in the sent JSON, reset the tree so that we can restart the question process. + if(inputQuestionnaireFromRequest.getItem().size() < 1) { + questionnaireTrees.remove(questionnaireId); + } + + if(!questionnaireTrees.containsKey(questionnaireId)){ // Import the requested CDS-Library Questionnaire (Couldn't get CDS to work with it, just reading it in it locally for now. In future will need to be pulled from CDS.) Questionnaire cdsQuestionnaire = null; @@ -251,7 +258,7 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet e.printStackTrace(); } if(cdsQuestionnaire == null) { - throw new RuntimeException("Requested CDS Questionnaire XXX was not imported and may not exist."); + throw new RuntimeException("Requested CDS Questionnaire \'" + questionnaireId + "\' was not imported and may not exist."); } // Pull the first question from the CDS Questionnaire because it should be the top-level question (and the only item in the list). QuestionnaireItemComponent topQuestionItem = cdsQuestionnaire.getItemFirstRep(); @@ -261,12 +268,12 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet // Build the tree and don't expect any answers since we only just received the required questions. AdaptiveQuestionnaireTree newTree = new AdaptiveQuestionnaireTree(cdsQuestionnaire); - questionnaireTrees.put(inputQuestionnaireFromRequest.getId(), newTree); + questionnaireTrees.put(questionnaireId, newTree); - logger.info("--- Built Questionnaire Tree for " + inputQuestionnaireFromRequest.getId()); + logger.info("--- Built Questionnaire Tree for " + questionnaireId); } else { // Pull in the current tree for the requested questionnaire id. - AdaptiveQuestionnaireTree currentTree = questionnaireTrees.get(inputQuestionnaireFromRequest.getId()); + AdaptiveQuestionnaireTree currentTree = questionnaireTrees.get(questionnaireId); // Previous question Id String previousQuestionId = currentTree.getCurrentQuestionId(); // Get the answer component of the item with the previous question id from the recieved resource. @@ -282,15 +289,15 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet // Add the next question to the QuestionnaireResponse.contained[0].item[]. Questionnaire containedQuestionnaire = (Questionnaire) inputQuestionnaireResponse.getContained().get(0); containedQuestionnaire.addItem(nextQuestionItemResult); - logger.info("--- Added next question for questionnaire \'" + inputQuestionnaireFromRequest.getId() + "\' for response \'" + response + "\''."); - // If this question is a leaf node and is the final question, set status to "completed" + logger.info("--- Added next question for questionnaire \'" + questionnaireId + "\' for response \'" + response + "\'."); + // If this question is a leaf node and is the final question, set the status to "completed" if (currentTree.reachedLeafNode()) { inputQuestionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); logger.info("--- Questionnaire leaf node reached, setting status to \"completed\"."); } } - logger.info("--- Get next question for questionnaire " + inputQuestionnaireFromRequest.getId()); + logger.info("--- Get next question for questionnaire " + questionnaireId); logger.info("---- Get meta profile " + inputQuestionnaireFromRequest.getMeta().getProfile().get(0).getValue()); // Build and send the response. @@ -319,10 +326,58 @@ private static QuestionnaireItemComponent removeChildrenFromQuestionItem(Questio questionItemNoChildren.setType(inputQuestionItem.getType()); questionItemNoChildren.setRequired(inputQuestionItem.getRequired()); questionItemNoChildren.setAnswerOption(inputQuestionItem.getAnswerOption()); - // Can't just remove the children because then that would alter the original object in the tree. // questionItemNoChildren.setItem(null); - return questionItemNoChildren; } -} \ No newline at end of file +} + + +// --- QuestionniareResponse inital request format: +// { +// "resourceType": "QuestionnaireResponse", +// "meta": { +// "profile": [ +// "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse-adapt" +// ] +// }, +// "contained": [ +// { +// "resourceType": "Questionnaire", +// "id": "HomeOxygenTherapyAdditional", +// "meta": { +// "profile": [ +// "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-adapt" +// ] +// }, +// "url": "http://example.com/questionnaire/adaptive-form#HomeOxygenTherapyAdditional", +// "item": [] +// } +// ], +// "extension": [ +// { +// "url": "http://hl7.org/fhir/StructureDefinition/contained-id", +// "valueReference": { +// "reference": "#HomeOxygenTherapyAdditional" +// } +// } +// ], +// "questionnaire": "#HomeOxygenTherapyAdditional", +// "status": "in-progress", +// "item": [] +// } + +// --- Answer item format: +// "item": [ +// { +// "linkId": "1", +// "text": "Order Reason", +// "answer": [ +// { +// "valueCoding": { +// "code": "Replacement" +// } +// } +// ] +// } +// ] \ No newline at end of file From 3f169c554a374e9ab6b1c8b0b8ff4e42eef8560f Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Mon, 25 Oct 2021 14:19:24 -0400 Subject: [PATCH 10/30] Retrieve the Questions file from the FileStore. --- .../controllers/QuestionnaireController.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index ead2570f1..7e7bd00e8 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -1,7 +1,10 @@ package org.hl7.davinci.endpoint.controllers; import org.hl7.davinci.endpoint.Application; +import org.hl7.davinci.endpoint.files.FileResource; +import org.hl7.davinci.endpoint.files.FileStore; import org.hl7.fhir.instance.model.api.IDomainResource; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -18,6 +21,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; +import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -49,6 +53,10 @@ @RequestMapping("/Questionnaire") public class QuestionnaireController { + + @Autowired + private FileStore fileStore; + /** * An inner class that demos a tree to define next questions based on responses. */ @@ -250,12 +258,17 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet // Import the requested CDS-Library Questionnaire (Couldn't get CDS to work with it, just reading it in it locally for now. In future will need to be pulled from CDS.) Questionnaire cdsQuestionnaire = null; try { - cdsQuestionnaire = (Questionnaire) parser.parseResource(Questionnaire.class, new FileReader(new File("/Users/rscalfani/Documents/code/drlsroot/CRD/server/src/main/java/org/hl7/davinci/endpoint/controllers/Questions-HomeOxygenTherapyAdditional.json"))); + //TODO: need to determine topic, filename, and fhir version without having them hard coded + // File is pulled from the file store + FileResource fileResource = fileStore.getFile("HomeOxygenTherapy", "Questions-HomeOxygenTherapyAdditional.json", "R4", false); + cdsQuestionnaire = (Questionnaire) parser.parseResource(fileResource.getResource().getInputStream()); logger.info("--- Imported Questionnaire " + cdsQuestionnaire.getId()); } catch (DataFormatException e) { e.printStackTrace(); } catch (FileNotFoundException e) { e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); } if(cdsQuestionnaire == null) { throw new RuntimeException("Requested CDS Questionnaire \'" + questionnaireId + "\' was not imported and may not exist."); From 85a93c06e2cbab69fed603c6bb14f50f6be91e4a Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Mon, 25 Oct 2021 16:41:39 -0400 Subject: [PATCH 11/30] Fix Java 8 build error. --- .../davinci/endpoint/cdshooks/services/crd/CdsService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java index 327fc1399..3c4051ba4 100755 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java @@ -1,5 +1,6 @@ package org.hl7.davinci.endpoint.cdshooks.services.crd; +import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; @@ -351,7 +352,11 @@ private Link smartLinkBuilder(String patientId, String fhirBase, URL application appContext = appContext + "&filepath=" + applicationBaseUrl + "/"; if (myConfig.getUrlEncodeAppContext()) { logger.info("CdsService::smartLinkBuilder: URL encoding appcontext"); - appContext = URLEncoder.encode(appContext, StandardCharsets.UTF_8).toString(); + try { + appContext = URLEncoder.encode(appContext, StandardCharsets.UTF_8.name()).toString(); + } catch (UnsupportedEncodingException e) { + logger.error("CdsService::smartLinkBuilder: failed to encode URL: " + e.getMessage()); + } } logger.info("smarLinkBuilder: appContext: " + appContext); From c2049d1897007033ff241235eb2e6b40ad201999 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Thu, 28 Oct 2021 16:49:25 -0400 Subject: [PATCH 12/30] Cleanup. --- .../controllers/QuestionnaireController.java | 82 ++++--------------- 1 file changed, 15 insertions(+), 67 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index 7e7bd00e8..7fd1f0a63 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -18,9 +18,7 @@ import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; -import java.io.File; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.util.HashMap; import java.util.List; @@ -127,13 +125,17 @@ private class AdaptiveQuestionnaireNode { // Map of (answerResponse->childQuestionItemNode) (The child could have answer options within it or be a leaf node. It does have a question item component though). private Map children; + /** + * Constructor + * @param questionItem + */ public AdaptiveQuestionnaireNode(QuestionnaireItemComponent questionItem) { this.questionItem = questionItem; // The number of answer options should always equal the number of subquestion items. if((this.questionItem.getAnswerOption().size() != this.questionItem.getItem().size())){ - throw new RuntimeException("There should be the same number of answer options as sub-items. Answer options: " + this.questionItem.getAnswerOption().size() + ", Sub-tems: " + this.questionItem.getItem().size()); - } + throw new RuntimeException("There should be the same number of answer options as sub-items. Answer options: " + this.questionItem.getAnswerOption().size() + ", sub-items: " + this.questionItem.getItem().size()); + } Map childIdsToResponses = new HashMap(); // This loop iterates over the possible answer options of this questionitem and links the linkId to its possible responses. @@ -170,7 +172,6 @@ public AdaptiveQuestionnaireNode(QuestionnaireItemComponent questionItem) { */ private boolean isLeafNode() { return this.children.size() < 1; - // return !this.questionItem.hasAnswerOption(); } /** @@ -209,7 +210,7 @@ public ResponseEntity retrieveNextQuestion(HttpServletRequest request, H } /** - * + * * @param body * @param request * @return @@ -254,8 +255,8 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } if(!questionnaireTrees.containsKey(questionnaireId)){ - - // Import the requested CDS-Library Questionnaire (Couldn't get CDS to work with it, just reading it in it locally for now. In future will need to be pulled from CDS.) + // If there is not already a tree that matches the requested questionnaire id, build it. + // Import the requested CDS-Library Questionnaire. Questionnaire cdsQuestionnaire = null; try { //TODO: need to determine topic, filename, and fhir version without having them hard coded @@ -282,14 +283,14 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet // Build the tree and don't expect any answers since we only just received the required questions. AdaptiveQuestionnaireTree newTree = new AdaptiveQuestionnaireTree(cdsQuestionnaire); questionnaireTrees.put(questionnaireId, newTree); - logger.info("--- Built Questionnaire Tree for " + questionnaireId); } else { - // Pull in the current tree for the requested questionnaire id. + // If there is already a tree with the requested questionnaire id, execute next-question on it with the new request. + // Pull the current tree for the requested questionnaire id. AdaptiveQuestionnaireTree currentTree = questionnaireTrees.get(questionnaireId); - // Previous question Id + // Get the previous question Id. String previousQuestionId = currentTree.getCurrentQuestionId(); - // Get the answer component of the item with the previous question id from the recieved resource. + // Get the request's answer component of the item with the previous question id. List allQuestions = inputQuestionnaireResponse.getItem(); allQuestions = allQuestions.stream().filter((QuestionnaireResponseItemComponent item) -> item.getLinkId().equals(previousQuestionId)).collect(Collectors.toList()); QuestionnaireResponseItemAnswerComponent answerComponent = allQuestions.get(0).getAnswerFirstRep(); @@ -298,11 +299,11 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet // Pull the resulting next question that the recieved response points to from the tree without including its children. QuestionnaireItemComponent nextQuestionItemResult = currentTree.getNextQuestionForResponse(response); nextQuestionItemResult = removeChildrenFromQuestionItem(nextQuestionItemResult); - // Add the next question to the QuestionnaireResponse.contained[0].item[]. Questionnaire containedQuestionnaire = (Questionnaire) inputQuestionnaireResponse.getContained().get(0); containedQuestionnaire.addItem(nextQuestionItemResult); logger.info("--- Added next question for questionnaire \'" + questionnaireId + "\' for response \'" + response + "\'."); + // If this question is a leaf node and is the final question, set the status to "completed" if (currentTree.reachedLeafNode()) { inputQuestionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); @@ -310,7 +311,6 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } } - logger.info("--- Get next question for questionnaire " + questionnaireId); logger.info("---- Get meta profile " + inputQuestionnaireFromRequest.getMeta().getProfile().get(0).getValue()); // Build and send the response. @@ -339,58 +339,6 @@ private static QuestionnaireItemComponent removeChildrenFromQuestionItem(Questio questionItemNoChildren.setType(inputQuestionItem.getType()); questionItemNoChildren.setRequired(inputQuestionItem.getRequired()); questionItemNoChildren.setAnswerOption(inputQuestionItem.getAnswerOption()); - // Can't just remove the children because then that would alter the original object in the tree. - // questionItemNoChildren.setItem(null); return questionItemNoChildren; } -} - - -// --- QuestionniareResponse inital request format: -// { -// "resourceType": "QuestionnaireResponse", -// "meta": { -// "profile": [ -// "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaireresponse-adapt" -// ] -// }, -// "contained": [ -// { -// "resourceType": "Questionnaire", -// "id": "HomeOxygenTherapyAdditional", -// "meta": { -// "profile": [ -// "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-adapt" -// ] -// }, -// "url": "http://example.com/questionnaire/adaptive-form#HomeOxygenTherapyAdditional", -// "item": [] -// } -// ], -// "extension": [ -// { -// "url": "http://hl7.org/fhir/StructureDefinition/contained-id", -// "valueReference": { -// "reference": "#HomeOxygenTherapyAdditional" -// } -// } -// ], -// "questionnaire": "#HomeOxygenTherapyAdditional", -// "status": "in-progress", -// "item": [] -// } - -// --- Answer item format: -// "item": [ -// { -// "linkId": "1", -// "text": "Order Reason", -// "answer": [ -// { -// "valueCoding": { -// "code": "Replacement" -// } -// } -// ] -// } -// ] \ No newline at end of file +} \ No newline at end of file From bade0a0dbc2f03d3cbf85d02407a09684a3e6c52 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Mon, 1 Nov 2021 17:24:29 -0400 Subject: [PATCH 13/30] Added funcionality for an adaptive questionnaire to return multiple questions for a request. --- .../controllers/QuestionnaireController.java | 228 +++++++++++------- 1 file changed, 146 insertions(+), 82 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index 7fd1f0a63..c1ad0a5c2 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -20,6 +20,7 @@ import java.io.FileNotFoundException; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -51,7 +52,6 @@ @RequestMapping("/Questionnaire") public class QuestionnaireController { - @Autowired private FileStore fileStore; @@ -69,16 +69,11 @@ private class AdaptiveQuestionnaireTree { */ public AdaptiveQuestionnaireTree(Questionnaire inputQuestionnaire) { - // Because of the nested structure of the input JSON, there can be only one super-parent questionitem in the inputQuestionnaire. - if(inputQuestionnaire.getItem().size() != 1){ - throw new RuntimeException("An input Adaptive next-question questionnaire can have only one super-parent question.item, found " + inputQuestionnaire.getItem().size() + "."); - } - - // Top level parent question item. This is also the first question. - QuestionnaireItemComponent topQuestion = inputQuestionnaire.getItemFirstRep(); + // Top level parent question item, the first question page. + QuestionnaireItemComponent topLevelQuestion = inputQuestionnaire.getItemFirstRep(); // Start the root building. - this.root = new AdaptiveQuestionnaireNode(topQuestion); + this.root = new AdaptiveQuestionnaireNode(topLevelQuestion); } /** @@ -86,16 +81,20 @@ public AdaptiveQuestionnaireTree(Questionnaire inputQuestionnaire) { * @param response The response given to this question. * @return */ - public QuestionnaireItemComponent getNextQuestionForResponse(String response){ - if(!root.children.containsKey(response)){ - throw new RuntimeException("Not a valid response for question: \'" + this.root.questionItem.getText() + "\' with response \'" + response + "\'. Possible responses for this question: \'" + root.children.keySet() + "\'."); + public List getNextQuestionsFromResponse(String response){ + + if(!root.hasResponse(response)){ + throw new RuntimeException("Not a valid response for question: \'" + this.root.determinantQuestionItem.getText() + "\' with response \'" + response + "\'. Possible responses for this question: \'" + root.children.keySet() + "\'."); } - // Pull the current question. - QuestionnaireItemComponent currentQuestionnaireItem = this.root.getChildForResponse(response).questionItem; + AdaptiveQuestionnaireNode nextQuestionSetNode = this.root.getChildForResponse(response); + // Pull the current question set. + List currentQuestionnaireItems = nextQuestionSetNode.getQuestionSet(); // Set the new next question. - this.root = this.root.getChildForResponse(response); + this.root = nextQuestionSetNode; // Return the prior current question. - return currentQuestionnaireItem; + + currentQuestionnaireItems.get(0).getModifierExtensionFirstRep().getValue(); + return currentQuestionnaireItems; } /** @@ -111,8 +110,8 @@ public boolean reachedLeafNode() { * Returns the linkid of the current question. * @return */ - public String getCurrentQuestionId() { - return this.root.getQuestionId(); + public String getCurrentDeterminantQuestionId() { + return this.root.getDeterminantQuestionId(); } /** @@ -120,49 +119,124 @@ public String getCurrentQuestionId() { */ private class AdaptiveQuestionnaireNode { - // Contains the current question item of the node. - private QuestionnaireItemComponent questionItem; + // Contains the list of additional questions that should be displayed with this question. + private List supplementalQuestions; + // Contains the current question item that dictates the next question of the node. + private QuestionnaireItemComponent determinantQuestionItem; // Map of (answerResponse->childQuestionItemNode) (The child could have answer options within it or be a leaf node. It does have a question item component though). private Map children; /** * Constructor - * @param questionItem + * @param determinantQuestionItem */ - public AdaptiveQuestionnaireNode(QuestionnaireItemComponent questionItem) { - this.questionItem = questionItem; - - // The number of answer options should always equal the number of subquestion items. - if((this.questionItem.getAnswerOption().size() != this.questionItem.getItem().size())){ - throw new RuntimeException("There should be the same number of answer options as sub-items. Answer options: " + this.questionItem.getAnswerOption().size() + ", sub-items: " + this.questionItem.getItem().size()); + public AdaptiveQuestionnaireNode(QuestionnaireItemComponent determinantQuestion) { + + this.determinantQuestionItem = determinantQuestion; + // Get the child and supplemental question items of this question. + List subQuestions = determinantQuestion.getItem(); + // Extract the supplemental questions which do not have a child link-id branch from the determinant questions. + List nonSupplementLinkIds = determinantQuestionItem.getAnswerOption().stream().map(answerOption -> answerOption.getModifierExtensionFirstRep().getUrl()).collect(Collectors.toList()); + List childQuestions = this.extractChildQuestions(subQuestions, nonSupplementLinkIds); + // Extract the remaining questions as supplemental questions. + this.supplementalQuestions = this.extractSupplementalQuestions(subQuestions, nonSupplementLinkIds); + + // The number of answer options of the determinant question should always equal the number of child question items. + if((this.determinantQuestionItem.getAnswerOption().size() != childQuestions.size())){ + throw new RuntimeException("There should be the same number of answer options as sub-items. Answer options: " + this.determinantQuestionItem.getAnswerOption().size() + ", sub-items: " + childQuestions.size()); } - Map childIdsToResponses = new HashMap(); - // This loop iterates over the possible answer options of this questionitem and links the linkId to its possible responses. - for(QuestionnaireItemAnswerOptionComponent answerOption : questionItem.getAnswerOption()) { - // The Id of this answer response's next question. - String answerNextQuestionId = answerOption.getModifierExtensionFirstRep().getUrl(); - // The response that indicates this answer to the question. - String possibleAnswerResponse = answerOption.getValueCoding().getCode(); - // Check for issues. - if(answerNextQuestionId == null || possibleAnswerResponse == null){ - throw new RuntimeException("Malformed Adaptive Questionnaire. Missing a questionID or answer response."); + // If the determinant question item does not have any answer options, then this is a leaf node and should not generate any children. + if(determinantQuestionItem.hasAnswerOption()) { + Map childIdsToResponses = new HashMap(); + // This loop iterates over the possible answer options of this questionitem and links the linkId to its possible responses. + for(QuestionnaireItemAnswerOptionComponent answerOption : determinantQuestionItem.getAnswerOption()) { + // The Id of this answer response's next question. + String answerNextQuestionId = answerOption.getModifierExtensionFirstRep().getUrl(); + // The response that indicates this answer to the question. + String possibleAnswerResponse = answerOption.getValueCoding().getCode(); + // Check for issues. + if(answerNextQuestionId == null || possibleAnswerResponse == null){ + throw new RuntimeException("Malformed Adaptive Questionnaire. Missing a questionID or answer response."); + } + // Add the key-value pair of next question id to its assocated answer response. + childIdsToResponses.put(answerNextQuestionId, possibleAnswerResponse); + } + + // Create the map of answerResponses->subQuestionItems + this.children = new HashMap(); + List subQuestionItems = determinantQuestionItem.getItem(); + for(QuestionnaireItemComponent subQuestionItem : subQuestionItems){ + // SubQuestion linkId. + String subQuestionLinkId = subQuestionItem.getLinkId(); + // SubQuestion's associated response. + String subQuestionResponse = childIdsToResponses.get(subQuestionLinkId); + // Create a new node for this subQuestion. + AdaptiveQuestionnaireNode subQuestionNode = new AdaptiveQuestionnaireNode(subQuestionItem); + this.children.put(subQuestionResponse, subQuestionNode); } - // Add the key-value pair of next question id to its assocated answer response. - childIdsToResponses.put(answerNextQuestionId, possibleAnswerResponse); } + } + + /** + * Returns the question items in the given list that do not have the linkids of the given list of strings. + * @param questionItems + * @param nonSupplementQuestions + * @return + */ + private List extractSupplementalQuestions( + List questionItems, List nonSupplementLinkIds) { + return questionItems.stream().filter(questionItem -> !nonSupplementLinkIds.contains(questionItem.getLinkId())).collect(Collectors.toList()); + } + + /** + * Returns the question items in the given list that do have the linkids of the given list of strings. + * @param questionItems + * @param nonSupplementQuestions + * @return + */ + private List extractChildQuestions( + List questionItems, List nonSupplementLinkIds) { + return questionItems.stream().filter(questionItem -> nonSupplementLinkIds.contains(questionItem.getLinkId())).collect(Collectors.toList()); + } + + /** + * Returns the set of questions associated with the node. Incldues all questions in the set, determinant and non-determinant. + * @return + */ + public List getQuestionSet() { + QuestionnaireItemComponent determinantQuestionNoChildren = this.removeChildrenFromQuestionItem(this.determinantQuestionItem); + List questionSet = new ArrayList(); + questionSet.add(determinantQuestionNoChildren); + questionSet.addAll(this.supplementalQuestions); + return questionSet; + } - // Create the map of answerResponses->subQuestionItems - this.children = new HashMap(); - List subQuestionItems = questionItem.getItem(); - for(QuestionnaireItemComponent subQuestionItem : subQuestionItems){ - // SubQuestion linkId. - String subQuestionLinkId = subQuestionItem.getLinkId(); - // SubQuestion's associated response. - String subQuestionResponse = childIdsToResponses.get(subQuestionLinkId); - // Create a new node for this subQuestion. - AdaptiveQuestionnaireNode subQuestionNode = new AdaptiveQuestionnaireNode(subQuestionItem); - this.children.put(subQuestionResponse, subQuestionNode); + /** + * Returns a new question item that is indentical to the input qusetion item except without the children. + * @param inputQuestionItem + * @return + */ + private QuestionnaireItemComponent removeChildrenFromQuestionItem(QuestionnaireItemComponent inputQuestionItem){ + QuestionnaireItemComponent questionItemNoChildren = new QuestionnaireItemComponent(); + questionItemNoChildren.setLinkId(inputQuestionItem.getLinkId()); + questionItemNoChildren.setText(inputQuestionItem.getText()); + questionItemNoChildren.setType(inputQuestionItem.getType()); + questionItemNoChildren.setRequired(inputQuestionItem.getRequired()); + questionItemNoChildren.setAnswerOption(inputQuestionItem.getAnswerOption()); + return questionItemNoChildren; + } + + /** + * Returns whether the determinant question contains the requested response. + * @param response + * @return + */ + public boolean hasResponse(String response) { + try { + return this.children.containsKey(response); + } catch (NullPointerException e) { + throw new NullPointerException("Null pointer thrown for children " + this.children + " with response " + response); } } @@ -171,7 +245,7 @@ public AdaptiveQuestionnaireNode(QuestionnaireItemComponent questionItem) { * @return */ private boolean isLeafNode() { - return this.children.size() < 1; + return this.children == null || this.children.size() < 1; } /** @@ -187,10 +261,14 @@ private AdaptiveQuestionnaireNode getChildForResponse(String response) { * Returns the question linkid for this node question. * @return */ - public String getQuestionId() { - return this.questionItem.getLinkId(); + public String getDeterminantQuestionId() { + return this.determinantQuestionItem.getLinkId(); } } + + public List getCurrentQuestionSet() { + return this.root.getQuestionSet(); + } } // Logger. @@ -199,7 +277,7 @@ public String getQuestionId() { private static final Map questionnaireTrees = new HashMap(); /** - * + * Retrieves the next question based on the request. * @param request * @param entity * @return @@ -210,7 +288,7 @@ public ResponseEntity retrieveNextQuestion(HttpServletRequest request, H } /** - * + * Returns the next question based on the request. * @param body * @param request * @return @@ -274,35 +352,36 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet if(cdsQuestionnaire == null) { throw new RuntimeException("Requested CDS Questionnaire \'" + questionnaireId + "\' was not imported and may not exist."); } - // Pull the first question from the CDS Questionnaire because it should be the top-level question (and the only item in the list). - QuestionnaireItemComponent topQuestionItem = cdsQuestionnaire.getItemFirstRep(); - topQuestionItem = removeChildrenFromQuestionItem(topQuestionItem); - // Add the first Question item to the contained Questionnaire in the response/request QuestionnaireResponse JSON as part of the response. - inputQuestionnaireFromRequest.addItem(topQuestionItem); // Build the tree and don't expect any answers since we only just received the required questions. AdaptiveQuestionnaireTree newTree = new AdaptiveQuestionnaireTree(cdsQuestionnaire); questionnaireTrees.put(questionnaireId, newTree); logger.info("--- Built Questionnaire Tree for " + questionnaireId); + + List questionSet = newTree.getCurrentQuestionSet(); + final Questionnaire tempInputQuestionnaireFromRequest = inputQuestionnaireFromRequest; + questionSet.forEach(questionItem -> tempInputQuestionnaireFromRequest.addItem(questionItem)); } else { // If there is already a tree with the requested questionnaire id, execute next-question on it with the new request. // Pull the current tree for the requested questionnaire id. AdaptiveQuestionnaireTree currentTree = questionnaireTrees.get(questionnaireId); // Get the previous question Id. - String previousQuestionId = currentTree.getCurrentQuestionId(); + String previousQuestionId = currentTree.getCurrentDeterminantQuestionId(); // Get the request's answer component of the item with the previous question id. List allQuestions = inputQuestionnaireResponse.getItem(); - allQuestions = allQuestions.stream().filter((QuestionnaireResponseItemComponent item) -> item.getLinkId().equals(previousQuestionId)).collect(Collectors.toList()); + allQuestions = allQuestions.stream().filter(item -> item.getLinkId().equals(previousQuestionId)).collect(Collectors.toList()); + if(allQuestions.size() < 1) { + throw new RuntimeException("No given answers in the request references the current question asked. Current Question ID: " + previousQuestionId + "."); + } QuestionnaireResponseItemAnswerComponent answerComponent = allQuestions.get(0).getAnswerFirstRep(); // Pull the string response the person gave. String response = answerComponent.getValueCoding().getCode(); // Pull the resulting next question that the recieved response points to from the tree without including its children. - QuestionnaireItemComponent nextQuestionItemResult = currentTree.getNextQuestionForResponse(response); - nextQuestionItemResult = removeChildrenFromQuestionItem(nextQuestionItemResult); - // Add the next question to the QuestionnaireResponse.contained[0].item[]. + List nextQuestionsItemResult = currentTree.getNextQuestionsFromResponse(response); + // Add the next question set to the QuestionnaireResponse.contained[0].item[]. Questionnaire containedQuestionnaire = (Questionnaire) inputQuestionnaireResponse.getContained().get(0); - containedQuestionnaire.addItem(nextQuestionItemResult); - logger.info("--- Added next question for questionnaire \'" + questionnaireId + "\' for response \'" + response + "\'."); + nextQuestionsItemResult.forEach(questionItem -> containedQuestionnaire.addItem(questionItem)); + logger.info("--- Added next question set for questionnaire \'" + questionnaireId + "\' for response \'" + response + "\'."); // If this question is a leaf node and is the final question, set the status to "completed" if (currentTree.reachedLeafNode()) { @@ -326,19 +405,4 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } } - - /** - * Returns a new question item that is indentical to the input qusetion item except without the children. - * @param inputQuestionItem - * @return - */ - private static QuestionnaireItemComponent removeChildrenFromQuestionItem(QuestionnaireItemComponent inputQuestionItem){ - QuestionnaireItemComponent questionItemNoChildren = new QuestionnaireItemComponent(); - questionItemNoChildren.setLinkId(inputQuestionItem.getLinkId()); - questionItemNoChildren.setText(inputQuestionItem.getText()); - questionItemNoChildren.setType(inputQuestionItem.getType()); - questionItemNoChildren.setRequired(inputQuestionItem.getRequired()); - questionItemNoChildren.setAnswerOption(inputQuestionItem.getAnswerOption()); - return questionItemNoChildren; - } } \ No newline at end of file From d9d40414e87c638c714c42666ab69db24baeea5e Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Wed, 10 Nov 2021 14:00:40 -0500 Subject: [PATCH 14/30] Added multiple questions to be sent based on current next-question. Updated CDS-Library extraction. --- .../controllers/QuestionnaireController.java | 43 +++++++++++++------ .../endpoint/files/CommonFileStore.java | 8 +++- .../files/SubQuestionnaireProcessor.java | 4 ++ 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index c1ad0a5c2..aa238744d 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -17,9 +17,11 @@ import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; +import org.hl7.davinci.endpoint.Utils; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -339,8 +341,12 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet try { //TODO: need to determine topic, filename, and fhir version without having them hard coded // File is pulled from the file store - FileResource fileResource = fileStore.getFile("HomeOxygenTherapy", "Questions-HomeOxygenTherapyAdditional.json", "R4", false); - cdsQuestionnaire = (Questionnaire) parser.parseResource(fileResource.getResource().getInputStream()); + String baseUrl = Utils.getApplicationBaseUrl(request).toString() + "/"; + FileResource fileResource = fileStore.getFhirResourceById("r4", "questionnaire", "HomeOxygenTherapyAdditional", baseUrl); + org.springframework.core.io.Resource resource = fileResource.getResource(); + InputStream resourceStream = resource.getInputStream(); + cdsQuestionnaire = (Questionnaire) parser.parseResource(resourceStream); + logger.info("--- Imported Questionnaire " + cdsQuestionnaire.getId()); } catch (DataFormatException e) { e.printStackTrace(); @@ -358,9 +364,9 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet questionnaireTrees.put(questionnaireId, newTree); logger.info("--- Built Questionnaire Tree for " + questionnaireId); + // Add the set of questions to the response. List questionSet = newTree.getCurrentQuestionSet(); - final Questionnaire tempInputQuestionnaireFromRequest = inputQuestionnaireFromRequest; - questionSet.forEach(questionItem -> tempInputQuestionnaireFromRequest.addItem(questionItem)); + addQuestionSetToQuestionnaireResponse(inputQuestionnaireResponse, questionSet); } else { // If there is already a tree with the requested questionnaire id, execute next-question on it with the new request. // Pull the current tree for the requested questionnaire id. @@ -368,19 +374,18 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet // Get the previous question Id. String previousQuestionId = currentTree.getCurrentDeterminantQuestionId(); // Get the request's answer component of the item with the previous question id. - List allQuestions = inputQuestionnaireResponse.getItem(); - allQuestions = allQuestions.stream().filter(item -> item.getLinkId().equals(previousQuestionId)).collect(Collectors.toList()); - if(allQuestions.size() < 1) { + List allResponses = inputQuestionnaireResponse.getItem(); + List currentResponses = allResponses.stream().filter(item -> item.getLinkId().equals(previousQuestionId)).collect(Collectors.toList()); + if(currentResponses.size() < 1) { throw new RuntimeException("No given answers in the request references the current question asked. Current Question ID: " + previousQuestionId + "."); } - QuestionnaireResponseItemAnswerComponent answerComponent = allQuestions.get(0).getAnswerFirstRep(); - // Pull the string response the person gave. + // Pull the string response given by the current response. + QuestionnaireResponseItemAnswerComponent answerComponent = currentResponses.get(0).getAnswerFirstRep(); String response = answerComponent.getValueCoding().getCode(); // Pull the resulting next question that the recieved response points to from the tree without including its children. - List nextQuestionsItemResult = currentTree.getNextQuestionsFromResponse(response); - // Add the next question set to the QuestionnaireResponse.contained[0].item[]. - Questionnaire containedQuestionnaire = (Questionnaire) inputQuestionnaireResponse.getContained().get(0); - nextQuestionsItemResult.forEach(questionItem -> containedQuestionnaire.addItem(questionItem)); + List nextQuestionSetResult = currentTree.getNextQuestionsFromResponse(response); + // Add the next set of questions to the response. + addQuestionSetToQuestionnaireResponse(inputQuestionnaireResponse, nextQuestionSetResult); logger.info("--- Added next question set for questionnaire \'" + questionnaireId + "\' for response \'" + response + "\'."); // If this question is a leaf node and is the final question, set the status to "completed" @@ -402,7 +407,17 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet .header(HttpHeaders.CONTENT_TYPE, "application/fhir+json" + "; charset=utf-8") .body("Invalid input questionnaire does not exist"); } - } } + + /** + * Adds the given set of questions to the contained questionniare in the questionnaire response. + * @param inputQuestionnaireResponse + * @param questionSet + */ + private void addQuestionSetToQuestionnaireResponse(QuestionnaireResponse inputQuestionnaireResponse, List questionSet) { + // Add the next question set to the QuestionnaireResponse.contained[0].item[]. + Questionnaire containedQuestionnaire = (Questionnaire) inputQuestionnaireResponse.getContained().get(0); + questionSet.forEach(questionItem -> containedQuestionnaire.addItem(questionItem)); + } } \ No newline at end of file diff --git a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java index a10381674..f4497e39b 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java @@ -128,9 +128,15 @@ public FileResource getFhirResourceById(String fhirVersion, String resourceType, logger.info("CommonFileStore::getFhirResourceById(): " + fhirVersion + "/" + resourceType + "/" + id); FhirResourceCriteria criteria = new FhirResourceCriteria(); - criteria.setFhirVersion(fhirVersion).setResourceType(resourceType).setId(id); + criteria.setFhirVersion(fhirVersion).setResourceType(resourceType).setId(id).setName(id); List fhirResourceList = fhirResources.findById(criteria); FileResource resource = readFhirResourceFromFiles(fhirResourceList, fhirVersion, baseUrl); + System.out.println("Resource Pulled: " + resource + resource.getFilename()); + System.out.println("Fhir Resource List: " + fhirResourceList); + System.out.println("Criteria ID: " + criteria); + System.out.println("BaseUrl: " + baseUrl); + System.out.println("--------------------------"); + if ((resource != null) && fhirVersion.equalsIgnoreCase("r4")) { diff --git a/server/src/main/java/org/hl7/davinci/endpoint/files/SubQuestionnaireProcessor.java b/server/src/main/java/org/hl7/davinci/endpoint/files/SubQuestionnaireProcessor.java index f4fcf781b..953509983 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/files/SubQuestionnaireProcessor.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/files/SubQuestionnaireProcessor.java @@ -76,6 +76,10 @@ private void processItemList(List itemList, FileStor for (int i = 0; i < itemList.size();) { List returnedItemList = processItem(itemList.get(i), fileStore, baseUrl, containedList, extensionList); + + // if(itemList.get(i).getItem().size() > 0){ + // this.processItemList(itemList.get(i).getItem(), fileStore, baseUrl, containedList, extensionList); + // } if (returnedItemList.size() == 0) { continue; From 79b23b8ca81a76326f785ca406adf9769cbc74be Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Tue, 30 Nov 2021 16:48:19 -0500 Subject: [PATCH 15/30] Updated CDS-Library resource call for adform. --- .../controllers/QuestionnaireController.java | 28 ++++++++++++++----- .../endpoint/files/CommonFileStore.java | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index aa238744d..179bc1bdc 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -340,12 +340,25 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet Questionnaire cdsQuestionnaire = null; try { //TODO: need to determine topic, filename, and fhir version without having them hard coded - // File is pulled from the file store - String baseUrl = Utils.getApplicationBaseUrl(request).toString() + "/"; - FileResource fileResource = fileStore.getFhirResourceById("r4", "questionnaire", "HomeOxygenTherapyAdditional", baseUrl); - org.springframework.core.io.Resource resource = fileResource.getResource(); - InputStream resourceStream = resource.getInputStream(); - cdsQuestionnaire = (Questionnaire) parser.parseResource(resourceStream); + boolean pullFromResources = false; + if(pullFromResources){ + // Resource is pulled from the file store as a resource. + String baseUrl = Utils.getApplicationBaseUrl(request).toString() + "/"; + FileResource fileResource = fileStore.getFhirResourceById("r4", "questionnaire", "HomeOxygenTherapyAdditional", baseUrl); + org.springframework.core.io.Resource resource = fileResource.getResource(); + InputStream resourceStream = resource.getInputStream(); + cdsQuestionnaire = (Questionnaire) parser.parseResource(resourceStream); + } else { + // File is pulled from the file store as a file. + FileResource fileResource = fileStore.getFile("HomeOxygenTherapy", "Questions-HomeOxygenTherapyAdditionalAdaptive.json", "R4", false); + if(fileResource == null) { + throw new RuntimeException("File resource pulled from the filestore is null."); + } + if(fileResource.getResource() == null) { + throw new RuntimeException("File resource pulled from the filestore has a null getResource()."); + } + cdsQuestionnaire = (Questionnaire) parser.parseResource(fileResource.getResource().getInputStream()); + } logger.info("--- Imported Questionnaire " + cdsQuestionnaire.getId()); } catch (DataFormatException e) { @@ -396,7 +409,8 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } logger.info("---- Get meta profile " + inputQuestionnaireFromRequest.getMeta().getProfile().get(0).getValue()); - + logger.info("---- Sent response back " + inputQuestionnaireFromRequest.getId()); + // Build and send the response. String formattedResourceString = ctx.newJsonParser().encodeResourceToString(inputQuestionnaireResponse); return ResponseEntity.status(HttpStatus.ACCEPTED).contentType(MediaType.APPLICATION_JSON) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java index f4497e39b..0f47acb0a 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java @@ -131,7 +131,7 @@ public FileResource getFhirResourceById(String fhirVersion, String resourceType, criteria.setFhirVersion(fhirVersion).setResourceType(resourceType).setId(id).setName(id); List fhirResourceList = fhirResources.findById(criteria); FileResource resource = readFhirResourceFromFiles(fhirResourceList, fhirVersion, baseUrl); - System.out.println("Resource Pulled: " + resource + resource.getFilename()); + System.out.println("Resource Pulled: " + resource + "-" + resource.getFilename()); System.out.println("Fhir Resource List: " + fhirResourceList); System.out.println("Criteria ID: " + criteria); System.out.println("BaseUrl: " + baseUrl); From 19c0c193814d7de7b52aa32e8b6eb4970bf38631 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Wed, 1 Dec 2021 12:41:11 -0500 Subject: [PATCH 16/30] Updated adform tree to rerun with each request so that any input can produce the correct results. --- .../controllers/QuestionnaireController.java | 156 +++++++----------- 1 file changed, 56 insertions(+), 100 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index 179bc1bdc..8ef7a66c7 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -40,6 +40,7 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseStatus; +import org.hl7.fhir.r4.model.Reference; // --- ORDER OF RESPONSE-REQUEST OPERATIONS // (REQUEST) External user sends the initial QuestionnaireResponse JSON that contains which questionnaire it would like to trigger as n element the "contained" field. @@ -62,7 +63,7 @@ public class QuestionnaireController { */ private class AdaptiveQuestionnaireTree { - // The current question (defiend within the node). + // The current question (defined within the node). private AdaptiveQuestionnaireNode root; /** @@ -80,41 +81,24 @@ public AdaptiveQuestionnaireTree(Questionnaire inputQuestionnaire) { /** * Returns the next question based on the response to the current question. Also sets the next question based on that response. - * @param response The response given to this question. + * @param allAnswerItems The set of answer items given to this tree. * @return */ - public List getNextQuestionsFromResponse(String response){ - - if(!root.hasResponse(response)){ - throw new RuntimeException("Not a valid response for question: \'" + this.root.determinantQuestionItem.getText() + "\' with response \'" + response + "\'. Possible responses for this question: \'" + root.children.keySet() + "\'."); + public List getNextQuestionsForAnswers(List allResponseItems) { + if(allResponseItems == null) { + throw new NullPointerException("Input answer items is null."); } - AdaptiveQuestionnaireNode nextQuestionSetNode = this.root.getChildForResponse(response); - // Pull the current question set. - List currentQuestionnaireItems = nextQuestionSetNode.getQuestionSet(); - // Set the new next question. - this.root = nextQuestionSetNode; - // Return the prior current question. - - currentQuestionnaireItems.get(0).getModifierExtensionFirstRep().getValue(); - return currentQuestionnaireItems; + return this.root.getNextQuestionForAnswers(allResponseItems); } /** - * Returns whether this has reached a leaf node. + * TODO Returns whether this has reached a leaf node. TODO - NEEDS TO BE UPDATED * @param response * @return */ public boolean reachedLeafNode() { return this.root.isLeafNode(); } - - /** - * Returns the linkid of the current question. - * @return - */ - public String getCurrentDeterminantQuestionId() { - return this.root.getDeterminantQuestionId(); - } /** * Inner class that describes a node of the tree. @@ -180,6 +164,30 @@ public AdaptiveQuestionnaireNode(QuestionnaireItemComponent determinantQuestion) } } + /** + * Returns the next question based on the set of provided answers. + * @param allResponseItems + * @return + */ + public List getNextQuestionForAnswers(List allResponseItems) { + + // Extract the current question being answered from the list if answer items. + String currentQuestionId = this.determinantQuestionItem.getLinkId(); + List currentQuestionResponses = allResponseItems.stream().filter(answerItem -> answerItem.getLinkId().equals(currentQuestionId)).collect(Collectors.toList()); + if(currentQuestionResponses.size() != 1) { + // If there are no more answer items to check, we've reached the end of the recursion. + return this.getQuestionSet(); + } + + QuestionnaireResponseItemComponent currentQuestionResponse = currentQuestionResponses.get(0); + QuestionnaireResponseItemAnswerComponent currentQuestionAnswer = currentQuestionResponse.getAnswerFirstRep(); + + // With the currrent question answer in hand, extract the next question. + AdaptiveQuestionnaireNode nextNode = this.children.get(currentQuestionAnswer.getValueCoding().getCode()); + + return nextNode.getNextQuestionForAnswers(allResponseItems.stream().filter(responseItem -> !responseItem.equals(currentQuestionResponse)).collect(Collectors.toList())); + } + /** * Returns the question items in the given list that do not have the linkids of the given list of strings. * @param questionItems @@ -230,47 +238,15 @@ private QuestionnaireItemComponent removeChildrenFromQuestionItem(QuestionnaireI } /** - * Returns whether the determinant question contains the requested response. - * @param response - * @return - */ - public boolean hasResponse(String response) { - try { - return this.children.containsKey(response); - } catch (NullPointerException e) { - throw new NullPointerException("Null pointer thrown for children " + this.children + " with response " + response); - } - } - - /** - * Returns whether this questionniare is a leaf node. + * TODO Returns whether this questionniare is a leaf node. TODO - NEEDS TO BE UPDATED. * @return */ private boolean isLeafNode() { return this.children == null || this.children.size() < 1; } - /** - * Returns the child associated with this node for the given response. - * @param response - * @return - */ - private AdaptiveQuestionnaireNode getChildForResponse(String response) { - return this.children.get(response); - } - - /** - * Returns the question linkid for this node question. - * @return - */ - public String getDeterminantQuestionId() { - return this.determinantQuestionItem.getLinkId(); - } } - public List getCurrentQuestionSet() { - return this.root.getQuestionSet(); - } } // Logger. @@ -325,15 +301,11 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } } - String questionnaireId = inputQuestionnaireFromRequest.getId(); + String questionnaireId = ((Reference) inputQuestionnaireResponse.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/contained-id").getValue()).getReference(); + System.out.println("Input Questionnaire: " + questionnaireId); if (inputQuestionnaireFromRequest != null) { - // If there are no item answer responses in the sent JSON, reset the tree so that we can restart the question process. - if(inputQuestionnaireFromRequest.getItem().size() < 1) { - questionnaireTrees.remove(questionnaireId); - } - if(!questionnaireTrees.containsKey(questionnaireId)){ // If there is not already a tree that matches the requested questionnaire id, build it. // Import the requested CDS-Library Questionnaire. @@ -368,48 +340,32 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } catch (IOException e) { e.printStackTrace(); } - if(cdsQuestionnaire == null) { - throw new RuntimeException("Requested CDS Questionnaire \'" + questionnaireId + "\' was not imported and may not exist."); - } - // Build the tree and don't expect any answers since we only just received the required questions. + // Build the tree. AdaptiveQuestionnaireTree newTree = new AdaptiveQuestionnaireTree(cdsQuestionnaire); questionnaireTrees.put(questionnaireId, newTree); logger.info("--- Built Questionnaire Tree for " + questionnaireId); + } - // Add the set of questions to the response. - List questionSet = newTree.getCurrentQuestionSet(); - addQuestionSetToQuestionnaireResponse(inputQuestionnaireResponse, questionSet); - } else { - // If there is already a tree with the requested questionnaire id, execute next-question on it with the new request. - // Pull the current tree for the requested questionnaire id. - AdaptiveQuestionnaireTree currentTree = questionnaireTrees.get(questionnaireId); - // Get the previous question Id. - String previousQuestionId = currentTree.getCurrentDeterminantQuestionId(); - // Get the request's answer component of the item with the previous question id. - List allResponses = inputQuestionnaireResponse.getItem(); - List currentResponses = allResponses.stream().filter(item -> item.getLinkId().equals(previousQuestionId)).collect(Collectors.toList()); - if(currentResponses.size() < 1) { - throw new RuntimeException("No given answers in the request references the current question asked. Current Question ID: " + previousQuestionId + "."); - } - // Pull the string response given by the current response. - QuestionnaireResponseItemAnswerComponent answerComponent = currentResponses.get(0).getAnswerFirstRep(); - String response = answerComponent.getValueCoding().getCode(); - // Pull the resulting next question that the recieved response points to from the tree without including its children. - List nextQuestionSetResult = currentTree.getNextQuestionsFromResponse(response); - // Add the next set of questions to the response. - addQuestionSetToQuestionnaireResponse(inputQuestionnaireResponse, nextQuestionSetResult); - logger.info("--- Added next question set for questionnaire \'" + questionnaireId + "\' for response \'" + response + "\'."); - - // If this question is a leaf node and is the final question, set the status to "completed" - if (currentTree.reachedLeafNode()) { - inputQuestionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); - logger.info("--- Questionnaire leaf node reached, setting status to \"completed\"."); - } + // Pull the tree for the requested questionnaire id. + AdaptiveQuestionnaireTree currentTree = questionnaireTrees.get(questionnaireId); + // Get the request's set of answer responses. + List allResponses = inputQuestionnaireResponse.getItem(); + // Pull the resulting next question that the recieved responses and answers point to from the tree without including its children. + List nextQuestionSetResults = currentTree.getNextQuestionsForAnswers(allResponses); + // Add the next set of questions to the response. + QuestionnaireController.addQuestionSetToQuestionnaireResponse(inputQuestionnaireResponse, nextQuestionSetResults); + + logger.info("--- Added next question set for questionnaire \'" + questionnaireId + "\' for response \'" + allResponses + "\'."); + + // If this question is a leaf node and is the final question, set the status to "completed" + if (currentTree.reachedLeafNode()) { + inputQuestionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); + logger.info("--- Questionnaire leaf node reached, setting status to \"completed\"."); } - logger.info("---- Get meta profile " + inputQuestionnaireFromRequest.getMeta().getProfile().get(0).getValue()); - logger.info("---- Sent response back " + inputQuestionnaireFromRequest.getId()); + logger.info("---- Get meta profile: " + inputQuestionnaireFromRequest.getMeta().getProfile().get(0).getValue()); + logger.info("---- Sending response: " + inputQuestionnaireFromRequest.getId()); // Build and send the response. String formattedResourceString = ctx.newJsonParser().encodeResourceToString(inputQuestionnaireResponse); @@ -419,7 +375,7 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } else { return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON) .header(HttpHeaders.CONTENT_TYPE, "application/fhir+json" + "; charset=utf-8") - .body("Invalid input questionnaire does not exist"); + .body("Input questionnaire from the request does not exist."); } } } @@ -429,7 +385,7 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet * @param inputQuestionnaireResponse * @param questionSet */ - private void addQuestionSetToQuestionnaireResponse(QuestionnaireResponse inputQuestionnaireResponse, List questionSet) { + private static void addQuestionSetToQuestionnaireResponse(QuestionnaireResponse inputQuestionnaireResponse, List questionSet) { // Add the next question set to the QuestionnaireResponse.contained[0].item[]. Questionnaire containedQuestionnaire = (Questionnaire) inputQuestionnaireResponse.getContained().get(0); questionSet.forEach(questionItem -> containedQuestionnaire.addItem(questionItem)); From 321c40723a2d2e2c85f1492f028059fd8026d120 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Wed, 1 Dec 2021 12:48:51 -0500 Subject: [PATCH 17/30] Cleanup, added TODO comments with missing functionality. --- .../controllers/QuestionnaireController.java | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index 8ef7a66c7..a07052cf3 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -90,15 +90,6 @@ public List getNextQuestionsForAnswers(List getNextQuestionForAnswers(List !responseItem.equals(currentQuestionResponse)).collect(Collectors.toList())); + // Has to be done this way without removing the previous answer response so that we don't alter the original list object. + List nextResponseItems = allResponseItems.stream().filter(responseItem -> !responseItem.equals(currentQuestionResponse)).collect(Collectors.toList()); + return nextNode.getNextQuestionForAnswers(nextResponseItems); } /** @@ -244,9 +242,7 @@ private QuestionnaireItemComponent removeChildrenFromQuestionItem(QuestionnaireI private boolean isLeafNode() { return this.children == null || this.children.size() < 1; } - } - } // Logger. From fa760d45ad108e2248f712ec8f67dc2ba675e3a4 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Wed, 1 Dec 2021 13:27:37 -0500 Subject: [PATCH 18/30] Commented out leaf node code --- .../endpoint/controllers/QuestionnaireController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index a07052cf3..af062fc0c 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -355,10 +355,10 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet logger.info("--- Added next question set for questionnaire \'" + questionnaireId + "\' for response \'" + allResponses + "\'."); // If this question is a leaf node and is the final question, set the status to "completed" - if (currentTree.reachedLeafNode()) { - inputQuestionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); - logger.info("--- Questionnaire leaf node reached, setting status to \"completed\"."); - } + // if (currentTree.reachedLeafNode()) { + // inputQuestionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); + // logger.info("--- Questionnaire leaf node reached, setting status to \"completed\"."); + // } logger.info("---- Get meta profile: " + inputQuestionnaireFromRequest.getMeta().getProfile().get(0).getValue()); logger.info("---- Sending response: " + inputQuestionnaireFromRequest.getId()); From fe21db8975e25856a5338bb2844c72e5c9acf7d7 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Wed, 1 Dec 2021 15:20:17 -0500 Subject: [PATCH 19/30] Added support for string-based answers, added leaf node functionality. Refactored CDS import. --- .../controllers/QuestionnaireController.java | 127 +++++++++++------- 1 file changed, 77 insertions(+), 50 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index af062fc0c..7aad16276 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -1,5 +1,6 @@ package org.hl7.davinci.endpoint.controllers; +import org.aspectj.weaver.patterns.TypePatternQuestions.Question; import org.hl7.davinci.endpoint.Application; import org.hl7.davinci.endpoint.files.FileResource; import org.hl7.davinci.endpoint.files.FileStore; @@ -31,10 +32,13 @@ import javax.servlet.http.HttpServletRequest; +import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.ResourceType; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Type; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemAnswerOptionComponent; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; @@ -63,7 +67,7 @@ public class QuestionnaireController { */ private class AdaptiveQuestionnaireTree { - // The current question (defined within the node). + // The initial question node of the tree. private AdaptiveQuestionnaireNode root; /** @@ -81,14 +85,15 @@ public AdaptiveQuestionnaireTree(Questionnaire inputQuestionnaire) { /** * Returns the next question based on the response to the current question. Also sets the next question based on that response. + * @param inputQuestionnaireResponse * @param allAnswerItems The set of answer items given to this tree. * @return */ - public List getNextQuestionsForAnswers(List allResponseItems) { + public List getNextQuestionsForAnswers(List allResponseItems, QuestionnaireResponse inputQuestionnaireResponse) { if(allResponseItems == null) { throw new NullPointerException("Input answer items is null."); } - return this.root.getNextQuestionForAnswers(allResponseItems); + return this.root.getNextQuestionForAnswers(allResponseItems, inputQuestionnaireResponse); } /** @@ -158,9 +163,10 @@ public AdaptiveQuestionnaireNode(QuestionnaireItemComponent determinantQuestion) /** * Returns the next question based on the set of provided answers. * @param allResponseItems + * @param inputQuestionnaireResponse * @return */ - public List getNextQuestionForAnswers(List allResponseItems) { + public List getNextQuestionForAnswers(List allResponseItems, QuestionnaireResponse inputQuestionnaireResponse) { // Extract the current question being answered from the list if answer items. String currentQuestionId = this.determinantQuestionItem.getLinkId(); @@ -174,16 +180,32 @@ public List getNextQuestionForAnswers(List nextResponseItems = allResponseItems.stream().filter(responseItem -> !responseItem.equals(currentQuestionResponse)).collect(Collectors.toList()); - return nextNode.getNextQuestionForAnswers(nextResponseItems); + return nextNode.getNextQuestionForAnswers(nextResponseItems, inputQuestionnaireResponse); } /** @@ -236,7 +258,7 @@ private QuestionnaireItemComponent removeChildrenFromQuestionItem(QuestionnaireI } /** - * TODO Returns whether this questionniare is a leaf node. TODO - NEEDS TO BE UPDATED. + * Returns whether this questionniare is a leaf node. * @return */ private boolean isLeafNode() { @@ -305,38 +327,8 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet if(!questionnaireTrees.containsKey(questionnaireId)){ // If there is not already a tree that matches the requested questionnaire id, build it. // Import the requested CDS-Library Questionnaire. - Questionnaire cdsQuestionnaire = null; - try { - //TODO: need to determine topic, filename, and fhir version without having them hard coded - boolean pullFromResources = false; - if(pullFromResources){ - // Resource is pulled from the file store as a resource. - String baseUrl = Utils.getApplicationBaseUrl(request).toString() + "/"; - FileResource fileResource = fileStore.getFhirResourceById("r4", "questionnaire", "HomeOxygenTherapyAdditional", baseUrl); - org.springframework.core.io.Resource resource = fileResource.getResource(); - InputStream resourceStream = resource.getInputStream(); - cdsQuestionnaire = (Questionnaire) parser.parseResource(resourceStream); - } else { - // File is pulled from the file store as a file. - FileResource fileResource = fileStore.getFile("HomeOxygenTherapy", "Questions-HomeOxygenTherapyAdditionalAdaptive.json", "R4", false); - if(fileResource == null) { - throw new RuntimeException("File resource pulled from the filestore is null."); - } - if(fileResource.getResource() == null) { - throw new RuntimeException("File resource pulled from the filestore has a null getResource()."); - } - cdsQuestionnaire = (Questionnaire) parser.parseResource(fileResource.getResource().getInputStream()); - } - - logger.info("--- Imported Questionnaire " + cdsQuestionnaire.getId()); - } catch (DataFormatException e) { - e.printStackTrace(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - + Questionnaire cdsQuestionnaire = QuestionnaireController.importCdsQuestionnaire(request, parser, fileStore, "Questions-HomeOxygenTherapyAdditionalAdaptive.json"); + // Build the tree. AdaptiveQuestionnaireTree newTree = new AdaptiveQuestionnaireTree(cdsQuestionnaire); questionnaireTrees.put(questionnaireId, newTree); @@ -348,18 +340,11 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet // Get the request's set of answer responses. List allResponses = inputQuestionnaireResponse.getItem(); // Pull the resulting next question that the recieved responses and answers point to from the tree without including its children. - List nextQuestionSetResults = currentTree.getNextQuestionsForAnswers(allResponses); + List nextQuestionSetResults = currentTree.getNextQuestionsForAnswers(allResponses, inputQuestionnaireResponse); // Add the next set of questions to the response. QuestionnaireController.addQuestionSetToQuestionnaireResponse(inputQuestionnaireResponse, nextQuestionSetResults); logger.info("--- Added next question set for questionnaire \'" + questionnaireId + "\' for response \'" + allResponses + "\'."); - - // If this question is a leaf node and is the final question, set the status to "completed" - // if (currentTree.reachedLeafNode()) { - // inputQuestionnaireResponse.setStatus(QuestionnaireResponseStatus.COMPLETED); - // logger.info("--- Questionnaire leaf node reached, setting status to \"completed\"."); - // } - logger.info("---- Get meta profile: " + inputQuestionnaireFromRequest.getMeta().getProfile().get(0).getValue()); logger.info("---- Sending response: " + inputQuestionnaireFromRequest.getId()); @@ -376,6 +361,48 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } } + /** + * Imports the requested questionnaire from the CDS-Library. + * @param fileStore2 + * @param parser + * @param request + * @return + */ + private static Questionnaire importCdsQuestionnaire(HttpServletRequest request, IParser parser, FileStore fileStore, String questionnaireId) { + Questionnaire cdsQuestionnaire = null; + try { + //TODO: need to determine topic, filename, and fhir version without having them hard coded + boolean pullFromResources = false; + if(pullFromResources){ + // Resource is pulled from the file store as a resource. + String baseUrl = Utils.getApplicationBaseUrl(request).toString() + "/"; + FileResource fileResource = fileStore.getFhirResourceById("r4", "questionnaire", questionnaireId, baseUrl); + org.springframework.core.io.Resource resource = fileResource.getResource(); + InputStream resourceStream = resource.getInputStream(); + cdsQuestionnaire = (Questionnaire) parser.parseResource(resourceStream); + } else { + // File is pulled from the file store as a file. + FileResource fileResource = fileStore.getFile("HomeOxygenTherapy", questionnaireId, "R4", false); + if(fileResource == null) { + throw new RuntimeException("File resource pulled from the filestore is null."); + } + if(fileResource.getResource() == null) { + throw new RuntimeException("File resource pulled from the filestore has a null getResource()."); + } + cdsQuestionnaire = (Questionnaire) parser.parseResource(fileResource.getResource().getInputStream()); + } + + logger.info("--- Imported Questionnaire " + cdsQuestionnaire.getId()); + } catch (DataFormatException e) { + e.printStackTrace(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return cdsQuestionnaire; + } + /** * Adds the given set of questions to the contained questionniare in the questionnaire response. * @param inputQuestionnaireResponse From 6d5de01a030075b8803d8bb5a027250de44bfdcb Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Wed, 1 Dec 2021 17:03:15 -0500 Subject: [PATCH 20/30] Fixed an issue where duplicate questions would be sent with the final question in a branch. --- .../controllers/QuestionnaireController.java | 27 +++++++++++++------ .../endpoint/files/CommonFileStore.java | 5 ---- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index 7aad16276..e20d29396 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -25,6 +25,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.logging.Logger; @@ -75,10 +76,8 @@ private class AdaptiveQuestionnaireTree { * @param inputQuestionnaire The input questionnaire from the CDS-Library. */ public AdaptiveQuestionnaireTree(Questionnaire inputQuestionnaire) { - - // Top level parent question item, the first question page. + // Top level parent question item; the first set of questions. QuestionnaireItemComponent topLevelQuestion = inputQuestionnaire.getItemFirstRep(); - // Start the root building. this.root = new AdaptiveQuestionnaireNode(topLevelQuestion); } @@ -92,6 +91,8 @@ public AdaptiveQuestionnaireTree(Questionnaire inputQuestionnaire) { public List getNextQuestionsForAnswers(List allResponseItems, QuestionnaireResponse inputQuestionnaireResponse) { if(allResponseItems == null) { throw new NullPointerException("Input answer items is null."); + } else if ((new HashSet(allResponseItems.stream().map(item -> item.getLinkId()).collect(Collectors.toList()))).size() != allResponseItems.size()){ + throw new RuntimeException("Detected duplicate answers to the same question."); } return this.root.getNextQuestionForAnswers(allResponseItems, inputQuestionnaireResponse); } @@ -173,8 +174,8 @@ public List getNextQuestionForAnswers(List currentQuestionResponses = allResponseItems.stream().filter(answerItem -> answerItem.getLinkId().equals(currentQuestionId)).collect(Collectors.toList()); if(currentQuestionResponses.size() != 1) { // If there are no more answer items to check, we've reached the end of the recursion. - return this.getQuestionSet(); // TODO - this could cause an unexpected end-of-questionnaire issue if incorrect responses are given. + return this.getQuestionSet(); } QuestionnaireResponseItemComponent currentQuestionResponse = currentQuestionResponses.get(0); @@ -200,7 +201,7 @@ public List getNextQuestionForAnswers(List getQuestionSet() { List questionSet = new ArrayList(); questionSet.add(determinantQuestionNoChildren); questionSet.addAll(this.supplementalQuestions); + logger.info("--- Question Set: " + questionSet.stream().map(item -> item.getLinkId()).collect(Collectors.toList())); return questionSet; } @@ -319,6 +321,12 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet } } + logger.info("--- Received questionnaire response: " + ctx.newJsonParser().encodeResourceToString(inputQuestionnaireResponse)); + // Check that there are no duplicates in the recieved set of questions. + if ((new HashSet(((Questionnaire) inputQuestionnaireResponse.getContained().get(0)).getItem().stream().map(item -> item.getLinkId()).collect(Collectors.toList()))).size() != ((Questionnaire) inputQuestionnaireResponse.getContained().get(0)).getItem().size()){ + throw new RuntimeException("Received a set of questions with duplicates."); + } + String questionnaireId = ((Reference) inputQuestionnaireResponse.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/contained-id").getValue()).getReference(); System.out.println("Input Questionnaire: " + questionnaireId); @@ -343,13 +351,16 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet List nextQuestionSetResults = currentTree.getNextQuestionsForAnswers(allResponses, inputQuestionnaireResponse); // Add the next set of questions to the response. QuestionnaireController.addQuestionSetToQuestionnaireResponse(inputQuestionnaireResponse, nextQuestionSetResults); + // Check that there no duplicates in the set of questions. + if ((new HashSet(((Questionnaire) inputQuestionnaireResponse.getContained().get(0)).getItem().stream().map(item -> item.getLinkId()).collect(Collectors.toList()))).size() != ((Questionnaire) inputQuestionnaireResponse.getContained().get(0)).getItem().size()){ + throw new RuntimeException("Attempted to send a set of questions with duplicates. Question IDs are: " + (((Questionnaire) inputQuestionnaireResponse.getContained().get(0)).getItem().stream().map(item -> item.getLinkId()).collect(Collectors.toList()))); + } - logger.info("--- Added next question set for questionnaire \'" + questionnaireId + "\' for response \'" + allResponses + "\'."); - logger.info("---- Get meta profile: " + inputQuestionnaireFromRequest.getMeta().getProfile().get(0).getValue()); - logger.info("---- Sending response: " + inputQuestionnaireFromRequest.getId()); + logger.info("--- Added next question set for questionnaire \'" + questionnaireId + "\' for responses \'" + allResponses + "\'."); // Build and send the response. String formattedResourceString = ctx.newJsonParser().encodeResourceToString(inputQuestionnaireResponse); + logger.info("--- Sending questionnaire response: " + formattedResourceString); return ResponseEntity.status(HttpStatus.ACCEPTED).contentType(MediaType.APPLICATION_JSON) .header(HttpHeaders.CONTENT_TYPE, "application/fhir+json" + "; charset=utf-8") .body(formattedResourceString); diff --git a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java index 0f47acb0a..406b734b2 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java @@ -132,11 +132,6 @@ public FileResource getFhirResourceById(String fhirVersion, String resourceType, List fhirResourceList = fhirResources.findById(criteria); FileResource resource = readFhirResourceFromFiles(fhirResourceList, fhirVersion, baseUrl); System.out.println("Resource Pulled: " + resource + "-" + resource.getFilename()); - System.out.println("Fhir Resource List: " + fhirResourceList); - System.out.println("Criteria ID: " + criteria); - System.out.println("BaseUrl: " + baseUrl); - System.out.println("--------------------------"); - if ((resource != null) && fhirVersion.equalsIgnoreCase("r4")) { From 9041344cff702f6d82c62164d0a2c9cc909f7e76 Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Thu, 2 Dec 2021 13:45:05 -0500 Subject: [PATCH 21/30] Update gradle to newest version --- server/build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index 72e0916d0..1a2c2c79b 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -38,7 +38,6 @@ dependencies { implementation("io.jsonwebtoken:jjwt:0.7.0") -// implementation 'javax.servlet:javax.servlet-api:3.1.0' implementation 'commons-beanutils:commons-beanutils:1.9.3' @@ -60,7 +59,7 @@ dependencies { exclude group: 'org.slf4j', module: 'slf4j-log4j12' } - //Use locally implementationd cql libs (engine and fhir) + //Use locally compiled cql libs (engine and fhir) implementation fileTree(dir: 'libs', include: ['*.jar']) implementation group: 'info.cqframework', name: 'cql-to-elm', version: '1.5.1' From 2aa86d108dc4ace3bb6f35f5fe9e8a7f377e84ad Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Fri, 10 Dec 2021 09:31:09 -0500 Subject: [PATCH 22/30] Cleaned up code by removing extranseous file, comments, updated question value type determination. --- .../controllers/QuestionnaireController.java | 9 +- ...Questions-HomeOxygenTherapyAdditional.json | 177 ------------------ .../files/SubQuestionnaireProcessor.java | 7 - 3 files changed, 2 insertions(+), 191 deletions(-) delete mode 100644 server/src/main/java/org/hl7/davinci/endpoint/controllers/Questions-HomeOxygenTherapyAdditional.json diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index e20d29396..d434c7666 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -1,6 +1,5 @@ package org.hl7.davinci.endpoint.controllers; -import org.aspectj.weaver.patterns.TypePatternQuestions.Question; import org.hl7.davinci.endpoint.Application; import org.hl7.davinci.endpoint.files.FileResource; import org.hl7.davinci.endpoint.files.FileStore; @@ -181,14 +180,11 @@ public List getNextQuestionForAnswers(List itemList, FileStor for (int i = 0; i < itemList.size();) { List returnedItemList = processItem(itemList.get(i), fileStore, baseUrl, containedList, extensionList); - - // if(itemList.get(i).getItem().size() > 0){ - // this.processItemList(itemList.get(i).getItem(), fileStore, baseUrl, containedList, extensionList); - // } if (returnedItemList.size() == 0) { continue; From c0301371284ef912bb36a40d9b61cedcc8cc7729 Mon Sep 17 00:00:00 2001 From: Robi Scalfani Date: Fri, 10 Dec 2021 11:32:35 -0500 Subject: [PATCH 23/30] Removed hard-coded home oxygen file names --- .../controllers/QuestionnaireController.java | 41 +++++++------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java index d434c7666..ea583a748 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/QuestionnaireController.java @@ -17,11 +17,9 @@ import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; -import org.hl7.davinci.endpoint.Utils; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -32,13 +30,10 @@ import javax.servlet.http.HttpServletRequest; -import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.ResourceType; -import org.hl7.fhir.r4.model.StringType; -import org.hl7.fhir.r4.model.Type; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemAnswerOptionComponent; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; @@ -328,10 +323,10 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet if (inputQuestionnaireFromRequest != null) { - if(!questionnaireTrees.containsKey(questionnaireId)){ + if (!questionnaireTrees.containsKey(questionnaireId)) { // If there is not already a tree that matches the requested questionnaire id, build it. // Import the requested CDS-Library Questionnaire. - Questionnaire cdsQuestionnaire = QuestionnaireController.importCdsQuestionnaire(request, parser, fileStore, "Questions-HomeOxygenTherapyAdditionalAdaptive.json"); + Questionnaire cdsQuestionnaire = QuestionnaireController.importCdsAdaptiveQuestionnaire(request, parser, fileStore, questionnaireId); // Build the tree. AdaptiveQuestionnaireTree newTree = new AdaptiveQuestionnaireTree(cdsQuestionnaire); @@ -375,29 +370,21 @@ private ResponseEntity getNextQuestionOperation(String body, HttpServlet * @param request * @return */ - private static Questionnaire importCdsQuestionnaire(HttpServletRequest request, IParser parser, FileStore fileStore, String questionnaireId) { + private static Questionnaire importCdsAdaptiveQuestionnaire(HttpServletRequest request, IParser parser, FileStore fileStore, String questionnaireId) { Questionnaire cdsQuestionnaire = null; try { - //TODO: need to determine topic, filename, and fhir version without having them hard coded - boolean pullFromResources = false; - if(pullFromResources){ - // Resource is pulled from the file store as a resource. - String baseUrl = Utils.getApplicationBaseUrl(request).toString() + "/"; - FileResource fileResource = fileStore.getFhirResourceById("r4", "questionnaire", questionnaireId, baseUrl); - org.springframework.core.io.Resource resource = fileResource.getResource(); - InputStream resourceStream = resource.getInputStream(); - cdsQuestionnaire = (Questionnaire) parser.parseResource(resourceStream); - } else { - // File is pulled from the file store as a file. - FileResource fileResource = fileStore.getFile("HomeOxygenTherapy", questionnaireId, "R4", false); - if(fileResource == null) { - throw new RuntimeException("File resource pulled from the filestore is null."); - } - if(fileResource.getResource() == null) { - throw new RuntimeException("File resource pulled from the filestore has a null getResource()."); - } - cdsQuestionnaire = (Questionnaire) parser.parseResource(fileResource.getResource().getInputStream()); + String adaptiveQuestionniareFile = ("Questions-" + questionnaireId + "Adaptive.json").replace("#", ""); // The filename should be the questionnaire ID with these added values. + String topic = questionnaireId.replace("Additional", "").replace("#", ""); // The topic should be the questionnaire ID but without the 'Additional' tag. + // File is pulled from the file store as a file. + logger.info("--- Importing questionniare file: " + adaptiveQuestionniareFile + " from topic: " + topic); + FileResource fileResource = fileStore.getFile(topic, adaptiveQuestionniareFile, "R4", false); + if(fileResource == null) { + throw new RuntimeException("File resource pulled from the filestore is null."); + } + if(fileResource.getResource() == null) { + throw new RuntimeException("File resource pulled from the filestore has a null getResource()."); } + cdsQuestionnaire = (Questionnaire) parser.parseResource(fileResource.getResource().getInputStream()); logger.info("--- Imported Questionnaire " + cdsQuestionnaire.getId()); } catch (DataFormatException e) { e.printStackTrace(); From 9ccbfb26be98d746a2f8329c95589cdf78ef8bdf Mon Sep 17 00:00:00 2001 From: Patrick LaRocque <41444457+plarocque4@users.noreply.github.com> Date: Wed, 22 Dec 2021 16:38:10 -0500 Subject: [PATCH 24/30] Preemptive prior auth (#255) * Support for preemptive prior authorization. * Add missing items to Card, Suggestion, and action including the selectionBehavior for handling multiple suggestions, the UUIDs and the resourceId in the Action. * Missed a place where the selectionBehavior needed to be set. --- .../src/main/java/org/cdshooks/Action.java | 14 +- .../java/org/cdshooks/ActionSerializer.java | 2 + .../src/main/java/org/cdshooks/Card.java | 48 ++++++- .../org/cdshooks/CoverageRequirements.java | 25 ++++ .../main/java/org/cdshooks/Suggestion.java | 12 +- .../java/org/hl7/davinci/r4/Utilities.java | 122 +++++++++++++--- .../cdshooks/services/crd/CdsService.java | 18 ++- .../services/crd/r4/FhirRequestProcessor.java | 41 ++++++ .../services/crd/r4/OrderSignService.java | 12 ++ .../endpoint/components/CardBuilder.java | 134 ++++++++++++++---- .../database/FhirResourceCriteria.java | 60 ++++++-- .../database/FhirResourceRepository.java | 6 +- .../endpoint/files/CommonFileStore.java | 19 +-- .../CoverageRequirementRuleCriteria.java | 11 +- 14 files changed, 443 insertions(+), 81 deletions(-) diff --git a/resources/src/main/java/org/cdshooks/Action.java b/resources/src/main/java/org/cdshooks/Action.java index 84cf4b44e..edac2a4ef 100755 --- a/resources/src/main/java/org/cdshooks/Action.java +++ b/resources/src/main/java/org/cdshooks/Action.java @@ -15,6 +15,7 @@ public Action(FhirComponentsT fhirComponents) { private TypeEnum type = null; private String description = null; private IBaseResource resource = null; + private String resourceId = null; private FhirComponentsT fhirComponents; @@ -26,9 +27,7 @@ public void setType(TypeEnum type) { this.type = type; } - public String getDescription() { - return description; - } + public String getDescription() { return description; } public void setDescription(String description) { this.description = description; @@ -36,7 +35,14 @@ public void setDescription(String description) { public IBaseResource getResource() { return resource; } - public void setResource(IBaseResource resource) { this.resource = resource; } + public void setResource(IBaseResource resource) { + this.resource = resource; + setResourceId(resource.fhirType() + "/" + resource.getIdElement().getIdPart()); + } + + public String getResourceId() { return resourceId; } + + public void setResourceId(String resourceId) { this.resourceId = resourceId; } public FhirComponentsT getFhirComponents() { return this.fhirComponents; diff --git a/resources/src/main/java/org/cdshooks/ActionSerializer.java b/resources/src/main/java/org/cdshooks/ActionSerializer.java index c22dc6b03..4b2a4448d 100644 --- a/resources/src/main/java/org/cdshooks/ActionSerializer.java +++ b/resources/src/main/java/org/cdshooks/ActionSerializer.java @@ -38,6 +38,8 @@ public void serialize( new ObjectMapper().readValue(jsonStrNew, HashMap.class); jgen.writeObjectField("resource", map); + jgen.writeStringField("resourceId", value.getResourceId()); + jgen.writeEndObject(); } } diff --git a/resources/src/main/java/org/cdshooks/Card.java b/resources/src/main/java/org/cdshooks/Card.java index 73d8d2358..a4b125703 100755 --- a/resources/src/main/java/org/cdshooks/Card.java +++ b/resources/src/main/java/org/cdshooks/Card.java @@ -5,16 +5,24 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.UUID; public class Card { + private String uuid = null; private String summary = null; - private String detail = null; private IndicatorEnum indicator = null; private Source source = null; private List suggestions = null; + private SelectionBehaviorEnum selectionBehavior = null; private List links = null; + public Card() { this.uuid = UUID.randomUUID().toString(); } + + public String getUuid() { return uuid; } + + private void setUuid(String uuid) { this.uuid = uuid; } + public String getSummary() { return summary; } @@ -68,6 +76,10 @@ public void setSuggestions(List suggestions) { this.suggestions = suggestions; } + public SelectionBehaviorEnum getSelectionBehavior() { return selectionBehavior; } + + public void setSelectionBehavior(SelectionBehaviorEnum selectionBehavior) { this.selectionBehavior = selectionBehavior; } + /** * Add a link. * @param linksItem The link. @@ -129,4 +141,38 @@ public String toString() { return String.valueOf(value); } } + + public enum SelectionBehaviorEnum { + ANY("any"), + AT_MOST_ONE("at-most-one"); + + private String value; + + SelectionBehaviorEnum(String value) { this.value = value; } + + /** + * Create the enum value from a string. Needed because the values have illegal java chars. + * @param value One of the enum values. + * @return selectionBehaviorEnum + */ + @JsonCreator + public static SelectionBehaviorEnum fromValue(String value) throws IOException { + for (SelectionBehaviorEnum selectionBehaviorEnum : SelectionBehaviorEnum.values()) { + if (selectionBehaviorEnum.toString().equals(value)) { + return selectionBehaviorEnum; + } + } + return null; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } } diff --git a/resources/src/main/java/org/cdshooks/CoverageRequirements.java b/resources/src/main/java/org/cdshooks/CoverageRequirements.java index 135e3b5b6..cea3e3908 100644 --- a/resources/src/main/java/org/cdshooks/CoverageRequirements.java +++ b/resources/src/main/java/org/cdshooks/CoverageRequirements.java @@ -1,5 +1,7 @@ package org.cdshooks; +import java.util.UUID; + public class CoverageRequirements { private boolean applies; private String summary; @@ -17,6 +19,9 @@ public class CoverageRequirements { private boolean priorAuthRequired; private boolean documentationRequired; + private boolean priorAuthApproved; + private String priorAuthId; + public boolean getApplies() { return applies; } public CoverageRequirements setApplies(boolean applies) { @@ -139,6 +144,26 @@ public CoverageRequirements setDocumentationRequired(boolean documentationRequir return this; } + public boolean isPriorAuthApproved() { return priorAuthApproved; } + + public CoverageRequirements setPriorAuthApproved(boolean priorAuthApproved) { + this.priorAuthApproved = priorAuthApproved; + return this; + } + + public String getPriorAuthId() { return priorAuthId; } + + public CoverageRequirements setPriorAuthId(String priorAuthId) { + this.priorAuthId = priorAuthId; + return this; + } + + public CoverageRequirements generatePriorAuthId() { + // Generate a random UUID as the ID. This is the same method that Prior Auth (PAS) uses. + this.priorAuthId = UUID.randomUUID().toString(); + return this; + } + public String getQuestionnaireAdditionalUri() { return questionnaireAdditionalUri; } diff --git a/resources/src/main/java/org/cdshooks/Suggestion.java b/resources/src/main/java/org/cdshooks/Suggestion.java index e1c88aca1..0eb4e6d87 100755 --- a/resources/src/main/java/org/cdshooks/Suggestion.java +++ b/resources/src/main/java/org/cdshooks/Suggestion.java @@ -7,11 +7,13 @@ public class Suggestion { private String label = null; - private UUID uuid = null; + private String uuid = null; private List actions = null; - private boolean isRecommended = false; + private boolean isRecommended = true; + + public Suggestion() { this.uuid = UUID.randomUUID().toString(); } public String getLabel() { return label; @@ -21,11 +23,9 @@ public void setLabel(String label) { this.label = label; } - public UUID getUuid() { - return uuid; - } + public String getUuid() { return uuid; } - public void setUuid(UUID uuid) { + public void setUuid(String uuid) { this.uuid = uuid; } diff --git a/resources/src/main/java/org/hl7/davinci/r4/Utilities.java b/resources/src/main/java/org/hl7/davinci/r4/Utilities.java index 6cfd033b4..e1821bc71 100644 --- a/resources/src/main/java/org/hl7/davinci/r4/Utilities.java +++ b/resources/src/main/java/org/hl7/davinci/r4/Utilities.java @@ -1,35 +1,16 @@ package org.hl7.davinci.r4; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.util.*; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; -import org.hl7.davinci.FhirResourceInfo; -import org.hl7.davinci.PatientInfo; -import org.hl7.davinci.PractitionerRoleInfo; -import org.hl7.davinci.RequestIncompleteException; -import org.hl7.davinci.SharedUtilities; -import org.hl7.davinci.SuppressParserErrorHandler; +import org.hl7.davinci.*; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.Address; +import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Address.AddressType; import org.hl7.fhir.r4.model.Address.AddressUse; -import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.r4.model.Coverage; -import org.hl7.fhir.r4.model.DomainResource; -import org.hl7.fhir.r4.model.Library; -import org.hl7.fhir.r4.model.Location; -import org.hl7.fhir.r4.model.Organization; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.PractitionerRole; -import org.hl7.fhir.r4.model.Questionnaire; -import org.hl7.fhir.r4.model.QuestionnaireResponse; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.Resource; -import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.r4.model.ClaimResponse.AdjudicationComponent; public class Utilities { /** @@ -248,4 +229,99 @@ public static FhirResourceInfo getFhirResourceInfo(String resourceString) { return getFhirResourceInfo(parseFhirData(resourceString)); } + public static Reference convertIdToReference(String id, String fhirType) { + Reference reference = new Reference(); + if (id.toUpperCase().startsWith(fhirType.toUpperCase() + "/")) { + reference.setReference(id); + } else { + reference.setReference(fhirType + "/" + id); + } + return reference; + } + + public static ClaimResponse createClaimResponse(String priorAuthId, String patientId, String payerId, String providerId, String applicationFhirPath) { + Date now = new Date(); + + ClaimResponse claimResponse = new ClaimResponse(); + + claimResponse.setId(priorAuthId); + + Meta meta = new Meta(); + meta.addProfile("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/profile-claimresponse"); + claimResponse.setMeta(meta); + + claimResponse.addIdentifier(new Identifier().setSystem(applicationFhirPath).setValue(claimResponse.getId())); + + claimResponse.setStatus(ClaimResponse.ClaimResponseStatus.ACTIVE); + + CodeableConcept codeableConcept = new CodeableConcept(); + Coding coding = new Coding(); + coding.setSystem("http://terminology.hl7.org/CodeSystem/claim-type"); + coding.setCode("professional"); + coding.setDisplay("Professional"); + codeableConcept.addCoding(coding); + claimResponse.setType(codeableConcept); + + claimResponse.setUse(ClaimResponse.Use.PREAUTHORIZATION); + + claimResponse.setPatient(convertIdToReference(patientId, "Patient")); + + claimResponse.setCreated(now); + + claimResponse.setInsurer(convertIdToReference(payerId, "Organization")); + + claimResponse.setOutcome(ClaimResponse.RemittanceOutcome.COMPLETE); + + claimResponse.setDisposition("Granted"); + + claimResponse.setPreAuthRef(claimResponse.getId()); + + ClaimResponse.ItemComponent itemComponent = new ClaimResponse.ItemComponent(); + + Extension reviewActionExtension = new Extension(); + reviewActionExtension.setUrl("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-reviewAction"); + reviewActionExtension.addExtension( + new Extension() + .setUrl("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-reviewActionCode") + .setValue(new CodeableConcept().addCoding( + new Coding() + .setSystem("https://valueset.x12.org/x217/005010/response/2000F/HCR/1/01/00/306") + .setCode("A1")))); + reviewActionExtension.addExtension( + new Extension() + .setUrl("number") + .setValue(new StringType(UUID.randomUUID().toString()))); + itemComponent.addExtension(reviewActionExtension); + + itemComponent.addExtension( + new Extension() + .setUrl("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-itemPreAuthIssueDate") + .setValue(new DateType().setValue(now))); + + itemComponent.addExtension( + new Extension() + .setUrl("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-authorizationNumber") + .setValue(new StringType(UUID.randomUUID().toString()))); + + Extension providerExtension = new Extension(); + providerExtension.setUrl("http://hl7.org/fhir/us/davinci-pas/StructureDefinition/extension-itemAuthorizedProvider"); + providerExtension.addExtension( + new Extension() + .setUrl("provider") + .setValue(new Reference().setReference(convertIdToReference(providerId, "Practitioner").getReference()))); + itemComponent.addExtension(providerExtension); + + itemComponent.setItemSequence(1); + + itemComponent.addAdjudication( + new AdjudicationComponent().setCategory( + new CodeableConcept().addCoding( + new Coding().setSystem("http://terminology.hl7.org/CodeSystem/adjudication") + .setCode("submitted")))); + + claimResponse.addItem(itemComponent); + + return claimResponse; + } + } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java index 3c4051ba4..b5664cac2 100755 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java @@ -20,6 +20,7 @@ import org.hl7.davinci.endpoint.components.CardBuilder; import org.hl7.davinci.endpoint.components.CardBuilder.CqlResultsForCard; import org.hl7.davinci.endpoint.components.PrefetchHydrator; +import org.hl7.davinci.endpoint.database.FhirResourceRepository; import org.hl7.davinci.endpoint.database.RequestLog; import org.hl7.davinci.endpoint.database.RequestService; import org.hl7.davinci.endpoint.files.FileStore; @@ -76,6 +77,9 @@ public abstract class CdsService> { @Autowired FileStore fileStore; + @Autowired + private FhirResourceRepository fhirResourceRepository; + private final List prefetchElements; protected FhirComponentsT fhirComponents; @@ -188,7 +192,14 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a if (results.getCoverageRequirements().getApplies()) { - if (coverageRequirements.isDocumentationRequired() || coverageRequirements.isPriorAuthRequired()) { + // if prior auth already approved + if (coverageRequirements.isPriorAuthApproved()) { + response.addCard(CardBuilder.priorAuthCard(results, results.getRequest(), fhirComponents, coverageRequirements.getPriorAuthId(), + request.getContext().getPatientId(), lookupResult.getCriteria().getPayorId(), request.getContext().getUserId(), + applicationBaseUrl.toString() + "/fhir/" + fhirComponents.getFhirVersion().toString(), + fhirResourceRepository)); + + } else if (coverageRequirements.isDocumentationRequired() || coverageRequirements.isPriorAuthRequired()) { if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireOrderUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireFaceToFaceUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireLabUri()) @@ -217,7 +228,10 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a // no prior auth or documentation required logger.info("Add the no doc or prior auth required card"); Card card = CardBuilder.transform(results); - card = CardBuilder.createSuggestionsWithNote(card, results, fhirComponents); + card.addSuggestionsItem(CardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, + "Save Update To EHR", "Update original " + results.getRequest().fhirType() + " to add note", + true)); + card.setSelectionBehavior(Card.SelectionBehaviorEnum.ANY); response.addCard(card); } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java index 3c92f68b3..912b97d8d 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirRequestProcessor.java @@ -1,6 +1,7 @@ package org.hl7.davinci.endpoint.cdshooks.services.crd.r4; import org.cdshooks.AlternativeTherapy; +import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.*; import org.slf4j.Logger; @@ -123,4 +124,44 @@ public static IBaseResource addNoteToRequest(IBaseResource request, Annotation n return output; } + + public static IBaseResource addSupportingInfoToRequest(IBaseResource request, Reference reference) { + IBaseResource output = request; + + switch (request.fhirType()) { + case "DeviceRequest": + DeviceRequest deviceRequest = ((DeviceRequest) request).copy(); + deviceRequest.addSupportingInfo(reference); + output = deviceRequest; + break; + case "MedicationRequest": + MedicationRequest medicationRequest = ((MedicationRequest) request).copy(); + medicationRequest.addSupportingInformation(reference); + output = medicationRequest; + break; + case "MedicationDispense": + MedicationDispense medicationDispense = ((MedicationDispense) request).copy(); + medicationDispense.addSupportingInformation(reference); + output = medicationDispense; + break; + case "ServiceRequest": + ServiceRequest serviceRequest = ((ServiceRequest) request).copy(); + serviceRequest.addSupportingInfo(reference); + output = serviceRequest; + break; + case "Appointment": + Appointment appointment = ((Appointment) request).copy(); + appointment.addSupportingInformation(reference); + output = appointment; + break; + case "NutritionOrder": + case "SupplyRequest": + case "Encounter": + default: + logger.info("Unsupported fhir R4 resource type (" + request.fhirType() + ") when adding note"); + throw new RuntimeException("Unsupported fhir R4 resource type " + request.fhirType()); + } + + return output; + } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java index df46cdcd4..8b9c7d2ba 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSignService.java @@ -93,6 +93,18 @@ protected CqlResultsForCard executeCqlAndGetRelevantResults(Context context, Str logger.info("Prior Auth Required"); coverageRequirements.setSummary(humanReadableTopic + ": Prior Authorization required.") .setDetails("Prior Authorization required, follow the attached link for information."); + + // check if prior auth is automatically approved + if (evaluateStatement("APPROVE_PRIORAUTH", context) != null) { + coverageRequirements.setPriorAuthApproved((Boolean) evaluateStatement("APPROVE_PRIORAUTH", context)); + if (coverageRequirements.isPriorAuthApproved()) { + coverageRequirements.generatePriorAuthId(); + logger.info("Prior Auth Approved: " + coverageRequirements.getPriorAuthId()); + coverageRequirements.setSummary(humanReadableTopic + ": Prior Authorization approved.") + .setDetails("Prior Authorization approved, ID is " + coverageRequirements.getPriorAuthId()); + } + } + } else if (coverageRequirements.isDocumentationRequired()) { logger.info("Documentation Required"); coverageRequirements.setSummary(humanReadableTopic + ": Documentation Required.") diff --git a/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java b/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java index 92a5633aa..e67ff6f42 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/components/CardBuilder.java @@ -1,18 +1,16 @@ package org.hl7.davinci.endpoint.components; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; +import java.util.*; import org.cdshooks.*; import org.hl7.davinci.FhirComponentsT; import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.FhirRequestProcessor; +import org.hl7.davinci.endpoint.database.FhirResource; +import org.hl7.davinci.endpoint.database.FhirResourceRepository; +import org.hl7.davinci.r4.Utilities; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.Annotation; -import org.hl7.fhir.r4.model.DeviceRequest; -import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,21 +80,33 @@ public CqlResultsForCard setRequest(IBaseResource request) { * @param cqlResults * @return card with appropriate information */ - public static Card transform(CqlResultsForCard cqlResults) { + public static Card transform(CqlResultsForCard cqlResults, Boolean addLink) { Card card = baseCard(); - Link link = new Link(); - link.setUrl(cqlResults.getCoverageRequirements().getInfoLink()); - link.setType("absolute"); - link.setLabel("Documentation Requirements"); + if (addLink) { + Link link = new Link(); + link.setUrl(cqlResults.getCoverageRequirements().getInfoLink()); + link.setType("absolute"); + link.setLabel("Documentation Requirements"); + card.setLinks(Arrays.asList(link)); + } - card.setLinks(Arrays.asList(link)); card.setSummary(cqlResults.getCoverageRequirements().getSummary()); card.setDetail(cqlResults.getCoverageRequirements().getDetails()); return card; } + /** + * Transforms a result from the database into a card, defaults to adding the link. + * + * @param cqlResults + * @return card with appropriate information + */ + public static Card transform(CqlResultsForCard cqlResults) { + return transform(cqlResults, true); + } + /** * Transforms a result from the database into a card. * @@ -180,6 +190,8 @@ public static Card alternativeTherapyCard(AlternativeTherapy alternativeTherapy, suggestionList.add(alternativeTherapySuggestion); card.setSuggestions(suggestionList); + card.setSelectionBehavior(Card.SelectionBehaviorEnum.ANY); + return card; } @@ -192,14 +204,90 @@ public static Card drugInteractionCard(DrugInteraction drugInteraction) { return card; } - public static Card createSuggestionsWithNote(Card card, - CqlResultsForCard cqlResults, - FhirComponentsT fhirComponents) { - List suggestionList = new ArrayList<>(); + public static Card priorAuthCard(CqlResultsForCard cqlResults, + IBaseResource request, + FhirComponentsT fhirComponents, + String priorAuthId, + String patientId, + String payerId, + String providerId, + String applicationFhirPath, + FhirResourceRepository fhirResourceRepository) { + logger.info("Build Prior Auth Card"); + + Card card = transform(cqlResults, false); + + // create the ClaimResponse + ClaimResponse claimResponse = Utilities.createClaimResponse(priorAuthId, patientId, payerId, providerId, applicationFhirPath); + + // build the FhirResource and save to the database + FhirResource fhirResource = new FhirResource(); + fhirResource.setFhirVersion(fhirComponents.getFhirVersion().toString()) + .setResourceType(claimResponse.fhirType()) + .setData(fhirComponents.getJsonParser().encodeResourceToString(claimResponse)); + fhirResource.setId(claimResponse.getId()); + fhirResource.setName(claimResponse.getId()); + FhirResource newFhirResource = fhirResourceRepository.save(fhirResource); + logger.info("stored: " + newFhirResource.getFhirVersion() + "/" + newFhirResource.getResourceType() + "/" + newFhirResource.getId()); + + // create the reference to the ClaimResponse + Reference claimResponseReference = new Reference(); + claimResponseReference.setReference("ClaimResponse/" + claimResponse.getId()); + + // add SupportingInfo to the Request + IBaseResource outputRequest = FhirRequestProcessor.addSupportingInfoToRequest(request, claimResponseReference); + + // add suggestion with ClaimResponse + Suggestion suggestionWithClaimResponse = createSuggestionWithResource(outputRequest, claimResponse, fhirComponents, + "Store the prior authorization in the EHR"); + card.addSuggestionsItem(suggestionWithClaimResponse); + + // add suggestion with annotation + Suggestion suggestionWithAnnotation = createSuggestionWithNote(card, outputRequest, fhirComponents, + "Store prior authorization as an annotation to the order", "Add authorization to record", + false); + card.addSuggestionsItem(suggestionWithAnnotation); + + card.setSelectionBehavior(Card.SelectionBehaviorEnum.AT_MOST_ONE); + + return card; + } + + public static Suggestion createSuggestionWithResource(IBaseResource request, + IBaseResource resource, + FhirComponentsT fhirComponents, + String label) { + Suggestion suggestion = new Suggestion(); + + suggestion.setLabel(label); + suggestion.setIsRecommended(true); + + // build the create Action + Action createAction = new Action(fhirComponents); + createAction.setType(Action.TypeEnum.create); + createAction.setDescription("Store " + resource.fhirType()); + createAction.setResource(resource); + suggestion.addActionsItem(createAction); + + // build the update Action + Action updateAction = new Action(fhirComponents); + updateAction.setType(Action.TypeEnum.update); + updateAction.setDescription("Update to the resource " + request.fhirType()); + updateAction.setResource(request); + suggestion.addActionsItem(updateAction); + + return suggestion; + } + public static Suggestion createSuggestionWithNote(Card card, + IBaseResource request, + FhirComponentsT fhirComponents, + String label, + String description, + boolean isRecommended) { Suggestion requestWithNoteSuggestion = new Suggestion(); - requestWithNoteSuggestion.setLabel("Save Update To EHR"); - requestWithNoteSuggestion.setIsRecommended(true); + requestWithNoteSuggestion.setLabel(label); + requestWithNoteSuggestion.setIsRecommended(isRecommended); List actionList = new ArrayList<>(); // build the Annotation @@ -210,19 +298,17 @@ public static Card createSuggestionsWithNote(Card card, String text = card.getSummary() + ": " + card.getDetail(); annotation.setText(text); annotation.setTime(new Date()); // set the date and time to now - IBaseResource resource = FhirRequestProcessor.addNoteToRequest(cqlResults.getRequest(), annotation); + IBaseResource resource = FhirRequestProcessor.addNoteToRequest(request, annotation); Action updateAction = new Action(fhirComponents); updateAction.setType(Action.TypeEnum.update); - updateAction.setDescription("Update original " + cqlResults.getRequest().fhirType() + " to add note"); + updateAction.setDescription(description); updateAction.setResource(resource); actionList.add(updateAction); requestWithNoteSuggestion.setActions(actionList); - suggestionList.add(requestWithNoteSuggestion); - card.setSuggestions(suggestionList); - return card; + return requestWithNoteSuggestion; } /** diff --git a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java index ce1ccd6fa..08f8e50af 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceCriteria.java @@ -2,11 +2,11 @@ public class FhirResourceCriteria { - private String fhirVersion; - private String resourceType; - private String name; - private String id; - private String url; + private String fhirVersion = null; + private String resourceType = null; + private String name = null; + private String id = null; + private String url = null; public String getFhirVersion() { return fhirVersion; } @@ -44,8 +44,52 @@ public FhirResourceCriteria setUrl(String url) { } public String toString() { - return String.format( - "fhirVersion=%s, resourceType=%s, name=%s", fhirVersion, resourceType, name - ); + String string = new String(); + boolean first = true; + if (fhirVersion != null) { + if (first) { + first = false; + } else { + string = string + ", "; + } + string = string + "fhirVersion=" + fhirVersion; + } + + if (resourceType != null) { + if (first) { + first = false; + } else { + string = string + ", "; + } + string = string + "resourceType=" + resourceType; + } + + if (id != null) { + if (first) { + first = false; + } else { + string = string + ", "; + } + string = string + "id=" + id; + } + + if (name != null) { + if (first) { + first = false; + } else { + string = string + ", "; + } + string = string + "name=" + name; + } + + if (url != null) { + if (first) { + first = false; + } else { + string = string + ", "; + } + string = string + "url=" + url; + } + return string; } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java index adacc77e9..5d01a8a0f 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/database/FhirResourceRepository.java @@ -17,7 +17,7 @@ public interface FhirResourceRepository extends CrudRepository findById( @Param("criteria") FhirResourceCriteria criteria @@ -26,7 +26,7 @@ List findById( @Query( "SELECT r FROM FhirResource r WHERE " + "r.fhirVersion = :#{#criteria.fhirVersion} " - + "and r.resourceType = :#{#criteria.resourceType} " + + "and LOWER(r.resourceType) = :#{#criteria.resourceType} " + "and r.name = :#{#criteria.name}") List findByName( @Param("criteria") FhirResourceCriteria criteria @@ -35,7 +35,7 @@ List findByName( @Query( "SELECT r FROM FhirResource r WHERE " + "r.fhirVersion = :#{#criteria.fhirVersion} " - + "and r.resourceType = :#{#criteria.resourceType} " + + "and LOWER(r.resourceType) = :#{#criteria.resourceType} " + "and r.url = :#{#criteria.url}") List findByUrl( @Param("criteria") FhirResourceCriteria criteria diff --git a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java index 406b734b2..a0b52c5ed 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/files/CommonFileStore.java @@ -21,6 +21,7 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.util.List; +import java.util.Locale; public abstract class CommonFileStore implements FileStore { @@ -101,10 +102,10 @@ protected FileResource readFhirResourceFromFiles(List fhirResource } public FileResource getFhirResourceByTopic(String fhirVersion, String resourceType, String name, String baseUrl) { - logger.info("CommonFileStore::getFhirResourceByTopic(): " + fhirVersion + "/" + resourceType + "/" + name); - FhirResourceCriteria criteria = new FhirResourceCriteria(); - criteria.setFhirVersion(fhirVersion).setResourceType(resourceType).setName(name); + criteria.setFhirVersion(fhirVersion).setResourceType(resourceType.toLowerCase()).setName(name); + logger.info("CommonFileStore::getFhirResourceByTopic(): " + criteria.toString()); + List fhirResourceList = fhirResources.findByName(criteria); FileResource resource = readFhirResourceFromFiles(fhirResourceList, fhirVersion, baseUrl); @@ -125,10 +126,10 @@ public FileResource getFhirResourceById(String fhirVersion, String resourceType, public FileResource getFhirResourceById(String fhirVersion, String resourceType, String id, String baseUrl, boolean isRoot) { - logger.info("CommonFileStore::getFhirResourceById(): " + fhirVersion + "/" + resourceType + "/" + id); - FhirResourceCriteria criteria = new FhirResourceCriteria(); - criteria.setFhirVersion(fhirVersion).setResourceType(resourceType).setId(id).setName(id); + criteria.setFhirVersion(fhirVersion).setResourceType(resourceType.toLowerCase()).setId(id); + logger.info("CommonFileStore::getFhirResourceById(): " + criteria.toString()); + List fhirResourceList = fhirResources.findById(criteria); FileResource resource = readFhirResourceFromFiles(fhirResourceList, fhirVersion, baseUrl); System.out.println("Resource Pulled: " + resource + "-" + resource.getFilename()); @@ -154,10 +155,10 @@ public FileResource getFhirResourceById(String fhirVersion, String resourceType, } public FileResource getFhirResourceByUrl(String fhirVersion, String resourceType, String url, String baseUrl) { - logger.info("CommonFileStore::getFhirResourceByUrl(): " + fhirVersion + "/" + resourceType + "/" + url); - FhirResourceCriteria criteria = new FhirResourceCriteria(); - criteria.setFhirVersion(fhirVersion).setResourceType(resourceType).setUrl(url); + criteria.setFhirVersion(fhirVersion).setResourceType(resourceType.toLowerCase()).setUrl(url); + logger.info("CommonFileStore::getFhirResourceByUrl(): " + criteria.toString()); + List fhirResourceList = fhirResources.findByUrl(criteria); FileResource resource = readFhirResourceFromFiles(fhirResourceList, fhirVersion, baseUrl); diff --git a/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleCriteria.java b/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleCriteria.java index e630a7bad..b6728f847 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleCriteria.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/rules/CoverageRequirementRuleCriteria.java @@ -8,6 +8,7 @@ public class CoverageRequirementRuleCriteria { private String payor; + private String payorId; private String code; private String codeSystem; private String fhirVersion; @@ -45,6 +46,13 @@ public CoverageRequirementRuleCriteria setPayor(String payor) { return this; } + public String getPayorId() { return payorId; } + + public CoverageRequirementRuleCriteria setPayorId(String payorId) { + this.payorId = payorId; + return this; + } + public String getFhirVersion() { return fhirVersion; } public CoverageRequirementRuleCriteria setFhirVersion(String fhirVersion) { @@ -71,8 +79,9 @@ public static List createQueriesFromR4(List Date: Wed, 12 Jan 2022 02:11:16 -0500 Subject: [PATCH 25/30] Update build.gradle --- server/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/build.gradle b/server/build.gradle index 33362bfad..2a4c139ef 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -142,7 +142,7 @@ void cloneCdsLibraryScript(branch) { println "GIT: clone CDS-Library branch " + branch exec { workingDir './' - commandLine 'git', 'clone', 'https://github.com/mcode/CDS-Library.git', 'CDS-Library' + commandLine 'git', 'clone', 'https://github.com/HL7-DaVinci/CDS-Library.git', 'CDS-Library' } checkoutCdsLibraryScript(branch) } From d2386de6105003cf37688e9542a91515f3dff237 Mon Sep 17 00:00:00 2001 From: smalho01 <88040167+smalho01@users.noreply.github.com> Date: Wed, 12 Jan 2022 02:11:57 -0500 Subject: [PATCH 26/30] Delete CdsAbstract.java --- .../cdshooks/services/crd/CdsAbstract.java | 161 ------------------ 1 file changed, 161 deletions(-) delete mode 100644 server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsAbstract.java diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsAbstract.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsAbstract.java deleted file mode 100644 index 750e37322..000000000 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsAbstract.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.hl7.davinci.endpoint.cdshooks.services.crd; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import org.cdshooks.*; -import org.hl7.davinci.FhirComponentsT; -import org.hl7.davinci.PrefetchTemplateElement; -import org.hl7.davinci.endpoint.config.YamlConfig; -import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleCriteria; -import org.hl7.davinci.r4.crdhook.DiscoveryExtension; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.List; - -@Component -public abstract class CdsAbstract>{ - static final Logger logger = LoggerFactory.getLogger(CdsAbstract.class); - - /** - * The {id} portion of the URL to this service which is available at - * {baseUrl}/cds-services/{id}. REQUIRED - */ - public String id; - - /** - * The hook this service should be invoked on. REQUIRED - */ - public Hook hook; - - /** - * The human-friendly name of this service. RECOMMENDED - */ - public String title; - - /** - * The description of this service. REQUIRED - */ - public String description; - - /** - * An object containing key/value pairs of FHIR queries that this service is - * requesting that the EHR prefetch and provide on each service call. The key is - * a string that describes the type of data being requested and the value is a - * string representing the FHIR query. OPTIONAL - */ - public Prefetch prefetch; - - private final List prefetchElements; - - protected FhirComponentsT fhirComponents; - - private final DiscoveryExtension extension; - - @Autowired - @JsonIgnore - public YamlConfig myConfig; - - public CdsAbstract(String id, Hook hook, String title, String description, - List prefetchElements, FhirComponentsT fhirComponents, - DiscoveryExtension extension) { - - if (id == null) { - throw new NullPointerException("CDSService id cannot be null"); - } - if (hook == null) { - throw new NullPointerException("CDSService hook cannot be null"); - } - if (description == null) { - throw new NullPointerException("CDSService description cannot be null"); - } - this.id = id; - this.hook = hook; - this.title = title; - this.description = description; - this.prefetchElements = prefetchElements; - prefetch = new Prefetch(); - for (PrefetchTemplateElement prefetchElement : prefetchElements) { - this.prefetch.put(prefetchElement.getKey(), prefetchElement.getQuery()); - } - this.fhirComponents = fhirComponents; - this.extension = extension; - } - - public DiscoveryExtension getExtension() { return extension; } - - public List getPrefetchElements() { - return prefetchElements; - } - - protected Link smartLinkBuilder(String patientId, String fhirBase, URL applicationBaseUrl, String questionnaireUri, - String reqResourceId, CoverageRequirementRuleCriteria criteria, boolean priorAuthRequired, String label) { - URI configLaunchUri = myConfig.getLaunchUrl(); - questionnaireUri = applicationBaseUrl + "/fhir/r4/" + questionnaireUri; - - String launchUrl; - if (myConfig.getLaunchUrl().isAbsolute()) { - launchUrl = myConfig.getLaunchUrl().toString(); - } else { - try { - launchUrl = new URL(applicationBaseUrl.getProtocol(), applicationBaseUrl.getHost(), - applicationBaseUrl.getPort(), applicationBaseUrl.getFile() + configLaunchUri.toString(), null).toString(); - } catch (MalformedURLException e) { - String msg = "Error creating smart launch URL"; - logger.error(msg); - throw new RuntimeException(msg); - } - } - - if (fhirBase != null && fhirBase.endsWith("/")) { - fhirBase = fhirBase.substring(0, fhirBase.length() - 1); - } - if (patientId != null && patientId.startsWith("Patient/")) { - patientId = patientId.substring(8); - } - - // PARAMS: - // template is the uri of the questionnaire - // request is the ID of the device request or medrec (not the full URI like the - // IG says, since it should be taken from fhirBase - - String filepath = "../../getfile/" + criteria.getQueryString(); - - String appContext = "template=" + questionnaireUri + "&request=" + reqResourceId; - appContext = appContext + "&fhirpath=" + applicationBaseUrl + "/fhir/"; - - appContext = appContext + "&priorauth=" + (priorAuthRequired ? "true" : "false"); - appContext = appContext + "&filepath=" + applicationBaseUrl + "/"; - if (myConfig.getUrlEncodeAppContext()) { - logger.info("CdsService::smartLinkBuilder: URL encoding appcontext"); - appContext = URLEncoder.encode(appContext, StandardCharsets.UTF_8).toString(); - } - - logger.info("smarLinkBuilder: appContext: " + appContext); - - if (myConfig.isAppendParamsToSmartLaunchUrl()) { - launchUrl = launchUrl + "?iss=" + fhirBase + "&patientId=" + patientId + "&template=" + questionnaireUri - + "&request=" + reqResourceId; - } else { - // TODO: The iss should be set by the EHR? - launchUrl = launchUrl; - } - - Link link = new Link(); - link.setType("smart"); - link.setLabel(label); - link.setUrl(launchUrl); - - link.setAppContext(appContext); - - return link; - } - - public abstract CdsResponse handleRequest(requestTypeT request, URL applicationBaseUrl); -} From e40cdd9dd9f99e56bf14f4e89b9a219a3de073a7 Mon Sep 17 00:00:00 2001 From: smalho01 <88040167+smalho01@users.noreply.github.com> Date: Wed, 12 Jan 2022 02:21:02 -0500 Subject: [PATCH 27/30] Delete CdsServiceRems.java --- .../cdshooks/services/crd/CdsServiceRems.java | 86 ------------------- 1 file changed, 86 deletions(-) delete mode 100644 server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceRems.java diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceRems.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceRems.java deleted file mode 100644 index f6e5dbe63..000000000 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceRems.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.hl7.davinci.endpoint.cdshooks.services.crd; - -import org.cdshooks.Card; -import org.cdshooks.CdsRequest; -import org.cdshooks.CdsResponse; -import org.cdshooks.Hook; -import org.hl7.davinci.FhirComponentsT; -import org.hl7.davinci.PrefetchTemplateElement; -import org.hl7.davinci.endpoint.components.CardBuilder; -import org.hl7.davinci.r4.crdhook.DiscoveryExtension; -import org.hl7.fhir.r4.model.Coding; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.RequestBody; - -import javax.validation.Valid; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -@Component -public abstract class CdsServiceRems> extends CdsAbstract { - static final Logger logger = LoggerFactory.getLogger(CdsServiceRems.class); - - public List remsDrugs = new ArrayList<>(); - - public CdsServiceRems(String id, Hook hook, String title, String description, - List prefetchElements, FhirComponentsT fhirComponents, - DiscoveryExtension extension) { - super(id, hook, title, description, prefetchElements, fhirComponents, extension); - Coding turalio = new Coding() - .setCode("2183126") - .setSystem("http://www.nlm.nih.gov/research/umls/rxnorm") - .setDisplay("Turalio"); - Coding iPledge = new Coding() - .setCode("6064") - .setSystem("http://www.nlm.nih.gov/research/umls/rxnorm") - .setDisplay("Isotretinoin"); - Coding revlimid = new Coding() - .setCode("337535") - .setSystem("http://www.nlm.nih.gov/research/umls/rxnorm") - .setDisplay("Revlimid"); - Coding abstral = new Coding() - .setCode("1053648") - .setSystem("http://www.nlm.nih.gov/research/umls/rxnorm") - .setDisplay("Abstral"); - remsDrugs.add(turalio); - remsDrugs.add(iPledge); - remsDrugs.add(revlimid); - remsDrugs.add(abstral); - } - - /** - * Performs generic operations for incoming requests of any type. - * - * @param request the generically typed incoming request - * @return The response from the server - */ - public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL applicationBaseUrl) { - CdsResponse response = new CdsResponse(); - List medications = getMedications(request); - for (Coding medication : medications) { - Card card = CardBuilder.summaryCard(""); - if (isRemsDrug(medication)) { - card.setSummary(String.format("%s is a REMS drug", medication.getDisplay())); - } else { - card.setSummary(String.format("%s is not a REMS drug", medication.getDisplay())); - } - response.addCard(card); - } - return response; - } - - private boolean isRemsDrug(Coding medication) { - for( Coding remsDrug : remsDrugs){ - if (remsDrug.getCode().equals(medication.getCode())){ - return true; - } - } - return false; - } - - public abstract List getMedications(requestTypeT request); - -} \ No newline at end of file From 72e13157b4fbb333efad0f09aa30ac381f7fb832 Mon Sep 17 00:00:00 2001 From: smalho01 <88040167+smalho01@users.noreply.github.com> Date: Wed, 12 Jan 2022 02:22:38 -0500 Subject: [PATCH 28/30] Delete OrderSelectServiceRems.java --- .../crd/r4/OrderSelectServiceRems.java | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSelectServiceRems.java diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSelectServiceRems.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSelectServiceRems.java deleted file mode 100644 index a5cf2824b..000000000 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/OrderSelectServiceRems.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.hl7.davinci.endpoint.cdshooks.services.crd.r4; - -import org.cdshooks.Hook; -import org.hl7.davinci.PrefetchTemplateElement; -import org.hl7.davinci.endpoint.Utils; -import org.hl7.davinci.endpoint.cdshooks.services.crd.CdsServiceRems; -import org.hl7.davinci.r4.FhirComponents; -import org.hl7.davinci.r4.Utilities; -import org.hl7.davinci.r4.crdhook.CrdPrefetch; -import org.hl7.davinci.r4.crdhook.orderselect.CrdPrefetchTemplateElements; -import org.hl7.davinci.r4.crdhook.orderselect.OrderSelectRequest; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.MedicationRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - - -@Component("r4_OrderSelectServiceRems") -public class OrderSelectServiceRems extends CdsServiceRems { - - public static final String ID = "order-select-rems"; - public static final String TITLE = "order-select-rems Coverage Requirements Discovery"; - public static final Hook HOOK = Hook.ORDER_SELECT; - public static final String DESCRIPTION = - "Get information regarding the coverage requirements for REMS drugs"; - public static final List PREFETCH_ELEMENTS = Arrays.asList( - CrdPrefetchTemplateElements.MEDICATION_STATEMENT_BUNDLE, - CrdPrefetchTemplateElements.MEDICATION_REQUEST_BUNDLE); - public static final FhirComponents FHIRCOMPONENTS = new FhirComponents(); - static final Logger logger = LoggerFactory.getLogger(OrderSelectServiceRems.class); - - public List remsDrugs = new ArrayList<>(); - - public OrderSelectServiceRems() { - super(ID, HOOK, TITLE, DESCRIPTION, PREFETCH_ELEMENTS, FHIRCOMPONENTS, null); - } - - - public List getMedications(OrderSelectRequest request) { - List selections = Arrays.asList(request.getContext().getSelections()); - CrdPrefetch prefetch = request.getPrefetch(); - List medications = getSelections(prefetch, selections); - return medications; - } - public List getSelections(CrdPrefetch prefetch, List selections) { - Bundle medicationRequestBundle = prefetch.getMedicationRequestBundle(); - List medicationRequestList = Utilities.getResourcesOfTypeFromBundle(MedicationRequest.class, medicationRequestBundle); - List codings = new ArrayList<>(); - - if (!medicationRequestList.isEmpty()) { - for (MedicationRequest medicationRequest : medicationRequestList) { - if (Utils.idInSelectionsList(medicationRequest.getId(), selections)) { - codings.add(medicationRequest.getMedicationCodeableConcept().getCodingFirstRep()); // assume there is only 1 coding per MR - } - } - } - - return codings; - } - -} From b2973158b30efa0514bbbae52e23e6de23f20c8f Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 12 Jan 2022 02:23:44 -0500 Subject: [PATCH 29/30] revert accidental merges from mcode space --- .../java/org/hl7/davinci/endpoint/Utils.java | 24 -- .../cdshooks/services/crd/CdsService.java | 227 +++++++++++++++--- .../services/crd/CdsServiceInformation.java | 8 +- .../services/crd/r4/FhirBundleProcessor.java | 37 ++- .../controllers/r4/CdsHooksController.java | 30 +-- 5 files changed, 242 insertions(+), 84 deletions(-) diff --git a/server/src/main/java/org/hl7/davinci/endpoint/Utils.java b/server/src/main/java/org/hl7/davinci/endpoint/Utils.java index c85e2f452..4d407e93c 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/Utils.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/Utils.java @@ -2,7 +2,6 @@ import java.net.MalformedURLException; import java.net.URL; -import java.util.List; import javax.servlet.http.HttpServletRequest; import com.google.common.net.HttpHeaders; @@ -39,27 +38,4 @@ public static URL getApplicationBaseUrl(HttpServletRequest request) { } } - public static String stripResourceType(String identifier) { - int indexOfDivider = identifier.indexOf('/'); - if (indexOfDivider+1 == identifier.length()) { - // remove the trailing '/' - return identifier.substring(0, indexOfDivider); - } else { - return identifier.substring(indexOfDivider+1); - } - } - - public static boolean idInSelectionsList(String identifier, List selections) { - if (selections.isEmpty()) { - return true; - } else { - for ( String selection : selections) { - if (identifier.contains(stripResourceType(selection))) { - return true; - } - } - return false; - } - } - } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java index a2aec34ea..b5664cac2 100755 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsService.java @@ -1,19 +1,33 @@ package org.hl7.davinci.endpoint.cdshooks.services.crd; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.ArrayList; +import java.util.Date; +import javax.validation.Valid; + import org.apache.commons.lang.StringUtils; import org.cdshooks.*; import org.hl7.davinci.FhirComponentsT; import org.hl7.davinci.PrefetchTemplateElement; import org.hl7.davinci.RequestIncompleteException; +import org.hl7.davinci.endpoint.config.YamlConfig; import org.hl7.davinci.endpoint.components.CardBuilder; import org.hl7.davinci.endpoint.components.CardBuilder.CqlResultsForCard; import org.hl7.davinci.endpoint.components.PrefetchHydrator; +import org.hl7.davinci.endpoint.database.FhirResourceRepository; import org.hl7.davinci.endpoint.database.RequestLog; import org.hl7.davinci.endpoint.database.RequestService; import org.hl7.davinci.endpoint.files.FileStore; +import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleCriteria; import org.hl7.davinci.endpoint.rules.CoverageRequirementRuleResult; -import org.hl7.davinci.r4.crdhook.DiscoveryExtension; import org.hl7.davinci.r4.crdhook.orderselect.OrderSelectRequest; +import org.hl7.davinci.r4.crdhook.DiscoveryExtension; import org.opencds.cqf.cql.engine.execution.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,26 +35,99 @@ import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestBody; -import javax.validation.Valid; -import java.net.URL; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - @Component -public abstract class CdsService> extends CdsAbstract { +public abstract class CdsService> { static final Logger logger = LoggerFactory.getLogger(CdsService.class); + /** + * The {id} portion of the URL to this service which is available at + * {baseUrl}/cds-services/{id}. REQUIRED + */ + public String id; + + /** + * The hook this service should be invoked on. REQUIRED + */ + public Hook hook; + + /** + * The human-friendly name of this service. RECOMMENDED + */ + public String title; + + /** + * The description of this service. REQUIRED + */ + public String description; + + /** + * An object containing key/value pairs of FHIR queries that this service is + * requesting that the EHR prefetch and provide on each service call. The key is + * a string that describes the type of data being requested and the value is a + * string representing the FHIR query. OPTIONAL + */ + public Prefetch prefetch; + + @Autowired + private YamlConfig myConfig; + @Autowired RequestService requestService; @Autowired FileStore fileStore; + @Autowired + private FhirResourceRepository fhirResourceRepository; + + private final List prefetchElements; + + protected FhirComponentsT fhirComponents; + + private final DiscoveryExtension extension; + + /** + * Create a new cdsservice. + * + * @param id Will be used in the url, should be unique. + * @param hook Which hook can call this. + * @param title Human title. + * @param description Human description. + * @param prefetchElements List of prefetch elements, will be in prefetch + * template. + * @param fhirComponents Fhir components to use + * @param extension Custom CDS Hooks extensions. + */ public CdsService(String id, Hook hook, String title, String description, List prefetchElements, FhirComponentsT fhirComponents, DiscoveryExtension extension) { - super(id, hook, title, description, prefetchElements, fhirComponents, extension); + + if (id == null) { + throw new NullPointerException("CDSService id cannot be null"); + } + if (hook == null) { + throw new NullPointerException("CDSService hook cannot be null"); + } + if (description == null) { + throw new NullPointerException("CDSService description cannot be null"); + } + this.id = id; + this.hook = hook; + this.title = title; + this.description = description; + this.prefetchElements = prefetchElements; + prefetch = new Prefetch(); + for (PrefetchTemplateElement prefetchElement : prefetchElements) { + this.prefetch.put(prefetchElement.getKey(), prefetchElement.getQuery()); + } + this.fhirComponents = fhirComponents; + this.extension = extension; + } + + public DiscoveryExtension getExtension() { return extension; } + + public List getPrefetchElements() { + return prefetchElements; } /** @@ -105,22 +192,30 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a if (results.getCoverageRequirements().getApplies()) { - if (coverageRequirements.isDocumentationRequired() || coverageRequirements.isPriorAuthRequired()) { + // if prior auth already approved + if (coverageRequirements.isPriorAuthApproved()) { + response.addCard(CardBuilder.priorAuthCard(results, results.getRequest(), fhirComponents, coverageRequirements.getPriorAuthId(), + request.getContext().getPatientId(), lookupResult.getCriteria().getPayorId(), request.getContext().getUserId(), + applicationBaseUrl.toString() + "/fhir/" + fhirComponents.getFhirVersion().toString(), + fhirResourceRepository)); + + } else if (coverageRequirements.isDocumentationRequired() || coverageRequirements.isPriorAuthRequired()) { if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireOrderUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireFaceToFaceUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireLabUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireProgressNoteUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePARequestUri()) || StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePlanOfCareUri()) - || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireDispenseUri())) { + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireDispenseUri()) + || StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireAdditionalUri())) { List smartAppLinks = createQuestionnaireLinks(request, applicationBaseUrl, lookupResult, results); response.addCard(CardBuilder.transform(results, smartAppLinks)); // add a card for an alternative therapy if there is one if (results.getAlternativeTherapy().getApplies() && hookConfiguration.getAlternativeTherapy()) { try { - response.addCard(CardBuilder.alternativeTherapyCard(results.getAlternativeTherapy(), results.getRequest(), - fhirComponents)); + response.addCard(CardBuilder.alternativeTherapyCard(results.getAlternativeTherapy(), + results.getRequest(), fhirComponents)); } catch (RuntimeException e) { logger.warn("Failed to process alternative therapy: " + e.getMessage()); } @@ -133,7 +228,10 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a // no prior auth or documentation required logger.info("Add the no doc or prior auth required card"); Card card = CardBuilder.transform(results); - card = CardBuilder.createSuggestionsWithNote(card, results, fhirComponents); + card.addSuggestionsItem(CardBuilder.createSuggestionWithNote(card, results.getRequest(), fhirComponents, + "Save Update To EHR", "Update original " + results.getRequest().fhirType() + " to add note", + true)); + card.setSelectionBehavior(Card.SelectionBehaviorEnum.ANY); response.addCard(card); } } @@ -157,7 +255,7 @@ public CdsResponse handleRequest(@Valid @RequestBody requestTypeT request, URL a CardBuilder.errorCardIfNonePresent(response); } - // Adding card to requestLog + // Ading card to requestLog requestLog.setCardListFromCards(response.getCards()); requestService.edit(requestLog); @@ -170,41 +268,47 @@ private List createQuestionnaireLinks(requestTypeT request, URL applicatio CoverageRequirements coverageRequirements = results.getCoverageRequirements(); if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireOrderUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnaireOrderUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Order Form")); + coverageRequirements.getQuestionnaireOrderUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Order Form")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireFaceToFaceUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnaireFaceToFaceUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Face to Face Encounter Form")); + coverageRequirements.getQuestionnaireFaceToFaceUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Face to Face Encounter Form")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireLabUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnaireLabUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Lab Form")); + coverageRequirements.getQuestionnaireLabUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Lab Form")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireProgressNoteUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnaireProgressNoteUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Progress Note")); + coverageRequirements.getQuestionnaireProgressNoteUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Progress Note")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePARequestUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnairePARequestUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "PA Request")); + coverageRequirements.getQuestionnairePARequestUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "PA Request")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnairePlanOfCareUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnairePlanOfCareUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Plan of Care/Certification")); + coverageRequirements.getQuestionnairePlanOfCareUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Plan of Care/Certification")); } if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireDispenseUri())) { listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, - coverageRequirements.getQuestionnaireDispenseUri(), coverageRequirements.getRequestId(), lookupResult.getCriteria(), - coverageRequirements.isPriorAuthRequired(), "Dispense Form")); + coverageRequirements.getQuestionnaireDispenseUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Dispense Form")); + } + + if (StringUtils.isNotEmpty(coverageRequirements.getQuestionnaireAdditionalUri())) { + listOfLinks.add(smartLinkBuilder(request.getContext().getPatientId(), request.getFhirServer(), applicationBaseUrl, + coverageRequirements.getQuestionnaireAdditionalUri(), coverageRequirements.getRequestId(), + lookupResult.getCriteria(), coverageRequirements.isPriorAuthRequired(), "Additional Form")); } return listOfLinks; } @@ -222,7 +326,72 @@ protected Object evaluateStatement(String statement, Context context) { } } + private Link smartLinkBuilder(String patientId, String fhirBase, URL applicationBaseUrl, String questionnaireUri, + String reqResourceId, CoverageRequirementRuleCriteria criteria, boolean priorAuthRequired, String label) { + URI configLaunchUri = myConfig.getLaunchUrl(); + questionnaireUri = applicationBaseUrl + "/fhir/r4/" + questionnaireUri; + + String launchUrl; + if (myConfig.getLaunchUrl().isAbsolute()) { + launchUrl = myConfig.getLaunchUrl().toString(); + } else { + try { + launchUrl = new URL(applicationBaseUrl.getProtocol(), applicationBaseUrl.getHost(), + applicationBaseUrl.getPort(), applicationBaseUrl.getFile() + configLaunchUri.toString(), null).toString(); + } catch (MalformedURLException e) { + String msg = "Error creating smart launch URL"; + logger.error(msg); + throw new RuntimeException(msg); + } + } + + if (fhirBase != null && fhirBase.endsWith("/")) { + fhirBase = fhirBase.substring(0, fhirBase.length() - 1); + } + if (patientId != null && patientId.startsWith("Patient/")) { + patientId = patientId.substring(8); + } + // PARAMS: + // template is the uri of the questionnaire + // request is the ID of the device request or medrec (not the full URI like the + // IG says, since it should be taken from fhirBase + + String filepath = "../../getfile/" + criteria.getQueryString(); + + String appContext = "template=" + questionnaireUri + "&request=" + reqResourceId; + appContext = appContext + "&fhirpath=" + applicationBaseUrl + "/fhir/"; + + appContext = appContext + "&priorauth=" + (priorAuthRequired ? "true" : "false"); + appContext = appContext + "&filepath=" + applicationBaseUrl + "/"; + if (myConfig.getUrlEncodeAppContext()) { + logger.info("CdsService::smartLinkBuilder: URL encoding appcontext"); + try { + appContext = URLEncoder.encode(appContext, StandardCharsets.UTF_8.name()).toString(); + } catch (UnsupportedEncodingException e) { + logger.error("CdsService::smartLinkBuilder: failed to encode URL: " + e.getMessage()); + } + } + + logger.info("smarLinkBuilder: appContext: " + appContext); + + if (myConfig.isAppendParamsToSmartLaunchUrl()) { + launchUrl = launchUrl + "?iss=" + fhirBase + "&patientId=" + patientId + "&template=" + questionnaireUri + + "&request=" + reqResourceId; + } else { + // TODO: The iss should be set by the EHR? + launchUrl = launchUrl; + } + + Link link = new Link(); + link.setType("smart"); + link.setLabel(label); + link.setUrl(launchUrl); + + link.setAppContext(appContext); + + return link; + } // Implement these in child class public abstract List createCqlExecutionContexts(requestTypeT request, diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceInformation.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceInformation.java index ca7ea731c..2e08fbcbc 100755 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceInformation.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/CdsServiceInformation.java @@ -4,14 +4,14 @@ import java.util.List; public class CdsServiceInformation { - private List services = null; + private List services = null; /** * Add a service. * @param servicesItem The service. * @return */ - public CdsServiceInformation addServicesItem(CdsAbstract servicesItem) { + public CdsServiceInformation addServicesItem(CdsService servicesItem) { if (this.services == null) { this.services = new ArrayList<>(); } @@ -19,11 +19,11 @@ public CdsServiceInformation addServicesItem(CdsAbstract servicesItem) { return this; } - public List getServices() { + public List getServices() { return services; } - public void setServices(List services) { + public void setServices(List services) { this.services = services; } } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java index a5be23692..538169ef9 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/cdshooks/services/crd/r4/FhirBundleProcessor.java @@ -18,8 +18,6 @@ import java.util.List; import java.util.stream.Collectors; -import static org.hl7.davinci.endpoint.Utils.idInSelectionsList; - public class FhirBundleProcessor { static final Logger logger = LoggerFactory.getLogger(FhirBundleProcessor.class); @@ -50,7 +48,7 @@ public void processDeviceRequests() { logger.info("r4/FhirBundleProcessor::getAndProcessDeviceRequests: DeviceRequest(s) found"); for (DeviceRequest deviceRequest : deviceRequestList) { - if (idInSelectionsList(deviceRequest.getId(), selections)) { + if (idInSelectionsList(deviceRequest.getId())) { List criteriaList = createCriteriaList(deviceRequest.getCodeCodeableConcept(), deviceRequest.getInsurance(), null); buildExecutionContexts(criteriaList, (Patient) deviceRequest.getSubject().getResource(), "device_request", deviceRequest); } @@ -65,7 +63,7 @@ public void processMedicationRequests() { logger.info("r4/FhirBundleProcessor::getAndProcessMedicationRequests: MedicationRequest(s) found"); for (MedicationRequest medicationRequest : medicationRequestList) { - if (idInSelectionsList(medicationRequest.getId(), selections)) { + if (idInSelectionsList(medicationRequest.getId())) { List criteriaList = createCriteriaList(medicationRequest.getMedicationCodeableConcept(), medicationRequest.getInsurance(), null); buildExecutionContexts(criteriaList, (Patient) medicationRequest.getSubject().getResource(), "medication_request", medicationRequest); } @@ -83,7 +81,7 @@ public void processMedicationDispenses() { medicationDispenseBundle); for (MedicationDispense medicationDispense : medicationDispenseList) { - if (idInSelectionsList(medicationDispense.getId(), selections)) { + if (idInSelectionsList(medicationDispense.getId())) { List criteriaList = createCriteriaList(medicationDispense.getMedicationCodeableConcept(), null, payorList); buildExecutionContexts(criteriaList, (Patient) medicationDispense.getSubject().getResource(), "medication_dispense", medicationDispense); } @@ -98,7 +96,7 @@ public void processServiceRequests() { logger.info("r4/FhirBundleProcessor::getAndProcessServiceRequests: ServiceRequest(s) found"); for (ServiceRequest serviceRequest : serviceRequestList) { - if (idInSelectionsList(serviceRequest.getId(), selections)) { + if (idInSelectionsList(serviceRequest.getId())) { List criteriaList = createCriteriaList(serviceRequest.getCode(), serviceRequest.getInsurance(), null); buildExecutionContexts(criteriaList, (Patient) serviceRequest.getSubject().getResource(), "service_request", serviceRequest); } @@ -118,7 +116,7 @@ public void processOrderSelectMedicationStatements() { // process each of the MedicationRequests for (MedicationRequest medicationRequest : medicationRequestList) { - if (idInSelectionsList(medicationRequest.getId(), selections)) { + if (idInSelectionsList(medicationRequest.getId())) { // run on each of the MedicationStatements for (MedicationStatement medicationStatement : medicationStatementList) { @@ -212,4 +210,29 @@ private void buildExecutionContexts(List criter } } + private String stripResourceType(String identifier) { + int indexOfDivider = identifier.indexOf('/'); + if (indexOfDivider+1 == identifier.length()) { + // remove the trailing '/' + return identifier.substring(0, indexOfDivider); + } else { + return identifier.substring(indexOfDivider+1); + } + } + + private boolean idInSelectionsList(String identifier) { + if (this.selections.isEmpty()) { + // if selections list is empty, just assume we should process the request + return true; + } else { + for ( String selection : selections) { + if (identifier.contains(stripResourceType(selection))) { + logger.info("r4/FhirBundleProcessor::idInSelectionsList(" + identifier + "): identifier found in selections list"); + return true; + } + } + return false; + } + } + } diff --git a/server/src/main/java/org/hl7/davinci/endpoint/controllers/r4/CdsHooksController.java b/server/src/main/java/org/hl7/davinci/endpoint/controllers/r4/CdsHooksController.java index b3afdfcc3..cb4ea5be6 100644 --- a/server/src/main/java/org/hl7/davinci/endpoint/controllers/r4/CdsHooksController.java +++ b/server/src/main/java/org/hl7/davinci/endpoint/controllers/r4/CdsHooksController.java @@ -1,10 +1,11 @@ package org.hl7.davinci.endpoint.controllers.r4; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; import org.cdshooks.CdsResponse; import org.hl7.davinci.endpoint.Utils; import org.hl7.davinci.endpoint.cdshooks.services.crd.CdsServiceInformation; import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.OrderSelectService; -import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.OrderSelectServiceRems; import org.hl7.davinci.endpoint.cdshooks.services.crd.r4.OrderSignService; import org.hl7.davinci.r4.crdhook.CrdPrefetch; import org.hl7.davinci.r4.crdhook.orderselect.OrderSelectRequest; @@ -12,10 +13,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import javax.validation.Valid; +import java.io.IOException; +import java.util.stream.Collectors; @RestController("r4_CdsHooksController") public class CdsHooksController { @@ -28,7 +33,7 @@ public class CdsHooksController { @Autowired private OrderSelectService orderSelectService; @Autowired private OrderSignService orderSignService; - @Autowired private OrderSelectServiceRems orderSelectServiceRems; + /** * The FHIR r4 services discovery endpoint. * @return A services object containing an array of all services available on this server @@ -59,21 +64,6 @@ public CdsResponse handleOrderSelect(@Valid @RequestBody OrderSelectRequest requ return orderSelectService.handleRequest(request, Utils.getApplicationBaseUrl(httpServletRequest)); } - /** - * The coverage requirement discovery endpoint for the order select rems hook. - * @param request An order select triggered cds request - * @return The card response - */ - @CrossOrigin - @PostMapping(value = FHIR_RELEASE + URL_BASE + "/" + OrderSelectServiceRems.ID, - consumes = "application/json;charset=UTF-8") - public CdsResponse handleOrderSelectRems(@Valid @RequestBody OrderSelectRequest request, final HttpServletRequest httpServletRequest) { - logger.info("r4/handleOrderSelectRems"); - if (request.getPrefetch() == null) { - request.setPrefetch(new CrdPrefetch()); - } - return orderSelectServiceRems.handleRequest(request, Utils.getApplicationBaseUrl(httpServletRequest)); - } /** * The coverage requirement discovery endpoint for the order sign hook. * @param request An order sign triggered cds request From 2e08689e5b00b95b903bfcea0941bdec12b5a476 Mon Sep 17 00:00:00 2001 From: smalho01 <88040167+smalho01@users.noreply.github.com> Date: Wed, 12 Jan 2022 02:24:18 -0500 Subject: [PATCH 30/30] Delete dockerRunner.sh --- dockerRunner.sh | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100755 dockerRunner.sh diff --git a/dockerRunner.sh b/dockerRunner.sh deleted file mode 100755 index fc2adb2ac..000000000 --- a/dockerRunner.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# Handle closing application on signal interrupt (ctrl + c) -trap 'kill $CONTINUOUS_BUILD_PID $SERVER_PID; gradle --stop; exit' INT - -# Reset log file content for new application boot -echo "*** Logs for 'gradle installBootDist --continuous' ***" > builder.log -echo "*** Logs for 'gradle bootRun' ***" > runner.log - -# Print that the application is starting in watch mode -echo "starting application in watch mode..." - -# Start the continious build listener process -echo "starting continuous build listener..." -gradle build --continuous 2>&1 | tee builder.log & CONTINUOUS_BUILD_PID=$! - -# Start server process once initial build finishes -( while ! grep -m1 'BUILD SUCCESSFUL' < builder.log; do - sleep 15 -done -echo "starting crd server..." -gradle bootRun 2>&1 | tee runner.log ) & SERVER_PID=$! - -# Handle application background process exiting -wait $CONTINUOUS_BUILD_PID $SERVER_PID -EXIT_CODE=$? -echo "application exited with exit code $EXIT_CODE..." \ No newline at end of file