diff --git a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/Questionnaires.java b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/Questionnaires.java index 033d160db6..65589b9ff1 100644 --- a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/Questionnaires.java +++ b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/Questionnaires.java @@ -1,11 +1,13 @@ package org.opencds.cqf.fhir.benchmark; import static org.opencds.cqf.fhir.benchmark.questionnaire.TestQuestionnaire.given; -import static org.opencds.cqf.fhir.utility.r4.Parameters.parameters; -import static org.opencds.cqf.fhir.utility.r4.Parameters.stringPart; +import static org.opencds.cqf.fhir.utility.Parameters.newPart; +import static org.opencds.cqf.fhir.utility.Parameters.newStringPart; import ca.uhn.fhir.context.FhirContext; +import java.util.Arrays; import java.util.concurrent.TimeUnit; +import org.hl7.fhir.instance.model.api.IBaseBackboneElement; import org.hl7.fhir.r4.model.IdType; import org.opencds.cqf.fhir.benchmark.questionnaire.TestQuestionnaire.When; import org.openjdk.jmh.annotations.Benchmark; @@ -33,11 +35,27 @@ public void setupTrial() throws Exception { this.result = given().repositoryFor(FHIR_CONTEXT, "r4/pa-aslp") .when() .questionnaireId(new IdType("Questionnaire", "ASLPA1")) - .subjectId("positive") - .parameters(parameters( - stringPart("Service Request Id", "SleepStudy"), - stringPart("Service Request Id", "SleepStudy2"), - stringPart("Coverage Id", "Coverage-positive"))); + .context(Arrays.asList( + (IBaseBackboneElement) newPart( + FHIR_CONTEXT, + "context", + newStringPart(FHIR_CONTEXT, "name", "patient"), + newPart(FHIR_CONTEXT, "Reference", "content", "Patient/positive")), + (IBaseBackboneElement) newPart( + FHIR_CONTEXT, + "context", + newStringPart(FHIR_CONTEXT, "name", "ServiceRequest"), + newPart(FHIR_CONTEXT, "Reference", "content", "ServiceRequest/SleepStudy")), + (IBaseBackboneElement) newPart( + FHIR_CONTEXT, + "context", + newStringPart(FHIR_CONTEXT, "name", "ServiceRequest"), + newPart(FHIR_CONTEXT, "Reference", "content", "ServiceRequest/SleepStudy2")), + (IBaseBackboneElement) newPart( + FHIR_CONTEXT, + "context", + newStringPart(FHIR_CONTEXT, "name", "Coverage"), + newPart(FHIR_CONTEXT, "Reference", "content", "Coverage/Coverage-positive")))); } @Benchmark diff --git a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/helpers/RequestHelpers.java b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/helpers/RequestHelpers.java index bee65c5066..e34cf4e562 100644 --- a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/helpers/RequestHelpers.java +++ b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/helpers/RequestHelpers.java @@ -2,6 +2,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import java.util.List; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseResource; import org.opencds.cqf.fhir.cql.LibraryEngine; @@ -101,7 +102,7 @@ public static GenerateRequest newGenerateRequestForVersion( } public static GenerateRequest newGenerateRequestForVersion(IBaseResource profile, LibraryEngine libraryEngine) { - return new GenerateRequest(profile, false, true, libraryEngine); + return new GenerateRequest(List.of(profile), false, true, libraryEngine); } public static PopulateRequest newPopulateRequestForVersion( @@ -111,7 +112,6 @@ public static PopulateRequest newPopulateRequestForVersion( Ids.newId(fhirVersion, Ids.ensureIdType(PATIENT_ID, "Patient")), null, null, - null, libraryEngine); } diff --git a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/helpers/TestEvaluationSettings.java b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/helpers/TestEvaluationSettings.java new file mode 100644 index 0000000000..6b04df5640 --- /dev/null +++ b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/helpers/TestEvaluationSettings.java @@ -0,0 +1,21 @@ +package org.opencds.cqf.fhir.benchmark.helpers; + +import org.opencds.cqf.fhir.cql.EvaluationSettings; +import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; +import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; +import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; + +public class TestEvaluationSettings { + + public static EvaluationSettings defaultTestEvaluationSettings() { + var evaluationSettings = EvaluationSettings.getDefault(); + evaluationSettings + .getRetrieveSettings() + .setSearchParameterMode(SEARCH_FILTER_MODE.FILTER_IN_MEMORY) + .setTerminologyParameterMode(TERMINOLOGY_FILTER_MODE.FILTER_IN_MEMORY); + evaluationSettings + .getTerminologySettings() + .setValuesetExpansionMode(VALUESET_EXPANSION_MODE.PERFORM_NAIVE_EXPANSION); + return evaluationSettings; + } +} diff --git a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/questionnaire/TestQuestionnaire.java b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/questionnaire/TestQuestionnaire.java index adb5fc4321..732be9df5f 100644 --- a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/questionnaire/TestQuestionnaire.java +++ b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/questionnaire/TestQuestionnaire.java @@ -1,27 +1,58 @@ package org.opencds.cqf.fhir.benchmark.questionnaire; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.opencds.cqf.fhir.benchmark.helpers.TestEvaluationSettings.defaultTestEvaluationSettings; import static org.opencds.cqf.fhir.test.Resources.getResourcePath; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.repository.IRepository; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBackboneElement; import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.json.JSONException; +import org.opencds.cqf.fhir.benchmark.helpers.DataRequirementsLibrary; +import org.opencds.cqf.fhir.benchmark.helpers.GeneratedPackage; import org.opencds.cqf.fhir.cql.EvaluationSettings; -import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; -import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; -import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; +import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.CrSettings; +import org.opencds.cqf.fhir.cr.common.IDataRequirementsProcessor; +import org.opencds.cqf.fhir.cr.common.IOperationProcessor; +import org.opencds.cqf.fhir.cr.common.IPackageProcessor; import org.opencds.cqf.fhir.cr.questionnaire.QuestionnaireProcessor; +import org.opencds.cqf.fhir.cr.questionnaire.generate.GenerateRequest; +import org.opencds.cqf.fhir.cr.questionnaire.generate.IGenerateProcessor; +import org.opencds.cqf.fhir.cr.questionnaire.populate.IPopulateProcessor; +import org.opencds.cqf.fhir.cr.questionnaire.populate.PopulateRequest; +import org.opencds.cqf.fhir.utility.Constants; +import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.VersionUtilities; +import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; +import org.opencds.cqf.fhir.utility.adapter.IItemComponentAdapter; +import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireAdapter; +import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireItemComponentAdapter; +import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireResponseAdapter; +import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireResponseItemAnswerComponentAdapter; +import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireResponseItemComponentAdapter; import org.opencds.cqf.fhir.utility.monad.Eithers; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; +import org.skyscreamer.jsonassert.JSONAssert; +@SuppressWarnings("UnstableApiUsage") public class TestQuestionnaire { public static final String CLASS_PATH = "org/opencds/cqf/fhir/cr/shared"; @@ -29,10 +60,15 @@ public static Given given() { return new Given(); } - @SuppressWarnings("UnstableApiUsage") public static class Given { private IRepository repository; private EvaluationSettings evaluationSettings; + private final List operationProcessors = new ArrayList<>(); + + public Given repository(IRepository repository) { + this.repository = repository; + return this; + } public Given repositoryFor(FhirContext fhirContext, String repositoryPath) { this.repository = new IgRepository( @@ -40,43 +76,82 @@ public Given repositoryFor(FhirContext fhirContext, String repositoryPath) { return this; } + public Given evaluationSettings(EvaluationSettings evaluationSettings) { + this.evaluationSettings = evaluationSettings; + return this; + } + + public Given generateProcessor(IGenerateProcessor generateProcessor) { + operationProcessors.add(generateProcessor); + return this; + } + + public Given packageProcessor(IPackageProcessor packageProcessor) { + operationProcessors.add(packageProcessor); + return this; + } + + public Given dataRequirementsProcessor(IDataRequirementsProcessor dataRequirementsProcessor) { + operationProcessors.add(dataRequirementsProcessor); + return this; + } + + public Given populateProcessor(IPopulateProcessor populateProcessor) { + operationProcessors.add(populateProcessor); + return this; + } + public QuestionnaireProcessor buildProcessor(IRepository repository) { if (evaluationSettings == null) { - evaluationSettings = EvaluationSettings.getDefault(); - evaluationSettings - .getRetrieveSettings() - .setSearchParameterMode(SEARCH_FILTER_MODE.FILTER_IN_MEMORY) - .setTerminologyParameterMode(TERMINOLOGY_FILTER_MODE.FILTER_IN_MEMORY); - - evaluationSettings - .getTerminologySettings() - .setValuesetExpansionMode(VALUESET_EXPANSION_MODE.PERFORM_NAIVE_EXPANSION); + evaluationSettings = defaultTestEvaluationSettings(); } var crSettings = CrSettings.getDefault().withEvaluationSettings(evaluationSettings); - return new QuestionnaireProcessor(repository, crSettings, null); + return new QuestionnaireProcessor(repository, crSettings, operationProcessors); } public When when() { - return new When(buildProcessor(repository)); + return new When(repository, buildProcessor(repository)); } } public static class When { - + private final IRepository repository; private final QuestionnaireProcessor processor; private IPrimitiveType questionnaireUrl; private IIdType questionnaireId; private IBaseResource questionnaire; private String subjectId; private List context; - private IBaseExtension launchContext; - private final boolean useServerData; + private boolean useServerData; private IBaseBundle data; private IBaseParameters parameters; + private Boolean isPut; + private IBaseResource profile; + private List> profileUrl; - When(QuestionnaireProcessor processor) { + When(IRepository repository, QuestionnaireProcessor processor) { + this.repository = repository; this.processor = processor; useServerData = true; + profileUrl = new ArrayList<>(); + } + + private FhirContext fhirContext() { + return repository.fhirContext(); + } + + private PopulateRequest buildRequest() { + return new PopulateRequest( + processor.resolveQuestionnaire(Eithers.for3(questionnaireUrl, questionnaireId, questionnaire)), + subjectId == null ? null : Ids.newId(fhirContext(), "Patient", subjectId), + context, + data, + new LibraryEngine(repository, processor.getSettings().getEvaluationSettings())); + } + + public When questionnaireUrl(IPrimitiveType url) { + questionnaireUrl = url; + return this; } public When questionnaireId(IIdType id) { @@ -84,6 +159,11 @@ public When questionnaireId(IIdType id) { return this; } + public When questionnaire(IBaseResource resource) { + questionnaire = resource; + return this; + } + public When subjectId(String id) { subjectId = id; return this; @@ -94,22 +174,285 @@ public When context(List context) { return this; } + public When useServerData(boolean value) { + useServerData = value; + return this; + } + + public When additionalData(IBaseBundle data) { + this.data = data; + return this; + } + public When parameters(IBaseParameters params) { parameters = params; return this; } + public When isPut(Boolean value) { + isPut = value; + return this; + } + + public When profile(IBaseResource profile) { + this.profile = profile; + return this; + } + + public When profileUrl(IPrimitiveType url) { + profileUrl.add(url); + return this; + } + public IBaseResource runPopulate() { return processor.populate( Eithers.for3(questionnaireUrl, questionnaireId, questionnaire), subjectId, context, - launchContext, data, useServerData, (IBaseResource) null, null, null); } + + public GeneratedQuestionnaireResponse thenPopulate(Boolean buildRequest) { + if (buildRequest) { + var populateRequest = buildRequest(); + return new GeneratedQuestionnaireResponse( + repository, populateRequest, processor.populate(populateRequest)); + } else { + return new GeneratedQuestionnaireResponse(repository, null, runPopulate()); + } + } + + public GeneratedPackage thenPackage() { + var param = Eithers.for3(questionnaireUrl, questionnaireId, questionnaire); + return new GeneratedPackage( + isPut == null + ? processor.packageQuestionnaire(param) + : processor.packageQuestionnaire(param, isPut), + fhirContext()); + } + + public GeneratedQuestionnaire thenGenerate() { + var profiles = new ArrayList(); + if (profile != null) { + profiles.add(profile); + } + profileUrl.stream() + .map(url -> (IBaseResource) processor.resolveStructureDefinition(Eithers.forLeft3(url))) + .forEach(profiles::add); + var request = new GenerateRequest( + profiles, + false, + true, + new LibraryEngine(repository, processor.getSettings().getEvaluationSettings())); + return new GeneratedQuestionnaire(repository, request, processor.generateQuestionnaire(request, null)); + } + + public DataRequirementsLibrary thenDataRequirements() { + var param = Eithers.for3(questionnaireUrl, questionnaireId, questionnaire); + return new DataRequirementsLibrary(processor.dataRequirements(param, parameters)); + } + } + + public static class GeneratedQuestionnaire { + public IQuestionnaireAdapter questionnaire; + IRepository repository; + IAdapterFactory adapterFactory; + IParser jsonParser; + GenerateRequest request; + List items; + IIdType expectedQuestionnaireId; + + private void populateItems(List itemList) { + itemList.forEach(item -> { + items.add(item.get()); + if (item.hasItem()) { + populateItems(item.getItem()); + } + }); + } + + public GeneratedQuestionnaire(IRepository repository, GenerateRequest request, IBaseResource questionnaire) { + this.repository = repository; + this.request = request; + adapterFactory = IAdapterFactory.forFhirContext(this.repository.fhirContext()); + this.questionnaire = adapterFactory.createQuestionnaire(questionnaire); + jsonParser = this.repository.fhirContext().newJsonParser().setPrettyPrint(true); + items = new ArrayList<>(); + if (request != null) { + populateItems(this.questionnaire.getItem()); + expectedQuestionnaireId = Ids.newId( + questionnaire.getClass(), + request.getQuestionnaire().getIdElement().getIdPart()); + } + } + + public void isEqualsToExpected(Class resourceType) { + try { + JSONAssert.assertEquals( + jsonParser.encodeResourceToString(repository.read(resourceType, expectedQuestionnaireId)), + jsonParser.encodeResourceToString(questionnaire.get()), + true); + } catch (JSONException e) { + e.printStackTrace(); + fail("Unable to compare Jsons: " + e.getMessage()); + } + } + + public GeneratedQuestionnaire hasItems(int expectedItemCount) { + assertEquals(expectedItemCount, items.size()); + + return this; + } + + public GeneratedQuestionnaire hasExtension(String url) { + return hasExtensions(url, 1); + } + + public GeneratedQuestionnaire hasExtensions(String url, int count) { + assertEquals(count, questionnaire.getExtensionsByUrl(url).size()); + return this; + } + + public GeneratedQuestionnaire itemHasInitial(String theLinkId) { + var matchingItems = items.stream() + .map(i -> IAdapterFactory.createAdapterForBase( + repository.fhirContext().getVersion().getVersion(), i)) + .map(IQuestionnaireItemComponentAdapter.class::cast) + .filter(i -> i.getLinkId().equals(theLinkId)) + .toList(); + for (var item : matchingItems) { + assertTrue(item.hasInitial()); + } + + return this; + } + + public GeneratedQuestionnaire hasErrors() { + assertTrue(questionnaire.hasExtension(Constants.CQF_MESSAGES)); + assertTrue(questionnaire.hasContained()); + assertTrue(questionnaire.getContained().stream() + .anyMatch(r -> r.fhirType().equals("OperationOutcome"))); + + return this; + } + } + + public static class GeneratedQuestionnaireResponse { + IRepository repository; + IAdapterFactory adapterFactory; + IParser jsonParser; + PopulateRequest request; + IQuestionnaireResponseAdapter questionnaireResponse; + Map items; + IIdType expectedId; + + private void populateItems(List itemList) { + itemList.stream() + .map(IQuestionnaireResponseItemComponentAdapter.class::cast) + .forEach(item -> { + items.put(item.getLinkId(), item); + if (item.hasItem()) { + populateItems(item.getItem()); + } + if (item.hasAnswer()) { + item.getAnswer().forEach(answer -> { + if (answer.hasItem()) { + populateItems(answer.getItem()); + } + }); + } + }); + } + + public GeneratedQuestionnaireResponse( + IRepository repository, PopulateRequest request, IBaseResource questionnaireResponse) { + this.repository = repository; + adapterFactory = IAdapterFactory.forFhirContext(this.repository.fhirContext()); + this.request = request; + this.questionnaireResponse = adapterFactory.createQuestionnaireResponse(questionnaireResponse); + jsonParser = this.repository.fhirContext().newJsonParser().setPrettyPrint(true); + items = new HashMap<>(); + if (request != null) { + populateItems(this.questionnaireResponse.getItem()); + expectedId = Ids.newId( + questionnaireResponse.getClass(), + "%s-%s" + .formatted( + request.getQuestionnaire() + .getIdElement() + .getIdPart(), + request.getSubjectId().getIdPart())); + } + } + + public GeneratedQuestionnaireResponse hasItems(int expectedItemCount) { + assertEquals(expectedItemCount, items.size()); + + return this; + } + + public GeneratedQuestionnaireResponse itemHasAnswer(String linkId) { + assertTrue(items.get(linkId).hasAnswer()); + return this; + } + + public GeneratedQuestionnaireResponse itemHasNoAnswer(String linkId) { + assertFalse(items.get(linkId).hasAnswer()); + return this; + } + + public GeneratedQuestionnaireResponse itemHasAnswerValue(String linkId, String value) { + return itemHasAnswerValue( + linkId, + VersionUtilities.stringTypeForVersion( + repository.fhirContext().getVersion().getVersion(), value)); + } + + public GeneratedQuestionnaireResponse itemHasAnswerValue(String linkId, IBase value) { + var answers = items.get(linkId).getAnswer().stream() + .map(IQuestionnaireResponseItemAnswerComponentAdapter::getValue) + .toList(); + assertNotNull(answers); + assertFalse(answers.isEmpty()); + if (value instanceof IPrimitiveType) { + assertTrue( + answers.stream().anyMatch(a -> a.toString().equals(value.toString())), + "expected answer to contain value: " + value); + } else { + assertTrue(answers.stream().anyMatch(value.getClass()::isInstance)); + } + return this; + } + + public GeneratedQuestionnaireResponse itemHasAuthorExt(String linkId) { + assertNotNull(items.get(linkId).getExtensionByUrl(Constants.QUESTIONNAIRE_RESPONSE_AUTHOR)); + return this; + } + + public GeneratedQuestionnaireResponse itemHasNoAuthorExt(String linkId) { + assertNull(items.get(linkId).getExtensionsByUrl(Constants.QUESTIONNAIRE_RESPONSE_AUTHOR)); + return this; + } + + public GeneratedQuestionnaireResponse hasErrors() { + assertTrue(questionnaireResponse.hasExtension(Constants.CQF_MESSAGES)); + assertTrue(questionnaireResponse.hasContained()); + assertTrue(questionnaireResponse.getContained().stream() + .anyMatch(r -> r.fhirType().equals("OperationOutcome"))); + + return this; + } + + public GeneratedQuestionnaireResponse hasNoErrors() { + assertFalse(questionnaireResponse.hasExtension(Constants.CQF_MESSAGES)); + assertTrue(questionnaireResponse.getContained().stream() + .noneMatch(r -> r.fhirType().equals("OperationOutcome"))); + + return this; + } } } diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/questionnaire/QuestionnairePopulateProvider.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/questionnaire/QuestionnairePopulateProvider.java index 2360db0cae..aa8b796061 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/questionnaire/QuestionnairePopulateProvider.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/questionnaire/QuestionnairePopulateProvider.java @@ -21,9 +21,7 @@ import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; -import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.QuestionnaireResponse; @@ -51,8 +49,6 @@ public QuestionnairePopulateProvider(IQuestionnaireProcessorFactory questionnair * @param id The id of the Questionnaire to populate. * @param subject The subject(s) that is/are the target of the Questionnaire. * @param context Resources containing information to be used to help populate the QuestionnaireResponse. - * @param launchContext The Questionnaire Launch Context extension containing Resources that provide context for form processing logic (pre-population) when creating/displaying/editing a QuestionnaireResponse. - * @param parameters This parameter has been deprecated and is no longer used * @param useServerData Whether to use data from the server performing the evaluation. * @param data Data to be made available during CQL evaluation. * @param bundle Legacy support for data parameter. @@ -70,8 +66,6 @@ public QuestionnaireResponse populate( @IdParam IdType id, @OperationParam(name = "subject") ParametersParameterComponent subject, @OperationParam(name = "context") List context, - @OperationParam(name = "launchContext") Extension launchContext, - @OperationParam(name = "parameters") Parameters parameters, @OperationParam(name = "local") BooleanType local, @OperationParam(name = "useServerData") BooleanType useServerData, @OperationParam(name = "data") Bundle data, @@ -87,7 +81,6 @@ public QuestionnaireResponse populate( Eithers.forMiddle3(id), getStringOrReferenceValue(fhirVersion, subject), context, - launchContext, data == null ? bundle : data, isUseServerData(local, useServerData), getEndpoint(fhirVersion, dataEndpoint), @@ -107,8 +100,6 @@ public QuestionnaireResponse populate( * @param version Version of the Questionnaire when invoked at the resource type level. This is exclusive with the questionnaire and canonical parameters. * @param subject The subject(s) that is/are the target of the Questionnaire. * @param context Resources containing information to be used to help populate the QuestionnaireResponse. - * @param launchContext The Questionnaire Launch Context extension containing Resources that provide context for form processing logic (pre-population) when creating/displaying/editing a QuestionnaireResponse. - * @param parameters This parameter has been deprecated and is no longer used * @param useServerData Whether to use data from the server performing the evaluation. * @param data Data to be made available during CQL evaluation. * @param bundle Legacy support for data parameter. @@ -129,8 +120,6 @@ public QuestionnaireResponse populate( @OperationParam(name = "version") StringType version, @OperationParam(name = "subject") ParametersParameterComponent subject, @OperationParam(name = "context") List context, - @OperationParam(name = "launchContext") Extension launchContext, - @OperationParam(name = "parameters") Parameters parameters, @OperationParam(name = "local") BooleanType local, @OperationParam(name = "useServerData") BooleanType useServerData, @OperationParam(name = "data") Bundle data, @@ -146,7 +135,6 @@ public QuestionnaireResponse populate( getQuestionnaireMonad(questionnaire, canonical, url, version), getStringOrReferenceValue(fhirVersion, subject), context, - launchContext, data == null ? bundle : data, isUseServerData(local, useServerData), getEndpoint(fhirVersion, dataEndpoint), diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/structuredefinition/StructureDefinitionQuestionnaireProvider.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/structuredefinition/StructureDefinitionQuestionnaireProvider.java index 94837b8e8a..fa77d0cd77 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/structuredefinition/StructureDefinitionQuestionnaireProvider.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/structuredefinition/StructureDefinitionQuestionnaireProvider.java @@ -1,24 +1,20 @@ package org.opencds.cqf.fhir.cr.hapi.r4.structuredefinition; -import static org.opencds.cqf.fhir.cr.hapi.common.CanonicalHelper.getCanonicalType; import static org.opencds.cqf.fhir.utility.EndpointHelper.getEndpoint; import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.provider.ProviderConstants; +import java.util.List; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Endpoint; -import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r4.model.Questionnaire; -import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StructureDefinition; import org.opencds.cqf.fhir.cr.hapi.common.IQuestionnaireProcessorFactory; -import org.opencds.cqf.fhir.utility.monad.Eithers; @SuppressWarnings("java:S107") public class StructureDefinitionQuestionnaireProvider { @@ -35,9 +31,10 @@ public StructureDefinitionQuestionnaireProvider(IQuestionnaireProcessorFactory q * "https://hl7.org/fhir/structuredefinition-operation-questionnaire.html">$questionnaire * operation. * - * @param id The id of the StructureDefinition. + * @param resource The StructureDefinition to base the Questionnaire on. Used when the operation is invoked at the 'type' level. + * @param url Canonical URL's of the StructureDefinitions when invoked at the resource type level. * @param supportedOnly If true (default: false), the questionnaire will only include those elements marked as "mustSupport='true'" in the StructureDefinition. - * @param requiredOnly If true (default: false), the questionnaire will only include those elements marked as "min>0" in the StructureDefinition. + * @param minimalOnly If true (default: false), the questionnaire will only include those elements marked as "min>0" in the StructureDefinition. * @param contentEndpoint The FHIR {@link Endpoint} Endpoint resource or url to use to access content (i.e. libraries) referenced by the StructureDefinition. * @param terminologyEndpoint The FHIR {@link Endpoint} Endpoint resource or url to use to access terminology (i.e. valuesets, codesystems, and membership testing) * referenced by the StructureDefinition. @@ -47,58 +44,20 @@ public StructureDefinitionQuestionnaireProvider(IQuestionnaireProcessorFactory q */ @Operation(name = ProviderConstants.CR_OPERATION_QUESTIONNAIRE, idempotent = true, type = StructureDefinition.class) public Questionnaire questionnaire( - @IdParam IdType id, + @OperationParam(name = "resource") StructureDefinition resource, + @OperationParam(name = "url", typeName = "uri") List> url, @OperationParam(name = "supportedOnly") BooleanType supportedOnly, - @OperationParam(name = "requiredOnly") BooleanType requiredOnly, + @OperationParam(name = "minimalOnly") BooleanType minimalOnly, @OperationParam(name = "contentEndpoint") ParametersParameterComponent contentEndpoint, @OperationParam(name = "terminologyEndpoint") ParametersParameterComponent terminologyEndpoint, RequestDetails requestDetails) { return (Questionnaire) questionnaireProcessorFactory .create(requestDetails) .generateQuestionnaire( - Eithers.forMiddle3(id), + resource, + url, supportedOnly == null ? Boolean.FALSE : supportedOnly.booleanValue(), - requiredOnly == null ? Boolean.FALSE : requiredOnly.booleanValue(), - getEndpoint(fhirVersion, contentEndpoint), - getEndpoint(fhirVersion, terminologyEndpoint), - null); - } - - /** - * Implements the $questionnaire - * operation. - * - * @param profile The StructureDefinition to base the Questionnaire on. Used when the operation is invoked at the 'type' level. - * @param canonical The canonical identifier for the StructureDefinition (optionally version-specific). - * @param url Canonical URL of the StructureDefinition when invoked at the resource type level. This is exclusive with the profile and canonical parameters. - * @param version Version of the StructureDefinition when invoked at the resource type level. This is exclusive with the profile and canonical parameters. - * @param supportedOnly If true (default: false), the questionnaire will only include those elements marked as "mustSupport='true'" in the StructureDefinition. - * @param requiredOnly If true (default: false), the questionnaire will only include those elements marked as "min>0" in the StructureDefinition. - * @param contentEndpoint The FHIR {@link Endpoint} Endpoint resource or url to use to access content (i.e. libraries) referenced by the StructureDefinition. - * @param terminologyEndpoint The FHIR {@link Endpoint} Endpoint resource or url to use to access terminology (i.e. valuesets, codesystems, and membership testing) - * referenced by the StructureDefinition. - * @param requestDetails The details (such as tenant) of this request. Usually - * autopopulated HAPI. - * @return The questionnaire form generated based on the StructureDefinition. - */ - @Operation(name = ProviderConstants.CR_OPERATION_QUESTIONNAIRE, idempotent = true, type = StructureDefinition.class) - public Questionnaire questionnaire( - @OperationParam(name = "profile") StructureDefinition profile, - @OperationParam(name = "canonical", typeName = "canonical") IPrimitiveType canonical, - @OperationParam(name = "url", typeName = "uri") IPrimitiveType url, - @OperationParam(name = "version") StringType version, - @OperationParam(name = "supportedOnly") BooleanType supportedOnly, - @OperationParam(name = "requiredOnly") BooleanType requiredOnly, - @OperationParam(name = "contentEndpoint") ParametersParameterComponent contentEndpoint, - @OperationParam(name = "terminologyEndpoint") ParametersParameterComponent terminologyEndpoint, - RequestDetails requestDetails) { - return (Questionnaire) questionnaireProcessorFactory - .create(requestDetails) - .generateQuestionnaire( - Eithers.for3(getCanonicalType(fhirVersion, canonical, url, version), null, profile), - supportedOnly == null ? Boolean.FALSE : supportedOnly.booleanValue(), - requiredOnly == null ? Boolean.FALSE : requiredOnly.booleanValue(), + minimalOnly == null ? Boolean.FALSE : minimalOnly.booleanValue(), getEndpoint(fhirVersion, contentEndpoint), getEndpoint(fhirVersion, terminologyEndpoint), null); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/DynamicValueProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/DynamicValueProcessor.java index d12d3a152a..4084d312f4 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/DynamicValueProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/DynamicValueProcessor.java @@ -100,7 +100,8 @@ protected void resolveDynamicValue( request, cqfExpression, context == null ? null : context.get(), - resourceAdapter == null ? null : resourceAdapter.get()); + resourceAdapter == null ? null : resourceAdapter.get(), + requestAction); if (result == null || result.isEmpty()) { var warning = "Null value received when evaluating dynamic value expression: %s" .formatted(cqfExpression.getExpression()); @@ -135,13 +136,21 @@ protected void resolveDynamicValue( } protected List getDynamicValueExpressionResult( - ICpgRequest request, CqfExpression cqfExpression, IBaseResource context, IBaseResource resource) { + ICpgRequest request, + CqfExpression cqfExpression, + IBaseResource context, + IBaseResource resource, + IElement requestAction) { + var rawParameters = request.getRawParameters(); + if (requestAction != null) { + rawParameters.put("%action", requestAction); + } return request.getLibraryEngine() .resolveExpression( request.getSubjectId().getIdPart(), cqfExpression, request.getParameters(), - request.getRawParameters(), + rawParameters, request.getData(), context, resource); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/InputParameterResolver.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/InputParameterResolver.java index 5d40e74e17..f1d4d88005 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/InputParameterResolver.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/InputParameterResolver.java @@ -99,7 +99,7 @@ protected final IParametersAdapter resolveParameters( .getResourceDefinition(subjectId.getResourceType()) .getImplementingClass(); var subject = readRepository(subjectClass, subjectId); - if (subject != null) { + if (subject != null && !params.hasParameter("%subject")) { params.addParameter("%subject", subject); } } @@ -108,7 +108,7 @@ protected final IParametersAdapter resolveParameters( .getResourceDefinition(encounterId.getResourceType()) .getImplementingClass(); var encounter = readRepository(encounterClass, encounterId); - if (encounter != null) { + if (encounter != null && !params.hasParameter("%encounter")) { params.addParameter("%encounter", encounter); } } @@ -117,7 +117,7 @@ protected final IParametersAdapter resolveParameters( .getResourceDefinition(practitionerId.getResourceType()) .getImplementingClass(); var practitioner = readRepository(practitionerClass, practitionerId); - if (practitioner != null) { + if (practitioner != null && !params.hasParameter("%practitioner")) { params.addParameter("%practitioner", practitioner); } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ApplyRequest.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ApplyRequest.java index da4ab8c377..82d7106f60 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ApplyRequest.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ApplyRequest.java @@ -169,7 +169,7 @@ public org.opencds.cqf.fhir.cr.activitydefinition.apply.ApplyRequest toActivityR } public GenerateRequest toGenerateRequest(IBaseResource profile) { - return new GenerateRequest(profile, false, true, libraryEngine) + return new GenerateRequest(List.of(profile), false, true, libraryEngine) .setReferencedLibraries(referencedLibraries) .setQuestionnaire(getQuestionnaire()); } @@ -234,7 +234,6 @@ public PopulateRequest toPopulateRequest() { // getQuestionnaireResponse(), subjectId, context, - null, data, libraryEngine); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessAction.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessAction.java index b5dbec3a4e..e88c85bc0e 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessAction.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessAction.java @@ -113,6 +113,7 @@ protected void addQuestionnaireItemForInput(ApplyRequest request, IPlanDefinitio if (!request.questionnaireItemExistsForProfile(profileUrl)) { var profile = searchRepositoryByCanonical(repository, profileUrl); var generateRequest = request.toGenerateRequest(profile); + generateRequest.setNextProfileAdapter(); var item = generateProcessor.generateItem(generateRequest); if (item != null) { // If input has text extension use it to override diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessDefinition.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessDefinition.java index dd133c2d51..fe4f7da5ea 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessDefinition.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessDefinition.java @@ -1,15 +1,30 @@ package org.opencds.cqf.fhir.cr.plandefinition.apply; import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.opencds.cqf.fhir.cr.common.ExtensionBuilders.buildReference; -import static org.opencds.cqf.fhir.utility.SearchHelper.searchRepositoryByCanonical; +import static org.opencds.cqf.fhir.utility.BundleHelper.addEntry; +import static org.opencds.cqf.fhir.utility.BundleHelper.getEntryResources; +import static org.opencds.cqf.fhir.utility.BundleHelper.newBundle; +import static org.opencds.cqf.fhir.utility.BundleHelper.newEntry; +import static org.opencds.cqf.fhir.utility.BundleHelper.newRequest; +import static org.opencds.cqf.fhir.utility.BundleHelper.setEntryRequest; +import static org.opencds.cqf.fhir.utility.Canonicals.getResourceType; +import static org.opencds.cqf.fhir.utility.Canonicals.getUrl; +import static org.opencds.cqf.fhir.utility.Canonicals.getVersion; +import static org.opencds.cqf.fhir.utility.SearchHelper.getBundleClass; +import static org.opencds.cqf.fhir.utility.SearchHelper.getResourceClass; +import static org.opencds.cqf.fhir.utility.search.Searches.byUrl; +import static org.opencds.cqf.fhir.utility.search.Searches.byUrlAndVersion; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.repository.IRepository; -import org.hl7.fhir.exceptions.FHIRException; +import java.util.ArrayList; +import java.util.List; import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r5.model.Enumerations.FHIRTypes; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.adapter.IPlanDefinitionActionAdapter; import org.opencds.cqf.fhir.utility.adapter.IRequestActionAdapter; @@ -20,6 +35,13 @@ @SuppressWarnings("UnstableApiUsage") public class ProcessDefinition { private static final Logger logger = LoggerFactory.getLogger(ProcessDefinition.class); + private static final List SUPPORTED_DEFINITION_TYPES = List.of( + "Questionnaire", + "ActivityDefinition", + "PlanDefinition", + "MessageDefinition", + "ObservationDefinition", + "SpecimenDefinition"); final IRepository repository; final ApplyProcessor applyProcessor; @@ -73,15 +95,95 @@ protected IBaseResource resolveDefinition(ApplyRequest request, IPrimitiveType applyNestedPlanDefinition(request, definition); - case ACTIVITYDEFINITION -> applyActivityDefinition(request, definition); - case QUESTIONNAIRE -> applyQuestionnaireDefinition(request, definition); - default -> throw new FHIRException("Unknown action definition: %s".formatted(definition.getValue())); + var referenceToContained = definition.getValue().startsWith("#"); + var resource = referenceToContained + ? resolveContained(request, definition.getValue()) + : resolveCanonicalByType(request, definition); + if (resource == null) { + request.logException( + String.format("Unable to find resource for definition: %s", definition.getValueAsString())); + return null; + } + return switch (resource.fhirType()) { + case "PlanDefinition" -> applyNestedPlanDefinition(request, resource); + case "ActivityDefinition" -> applyActivityDefinition(request, resource); + default -> applyDefinition(request, resource); }; } + /** + * Resolves a canonical reference by first attempting to determine the Resource type. If able + * a search against only that type will be issued. Otherwise, a single transaction Bundle + * will be issued that searches every supported definition resource type in parallel. + * + *

Resolution rules: + *

    + *
  • If exactly one resource matches (with or without version) — return it.
  • + *
  • If no resources match — return null.
  • + *
  • If multiple resources match — throw an {@link IllegalStateException}.
  • + *
+ */ + protected IBaseResource resolveCanonicalByType(ApplyRequest request, IPrimitiveType definition) { + var canonical = definition.getValue(); + var url = getUrl(canonical); + var version = getVersion(canonical); + var type = getResourceType(definition); + IBaseBundle response = null; + try { + response = type == null || !SUPPORTED_DEFINITION_TYPES.contains(type) + ? searchAllDefinitionTypes(request.getFhirVersion(), url, version) + : searchByCanonicalType(type, url, version); + } catch (Exception e) { + var message = "Error encountered searching for definition (%s): %s".formatted(canonical, e.getMessage()); + logger.debug(message); + request.logException(message); + } + var matches = collectMatchesFromResponse(response); + if (matches.isEmpty()) { + return null; + } + if (matches.size() == 1) { + return matches.get(0); + } + var errorHint = isNotBlank(version) + ? "Even with the specified version, multiple resources matched." + : "Specify a version to resolve the ambiguity."; + throw new IllegalStateException( + "Multiple resources (%d) found for canonical '%s'. %s".formatted(matches.size(), canonical, errorHint)); + } + + protected IBaseBundle searchAllDefinitionTypes(FhirVersionEnum fhirVersion, String url, String version) { + var searchQuery = isNotBlank(version) ? "?url=%s&version=%s".formatted(url, version) : "?url=%s".formatted(url); + var transaction = newBundle(fhirVersion, "transaction"); + for (var type : SUPPORTED_DEFINITION_TYPES) { + var searchUrl = "%s%s".formatted(type, searchQuery); + var requestEntry = newRequest(fhirVersion, "GET", searchUrl); + var entry = setEntryRequest(fhirVersion, newEntry(fhirVersion), requestEntry); + addEntry(transaction, entry); + } + + return repository.transaction(transaction); + } + + protected IBaseBundle searchByCanonicalType(String type, String url, String version) { + var searchParams = isNotBlank(version) ? byUrlAndVersion(url, version) : byUrl(url); + return repository.search(getBundleClass(repository), getResourceClass(repository, type), searchParams); + } + + protected List collectMatchesFromResponse(IBaseBundle response) { + var matches = new ArrayList(); + if (response != null) { + for (var resource : getEntryResources(response)) { + if (resource instanceof IBaseBundle resultBundle) { + matches.addAll(getEntryResources(resultBundle)); + } else { + matches.add(resource); + } + } + } + return matches; + } + protected Boolean isDefinitionCanonical(ApplyRequest request, IBase definition) { requireNonNull(request); return switch (request.getFhirVersion()) { @@ -100,35 +202,16 @@ protected Boolean isDefinitionUri(ApplyRequest request, IBase definition) { }; } - protected IBaseResource applyQuestionnaireDefinition(ApplyRequest request, IPrimitiveType definition) { - requireNonNull(definition); - IBaseResource result = null; - try { - var referenceToContained = definition.getValue().startsWith("#"); - if (referenceToContained) { - result = resolveContained(request, definition.getValue()); - } else { - result = resolveRepository(definition); - } - } catch (Exception e) { - var message = "ERROR: Questionnaire %s could not be applied and threw exception %s" - .formatted(definition.getValue(), e.toString()); - logger.error(message); - request.logException(message); - } - return result; + protected IBaseResource applyDefinition(ApplyRequest request, IBaseResource definition) { + // TODO: Should wrap this in a Task? + return definition; } - protected IBaseResource applyActivityDefinition(ApplyRequest request, IPrimitiveType definition) { - requireNonNull(definition); + protected IBaseResource applyActivityDefinition(ApplyRequest request, IBaseResource activityDefinition) { // Running into issues with invoking ActivityDefinition/$apply on a HapiFhirRepository that was created with // RequestDetails from PlanDefinition/$apply IBaseResource result = null; try { - var referenceToContained = definition.getValue().startsWith("#"); - var activityDefinition = (referenceToContained - ? resolveContained(request, definition.getValue()) - : resolveRepository(definition)); var activityRequest = request.toActivityRequest(activityDefinition); result = applyProcessor.applyActivityDefinition(activityRequest); // appending a count to the id when an ActivityDefinition is used in multiple actions @@ -146,21 +229,16 @@ protected IBaseResource applyActivityDefinition(ApplyRequest request, IPrimitive activityRequest.resolveOperationOutcome(request.getAdapterFactory().createResource(result)); } catch (Exception e) { var message = "ERROR: ActivityDefinition %s could not be applied and threw exception %s" - .formatted(definition.getValue(), e.toString()); + .formatted(activityDefinition.getIdElement().getValue(), e.toString()); logger.error(message); request.logException(message); } return result; } - protected IBaseResource applyNestedPlanDefinition(ApplyRequest request, IPrimitiveType definition) { - requireNonNull(definition); + protected IBaseResource applyNestedPlanDefinition(ApplyRequest request, IBaseResource planDefinition) { try { - var referenceToContained = definition.getValue().startsWith("#"); - var nextPlanDefinition = (referenceToContained - ? resolveContained(request, definition.getValue()) - : resolveRepository(definition)); - var nestedRequest = request.copy(nextPlanDefinition); + var nestedRequest = request.copy(planDefinition); var result = applyProcessor.applyPlanDefinition(nestedRequest); nestedRequest.resolveOperationOutcome(request.getAdapterFactory().createResource(result)); request.getRequestResources().addAll(nestedRequest.getRequestResources()); @@ -169,32 +247,13 @@ protected IBaseResource applyNestedPlanDefinition(ApplyRequest request, IPrimiti return result; } catch (Exception e) { var message = "ERROR: PlanDefinition %s could not be applied and threw exception %s" - .formatted(definition.getValue(), e.toString()); + .formatted(planDefinition.getIdElement().getValue(), e.toString()); logger.error(message); request.logException(message); return null; } } - protected IBaseResource resolveRepository(IPrimitiveType definition) { - return searchRepositoryByCanonical(repository, definition); - } - - protected String resolveResourceName(ApplyRequest request, IPrimitiveType canonical) { - requireNonNull(canonical); - if (canonical.hasValue()) { - var id = canonical.getValue(); - if (id.contains("/")) { - id = id.replace(id.substring(id.lastIndexOf("/")), ""); - return id.contains("/") ? id.substring(id.lastIndexOf("/") + 1) : id; - } else if (id.startsWith("#")) { - return resolveContained(request, id).fhirType(); - } - return null; - } - throw new FHIRException("CanonicalType must have a value for resource name extraction"); - } - protected IBaseResource resolveContained(ApplyRequest request, String id) { requireNonNull(id); var contained = request.getPlanDefinitionAdapter().getContained(); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessor.java index c44c856447..feeb7a171e 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessor.java @@ -7,10 +7,10 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.repository.IRepository; +import java.util.ArrayList; import java.util.List; import org.hl7.fhir.instance.model.api.IBaseBackboneElement; import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -31,6 +31,7 @@ import org.opencds.cqf.fhir.cr.questionnaire.populate.PopulateRequest; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.monad.Either3; +import org.opencds.cqf.fhir.utility.monad.Eithers; @SuppressWarnings("UnstableApiUsage") public class QuestionnaireProcessor { @@ -98,17 +99,13 @@ public IBaseResource generateQuestionnaire(String id) { } public , R extends IBaseResource> IBaseResource generateQuestionnaire( - Either3 profile) { - return generateQuestionnaire(profile, false, true); + R profile, List urls, boolean supportedOnly, boolean requiredOnly) { + return generateQuestionnaire(profile, urls, supportedOnly, requiredOnly, (IBaseResource) null, null, null); } public , R extends IBaseResource> IBaseResource generateQuestionnaire( - Either3 profile, boolean supportedOnly, boolean requiredOnly) { - return generateQuestionnaire(profile, supportedOnly, requiredOnly, (IBaseResource) null, null, null); - } - - public , R extends IBaseResource> IBaseResource generateQuestionnaire( - Either3 profile, + R profile, + List urls, boolean supportedOnly, boolean requiredOnly, IBaseResource contentEndpoint, @@ -116,6 +113,7 @@ public , R extends IBaseResource> IBaseResource String id) { return generateQuestionnaire( profile, + urls, supportedOnly, requiredOnly, createRestRepository(repository.fhirContext(), contentEndpoint), @@ -124,34 +122,45 @@ public , R extends IBaseResource> IBaseResource } public , R extends IBaseResource> IBaseResource generateQuestionnaire( - Either3 profile, + R profile, + List urls, boolean supportedOnly, boolean requiredOnly, IRepository contentRepository, IRepository terminologyRepository, String id) { repository = proxy(repository, true, null, contentRepository, terminologyRepository); - return generateQuestionnaire(profile, supportedOnly, requiredOnly, id); + return generateQuestionnaire(profile, urls, supportedOnly, requiredOnly, id); } public , R extends IBaseResource> IBaseResource generateQuestionnaire( - Either3 profile, boolean supportedOnly, boolean requiredOnly, String id) { - return generateQuestionnaire(profile, supportedOnly, requiredOnly, id, null); + R profile, List urls, boolean supportedOnly, boolean requiredOnly, String id) { + return generateQuestionnaire(profile, urls, supportedOnly, requiredOnly, id, null); } public , R extends IBaseResource> IBaseResource generateQuestionnaire( - Either3 profile, + R profile, + List urls, boolean supportedOnly, boolean requiredOnly, String id, LibraryEngine libraryEngine) { - var request = new GenerateRequest( - resolveStructureDefinition(profile), - supportedOnly, - requiredOnly, - libraryEngine != null - ? libraryEngine - : new LibraryEngine(repository, crSettings.getEvaluationSettings())); + var libraryEngineToUse = libraryEngine != null + ? libraryEngine + : new LibraryEngine(repository, crSettings.getEvaluationSettings()); + var profiles = new ArrayList(); + if (profile != null) { + profiles.add(profile); + } + if (urls != null && !urls.isEmpty()) { + urls.forEach(url -> { + var p = resolveStructureDefinition(Eithers.forLeft3(url)); + if (p != null) { + profiles.add(p); + } + }); + } + var request = new GenerateRequest(profiles, supportedOnly, requiredOnly, libraryEngineToUse); return generateQuestionnaire(request, id); } @@ -196,14 +205,12 @@ public PopulateRequest buildPopulateRequest( IBaseResource questionnaire, String subjectId, List context, - IBaseExtension launchContext, IBaseBundle data, LibraryEngine libraryEngine) { return new PopulateRequest( questionnaire, subjectId == null ? null : Ids.newId(fhirVersion, Ids.ensureIdType(subjectId, SUBJECT_TYPE)), context, - launchContext, data, libraryEngine != null ? libraryEngine @@ -214,7 +221,6 @@ public , R extends IBaseResource> IBaseResource Either3 questionnaire, String subjectId, List context, - IBaseExtension launchContext, IBaseBundle data, boolean useServerData, IBaseResource dataEndpoint, @@ -224,7 +230,6 @@ public , R extends IBaseResource> IBaseResource questionnaire, subjectId, context, - launchContext, data, useServerData, createRestRepository(repository.fhirContext(), dataEndpoint), @@ -236,7 +241,6 @@ public , R extends IBaseResource> IBaseResource Either3 questionnaire, String subjectId, List context, - IBaseExtension launchContext, IBaseBundle data, boolean useServerData, IRepository dataRepository, @@ -247,7 +251,6 @@ public , R extends IBaseResource> IBaseResource questionnaire, subjectId, context, - launchContext, data, new LibraryEngine(repository, crSettings.getEvaluationSettings())); } @@ -256,20 +259,18 @@ public , R extends IBaseResource> IBaseResource Either3 questionnaire, String subjectId, List context, - IBaseExtension launchContext, IBaseBundle data, LibraryEngine libraryEngine) { - return populate(resolveQuestionnaire(questionnaire), subjectId, context, launchContext, data, libraryEngine); + return populate(resolveQuestionnaire(questionnaire), subjectId, context, data, libraryEngine); } public IBaseResource populate( IBaseResource questionnaire, String subjectId, List context, - IBaseExtension launchContext, IBaseBundle data, LibraryEngine libraryEngine) { - return populate(buildPopulateRequest(questionnaire, subjectId, context, launchContext, data, libraryEngine)); + return populate(buildPopulateRequest(questionnaire, subjectId, context, data, libraryEngine)); } public IBaseResource populate(PopulateRequest request) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ElementHasCaseFeature.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ElementHasCaseFeature.java deleted file mode 100644 index 49e321aa0d..0000000000 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ElementHasCaseFeature.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.opencds.cqf.fhir.cr.questionnaire.generate; - -import java.util.ArrayList; -import org.apache.commons.lang3.StringUtils; -import org.opencds.cqf.fhir.cr.questionnaire.Helpers; -import org.opencds.cqf.fhir.utility.Constants; -import org.opencds.cqf.fhir.utility.CqfExpression; -import org.opencds.cqf.fhir.utility.adapter.IElementDefinitionAdapter; -import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireItemComponentAdapter; -import org.opencds.cqf.fhir.utility.adapter.IStructureDefinitionAdapter; - -public class ElementHasCaseFeature { - public void addProperties( - GenerateRequest request, - CqfExpression caseFeature, - IStructureDefinitionAdapter profile, - IElementDefinitionAdapter element, - IQuestionnaireItemComponentAdapter questionnaireItem) { - var elementId = element.getId(); - var elementIdentifiers = element.getPath().split("\\."); - var path = elementId.substring(elementId.indexOf(".")).replaceAll("\\[[^\\[]*]", ""); - var sliceName = Helpers.getSliceName(elementId); - if (StringUtils.isNotBlank(sliceName)) { - var sliceElements = profile.getSliceElements(sliceName); - if (!sliceElements.isEmpty()) { - var filter = new StringBuilder(".where("); - sliceElements.stream() - .filter(e -> !e.getId().equals(elementId) && e.hasDefaultOrFixedOrPattern()) - .forEach(slice -> { - var filterIdentifiers = new ArrayList(); - var sliceIdentifiers = slice.getPath().split("\\."); - for (int i = 0; i < elementIdentifiers.length; i++) { - if (!elementIdentifiers[i].equals(sliceIdentifiers[i])) { - filterIdentifiers.add(sliceIdentifiers[i]); - } - } - var filterPath = StringUtils.join(filterIdentifiers, "."); - var filterValue = slice.getDefaultOrFixedOrPattern(); - - filter.append("%s = '%s'" - .formatted( - filterPath, - request.getFhirContext() - .newJsonParser() - .encodeToString(filterValue))); - }); - filter.append(").single()"); - path = path.replace(":" + sliceName, filter); - } - } - var expression = "%" + "%s%s".formatted(caseFeature.getName(), path); - var expressionType = questionnaireItem.newExpression(expression); - var initialExpressionExt = questionnaireItem.addExtension(); - initialExpressionExt.setUrl(Constants.SDC_QUESTIONNAIRE_INITIAL_EXPRESSION); - initialExpressionExt.setValue(expressionType); - } -} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateProcessor.java index 694d5a0107..4723429bcf 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateProcessor.java @@ -5,10 +5,9 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.repository.IRepository; -import java.text.SimpleDateFormat; import java.util.Collections; -import java.util.Date; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -41,27 +40,27 @@ public GenerateProcessor(IRepository repository) { @Override public IBaseResource generate(String id) { var questionnaire = createQuestionnaire(); - if (id != null) { - var newId = Ids.newId(fhirVersion, Ids.ensureIdType(id, "Questionnaire")); - questionnaire.setId(newId); - } + setQuestionnaireId(questionnaire, id); return questionnaire.get(); } @Override public IBaseResource generate(GenerateRequest request, String id) { - request.setQuestionnaire( - generate(id == null ? request.getProfile().getIdElement().getIdPart() : id)); - var formatter = new SimpleDateFormat("yyyy-MM-dd-hh.mm.ss"); - request.getQuestionnaireAdapter() - .setVersion("%s-%s".formatted(request.getProfileAdapter().getVersion(), formatter.format(new Date()))); - var item = generateItem(request); - if (item != null) { - item.getLeft(); - request.addQuestionnaireItem(item.getLeft()); - if (!item.getRight().isEmpty()) { - request.addCqlLibraryExtension(); - request.addLaunchContextExtensions(item.getRight()); + var questionnaireId = id; + if (StringUtils.isBlank(questionnaireId)) { + questionnaireId = request.getProfiles().get(0).getId(); + } + request.setQuestionnaire(generate(questionnaireId)); + while (request.setNextProfileAdapter()) { + var item = generateItem(request); + if (item != null) { + // TODO: Set derivedFrom on the Questionnaire (constrained to Questionnaire canonical) + // or add a new CPG extension to link to the StructureDef? + request.addQuestionnaireItem(item.getLeft()); + if (!item.getRight().isEmpty()) { + request.addCqlLibraryExtension(); + request.addLaunchContextExtensions(item.getRight()); + } } } return request.getQuestionnaire(); @@ -105,4 +104,11 @@ protected IQuestionnaireAdapter createQuestionnaire() { questionnaire.setStatus("active"); return questionnaire; } + + protected void setQuestionnaireId(IQuestionnaireAdapter questionnaire, String id) { + if (StringUtils.isNotBlank(id)) { + var newId = Ids.newId(fhirVersion, Ids.ensureIdType(id, "Questionnaire")); + questionnaire.setId(newId); + } + } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateRequest.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateRequest.java index 694c35c5fa..982283cd27 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateRequest.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateRequest.java @@ -3,6 +3,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import ca.uhn.fhir.context.FhirVersionEnum; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -21,9 +23,11 @@ @SuppressWarnings("UnstableApiUsage") public class GenerateRequest implements IQuestionnaireRequest { - private final IStructureDefinitionAdapter profileAdapter; + private final List profiles; + private final List addedProfiles; + private IStructureDefinitionAdapter profileAdapter; private final boolean supportedOnly; - private final boolean requiredOnly; + private final boolean minimalOnly; private final LibraryEngine libraryEngine; private final FhirVersionEnum fhirVersion; private Map referencedLibraries; @@ -32,26 +36,50 @@ public class GenerateRequest implements IQuestionnaireRequest { private List snapshotElements; public GenerateRequest( - IBaseResource profile, boolean supportedOnly, boolean requiredOnly, LibraryEngine libraryEngine) { - checkNotNull(profile, "expected non-null value for profile"); + List profiles, boolean supportedOnly, boolean minimalOnly, LibraryEngine libraryEngine) { + checkNotNull(profiles, "expected non-null value for profiles"); checkNotNull(libraryEngine, "expected non-null value for libraryEngine"); - fhirVersion = profile.getStructureFhirVersionEnum(); - profileAdapter = (IStructureDefinitionAdapter) - getAdapterFactory().createKnowledgeArtifactAdapter((IDomainResource) profile); + if (profiles.isEmpty()) { + throw new IllegalArgumentException("expected non-empty list for profiles"); + } + fhirVersion = libraryEngine.getRepository().fhirContext().getVersion().getVersion(); + this.profiles = profiles.stream() + .map(p -> (IStructureDefinitionAdapter) + getAdapterFactory().createKnowledgeArtifactAdapter((IDomainResource) p)) + .toList(); + addedProfiles = new ArrayList<>(); this.supportedOnly = supportedOnly; - this.requiredOnly = requiredOnly; + this.minimalOnly = minimalOnly; this.libraryEngine = libraryEngine; - referencedLibraries = profileAdapter.getReferencedLibraries(); + referencedLibraries = new HashMap<>(); } public IBaseResource getProfile() { return profileAdapter.get(); } + public List getProfiles() { + return profiles; + } + public IStructureDefinitionAdapter getProfileAdapter() { return profileAdapter; } + public boolean setNextProfileAdapter() { + var profile = profiles.stream() + .filter(p -> !addedProfiles.contains(p.getId())) + .findFirst() + .orElse(null); + if (profile == null) { + return false; + } + profileAdapter = profile; + addedProfiles.add(profile.getId()); + referencedLibraries.putAll(profileAdapter.getReferencedLibraries()); + return true; + } + public IQuestionnaireAdapter getQuestionnaireAdapter() { return questionnaireAdapter; } @@ -83,8 +111,8 @@ public boolean getSupportedOnly() { return supportedOnly; } - public boolean getRequiredOnly() { - return requiredOnly; + public boolean getMinimalOnly() { + return minimalOnly; } public GenerateRequest setReferencedLibraries(Map libraries) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGenerator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGenerator.java index b4d07b5bc4..adc424d577 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGenerator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGenerator.java @@ -42,28 +42,20 @@ public class ItemGenerator { protected final IRepository repository; protected final ExpressionProcessor expressionProcessor; protected final ExtensionProcessor extensionProcessor; - protected final ElementHasCaseFeature elementHasCaseFeature; protected final ItemTypeIsChoice itemTypeIsChoice; public ItemGenerator(IRepository repository) { - this( - repository, - new ExpressionProcessor(), - new ExtensionProcessor(), - new ElementHasCaseFeature(), - new ItemTypeIsChoice(repository)); + this(repository, new ExpressionProcessor(), new ExtensionProcessor(), new ItemTypeIsChoice(repository)); } public ItemGenerator( IRepository repository, ExpressionProcessor expressionProcessor, ExtensionProcessor extensionProcessor, - ElementHasCaseFeature elementHasCaseFeature, ItemTypeIsChoice itemTypeIsChoice) { this.repository = repository; this.expressionProcessor = expressionProcessor; this.extensionProcessor = extensionProcessor; - this.elementHasCaseFeature = elementHasCaseFeature; this.itemTypeIsChoice = itemTypeIsChoice; } @@ -82,20 +74,37 @@ public ItemGenerator( if (childItems.isEmpty() && valueExtensions.isEmpty()) { return null; } - var questionnaireItem = createQuestionnaireItem(request, linkId); - if (!childItems.isEmpty()) { - questionnaireItem.addItems(childItems); + IQuestionnaireItemComponentAdapter questionnaireItem; + if (childItems.size() == 1) { + questionnaireItem = childItems.get(0); + questionnaireItem.setLinkId(linkId); + } else { + questionnaireItem = createQuestionnaireItem(request, linkId); + if (!childItems.isEmpty()) { + questionnaireItem.addItems(childItems); + } } + addDefinitionExtractExt(request, questionnaireItem); if (!valueExtensions.isEmpty()) { valueExtensions.forEach(questionnaireItem::addExtension); } - // If we have a caseFeature we need to include launchContext extensions and Item Population Context + // If we have a caseFeature we need to include launchContext extensions and Definition Population Context List launchContextExts = new ArrayList<>(); if (caseFeature != null) { - var itemContextExt = questionnaireItem.addExtension(); - itemContextExt.setUrl(Constants.SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT); - itemContextExt.setValue(caseFeature.toExpressionType(request.getFhirVersion())); + var definitionContextExt = questionnaireItem.addExtension(); + // Switch to SDC extension once new version is published + // definitionContextExt.setUrl(Constants.SDC_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT) + definitionContextExt.setUrl(Constants.CPG_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT); + var definitionContextExtAdapter = + questionnaireItem.getAdapterFactory().createBase(definitionContextExt); + var definitionExt = definitionContextExtAdapter.addExtension(); + definitionExt.setUrl("definition"); + definitionExt.setValue(canonicalTypeForVersion( + request.getFhirVersion(), request.getProfileAdapter().getCanonical())); + var expressionExt = definitionContextExtAdapter.addExtension(); + expressionExt.setUrl("expression"); + expressionExt.setValue(caseFeature.toExpressionType(request.getFhirVersion())); // Assume Patient for now - this should probably be the context of the Library if we can determine that launchContextExts.add(buildSdcLaunchContextExt(request, "patient", "Patient")); var featureLibrary = request.getAdapterFactory() @@ -107,7 +116,7 @@ public ItemGenerator( featureLibrary.getParameter().stream() .filter(p -> featureLibrary.resolvePathString(p, "use").equals("in")) .filter(p -> request.getFHIRTypes().contains(featureLibrary.resolvePathString(p, "type"))) - .map(p -> new ImmutablePair( + .map(p -> new ImmutablePair<>( featureLibrary.resolvePathString(p, "name"), featureLibrary.resolvePathString(p, "type"))) .forEach(p -> launchContextExts.add(buildSdcLaunchContextExt(request, p.left, p.right))); @@ -123,7 +132,7 @@ public ItemGenerator( @SuppressWarnings("unchecked") protected > T buildSdcLaunchContextExt( GenerateRequest request, String code, String type) { - var system = "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext"; + var system = Constants.SDC_LAUNCH_CONTEXT_SYSTEM; var display = ""; switch (code.toLowerCase()) { case "patient": @@ -148,7 +157,7 @@ public ItemGenerator( default: display = code; - system = "http://example.org/fhir/uv/sdc/CodeSystem/additionalLaunchContext"; + system = Constants.CPG_ADDITIONAL_LAUNCH_CONTEXT_SYSTEM; break; } var fhirVersion = request.getFhirVersion(); @@ -275,9 +284,6 @@ protected IQuestionnaireItemComponentAdapter createItemForElement( if (item.isChoiceItem()) { itemTypeIsChoice.addProperties(element, item); } - if (caseFeature != null) { - elementHasCaseFeature.addProperties(request, caseFeature, request.getProfileAdapter(), element, item); - } return item; } catch (Exception ex) { final String message = ITEM_CREATION_ERROR.formatted(ex.getMessage()); @@ -295,7 +301,7 @@ protected List getElements( // Add elements from the snapshot that were not in the differential if (request.getSnapshotElements() != null) { elements.addAll(request.getProfileAdapter().getSnapshotElements().stream() - .filter(e -> filterElement(request, e, elements, parentPath, sliceName, request.getRequiredOnly())) + .filter(e -> filterElement(request, e, elements, parentPath, sliceName, request.getMinimalOnly())) .toList()); } return elements; @@ -307,7 +313,7 @@ protected boolean filterElement( List existingElements, String parentPath, String sliceName, - boolean requiredOnly) { + boolean minimalOnly) { var path = element.getPath(); if (elementExists(existingElements, path)) { return false; @@ -325,7 +331,7 @@ protected boolean filterElement( if (sliceName != null) { return element.getId().contains(sliceName); } - if (requiredOnly) { + if (minimalOnly) { return element.isRequired(); } if (request.getSupportedOnly()) { @@ -382,11 +388,15 @@ public IQuestionnaireItemComponentAdapter createQuestionnaireItem(GenerateReques : request.getProfileAdapter().getName(); var item = initializeQuestionnaireItem( request, "group", request.getProfileAdapter().getUrl(), linkId, text, false, isRepeats(request)); + addDefinitionExtractExt(request, item); + return item; + } + + protected void addDefinitionExtractExt(GenerateRequest request, IQuestionnaireItemComponentAdapter item) { var definitionExtract = item.addExtension(); definitionExtract.setUrl(Constants.SDC_QUESTIONNAIRE_DEFINITION_EXTRACT); definitionExtract.setValue(canonicalTypeForVersion( request.getFhirVersion(), request.getProfileAdapter().getUrl())); - return item; } protected Boolean isRepeats(GenerateRequest request) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/ItemProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/ItemProcessor.java index afe2b61159..9052d7cfc8 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/ItemProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/ItemProcessor.java @@ -3,18 +3,23 @@ import static org.opencds.cqf.fhir.cr.common.ExtensionBuilders.QUESTIONNAIRE_RESPONSE_AUTHOR_EXTENSION; import static org.opencds.cqf.fhir.cr.common.ExtensionBuilders.buildReferenceExt; import static org.opencds.cqf.fhir.cr.common.ItemValueTransformer.transformValueToItem; +import static org.opencds.cqf.fhir.utility.Constants.CPG_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT; +import static org.opencds.cqf.fhir.utility.Constants.SDC_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT; +import static org.opencds.cqf.fhir.utility.Constants.SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT; import static org.opencds.cqf.fhir.utility.SearchHelper.searchRepositoryByCanonical; import static org.opencds.cqf.fhir.utility.VersionUtilities.canonicalTypeForVersion; import static org.opencds.cqf.fhir.utility.VersionUtilities.stringTypeForVersion; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IDomainResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -41,26 +46,39 @@ public ItemProcessor(ExpressionProcessor expressionProcessor) { this.expressionProcessor = expressionProcessor; } - public List processItem( - PopulateRequest request, IQuestionnaireItemComponentAdapter item) { - var populationContextExt = item.getExtensionByUrl(Constants.SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT); - return populationContextExt != null - ? processContextItem(request, item) - : List.of(processSingleItem(request, item)); + public void processItem( + PopulateRequest request, + IQuestionnaireItemComponentAdapter item, + List responseItem) { + if (item.isContextItem()) { + processContextItem(request, item, responseItem); + } else { + responseItem.add(item.newResponseItem()); + processSingleItem(request, item, responseItem); + } } - protected IQuestionnaireResponseItemComponentAdapter processSingleItem( - PopulateRequest request, IQuestionnaireItemComponentAdapter item) { - final var responseItem = item.newResponseItem(); + protected void processGroupItem( + PopulateRequest request, + IQuestionnaireResponseItemComponentAdapter responseItem, + List childItems) { + childItems.forEach(childItem -> { + var childResponseItem = new ArrayList(); + processItem(request, childItem, childResponseItem); + responseItem.addItems(childResponseItem); + }); + } + + protected List processSingleItem( + PopulateRequest request, + IQuestionnaireItemComponentAdapter item, + List responseItems) { + var responseItem = responseItems.get(0); var childItems = item.getItem().stream() .map(IQuestionnaireItemComponentAdapter.class::cast) - .toList(); + .collect(Collectors.toList()); if (item.isGroupItem()) { - final List groupChildItems = new ArrayList<>(); - childItems.forEach(childItem -> { - groupChildItems.addAll(processItem(request, childItem)); - }); - responseItem.setItem(groupChildItems); + processGroupItem(request, responseItem, childItems); } else { request.setContextVariable(responseItem.get()); var rawParams = request.getRawParameters(); @@ -69,7 +87,8 @@ protected IQuestionnaireResponseItemComponentAdapter processSingleItem( if (!childItems.isEmpty()) { // children of non group items go under each answer var childResponseItems = childItems.stream() - .map(c -> processSingleItem(request, c)) + .map(c -> processSingleItem(request, c, List.of(c.newResponseItem()))) + .flatMap(Collection::stream) .toList(); var answers = responseItem.getAnswer(); if (answers.isEmpty()) { @@ -79,18 +98,23 @@ protected IQuestionnaireResponseItemComponentAdapter processSingleItem( answers.forEach(a -> a.setItem(childResponseItems)); } } - return responseItem; + return responseItems; } - protected List processContextItem( - PopulateRequest request, IQuestionnaireItemComponentAdapter item) { + protected void processContextItem( + PopulateRequest request, + IQuestionnaireItemComponentAdapter item, + List responseItem) { var itemLinkId = item.getLinkId(); - if (!item.isGroupItem()) { + if (!item.isGroupItem() && item.hasExtension(SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT)) { throw new UnprocessableEntityException( "Encountered Item Population Context extension on a non group item: {}", itemLinkId); } IBaseResource profile = null; - var definition = item.getDefinition(); + var ctx = getContext(request, item); + var definition = ctx.component1(); + var contextExpression = ctx.component2(); + if (StringUtils.isNotBlank(definition)) { var profileUrl = definition.split("#")[0]; try { @@ -106,31 +130,23 @@ protected List processContextItem( ? null : (IStructureDefinitionAdapter) request.getAdapterFactory().createKnowledgeArtifactAdapter((IDomainResource) profile); - final CqfExpression contextExpression = expressionProcessor.getCqfExpression( - request, item.getExtension(), Constants.SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT); List> populationContext; - try { - populationContext = - expressionProcessor - .getExpressionResultForItem(request, contextExpression, itemLinkId, null, null) - .stream() - // .map(r -> { - // if (r != null && r.fhirType().equals("Tuple")) { - // return new Tuple() - // .withElements(request.getAdapterFactory() - // .createTuple(r) - // .getProperties()); - // } - // return r; - // }) - // filtering nulls here to prevent unnecessary duplicate responseItems - .filter(Objects::nonNull) - .map(r -> request.getAdapterFactory().createBase(r)) - .collect(Collectors.toList()); - - } catch (Exception e) { - logger.error(e.getMessage()); - request.logException(e.getMessage()); + if (contextExpression != null) { + try { + populationContext = + expressionProcessor + .getExpressionResultForItem(request, contextExpression, itemLinkId, null, null) + .stream() + // filtering nulls here to prevent unnecessary duplicate responseItems + .filter(Objects::nonNull) + .map(r -> request.getAdapterFactory().createBase(r)) + .collect(Collectors.toList()); + } catch (Exception e) { + logger.error(e.getMessage()); + request.logException(e.getMessage()); + populationContext = new ArrayList<>(); + } + } else { populationContext = new ArrayList<>(); } if (populationContext.isEmpty()) { @@ -142,10 +158,44 @@ protected List processContextItem( "Population context expression resulted in multiple values for a non repeating group: {}", contextExpression.getExpression()); } - return populationContext.stream() - .map(context -> - processPopulationContext(request, item, contextExpression.getName(), context, profileAdapter)) + var contextName = contextExpression == null ? null : contextExpression.getName(); + var newResponseItems = populationContext.stream() + .map(context -> processPopulationContext(request, item, contextName, context, profileAdapter)) .toList(); + responseItem.addAll(newResponseItems); + } + + protected kotlin.Pair getContext( + PopulateRequest request, IQuestionnaireItemComponentAdapter item) { + String definition; + CqfExpression contextExpression; + if (item.hasExtension(SDC_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT) + || item.hasExtension(CPG_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT)) { + var defPopExt = item.getExtensionByUrl(SDC_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT); + if (defPopExt == null) { + defPopExt = item.getExtensionByUrl(CPG_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT); + } + definition = defPopExt.getExtension().stream() + .map(IBaseExtension.class::cast) + .filter(e -> "definition".equals(e.getUrl())) + .map(IBaseExtension::getValue) + .map(IPrimitiveType.class::cast) + .map(IPrimitiveType::getValueAsString) + .findFirst() + .orElse(null); + contextExpression = defPopExt.getExtension().stream() + .map(IBaseExtension.class::cast) + .filter(e -> "expression".equals(e.getUrl())) + .map(e -> CqfExpression.of( + request.getAdapterFactory().createBase(e.getValue()), request.getReferencedLibraries())) + .findFirst() + .orElse(null); + } else { + definition = item.getDefinition(); + contextExpression = expressionProcessor.getCqfExpression( + request, item.getExtension(), SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT); + } + return new kotlin.Pair<>(definition, contextExpression); } protected void populateAnswer( @@ -194,30 +244,41 @@ protected void addAuthorExtension(PopulateRequest request, IQuestionnaireRespons protected IQuestionnaireResponseItemComponentAdapter processPopulationContext( PopulateRequest request, - IQuestionnaireItemComponentAdapter groupItem, + IQuestionnaireItemComponentAdapter item, String contextName, IAdapter context, IStructureDefinitionAdapter profile) { - final var contextItem = groupItem.newResponseItem(); - groupItem.getItem().stream() - .map(IQuestionnaireItemComponentAdapter.class::cast) - .forEach(item -> { - var childItems = item.getItem(); - if (!childItems.isEmpty()) { - var childGroupItem = processPopulationContext(request, item, contextName, context, profile); - contextItem.addItem(childGroupItem); - } else { - try { - var processedSubItem = - createResponseContextItem(request, item, contextName, context, profile); - contextItem.addItem(processedSubItem); - } catch (Exception e) { - logger.error(e.getMessage()); - request.logException(e.getMessage()); + if (item.hasItem()) { + final var contextItem = item.newResponseItem(); + item.getItem().stream() + .map(IQuestionnaireItemComponentAdapter.class::cast) + .forEach(childItem -> { + var childItems = childItem.getItem(); + if (!childItems.isEmpty()) { + var childItemWithChildren = + processPopulationContext(request, childItem, contextName, context, profile); + contextItem.addItem(childItemWithChildren); + } else { + try { + var processedSubItem = + createResponseContextItem(request, childItem, contextName, context, profile); + contextItem.addItem(processedSubItem); + } catch (Exception e) { + logger.error(e.getMessage()); + request.logException(e.getMessage()); + } } - } - }); - return contextItem; + }); + return contextItem; + } else { + try { + return createResponseContextItem(request, item, contextName, context, profile); + } catch (Exception e) { + logger.error(e.getMessage()); + request.logException(e.getMessage()); + return item.newResponseItem(); + } + } } @SuppressWarnings("unchecked") @@ -227,27 +288,26 @@ protected IQuestionnaireResponseItemComponentAdapter createResponseContextItem( String contextName, IAdapter context, IStructureDefinitionAdapter profile) { + final var responseItem = item.newResponseItem(); if (item.hasInitial()) { - return processSingleItem(request, item); + return processSingleItem(request, item, List.of(responseItem)).get(0); } - final var responseItem = item.newResponseItem(); request.setContextVariable(responseItem.get()); - // if we have a definition use it to populate + // if we have a definition and no initial expression use the definition to populate var definition = item.getDefinition(); - if (StringUtils.isNotBlank(definition) && profile != null && context != null) { + var initialExpressionExt = item.getExtensionByUrl(Constants.SDC_QUESTIONNAIRE_INITIAL_EXPRESSION); + if (StringUtils.isNotBlank(definition) && profile != null && context != null && initialExpressionExt == null) { final var pathValue = getPathValue(request, context, definition, profile); if (pathValue != null) { - final List answerValue = - pathValue instanceof List ? (List) pathValue : List.of((IBase) pathValue); + var answerValue = pathValue instanceof List ? (List) pathValue : List.of((IBase) pathValue); if (!answerValue.isEmpty()) { addAuthorExtension(request, responseItem); } populateAnswer(request, responseItem, answerValue); } } else { - var extension = item.getExtensionByUrl(Constants.SDC_QUESTIONNAIRE_INITIAL_EXPRESSION); // populate using expected initial expression extensions - if (extension != null) { + if (initialExpressionExt != null) { List initialValue = null; if (context != null) { // pass the context resource(s) as a parameter to the evaluation diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateProcessor.java index 2005f6635e..c4278a284d 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateProcessor.java @@ -1,7 +1,7 @@ package org.opencds.cqf.fhir.cr.questionnaire.populate; +import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -22,7 +22,7 @@ public PopulateProcessor() { this(null, null); } - private PopulateProcessor(ItemProcessor itemProcessor, ExpressionProcessor expressionProcessor) { + public PopulateProcessor(ItemProcessor itemProcessor, ExpressionProcessor expressionProcessor) { this.expressionProcessor = expressionProcessor != null ? expressionProcessor : new ExpressionProcessor(); this.itemProcessor = itemProcessor != null ? itemProcessor : new ItemProcessor(this.expressionProcessor); } @@ -33,9 +33,8 @@ public IBaseResource populate(PopulateRequest request) { "Performing $populate operation on Questionnaire/{}", request.getQuestionnaire().getIdElement().getIdPart()); // process root level variables - request.getQuestionnaireAdapter().getItem().forEach(item -> { - request.addQuestionnaireResponseItems(populateItem(request, item)); - }); + // getVariables(request, request.getQuestionnaire()) + request.getQuestionnaireAdapter().getItem().forEach(item -> populateItem(request, item)); request.resolveOperationOutcome(request.getQuestionnaireResponseAdapter()); logger.info("$populate operation completed"); return request.getQuestionnaireResponseAdapter().get(); @@ -60,23 +59,36 @@ protected Map getVariables(PopulateRequest request, IBase elemen } } } catch (Exception e) { - logger.error("Error encountered evaluating result for variable: {}", expression.getName()); + var message = + String.format("Error encountered evaluating result for variable: %s", expression.getName()); + logger.error(message); + request.logException(message); } }); return variables; } - protected List populateItem( - PopulateRequest request, IQuestionnaireItemComponentAdapter item) { + protected void populateItem(PopulateRequest request, IQuestionnaireItemComponentAdapter item) { var linkId = item.getLinkId(); logger.info("Processing item {}", linkId); - try { - return itemProcessor.processItem(request, item); + // This code is meant to handle a previous QuestionnaireResponse being passed into + // the $populate call. This functionality is still being discussed by the SDC WG + // and how this should be done has not yet been determined. + // var isNew = true; + // if (request.getPreviousQuestionnaireResponseAdapter() != null) { + // isNew = !request.getPreviousQuestionnaireResponseAdapter().hasItem(linkId); + // } + // var responseItem = isNew + // ? new ArrayList() + // : request.getPreviousQuestionnaireResponseAdapter().getItem(linkId); + var responseItem = new ArrayList(); + itemProcessor.processItem(request, item, responseItem); + request.addQuestionnaireResponseItems(responseItem); } catch (Exception e) { - logger.error(e.getMessage()); - request.logException(e.getMessage()); - return List.of(item.newResponseItem()); + var message = String.format("Error encountered populating item (%s): %s", linkId, e.getMessage()); + logger.error(message); + request.logException(message); } } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateRequest.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateRequest.java index 21dc3acc9b..ac145374cb 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateRequest.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateRequest.java @@ -11,12 +11,10 @@ import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBackboneElement; import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IDomainResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.opencds.cqf.fhir.cql.LibraryEngine; @@ -31,6 +29,7 @@ public class PopulateRequest implements IQuestionnaireRequest { private final IQuestionnaireAdapter questionnaireAdapter; + // private final IQuestionnaireResponseAdapter previousQuestionnaireResponseAdapter; private final IQuestionnaireResponseAdapter questionnaireResponseAdapter; private final IIdType subjectId; private final List context; @@ -44,31 +43,30 @@ public class PopulateRequest implements IQuestionnaireRequest { public PopulateRequest( IBaseResource questionnaire, + // IBaseResource questionnaireResponse, IIdType subjectId, List context, - IBaseExtension launchContext, IBaseBundle data, LibraryEngine libraryEngine) { checkNotNull(questionnaire, "expected non-null value for questionnaire"); checkNotNull(libraryEngine, "expected non-null value for libraryEngine"); fhirVersion = questionnaire.getStructureFhirVersionEnum(); - questionnaireAdapter = (IQuestionnaireAdapter) - getAdapterFactory().createKnowledgeArtifactAdapter((IDomainResource) questionnaire); + questionnaireAdapter = getAdapterFactory().createQuestionnaire(questionnaire); + this.libraryEngine = libraryEngine; + // this.previousQuestionnaireResponseAdapter = questionnaireResponse == null + // ? null + // : getAdapterFactory().createQuestionnaireResponse(questionnaireResponse); this.context = context == null ? new ArrayList<>() : context.stream() .map(c -> getAdapterFactory().createParametersParameter(c)) .collect(Collectors.toList()); - this.subjectId = getSubjectId(subjectId); + this.subjectId = getSubjectId(subjectId); // , previousQuestionnaireResponseAdapter); + questionnaireResponseAdapter = createQuestionnaireResponse(); this.data = data; - this.libraryEngine = libraryEngine; var launchContexts = questionnaireAdapter.getExtensionsByUrl(Constants.SDC_QUESTIONNAIRE_LAUNCH_CONTEXT); - if (launchContext != null) { - launchContexts.add(launchContext); - } var parameters = (IBaseParameters) Resources.newBaseForVersion("Parameters", fhirVersion); getAdapterFactory().createParameters(parameters).addParameter("%questionnaire", questionnaireAdapter.get()); - questionnaireResponseAdapter = createQuestionnaireResponse(); contextVariable = questionnaireResponseAdapter.get(); referencedLibraries = questionnaireAdapter.getReferencedLibraries(); inputParameterResolver = IInputParameterResolver.createResolver( @@ -77,6 +75,10 @@ public PopulateRequest( @SuppressWarnings("unchecked") protected IIdType getSubjectId(IIdType subject) { + // , IQuestionnaireResponseAdapter questionnaireResponse) { + // if (questionnaireResponse != null && questionnaireResponse.hasSubject()) { + // return questionnaireResponse.getSubject(); + // } var subjectContext = context.stream() .filter(c -> c.getPartValues("name").stream() .anyMatch(p -> @@ -189,6 +191,10 @@ public IQuestionnaireResponseAdapter getQuestionnaireResponseAdapter() { return questionnaireResponseAdapter; } + // public IQuestionnaireResponseAdapter getPreviousQuestionnaireResponseAdapter() { + // return previousQuestionnaireResponseAdapter; + // } + public void addQuestionnaireResponseItems(List items) { questionnaireResponseAdapter.addItems(items); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/ObservationResolver.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/ObservationResolver.java new file mode 100644 index 0000000000..d2a4dfeaef --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/ObservationResolver.java @@ -0,0 +1,144 @@ +package org.opencds.cqf.fhir.cr.questionnaireresponse.extract; + +import static java.util.Collections.singletonList; +import static org.opencds.cqf.fhir.utility.Resources.newBaseForVersion; +import static org.opencds.cqf.fhir.utility.VersionUtilities.decimalTypeForVersion; +import static org.opencds.cqf.fhir.utility.VersionUtilities.referenceTypeForVersion; +import static org.opencds.cqf.fhir.utility.VersionUtilities.stringTypeForVersion; +import static org.opencds.cqf.fhir.utility.adapter.IAdapter.newDateTimeType; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseCoding; +import org.hl7.fhir.instance.model.api.IBaseDatatype; +import org.hl7.fhir.instance.model.api.IBaseExtension; +import org.hl7.fhir.instance.model.api.IBaseHasExtensions; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.ICompositeType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.opencds.cqf.fhir.utility.Constants; +import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireItemComponentAdapter; +import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireResponseItemAnswerComponentAdapter; + +public class ObservationResolver { + // Observation-based extraction - + // http://build.fhir.org/ig/HL7/sdc/extraction.html#observation-based-extraction + + public IBaseResource resolve( + ExtractRequest request, + IQuestionnaireResponseItemAnswerComponentAdapter answerAdapter, + IQuestionnaireItemComponentAdapter itemAdapter, + String linkId, + IBaseReference subject, + Map> questionnaireCodeMap, + IBaseExtension categoryExt) { + var fhirVersion = request.getFhirVersion(); + var questionnaireResponseAdapter = request.getQuestionnaireResponseAdapter(); + var authoredDate = (questionnaireResponseAdapter.hasAuthored() + ? questionnaireResponseAdapter.getAuthored().toInstant() + : Instant.now()) + .toString(); + var category = (ICompositeType) (categoryExt == null + ? request.getAdapterFactory() + .createCodeableConcept(newCodeableConcept(request)) + .addCoding(Constants.SDC_OBSERVATION_CATEGORY, Constants.SDC_CATEGORY_SURVEY, null) + : request.getAdapterFactory().createCodeableConcept(categoryExt.getValue())) + .get(); + var code = (ICompositeType) request.getAdapterFactory() + .createCodeableConcept(newCodeableConcept(request)) + .setCoding(new ArrayList<>(questionnaireCodeMap.get(linkId))) + .get(); + var obs = request.getAdapterFactory() + .createObservation((IBaseResource) newBaseForVersion("Observation", fhirVersion)) + .setBasedOn(questionnaireResponseAdapter.getBasedOn()) + .setPartOf(questionnaireResponseAdapter.getPartOf()) + .setDerivedFrom(singletonList( + referenceTypeForVersion(fhirVersion, (IAnyResource) questionnaireResponseAdapter.get()))) + .setStatus("final") + .setCategory(singletonList(category)) + .setCode(code) + .setSubject(subject) + .setEncounter(questionnaireResponseAdapter.getEncounter()) + .setEffective(authoredDate) + .setIssued(authoredDate) + .setPerformer(singletonList(questionnaireResponseAdapter.getAuthor())) + .setValue(getAnswerValue(request, answerAdapter, itemAdapter)); + obs.setId(request.getExtractId() + "." + linkId); + var linkIdExtension = obs.addExtension(); + linkIdExtension.setUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/derivedFromLinkId"); + var innerLinkIdExtension = ((IBaseHasExtensions) linkIdExtension).addExtension(); + innerLinkIdExtension.setUrl("text"); + innerLinkIdExtension.setValue(stringTypeForVersion(fhirVersion, linkId)); + return obs.get(); + } + + protected IBaseDatatype getAnswerValue( + ExtractRequest request, + IQuestionnaireResponseItemAnswerComponentAdapter answerAdapter, + IQuestionnaireItemComponentAdapter itemAdapter) { + IBaseDatatype value; + switch (answerAdapter.getValue().fhirType()) { + case "Coding": + value = (IBaseDatatype) request.getAdapterFactory() + .createCodeableConcept(newCodeableConcept(request)) + .addCoding((IBaseCoding) answerAdapter.getValue()) + .get(); + break; + case "date": + //noinspection unchecked + value = newDateTimeType( + request.getFhirVersion(), ((IPrimitiveType) answerAdapter.getValue()).getValue()); + break; + case "decimal", "integer": + if (itemAdapter != null && itemAdapter.hasExtension(Constants.QUESTIONNAIRE_UNIT)) { + value = getQuantity( + request, + answerAdapter, + itemAdapter + .getExtensionByUrl(Constants.QUESTIONNAIRE_UNIT) + .getValue()); + } else { + value = (IBaseDatatype) answerAdapter.getValue(); + } + break; + default: + value = (IBaseDatatype) answerAdapter.getValue(); + } + return value; + } + + protected IBaseDatatype getQuantity( + ExtractRequest request, IQuestionnaireResponseItemAnswerComponentAdapter answer, IBaseDatatype unitCoding) { + var unit = request.getAdapterFactory().createCoding(unitCoding); + var quantity = request.getAdapterFactory().createBase(newBaseForVersion("Quantity", request.getFhirVersion())); + quantity.setValue("code", unit.getCodeType()); + quantity.setValue("unit", stringTypeForVersion(request.getFhirVersion(), unit.getDisplay())); + quantity.setValue("system", stringTypeForVersion(request.getFhirVersion(), unit.getSystem())); + var value = answer.getValue(); + if (value.fhirType().equals("decimal")) { + //noinspection unchecked + quantity.setValue( + "value", + decimalTypeForVersion(request.getFhirVersion(), ((IPrimitiveType) value).getValue())); + } + if (value.fhirType().equals("integer")) { + //noinspection unchecked + quantity.setValue( + "value", + decimalTypeForVersion( + request.getFhirVersion(), new BigDecimal(((IPrimitiveType) value).getValue()))); + } + return (IBaseDatatype) quantity.get(); + } + + protected IBase newCodeableConcept(ExtractRequest request) { + return newBaseForVersion("CodeableConcept", request.getFhirVersion()); + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/ProcessItem.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/ProcessItem.java index 18b8872955..2e290af322 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/ProcessItem.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/ProcessItem.java @@ -11,6 +11,12 @@ import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireResponseItemAnswerComponentAdapter; public class ProcessItem { + private final ObservationResolver observationResolver; + + public ProcessItem() { + observationResolver = new ObservationResolver(); + } + public void processItem( ExtractRequest request, ItemPair itemPair, @@ -59,18 +65,7 @@ private IBaseResource createObservationFromItemAnswer( IBaseReference subject, Map> questionnaireCodeMap, IBaseExtension categoryExt) { - // Observation-based extraction - - // http://build.fhir.org/ig/HL7/sdc/extraction.html#observation-based-extraction - return switch (request.getFhirVersion()) { - case R4 -> - new org.opencds.cqf.fhir.cr.questionnaireresponse.extract.r4.ObservationResolver() - .resolve( - request, answer, questionnaireItem, linkId, subject, questionnaireCodeMap, categoryExt); - case R5 -> - new org.opencds.cqf.fhir.cr.questionnaireresponse.extract.r5.ObservationResolver() - .resolve( - request, answer, questionnaireItem, linkId, subject, questionnaireCodeMap, categoryExt); - default -> null; - }; + return observationResolver.resolve( + request, answer, questionnaireItem, linkId, subject, questionnaireCodeMap, categoryExt); } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/r4/ObservationResolver.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/r4/ObservationResolver.java deleted file mode 100644 index b7dc2815d0..0000000000 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/r4/ObservationResolver.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.opencds.cqf.fhir.cr.questionnaireresponse.extract.r4; - -import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import org.hl7.fhir.instance.model.api.IBaseCoding; -import org.hl7.fhir.instance.model.api.IBaseExtension; -import org.hl7.fhir.instance.model.api.IBaseReference; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.CodeableConcept; -import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.DateTimeType; -import org.hl7.fhir.r4.model.DateType; -import org.hl7.fhir.r4.model.Extension; -import org.hl7.fhir.r4.model.InstantType; -import org.hl7.fhir.r4.model.Observation; -import org.hl7.fhir.r4.model.Quantity; -import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; -import org.hl7.fhir.r4.model.QuestionnaireResponse; -import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.StringType; -import org.opencds.cqf.fhir.cr.questionnaireresponse.extract.ExtractRequest; -import org.opencds.cqf.fhir.utility.Constants; -import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireItemComponentAdapter; -import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireResponseItemAnswerComponentAdapter; - -public class ObservationResolver { - public IBaseResource resolve( - ExtractRequest request, - IQuestionnaireResponseItemAnswerComponentAdapter answerAdapter, - IQuestionnaireItemComponentAdapter itemAdapter, - String linkId, - IBaseReference subject, - Map> questionnaireCodeMap, - IBaseExtension categoryExt) { - var questionnaireResponse = (QuestionnaireResponse) request.getQuestionnaireResponse(); - var answer = (QuestionnaireResponseItemAnswerComponent) answerAdapter.get(); - var item = (QuestionnaireItemComponent) (itemAdapter == null ? null : itemAdapter.get()); - var obs = new Observation(); - obs.setId(request.getExtractId() + "." + linkId); - obs.setBasedOn(questionnaireResponse.getBasedOn()); - obs.setPartOf(questionnaireResponse.getPartOf()); - obs.setStatus(Observation.ObservationStatus.FINAL); - - var qrCategoryCode = categoryExt == null - ? new CodeableConcept() - .addCoding(new Coding() - .setSystem(Constants.SDC_OBSERVATION_CATEGORY) - .setCode(Constants.SDC_CATEGORY_SURVEY)) - : (CodeableConcept) categoryExt.getValue(); - obs.setCategory(Collections.singletonList(qrCategoryCode)); - - obs.setCode(new CodeableConcept() - .setCoding(questionnaireCodeMap.get(linkId).stream() - .map(c -> (Coding) c) - .collect(Collectors.toList()))); - obs.setSubject((Reference) subject); - obs.setEncounter(questionnaireResponse.getEncounter()); - var authoredDate = new DateTimeType((questionnaireResponse.hasAuthored() - ? questionnaireResponse.getAuthored().toInstant() - : Instant.now()) - .toString()); - obs.setEffective(authoredDate); - obs.setIssuedElement(new InstantType(authoredDate)); - obs.setPerformer(Collections.singletonList(questionnaireResponse.getAuthor())); - - switch (answer.getValue().fhirType()) { - case "Coding": - obs.setValue(new CodeableConcept().addCoding(answer.getValueCoding())); - break; - case "date": - obs.setValue(new DateTimeType(((DateType) answer.getValue()).getValue())); - break; - case "decimal", "integer": - if (item != null && item.hasExtension(Constants.QUESTIONNAIRE_UNIT)) { - obs.setValue(getQuantity(answer, item)); - } else { - obs.setValue(answer.getValue()); - } - break; - default: - obs.setValue(answer.getValue()); - } - obs.setDerivedFrom(Collections.singletonList(new Reference(questionnaireResponse))); - - var linkIdExtension = new Extension(); - linkIdExtension.setUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/derivedFromLinkId"); - var innerLinkIdExtension = new Extension(); - innerLinkIdExtension.setUrl("text"); - innerLinkIdExtension.setValue(new StringType(linkId)); - linkIdExtension.setExtension(Collections.singletonList(innerLinkIdExtension)); - obs.addExtension(linkIdExtension); - return obs; - } - - protected Quantity getQuantity(QuestionnaireResponseItemAnswerComponent answer, QuestionnaireItemComponent item) { - var unit = (Coding) item.getExtensionByUrl(Constants.QUESTIONNAIRE_UNIT).getValue(); - var quantity = new Quantity() - .setUnit(unit.getDisplay()) - .setSystem(unit.getSystem()) - .setCode(unit.getCode()); - if (answer.hasValueDecimalType()) { - quantity.setValueElement(answer.getValueDecimalType()); - } - if (answer.hasValueIntegerType()) { - quantity.setValue(answer.getValueIntegerType().getValue()); - } - return quantity; - } -} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/r5/ObservationResolver.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/r5/ObservationResolver.java deleted file mode 100644 index a751de60bb..0000000000 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/r5/ObservationResolver.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.opencds.cqf.fhir.cr.questionnaireresponse.extract.r5; - -import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import org.hl7.fhir.instance.model.api.IBaseCoding; -import org.hl7.fhir.instance.model.api.IBaseExtension; -import org.hl7.fhir.instance.model.api.IBaseReference; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r5.model.CodeableConcept; -import org.hl7.fhir.r5.model.Coding; -import org.hl7.fhir.r5.model.DateTimeType; -import org.hl7.fhir.r5.model.DateType; -import org.hl7.fhir.r5.model.Enumerations.ObservationStatus; -import org.hl7.fhir.r5.model.Extension; -import org.hl7.fhir.r5.model.InstantType; -import org.hl7.fhir.r5.model.Observation; -import org.hl7.fhir.r5.model.Quantity; -import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent; -import org.hl7.fhir.r5.model.QuestionnaireResponse; -import org.hl7.fhir.r5.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; -import org.hl7.fhir.r5.model.Reference; -import org.hl7.fhir.r5.model.StringType; -import org.opencds.cqf.fhir.cr.questionnaireresponse.extract.ExtractRequest; -import org.opencds.cqf.fhir.utility.Constants; -import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireItemComponentAdapter; -import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireResponseItemAnswerComponentAdapter; - -public class ObservationResolver { - public IBaseResource resolve( - ExtractRequest request, - IQuestionnaireResponseItemAnswerComponentAdapter answerAdapter, - IQuestionnaireItemComponentAdapter itemAdapter, - String linkId, - IBaseReference subject, - Map> questionnaireCodeMap, - IBaseExtension categoryExt) { - var questionnaireResponse = (QuestionnaireResponse) request.getQuestionnaireResponse(); - var answer = (QuestionnaireResponseItemAnswerComponent) answerAdapter.get(); - var item = (QuestionnaireItemComponent) (itemAdapter == null ? null : itemAdapter.get()); - var obs = new Observation(); - obs.setId(request.getExtractId() + "." + linkId); - obs.setBasedOn(questionnaireResponse.getBasedOn()); - obs.setPartOf(questionnaireResponse.getPartOf()); - obs.setStatus(ObservationStatus.FINAL); - - var qrCategoryCode = categoryExt == null - ? new CodeableConcept() - .addCoding(new Coding() - .setSystem(Constants.SDC_OBSERVATION_CATEGORY) - .setCode(Constants.SDC_CATEGORY_SURVEY)) - : (CodeableConcept) categoryExt.getValue(); - obs.setCategory(Collections.singletonList(qrCategoryCode)); - - obs.setCode(new CodeableConcept() - .setCoding(questionnaireCodeMap.get(linkId).stream() - .map(c -> (Coding) c) - .collect(Collectors.toList()))); - obs.setSubject((Reference) subject); - obs.setEncounter(questionnaireResponse.getEncounter()); - var authoredDate = new DateTimeType((questionnaireResponse.hasAuthored() - ? questionnaireResponse.getAuthored().toInstant() - : Instant.now()) - .toString()); - obs.setEffective(authoredDate); - obs.setIssuedElement(new InstantType(authoredDate)); - obs.setPerformer(Collections.singletonList(questionnaireResponse.getAuthor())); - - switch (answer.getValue().fhirType()) { - case "Coding": - obs.setValue(new CodeableConcept().addCoding(answer.getValueCoding())); - break; - case "date": - obs.setValue(new DateTimeType(((DateType) answer.getValue()).getValue())); - break; - case "decimal", "integer": - if (item != null && item.hasExtension(Constants.QUESTIONNAIRE_UNIT)) { - obs.setValue(getQuantity(answer, item)); - } else { - obs.setValue(answer.getValue()); - } - break; - default: - obs.setValue(answer.getValue()); - } - obs.setDerivedFrom(Collections.singletonList(new Reference(questionnaireResponse))); - - var linkIdExtension = new Extension(); - linkIdExtension.setUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/derivedFromLinkId"); - var innerLinkIdExtension = new Extension(); - innerLinkIdExtension.setUrl("text"); - innerLinkIdExtension.setValue(new StringType(linkId)); - linkIdExtension.setExtension(Collections.singletonList(innerLinkIdExtension)); - obs.addExtension(linkIdExtension); - return obs; - } - - protected Quantity getQuantity(QuestionnaireResponseItemAnswerComponent answer, QuestionnaireItemComponent item) { - var unit = (Coding) item.getExtensionByUrl(Constants.QUESTIONNAIRE_UNIT).getValue(); - var quantity = new Quantity() - .setUnit(unit.getDisplay()) - .setSystem(unit.getSystem()) - .setCode(unit.getCode()); - if (answer.hasValueDecimalType()) { - quantity.setValueElement(answer.getValueDecimalType()); - } - if (answer.hasValueIntegerType()) { - quantity.setValue(answer.getValueIntegerType().getValue()); - } - return quantity; - } -} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/common/DynamicValueProcessorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/common/DynamicValueProcessorTests.java index 040f0dc6d6..06e437a510 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/common/DynamicValueProcessorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/common/DynamicValueProcessorTests.java @@ -60,7 +60,7 @@ void testNullExpressionResult() { .setLanguage("text/cql-identifier"))); var requestAction = new org.hl7.fhir.r4.model.RequestGroup.RequestGroupActionComponent(); doReturn(cqfExpression).when(fixture).getDynamicValueExpression(request, dynamicValue); - doReturn(null).when(fixture).getDynamicValueExpressionResult(request, cqfExpression, null, null); + doReturn(null).when(fixture).getDynamicValueExpressionResult(request, cqfExpression, null, null, requestAction); var logger = TestLoggerFactory.getTestLogger(DynamicValueProcessor.class); logger.clear(); @@ -90,7 +90,7 @@ void dstu3PriorityExt() { doReturn(cqfExpression).when(fixture).getDynamicValueExpression(requestDstu3, dvDstu3); doReturn(expressionResultsDstu3) .when(fixture) - .getDynamicValueExpressionResult(requestDstu3, cqfExpression, null, null); + .getDynamicValueExpressionResult(requestDstu3, cqfExpression, null, null, raDstu3); fixture.resolveDynamicValue(requestDstu3, dvDstu3, null, null, raDstu3); @@ -108,7 +108,7 @@ void dstu3PriorityExt() { doReturn(cqfExpression).when(fixture).getDynamicValueExpression(requestR4, dvR4); doReturn(expressionResultsR4) .when(fixture) - .getDynamicValueExpressionResult(requestR4, cqfExpression, null, null); + .getDynamicValueExpressionResult(requestR4, cqfExpression, null, null, raR4); assertThrows(IllegalArgumentException.class, () -> { fixture.resolveDynamicValue(requestR4, dvR4, null, null, raR4); }); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/common/InputParametersTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/common/InputParametersTest.java index a903c4cb14..19b137f73e 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/common/InputParametersTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/common/InputParametersTest.java @@ -5,6 +5,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; import static org.opencds.cqf.fhir.utility.Parameters.newPart; import static org.opencds.cqf.fhir.utility.Parameters.newStringPart; @@ -21,10 +22,12 @@ import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.DataRequirement; import org.hl7.fhir.r4.model.DataRequirement.DataRequirementCodeFilterComponent; +import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent; @@ -586,4 +589,46 @@ void testResolveInputParametersWithDataRequirements() { assertNotNull(parameter); assertEquals(obs, parameter.getResource()); } + + @Test + void testResolveParametersSkipsRefetchWhenAlreadyInBaseParametersR4() { + // When baseParameters already contains %subject/%encounter/%practitioner + // (e.g. passed in from a parent PlanDefinition's resolver into an ActivityDefinition's + // resolver), the resolver must not re-fetch and produce duplicate entries. + var patient = new Patient(); + patient.setIdElement(Ids.newId(fhirContextR4, "Patient", patientId)); + var encounter = new Encounter(); + encounter.setIdElement(Ids.newId(fhirContextR4, "Encounter", encounterId)); + var practitioner = new Practitioner(); + practitioner.setIdElement(Ids.newId(fhirContextR4, "Practitioner", practitionerId)); + + var baseParameters = new Parameters(); + baseParameters.addParameter().setName("%subject").setResource(patient); + baseParameters.addParameter().setName("%encounter").setResource(encounter); + baseParameters.addParameter().setName("%practitioner").setResource(practitioner); + + doReturn(fhirContextR4).when(repository).fhirContext(); + lenient().doReturn(patient).when(repository).read(Patient.class, patient.getIdElement()); + lenient().doReturn(encounter).when(repository).read(Encounter.class, encounter.getIdElement()); + lenient().doReturn(practitioner).when(repository).read(Practitioner.class, practitioner.getIdElement()); + + var resolver = IInputParameterResolver.createResolver( + repository, + patient.getIdElement(), + encounter.getIdElement(), + practitioner.getIdElement(), + baseParameters, + null, + null, + null); + var actual = (Parameters) resolver.getParameters(); + assertNotNull(actual); + assertEquals(3, actual.getParameter().size()); + assertEquals("%subject", actual.getParameter().get(0).getName()); + assertEquals(patient, actual.getParameter().get(0).getResource()); + assertEquals("%encounter", actual.getParameter().get(1).getName()); + assertEquals(encounter, actual.getParameter().get(1).getResource()); + assertEquals("%practitioner", actual.getParameter().get(2).getName()); + assertEquals(practitioner, actual.getParameter().get(2).getResource()); + } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/helpers/RequestHelpers.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/helpers/RequestHelpers.java index 4d15828472..134635d107 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/helpers/RequestHelpers.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/helpers/RequestHelpers.java @@ -2,6 +2,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import java.util.List; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.opencds.cqf.fhir.cql.LibraryEngine; @@ -107,7 +108,7 @@ public static GenerateRequest newGenerateRequestForVersion( public static GenerateRequest newGenerateRequestForVersion( FhirVersionEnum fhirVersion, IBaseResource profile, LibraryEngine libraryEngine) { - return new GenerateRequest(profile, false, true, libraryEngine); + return new GenerateRequest(List.of(profile), false, true, libraryEngine); } public static PopulateRequest newPopulateRequestForVersion( @@ -117,7 +118,6 @@ public static PopulateRequest newPopulateRequestForVersion( Ids.newId(fhirVersion, Ids.ensureIdType(PATIENT_ID, "Patient")), null, null, - null, libraryEngine); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessDefinitionTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessDefinitionTests.java index 114fb46446..4cb0fefa0e 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessDefinitionTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ProcessDefinitionTests.java @@ -1,26 +1,37 @@ package org.opencds.cqf.fhir.cr.plandefinition.apply; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.opencds.cqf.fhir.cr.helpers.RequestHelpers.newPDApplyRequestForVersion; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.repository.IRepository; -import org.hl7.fhir.exceptions.FHIRException; +import java.util.List; import org.hl7.fhir.r4.model.ActivityDefinition; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.CarePlan; +import org.hl7.fhir.r4.model.Library; import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.PlanDefinition; import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.Task; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; @@ -35,8 +46,6 @@ class ProcessDefinitionTests { private static final String QUESTIONNAIRE = "http://test.fhir.org/fhir/Questionnaire/test"; private static final String TASK = "http://test.fhir.org/fhir/Task/test"; - private final FhirContext fhirContextR4 = FhirContext.forR4Cached(); - @Mock IRepository repository; @@ -59,9 +68,9 @@ void setup() { @Test void applyActivityDefinitionShouldReturnNullOnException() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine); - var definition = new CanonicalType(ACTIVITYDEFINITION); - doReturn(fhirContextR4).when(repository).fhirContext(); - var result = fixture.applyActivityDefinition(request, definition); + var activityDef = new ActivityDefinition().setUrl(ACTIVITYDEFINITION); + activityDef.setId("test"); + var result = fixture.applyActivityDefinition(request, activityDef); assertNull(result); var oc = (OperationOutcome) request.getOperationOutcome(); assertTrue(oc.hasIssue()); @@ -70,9 +79,10 @@ void applyActivityDefinitionShouldReturnNullOnException() { @Test void applyNestedPlanDefinitionShouldReturnNullOnException() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine); - var definition = new CanonicalType(PLANDEFINITION); - doReturn(fhirContextR4).when(repository).fhirContext(); - var result = fixture.applyNestedPlanDefinition(request, definition); + var nestedPlanDef = new PlanDefinition().setUrl(PLANDEFINITION); + nestedPlanDef.setId("nested"); + doThrow(new RuntimeException("boom")).when(applyProcessor).applyPlanDefinition(any()); + var result = fixture.applyNestedPlanDefinition(request, nestedPlanDef); assertNull(result); var oc = (OperationOutcome) request.getOperationOutcome(); assertTrue(oc.hasIssue()); @@ -83,7 +93,10 @@ void resolveDefinitionShouldReturnQuestionnaire() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine); var definition = new CanonicalType(QUESTIONNAIRE); var expectedQuestionnaire = new Questionnaire().setUrl(QUESTIONNAIRE); - doReturn(expectedQuestionnaire).when(fixture).resolveRepository(definition); + expectedQuestionnaire.setId("q1"); + doReturn(toSearchBundle(List.of(expectedQuestionnaire))) + .when(fixture) + .searchByCanonicalType(eq("Questionnaire"), eq(QUESTIONNAIRE), isNull()); var result = fixture.resolveDefinition(request, definition); assertEquals(expectedQuestionnaire, result); var oc = request.getOperationOutcome(); @@ -96,38 +109,176 @@ void applyQuestionnaireDefinitionShouldReturnContainedQuestionnaire() { var definition = new CanonicalType("#Questionnaire/test"); var expectedQuestionnaire = new Questionnaire().setUrl(QUESTIONNAIRE); doReturn(expectedQuestionnaire).when(fixture).resolveContained(request, definition.getValue()); - var result = fixture.applyQuestionnaireDefinition(request, definition); + var result = fixture.resolveDefinition(request, definition); assertEquals(expectedQuestionnaire, result); var oc = request.getOperationOutcome(); assertNull(oc); } @Test - void applyQuestionnaireDefinitionShouldReturnNullOnException() { + void resolveDefinitionReturnsNullWhenCanonicalNotFound() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine); - var definition = new CanonicalType(QUESTIONNAIRE); - doReturn(fhirContextR4).when(repository).fhirContext(); - var result = fixture.applyQuestionnaireDefinition(request, definition); + var definition = new CanonicalType("http://test.fhir.org/fhir/Foo/test"); + doReturn(buildTransactionResponse(List.of(), List.of(), List.of(), List.of(), List.of(), List.of())) + .when(repository) + .transaction(any(Bundle.class)); + var result = fixture.resolveDefinition(request, definition); assertNull(result); var oc = (OperationOutcome) request.getOperationOutcome(); assertTrue(oc.hasIssue()); } @Test - void resolveDefinitionShouldFailOnInvalidAction() { + void resolveDefinitionThrowsWhenMultipleCanonicalMatches() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine); - var definition = new CanonicalType(TASK); - doReturn("Task").when(fixture).resolveResourceName(request, definition); - assertThrows(FHIRException.class, () -> { - fixture.resolveDefinition(request, definition); - }); + var definition = new CanonicalType("http://test.fhir.org/fhir/Foo/test"); + var a = new PlanDefinition().setUrl("http://test.fhir.org/fhir/Foo/test"); + var b = new PlanDefinition().setUrl("http://test.fhir.org/fhir/Foo/test"); + doReturn(buildTransactionResponse(List.of(), List.of(), List.of(a, b), List.of(), List.of(), List.of())) + .when(repository) + .transaction(any(Bundle.class)); + + var ex = assertThrows(IllegalStateException.class, () -> fixture.resolveDefinition(request, definition)); + assertTrue(ex.getMessage().contains("Multiple resources (2)")); + assertTrue(ex.getMessage().contains("Specify a version")); } @Test - void resolveResourceNameShouldFailIfCanonicalHasNoValue() { + void resolveDefinitionAmbiguityErrorMentionsVersionWhenVersionSpecified() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine); - final CanonicalType canonical = new CanonicalType(); - assertThrows(FHIRException.class, () -> fixture.resolveResourceName(request, canonical)); + var definition = new CanonicalType("http://test.fhir.org/fhir/Foo/test|1.0.0"); + var a = new PlanDefinition() + .setUrl("http://test.fhir.org/fhir/Foo/test") + .setVersion("1.0.0"); + var b = new PlanDefinition() + .setUrl("http://test.fhir.org/fhir/Foo/test") + .setVersion("1.0.0"); + doReturn(buildTransactionResponse(List.of(), List.of(), List.of(a, b), List.of(), List.of(), List.of())) + .when(repository) + .transaction(any(Bundle.class)); + + var ex = assertThrows(IllegalStateException.class, () -> fixture.resolveDefinition(request, definition)); + assertTrue(ex.getMessage().contains("Even with the specified version")); + } + + @Test + void resolveDefinitionSendsTransactionWithSearchEntriesForAllSupportedTypes() { + var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine); + var definition = new CanonicalType("http://test.fhir.org/fhir/Foo/test"); + var captor = ArgumentCaptor.forClass(Bundle.class); + doReturn(buildTransactionResponse(List.of(), List.of(), List.of(), List.of(), List.of(), List.of())) + .when(repository) + .transaction(captor.capture()); + + fixture.resolveDefinition(request, definition); + + var sent = captor.getValue(); + assertEquals(Bundle.BundleType.TRANSACTION, sent.getType()); + assertEquals(6, sent.getEntry().size()); + var urls = sent.getEntry().stream().map(e -> e.getRequest().getUrl()).toList(); + assertTrue(urls.contains("Questionnaire?url=http://test.fhir.org/fhir/Foo/test")); + assertTrue(urls.contains("ActivityDefinition?url=http://test.fhir.org/fhir/Foo/test")); + assertTrue(urls.contains("PlanDefinition?url=http://test.fhir.org/fhir/Foo/test")); + assertTrue(urls.contains("MessageDefinition?url=http://test.fhir.org/fhir/Foo/test")); + assertTrue(urls.contains("ObservationDefinition?url=http://test.fhir.org/fhir/Foo/test")); + assertTrue(urls.contains("SpecimenDefinition?url=http://test.fhir.org/fhir/Foo/test")); + sent.getEntry() + .forEach(e -> assertEquals(Bundle.HTTPVerb.GET, e.getRequest().getMethod())); + } + + @Test + void resolveDefinitionIncludesVersionInSearchUrlsWhenProvided() { + var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine); + var definition = new CanonicalType("http://test.fhir.org/fhir/Foo/test|1.2.3"); + var captor = ArgumentCaptor.forClass(Bundle.class); + doReturn(buildTransactionResponse(List.of(), List.of(), List.of(), List.of(), List.of(), List.of())) + .when(repository) + .transaction(captor.capture()); + + fixture.resolveDefinition(request, definition); + + var urls = captor.getValue().getEntry().stream() + .map(e -> e.getRequest().getUrl()) + .toList(); + urls.forEach(u -> assertTrue(u.contains("&version=1.2.3"), "Expected version param in: " + u)); + assertTrue(urls.contains("PlanDefinition?url=http://test.fhir.org/fhir/Foo/test&version=1.2.3")); + } + + @Test + void resolveDefinitionOmitsVersionInSearchUrlsWhenAbsent() { + var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine); + var definition = new CanonicalType("http://test.fhir.org/fhir/Foo/test"); + var captor = ArgumentCaptor.forClass(Bundle.class); + doReturn(buildTransactionResponse(List.of(), List.of(), List.of(), List.of(), List.of(), List.of())) + .when(repository) + .transaction(captor.capture()); + + fixture.resolveDefinition(request, definition); + + captor.getValue().getEntry().stream() + .map(e -> e.getRequest().getUrl()) + .forEach(u -> assertFalse(u.contains("version="), "Did not expect version param in: " + u)); + } + + @Test + void resolveDefinitionReturnsResourceAsIsForUnsupportedFhirType() { + var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine); + var definition = new CanonicalType("http://test.fhir.org/fhir/Library/test"); + var library = new Library().setUrl("http://test.fhir.org/fhir/Library/test"); + library.setId("lib-1"); + doReturn(buildTransactionResponse(List.of(), List.of(), List.of(library), List.of(), List.of(), List.of())) + .when(repository) + .transaction(any(Bundle.class)); + + var result = fixture.resolveDefinition(request, definition); + + assertNotNull(result); + assertEquals("Library", result.fhirType()); + assertEquals(library, result); + } + + @Test + void resolveDefinitionRoutesByFhirTypeNotByUrl() { + var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine); + var definition = new CanonicalType("http://test.fhir.org/fhir/Foo/test"); + var planDef = new PlanDefinition().setUrl("http://test.fhir.org/fhir/Foo/test"); + planDef.setId("nested-1"); + doReturn(buildTransactionResponse(List.of(), List.of(), List.of(planDef), List.of(), List.of(), List.of())) + .when(repository) + .transaction(any(Bundle.class)); + var carePlan = new CarePlan(); + carePlan.setId("cp-1"); + doReturn(carePlan).when(applyProcessor).applyPlanDefinition(any()); + + var result = fixture.resolveDefinition(request, definition); + + assertEquals(carePlan, result); + } + + private static Bundle buildTransactionResponse( + List questionnaireMatches, + List activityDefinitionMatches, + List planDefinitionMatches, + List messageDefinitionMatches, + List observationDefinitionMatches, + List specimenDefinitionMatches) { + var response = new Bundle(); + response.addEntry().setResource(toSearchBundle(questionnaireMatches)); + response.addEntry().setResource(toSearchBundle(activityDefinitionMatches)); + response.addEntry().setResource(toSearchBundle(planDefinitionMatches)); + response.addEntry().setResource(toSearchBundle(messageDefinitionMatches)); + response.addEntry().setResource(toSearchBundle(observationDefinitionMatches)); + response.addEntry().setResource(toSearchBundle(specimenDefinitionMatches)); + return response; + } + + private static Bundle toSearchBundle(List resources) { + var bundle = new Bundle(); + bundle.setType(Bundle.BundleType.SEARCHSET); + for (var r : resources) { + bundle.addEntry().setResource(r); + } + return bundle; } @Test @@ -141,9 +292,8 @@ void applyActivityDefinitionHandlesMultipleRequestResources() { request.getRequestResources().add(task); var newTask = new Task(); newTask.setId("test"); - doReturn(activityDef).when(fixture).resolveRepository(any()); doReturn(newTask).when(applyProcessor).applyActivityDefinition(any()); - var result = fixture.applyActivityDefinition(request, definition); + var result = fixture.applyActivityDefinition(request, activityDef); assertInstanceOf(Task.class, result); assertEquals("test2", result.getIdElement().getIdPart()); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessorTests.java index 1e95c9f4e2..f47c90a622 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessorTests.java @@ -27,8 +27,10 @@ import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.StructureDefinition; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cr.common.DataRequirementsProcessor; import org.opencds.cqf.fhir.cr.common.PackageProcessor; import org.opencds.cqf.fhir.cr.questionnaire.generate.GenerateProcessor; import org.opencds.cqf.fhir.cr.questionnaire.populate.PopulateProcessor; @@ -53,6 +55,7 @@ void processors() { .generateProcessor(new GenerateProcessor(repositoryR4)) .packageProcessor(new PackageProcessor(repositoryR4)) .populateProcessor(new PopulateProcessor()) + .dataRequirementsProcessor(new DataRequirementsProcessor(repositoryR4)) .when() .questionnaireId(Ids.newId(fhirContextR4, "Questionnaire", "OutpatientPriorAuthorizationRequest")) .isPut(Boolean.FALSE) @@ -326,14 +329,29 @@ void testItemContextPopulationWithoutDefinition() { .itemHasAnswerValue("1", new org.hl7.fhir.r4.model.Quantity(50)); } + @Test + void testGenerateR4() { + var profile = + repositoryR4.read(StructureDefinition.class, new IdType("StructureDefinition", "RouteOnePatient")); + given().repository(repositoryR4).when().profile(profile).thenGenerate().hasItems(8); + } + + @Test + void testGenerateR5() { + var profile = repositoryR5.read( + org.hl7.fhir.r5.model.StructureDefinition.class, new IdType("StructureDefinition", "RouteOnePatient")); + given().repository(repositoryR5).when().profile(profile).thenGenerate().hasItems(8); + } + @Test void testIntegration() { var patientId = "Patient1"; var questionnaire = given().repository(repositoryR4) .when() - .profileId(Ids.newId(fhirContextR4, "StructureDefinition", "LaunchContexts")) + .profileUrl(canonicalTypeForVersion( + FhirVersionEnum.R4, "http://fhir.org/test/StructureDefinition/LaunchContexts")) .thenGenerate() - .hasItems(2) + .hasItems(1) .hasExtensions(Constants.SDC_QUESTIONNAIRE_LAUNCH_CONTEXT, 5) .questionnaire .get(); @@ -353,9 +371,9 @@ void testIntegration() { newPart(fhirContextR4, "Reference", "content", "Encounter/Encounter1")))) .thenPopulate(true) .hasNoErrors() - .hasItems(2) - .itemHasAnswer("1.1") - .itemHasAuthorExt("1.1") + .hasItems(1) + .itemHasAnswer("1") + .itemHasAuthorExt("1") .questionnaireResponse .get(); assertEquals("Patient/" + patientId, questionnaireResponse.getSubject().getReference()); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestItemGenerator.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestItemGenerator.java index 4e3eb761ed..f31c477ff8 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestItemGenerator.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestItemGenerator.java @@ -14,6 +14,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -25,7 +26,6 @@ import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireAdapter; import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireItemComponentAdapter; -import org.opencds.cqf.fhir.utility.monad.Eithers; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; import org.skyscreamer.jsonassert.JSONAssert; @@ -48,6 +48,7 @@ public static Given given() { return new Given(); } + @SuppressWarnings("UnstableApiUsage") public static class Given { private IRepository repository; @@ -71,26 +72,27 @@ public When when() { } } + @SuppressWarnings("UnstableApiUsage") public static class When { private final IRepository repository; private final QuestionnaireProcessor processor; - private IIdType profileId; - private IPrimitiveType profileUrl; + private List> profileUrl; private IBaseResource profile; private String id; When(IRepository repository, QuestionnaireProcessor itemGenerator) { this.repository = repository; this.processor = itemGenerator; + this.profileUrl = new ArrayList<>(); } - public When profileId(IIdType id) { - profileId = id; + public When profileUrl(IPrimitiveType url) { + profileUrl.add(url); return this; } - public When profileUrl(IPrimitiveType url) { - profileUrl = url; + public When profileUrls(List> urls) { + profileUrl = urls; return this; } @@ -106,8 +108,8 @@ public When id(String id) { public GeneratedItem then() { IBaseResource result; - if (profileUrl != null || profileId != null || profile != null) { - result = processor.generateQuestionnaire(Eithers.for3(profileUrl, profileId, profile), false, true); + if (!profileUrl.isEmpty() || profile != null) { + result = processor.generateQuestionnaire(profile, profileUrl, false, true); } else { result = processor.generateQuestionnaire(id); } @@ -185,6 +187,14 @@ public GeneratedItem itemHasHiddenValueExtension(String linkId) { return this; } + public GeneratedItem itemHasNoInitialExpression(String linkId) { + var item = items.get(linkId); + assertNotNull(item); + assertTrue(item.getExtension().stream() + .noneMatch(e -> e.getUrl().equals(Constants.SDC_QUESTIONNAIRE_INITIAL_EXPRESSION))); + return this; + } + public GeneratedItem itemHasInitialExpression(String linkId) { var item = items.get(linkId); assertNotNull(item); @@ -193,11 +203,24 @@ public GeneratedItem itemHasInitialExpression(String linkId) { return this; } - public GeneratedItem itemHasExtractionValueExtension(String linkId) { + public GeneratedItem itemHasExtractionValueExtensions(String linkId, int count) { + var item = items.get(linkId); + assertNotNull(item); + assertEquals( + count, + item.getExtension().stream() + .filter(e -> e.getUrl().equals(Constants.SDC_QUESTIONNAIRE_DEFINITION_EXTRACT_VALUE)) + .toList() + .size()); + return this; + } + + public GeneratedItem itemHasDefinitionExtractExtension(String linkId) { var item = items.get(linkId); assertNotNull(item); assertTrue(item.getExtension().stream() - .anyMatch(e -> e.getUrl().equals(Constants.SDC_QUESTIONNAIRE_DEFINITION_EXTRACT_VALUE))); + .anyMatch(e -> e.getUrl().equals(Constants.SDC_QUESTIONNAIRE_DEFINITION_EXTRACT))); + return this; } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestQuestionnaire.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestQuestionnaire.java index 96b8fbcb5f..dfc7ba9ad3 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestQuestionnaire.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestQuestionnaire.java @@ -20,7 +20,6 @@ import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBackboneElement; import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -122,17 +121,18 @@ public static class When { private IBaseResource questionnaire; private String subjectId; private List context; - private IBaseExtension launchContext; private boolean useServerData; private IBaseBundle data; private IBaseParameters parameters; private Boolean isPut; - private IIdType profileId; + private IBaseResource profile; + private List> profileUrl; When(IRepository repository, QuestionnaireProcessor processor) { this.repository = repository; this.processor = processor; useServerData = true; + profileUrl = new ArrayList<>(); } private FhirContext fhirContext() { @@ -144,8 +144,6 @@ private PopulateRequest buildRequest() { processor.resolveQuestionnaire(Eithers.for3(questionnaireUrl, questionnaireId, questionnaire)), subjectId == null ? null : Ids.newId(fhirContext(), "Patient", subjectId), context, - launchContext, - // parameters, data, new LibraryEngine(repository, processor.crSettings.getEvaluationSettings())); } @@ -175,11 +173,6 @@ public When context(List context) { return this; } - public When launchContext(IBaseExtension extension) { - launchContext = extension; - return this; - } - public When useServerData(boolean value) { useServerData = value; return this; @@ -200,8 +193,13 @@ public When isPut(Boolean value) { return this; } - public When profileId(IIdType id) { - profileId = id; + public When profile(IBaseResource profile) { + this.profile = profile; + return this; + } + + public When profileUrl(IPrimitiveType url) { + profileUrl.add(url); return this; } @@ -210,8 +208,6 @@ public IBaseResource runPopulate() { Eithers.for3(questionnaireUrl, questionnaireId, questionnaire), subjectId, context, - launchContext, - // parameters, data, useServerData, (IBaseResource) null, @@ -239,11 +235,15 @@ public GeneratedPackage thenPackage() { } public GeneratedQuestionnaire thenGenerate() { + var profiles = new ArrayList(); + if (profile != null) { + profiles.add(profile); + } + profileUrl.stream() + .map(url -> (IBaseResource) processor.resolveStructureDefinition(Eithers.forLeft3(url))) + .forEach(profiles::add); var request = new GenerateRequest( - processor.resolveStructureDefinition(Eithers.for3(null, profileId, null)), - false, - true, - new LibraryEngine(repository, processor.crSettings.getEvaluationSettings())); + profiles, false, true, new LibraryEngine(repository, processor.crSettings.getEvaluationSettings())); return new GeneratedQuestionnaire(repository, request, processor.generateQuestionnaire(request, null)); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGeneratorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGeneratorTests.java index 52daf5c958..6777a1cd41 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGeneratorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGeneratorTests.java @@ -3,7 +3,9 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; import static org.opencds.cqf.fhir.cr.questionnaire.TestItemGenerator.given; +import static org.opencds.cqf.fhir.utility.VersionUtilities.canonicalTypeForVersion; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; @@ -18,7 +20,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.helpers.RequestHelpers; -import org.opencds.cqf.fhir.utility.Ids; @SuppressWarnings({"squid:S2699", "UnstableApiUsage"}) @ExtendWith(MockitoExtension.class) @@ -27,6 +28,7 @@ class ItemGeneratorTests { "http://fhir.org/guides/cdc/opioid-cds/StructureDefinition/RouteOnePatient"; private static final String SLEEP_STUDY_PROFILE = "http://example.org/sdh/dtr/aslp/StructureDefinition/aslp-sleep-study-order"; + private final FhirContext fhirContextDstu3 = FhirContext.forDstu3Cached(); private final FhirContext fhirContextR4 = FhirContext.forR4Cached(); private final FhirContext fhirContextR5 = FhirContext.forR5Cached(); @@ -42,6 +44,8 @@ class ItemGeneratorTests { @Test void testBuildSdcLaunchContextExt() { + when(libraryEngine.getRepository()).thenReturn(repository); + when(repository.fhirContext()).thenReturn(fhirContextDstu3); var dstu3Request = RequestHelpers.newGenerateRequestForVersion(FhirVersionEnum.DSTU3, libraryEngine); var dstu3Ext = (org.hl7.fhir.dstu3.model.Extension) fixture.buildSdcLaunchContextExt(dstu3Request, "patient", "Patient"); @@ -52,6 +56,8 @@ void testBuildSdcLaunchContextExt() { org.hl7.fhir.dstu3.model.CodeType.class, dstu3Ext.getExtensionByUrl("type").getValue()); + when(libraryEngine.getRepository()).thenReturn(repository); + when(repository.fhirContext()).thenReturn(fhirContextR4); var r4Request = RequestHelpers.newGenerateRequestForVersion(FhirVersionEnum.R4, libraryEngine); var r4Ext = (org.hl7.fhir.r4.model.Extension) fixture.buildSdcLaunchContextExt(r4Request, "user", "PractitionerRole"); @@ -63,6 +69,8 @@ void testBuildSdcLaunchContextExt() { org.hl7.fhir.r4.model.CodeType.class, r4Ext.getExtensionByUrl("type").getValue()); + when(libraryEngine.getRepository()).thenReturn(repository); + when(repository.fhirContext()).thenReturn(fhirContextR5); var r5Request = RequestHelpers.newGenerateRequestForVersion(FhirVersionEnum.R5, libraryEngine); var r5Ext = (org.hl7.fhir.r5.model.Extension) fixture.buildSdcLaunchContextExt(r5Request, "patient", "Patient"); assertNull(r5Ext.getValue()); @@ -76,6 +84,8 @@ void testBuildSdcLaunchContextExt() { @Test void testGenerateRequest() { + when(libraryEngine.getRepository()).thenReturn(repository); + when(repository.fhirContext()).thenReturn(fhirContextR4); var request = RequestHelpers.newGenerateRequestForVersion(FhirVersionEnum.R4, libraryEngine); assertThrows(UnsupportedOperationException.class, request::getSubjectId); assertThrows(UnsupportedOperationException.class, request::getData); @@ -135,7 +145,7 @@ void generateItemR4() { .profileUrl(new CanonicalType(ROUTE_ONE_PATIENT_PROFILE)) .then() .hasItemCount(8) - .itemHasInitialExpression("1.1.1"); + .itemHasNoInitialExpression("1.1.1"); } @Test @@ -145,7 +155,7 @@ void generateItemR5() { .profileUrl(new org.hl7.fhir.r5.model.CanonicalType(ROUTE_ONE_PATIENT_PROFILE)) .then() .hasItemCount(8) - .itemHasInitialExpression("1.1.1"); + .itemHasNoInitialExpression("1.1.1"); } @Test @@ -177,29 +187,38 @@ void generateQuestionnaire() { void generateItemForElementWithChildren() { given().repositoryFor(fhirContextR4, "r4") .when() - .profileId(Ids.newId(fhirContextR4, "sigmoidoscopy-complication-casefeature-definition")) + .profileUrl( + canonicalTypeForVersion( + fhirContextR4.getVersion().getVersion(), + "http://example.org/sdh/demo/StructureDefinition/sigmoidoscopy-complication-casefeature-definition")) .then() - .hasItemCount(2); + .hasItemCount(1); } @Test void generateItemWithDefaults() { given().repositoryFor(fhirContextR4, "r4") .when() - .profileId(Ids.newId(fhirContextR4, "sigmoidoscopy-complication-casefeature-definition2")) + .profileUrl( + canonicalTypeForVersion( + fhirContextR4.getVersion().getVersion(), + "http://example.org/sdh/demo/StructureDefinition/sigmoidoscopy-complication-casefeature-definition2")) .then() - .hasItemCount(2); + .hasItemCount(1); } @Test void generateWithLaunchContexts() { given().repositoryFor(fhirContextR4, "r4") .when() - .profileId(Ids.newId(fhirContextR4, "LaunchContexts")) + .profileUrl(canonicalTypeForVersion( + fhirContextR4.getVersion().getVersion(), + "http://fhir.org/test/StructureDefinition/LaunchContexts")) .then() - .hasItemCount(2) + .hasItemCount(1) .hasLaunchContextExtension(5) - .itemHasExtractionValueExtension("1"); + .itemHasExtractionValueExtensions("1", 2) + .itemHasDefinitionExtractExtension("1"); } @Test @@ -207,9 +226,11 @@ void generateWithLaunchContexts() { void generateItemForProfileWithExtensionSlices() { given().repositoryFor(fhirContextR4, "r4") .when() - .profileId(Ids.newId(fhirContextR4, "NzPatient")) + .profileUrl(canonicalTypeForVersion( + fhirContextR4.getVersion().getVersion(), + "http://hl7.org.nz/fhir/StructureDefinition/NzPatient")) .then() .hasItemCount(11) - .itemHasExtractionValueExtension("1"); + .itemHasExtractionValueExtensions("1", 2); } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/populate/ItemProcessorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/populate/ItemProcessorTests.java index ea449f2634..fe2d341aa2 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/populate/ItemProcessorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/populate/ItemProcessorTests.java @@ -89,11 +89,12 @@ void processItemShouldReturnQuestionnaireResponseItemComponentR4() { doReturn(expressionResults) .when(expressionProcessor) .getExpressionResultForItem(eq(populateRequest), eq(expression), eq("1"), any(), any()); + var responseItem = new ArrayList(); + responseItem.add(itemAdapter.newResponseItem()); // execute - final var actual = - itemProcessor.processItem(populateRequest, itemAdapter).get(0); + itemProcessor.processItem(populateRequest, itemAdapter, responseItem); // validate - final var answers = actual.getAnswer(); + final var answers = responseItem.get(0).getAnswer(); assertEquals(3, answers.size()); for (int i = 0; i < answers.size(); i++) { assertEquals(expressionResults.get(i), answers.get(i).getValue(), "value"); @@ -120,10 +121,12 @@ void processItemShouldReturnQuestionnaireResponseItemComponentR5() { .when(expressionProcessor) .getExpressionResultForItem(eq(populateRequest), eq(expression), eq("1"), any(), any()); // execute - final var actual = - itemProcessor.processItem(populateRequest, itemAdapter).get(0); + var responseItem = new ArrayList(); + responseItem.add(itemAdapter.newResponseItem()); + // execute + itemProcessor.processItem(populateRequest, itemAdapter, responseItem); // validate - final var answers = actual.getAnswer(); + final var answers = responseItem.get(0).getAnswer(); assertEquals(3, answers.size()); for (int i = 0; i < answers.size(); i++) { assertEquals(expressionResults.get(i), answers.get(i).getValue(), "value"); @@ -213,8 +216,12 @@ void processItemShouldCorrectlyProcessNonGroupItemsWithChildren() { final var itemAdapter = (IQuestionnaireItemComponentAdapter) IAdapterFactory.createAdapterForBase(fhirVersion, parentItem); doReturn(null).when(expressionProcessor).getItemInitialExpression(eq(populateRequest), any()); - var actual = (QuestionnaireResponseItemComponent) - itemProcessor.processItem(populateRequest, itemAdapter).get(0).get(); + var responseItem = new ArrayList(); + responseItem.add(itemAdapter.newResponseItem()); + // execute + itemProcessor.processItem(populateRequest, itemAdapter, responseItem); + // validate + var actual = (QuestionnaireResponseItemComponent) responseItem.get(0).get(); assertNotNull(actual); assertFalse(actual.hasItem()); assertTrue(actual.hasAnswer()); @@ -243,7 +250,9 @@ void testMissingProfileLogsException() { .getExpressionResultForItem(eq(populateRequest), eq(expression), eq("1"), any(), any()); var adapter = (IQuestionnaireItemComponentAdapter) IAdapterFactory.createAdapterForBase(populateRequest.getFhirVersion(), questionnaireItem); - itemProcessor.processContextItem(populateRequest, adapter); + var responseItems = new ArrayList(); + responseItems.add(adapter.newResponseItem()); + itemProcessor.processContextItem(populateRequest, adapter, responseItems); var operationOutcome = (OperationOutcome) populateRequest.getOperationOutcome(); assertTrue(operationOutcome.hasIssue()); assertEquals(1, operationOutcome.getIssue().size()); @@ -279,9 +288,10 @@ void testNoContextStillReturnsResponseItem() { .getExpressionResultForItem(eq(populateRequest), eq(cqfExpression), eq("1"), any(), any()); var adapter = (IQuestionnaireItemComponentAdapter) IAdapterFactory.createAdapterForBase(populateRequest.getFhirVersion(), questionnaireItem); - var actual = itemProcessor.processContextItem(populateRequest, adapter); - assertEquals(1, actual.size()); - assertTrue(actual.get(0).getAnswer().isEmpty()); + var responseItems = new ArrayList(); + itemProcessor.processContextItem(populateRequest, adapter, responseItems); + assertEquals(1, responseItems.size()); + assertTrue(responseItems.get(0).getAnswer().isEmpty()); } @Test @@ -336,12 +346,13 @@ void testTupleContextPopulatesGroupChildrenFromNamedMembers() { (IQuestionnaireItemComponentAdapter) IAdapterFactory.createAdapterForBase(fhirVersion, groupItem); // execute - final var actual = itemProcessor.processContextItem(populateRequest, adapter); + var responseItems = new ArrayList(); + itemProcessor.processContextItem(populateRequest, adapter, responseItems); // validate: the group's child item is populated from the tuple's "gender" member - assertEquals(1, actual.size()); + assertEquals(1, responseItems.size()); final var responseGroup = - (QuestionnaireResponseItemComponent) actual.get(0).get(); + (QuestionnaireResponseItemComponent) responseItems.get(0).get(); assertTrue(responseGroup.hasItem(), "group child items should be populated from the tuple"); final var responseChild = responseGroup.getItemFirstRep(); assertTrue(responseChild.hasAnswer(), "child item should have an answer populated from the tuple member"); @@ -366,10 +377,12 @@ void testGetInitialValueReturnsBooleanType() { var adapter = (IQuestionnaireItemComponentAdapter) IAdapterFactory.createAdapterForBase(populateRequest.getFhirVersion(), questionnaireItem); doReturn(null).when(expressionProcessor).getItemInitialExpression(eq(populateRequest), any()); - var actual = itemProcessor.processItem(populateRequest, adapter); - assertNotNull(actual); + var responseItems = new ArrayList(); + responseItems.add(adapter.newResponseItem()); + itemProcessor.processItem(populateRequest, adapter, responseItems); + assertNotNull(responseItems); var nestedActual = (IQuestionnaireResponseItemComponentAdapter) - actual.get(0).getItem().get(0); + responseItems.get(0).getItem().get(0); assertNotNull(nestedActual); assertTrue(nestedActual.hasAnswer()); assertFalse(((BooleanType) nestedActual.getAnswer().get(0).getValue()).booleanValue()); diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateProcessorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateProcessorTests.java index 78c211988c..050d1b21bc 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateProcessorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/populate/PopulateProcessorTests.java @@ -1,19 +1,21 @@ package org.opencds.cqf.fhir.cr.questionnaire.populate; +import static java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.verify; -import static org.opencds.cqf.fhir.cr.helpers.RequestHelpers.PATIENT_ID; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.spy; import static org.opencds.cqf.fhir.cr.helpers.RequestHelpers.newPopulateRequestForVersion; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.repository.IRepository; import java.util.List; -import java.util.stream.Stream; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Expression; @@ -21,8 +23,6 @@ import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType; -import org.hl7.fhir.r4.model.QuestionnaireResponse; -import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,10 +31,9 @@ import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.opencds.cqf.fhir.cql.LibraryEngine; +import org.opencds.cqf.fhir.cr.common.ExpressionProcessor; import org.opencds.cqf.fhir.utility.Constants; -import org.opencds.cqf.fhir.utility.adapter.IAdapter; import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; -import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireResponseItemComponentAdapter; import org.opencds.cqf.fhir.utility.adapter.IResourceAdapter; @SuppressWarnings("UnstableApiUsage") @@ -46,93 +45,19 @@ class PopulateProcessorTests { @Mock private LibraryEngine libraryEngine; + @Mock + private ExpressionProcessor expressionProcessor; + + @Mock + private ItemProcessor itemProcessor; + @Spy - private final PopulateProcessor fixture = new PopulateProcessor(); + private PopulateProcessor fixture; @BeforeEach void setup() { doReturn(repository).when(libraryEngine).getRepository(); - } - - @Test - void populateShouldReturnQuestionnaireResponseResourceWithPopulatedFieldsR4() { - // setup - final var fhirVersion = FhirVersionEnum.R4; - final var questionnaireUrl = "original-questionnaire-url"; - final var prePopulatedQuestionnaireId = "prepopulated-questionnaire-id"; - final var originalQuestionnaire = new Questionnaire(); - originalQuestionnaire.setId(prePopulatedQuestionnaireId); - originalQuestionnaire.setUrl(questionnaireUrl); - final var item = new QuestionnaireItemComponent().setLinkId("1").setType(QuestionnaireItemType.DECIMAL); - originalQuestionnaire.addItem(item); - doReturn(FhirContext.forR4Cached()).when(repository).fhirContext(); - final var request = newPopulateRequestForVersion(fhirVersion, libraryEngine, originalQuestionnaire); - final var expectedResponses = getExpectedResponses(request); - doReturn(expectedResponses).when(fixture).populateItem(eq(request), any()); - // execute - final var actual = (QuestionnaireResponse) fixture.populate(request); - // validate - assertEquals( - prePopulatedQuestionnaireId + "-" + PATIENT_ID, - actual.getIdElement().getIdPart()); - assertContainedOperationOutcome(request, actual, null); - assertEquals(questionnaireUrl, actual.getQuestionnaire()); - assertEquals(QuestionnaireResponse.QuestionnaireResponseStatus.INPROGRESS, actual.getStatus()); - assertEquals("Patient/" + PATIENT_ID, actual.getSubject().getReference()); - assertEquals(expectedResponses.stream().map(IAdapter::get).toList(), actual.getItem()); - verify(fixture).populateItem(eq(request), any()); - } - - @Test - void populateShouldReturnQuestionnaireResponseResourceWithPopulatedFieldsR5() { - // setup - final var fhirVersion = FhirVersionEnum.R5; - final var questionnaireUrl = "original-questionnaire-url"; - final var prePopulatedQuestionnaireId = "prepopulated-questionnaire-id"; - final var originalQuestionnaire = new org.hl7.fhir.r5.model.Questionnaire(); - originalQuestionnaire.setId(prePopulatedQuestionnaireId); - originalQuestionnaire.setUrl(questionnaireUrl); - final var item = new org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent() - .setLinkId("1") - .setType(org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemType.BOOLEAN); - originalQuestionnaire.addItem(item); - doReturn(FhirContext.forR5Cached()).when(repository).fhirContext(); - final var request = newPopulateRequestForVersion(fhirVersion, libraryEngine, originalQuestionnaire); - final var expectedResponses = getExpectedResponses(request); - doReturn(expectedResponses).when(fixture).populateItem(eq(request), any()); - // execute - final var actual = (org.hl7.fhir.r5.model.QuestionnaireResponse) fixture.populate(request); - // validate - assertEquals( - prePopulatedQuestionnaireId + "-" + PATIENT_ID, - actual.getIdElement().getIdPart()); - assertContainedOperationOutcome(request, actual, null); - assertEquals(questionnaireUrl, actual.getQuestionnaire()); - assertEquals( - org.hl7.fhir.r5.model.QuestionnaireResponse.QuestionnaireResponseStatus.INPROGRESS, actual.getStatus()); - assertEquals("Patient/" + PATIENT_ID, actual.getSubject().getReference()); - assertEquals(expectedResponses.stream().map(IAdapter::get).toList(), actual.getItem()); - verify(fixture).populateItem(eq(request), any()); - } - - private List getExpectedResponses(PopulateRequest request) { - return switch (request.getFhirVersion()) { - case R4 -> - Stream.of( - new QuestionnaireResponseItemComponent(), - new QuestionnaireResponseItemComponent(), - new QuestionnaireResponseItemComponent()) - .map(i -> request.getAdapterFactory().createQuestionnaireResponseItem(i)) - .toList(); - case R5 -> - Stream.of( - new org.hl7.fhir.r5.model.QuestionnaireResponse.QuestionnaireResponseItemComponent(), - new org.hl7.fhir.r5.model.QuestionnaireResponse.QuestionnaireResponseItemComponent(), - new org.hl7.fhir.r5.model.QuestionnaireResponse.QuestionnaireResponseItemComponent()) - .map(i -> request.getAdapterFactory().createQuestionnaireResponseItem(i)) - .toList(); - default -> List.of(); - }; + fixture = spy(new PopulateProcessor(itemProcessor, expressionProcessor)); } private void assertContainedOperationOutcome( @@ -200,8 +125,8 @@ void testGetVariablesReturnsObject() { var expectedResponse = new StringType("test"); doReturn(FhirContext.forR4Cached()).when(repository).fhirContext(); doReturn(List.of(expectedResponse)) - .when(libraryEngine) - .resolveExpression(eq(PATIENT_ID), any(), any(), any(), any(), any(), any()); + .when(expressionProcessor) + .getExpressionResult(any(), any(), eq(null), eq(null)); final var request = newPopulateRequestForVersion(FhirVersionEnum.R4, libraryEngine, questionnaire); var actual = fixture.getVariables(request, questionnaire); assertNotNull(actual); @@ -218,12 +143,63 @@ void testGetVariablesReturnsList() { questionnaire.addExtension(Constants.VARIABLE_EXTENSION, expression); var expectedResponse = List.of(new StringType("test1"), new StringType("test2")); doReturn(FhirContext.forR4Cached()).when(repository).fhirContext(); - doReturn(expectedResponse) - .when(libraryEngine) - .resolveExpression(eq(PATIENT_ID), any(), any(), any(), any(), any(), any()); + doReturn(expectedResponse).when(expressionProcessor).getExpressionResult(any(), any(), eq(null), eq(null)); final var request = newPopulateRequestForVersion(FhirVersionEnum.R4, libraryEngine, questionnaire); var actual = fixture.getVariables(request, questionnaire); assertNotNull(actual); assertEquals(expectedResponse, actual.get("testName")); } + + @Test + void testGetVariablesHandlesEmptyList() { + var questionnaire = new Questionnaire(); + var expression = new Expression() + .setLanguage("text/cql-expression") + .setExpression("test") + .setName("testName"); + questionnaire.addExtension(Constants.VARIABLE_EXTENSION, expression); + doReturn(FhirContext.forR4Cached()).when(repository).fhirContext(); + doReturn(emptyList()).when(expressionProcessor).getExpressionResult(any(), any(), eq(null), eq(null)); + final var request = newPopulateRequestForVersion(FhirVersionEnum.R4, libraryEngine, questionnaire); + var actual = fixture.getVariables(request, questionnaire); + assertNotNull(actual); + assertEquals(0, actual.size()); + } + + @Test + void testGetVariablesHandlesError() { + var questionnaire = new Questionnaire(); + var expression = new Expression() + .setLanguage("text/cql-expression") + .setExpression("test") + .setName("testName"); + questionnaire.addExtension(Constants.VARIABLE_EXTENSION, expression); + doReturn(FhirContext.forR4Cached()).when(repository).fhirContext(); + var errorMessage = "An error has occurred."; + final var request = newPopulateRequestForVersion(FhirVersionEnum.R4, libraryEngine, questionnaire); + doThrow(new RuntimeException(errorMessage)) + .when(expressionProcessor) + .getExpressionResult(any(), any(), eq(null), eq(null)); + var actual = fixture.getVariables(request, questionnaire); + assertNotNull(actual); + assertEquals(0, actual.size()); + var opOutcome = request.getOperationOutcome(); + assertNotNull(opOutcome); + assertFalse(((OperationOutcome) opOutcome).getIssue().isEmpty()); + } + + @Test + void testPopulateItemHandlesError() { + var questionnaire = new Questionnaire(); + doReturn(FhirContext.forR4Cached()).when(repository).fhirContext(); + var errorMessage = "An error has occurred."; + final var request = newPopulateRequestForVersion(FhirVersionEnum.R4, libraryEngine, questionnaire); + var item = request.getAdapterFactory() + .createQuestionnaireItem(new QuestionnaireItemComponent().setType(QuestionnaireItemType.BOOLEAN)); + doThrow(new RuntimeException(errorMessage)).when(itemProcessor).processItem(any(), any(), anyList()); + fixture.populateItem(request, item); + var opOutcome = request.getOperationOutcome(); + assertNotNull(opOutcome); + assertFalse(((OperationOutcome) opOutcome).getIssue().isEmpty()); + } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/ObservationResolverTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/ObservationResolverTests.java new file mode 100644 index 0000000000..7d2ac8d328 --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaireresponse/extract/ObservationResolverTests.java @@ -0,0 +1,131 @@ +package org.opencds.cqf.fhir.cr.questionnaireresponse.extract; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.opencds.cqf.fhir.cr.helpers.RequestHelpers.newExtractRequestForVersion; +import static org.opencds.cqf.fhir.utility.Constants.QUESTIONNAIRE_UNIT; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.repository.IRepository; +import java.time.Instant; +import java.util.Date; +import org.hl7.fhir.instance.model.api.IBaseDatatype; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Quantity; +import org.hl7.fhir.r4.model.Questionnaire; +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opencds.cqf.fhir.cql.LibraryEngine; + +@SuppressWarnings("UnstableApiUsage") +@ExtendWith(MockitoExtension.class) +class ObservationResolverTests { + private final FhirContext fhirContextR4 = FhirContext.forR4Cached(); + + @Mock + private IRepository repository; + + @Mock + private LibraryEngine libraryEngine; + + private ObservationResolver fixture; + + @BeforeEach + void setup() { + doReturn(fhirContextR4).when(repository).fhirContext(); + doReturn(repository).when(libraryEngine).getRepository(); + fixture = spy(new ObservationResolver()); + } + + @Test + void test_getAnswerValue() { + var fhirVersion = FhirVersionEnum.R4; + var questionnaire = new Questionnaire(); + var response = new QuestionnaireResponse(); + var request = newExtractRequestForVersion(fhirVersion, libraryEngine, response, questionnaire); + // test integer + var integerValue = new IntegerType(50); + var integerAnswer = new QuestionnaireResponseItemAnswerComponent().setValue(integerValue); + var integerActual = getValue(request, integerAnswer, null); + assertEquals(integerValue, integerActual); + // test decimal + var decimalValue = new DecimalType(50.5); + var decimalAnswer = new QuestionnaireResponseItemAnswerComponent().setValue(decimalValue); + var decimalActual = getValue(request, decimalAnswer, null); + assertEquals(decimalValue, decimalActual); + doReturn(decimalValue).when(fixture).getQuantity(eq(request), any(), any()); + var itemWithUnit = new QuestionnaireItemComponent(); + itemWithUnit.addExtension(new Extension(QUESTIONNAIRE_UNIT, new Coding())); + var decimalWithUnitActual = getValue(request, decimalAnswer, itemWithUnit); + assertEquals(decimalValue, decimalWithUnitActual); + // test Date + var dateValue = new DateTimeType(Date.from(Instant.now())); + var dateAnswer = new QuestionnaireResponseItemAnswerComponent().setValue(dateValue); + var dateActual = getValue(request, dateAnswer, null); + assertEquals(dateValue, dateActual); + // test Coding + var codingValue = new Coding("test.com", "test", "Test"); + var codingAnswer = new QuestionnaireResponseItemAnswerComponent().setValue(codingValue); + var codingActual = getValue(request, codingAnswer, null); + assertInstanceOf(CodeableConcept.class, codingActual); + assertEquals(codingValue, ((CodeableConcept) codingActual).getCodingFirstRep()); + } + + private IBaseDatatype getValue( + ExtractRequest request, QuestionnaireResponseItemAnswerComponent answer, QuestionnaireItemComponent item) { + return fixture.getAnswerValue( + request, + request.getAdapterFactory().createQuestionnaireResponseItemAnswer(answer), + item == null ? null : request.getAdapterFactory().createQuestionnaireItem(item)); + } + + @Test + void test_getQuantity() { + var fhirVersion = FhirVersionEnum.R4; + var questionnaire = new Questionnaire(); + var response = new QuestionnaireResponse(); + var request = newExtractRequestForVersion(fhirVersion, libraryEngine, response, questionnaire); + + var cmCoding = new Coding("http://unitsofmeasure.org", "cm", "Centimeter"); + var integerValue = new IntegerType(50); + var integerAnswer = request.getAdapterFactory() + .createQuestionnaireResponseItemAnswer( + new QuestionnaireResponseItemAnswerComponent().setValue(integerValue)); + var integerActual = fixture.getQuantity(request, integerAnswer, cmCoding); + assertInstanceOf(Quantity.class, integerActual); + var integerQuantity = (Quantity) integerActual; + assertEquals(integerValue.asStringValue(), integerQuantity.getValue().toString()); + assertEquals(cmCoding.getCode(), integerQuantity.getCode()); + assertEquals(cmCoding.getSystem(), integerQuantity.getSystem()); + assertEquals(cmCoding.getDisplay(), integerQuantity.getUnit()); + + var kgCoding = new Coding("http://unitsofmeasure.org", "kg", "Kilogram"); + var decimalValue = new DecimalType(50.5); + var decimalAnswer = request.getAdapterFactory() + .createQuestionnaireResponseItemAnswer( + new QuestionnaireResponseItemAnswerComponent().setValue(decimalValue)); + var decimalActual = fixture.getQuantity(request, decimalAnswer, kgCoding); + assertInstanceOf(Quantity.class, decimalActual); + var decimalQuantity = (Quantity) decimalActual; + assertEquals(decimalValue.getValue(), decimalQuantity.getValue()); + assertEquals(kgCoding.getCode(), decimalQuantity.getCode()); + assertEquals(kgCoding.getSystem(), decimalQuantity.getSystem()); + assertEquals(kgCoding.getDisplay(), decimalQuantity.getUnit()); + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r4/input/tests/Bundle-extract-QRSharonDecision.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r4/input/tests/Bundle-extract-QRSharonDecision.json index 9b7492efc3..44a1d59330 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r4/input/tests/Bundle-extract-QRSharonDecision.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r4/input/tests/Bundle-extract-QRSharonDecision.json @@ -43,7 +43,7 @@ "display": "Sharon Decision" }, "effectiveDateTime": "2021-12-28T18:10:32Z", - "issued": "2021-12-28T18:10:32.000+00:00", + "issued": "2021-12-28T18:10:32Z", "valueCodeableConcept": { "coding": [ { @@ -103,7 +103,7 @@ "display": "Sharon Decision" }, "effectiveDateTime": "2021-12-28T18:10:32Z", - "issued": "2021-12-28T18:10:32.000+00:00", + "issued": "2021-12-28T18:10:32Z", "valueCodeableConcept": { "coding": [ { @@ -163,7 +163,7 @@ "display": "Sharon Decision" }, "effectiveDateTime": "2021-12-28T18:10:32Z", - "issued": "2021-12-28T18:10:32.000+00:00", + "issued": "2021-12-28T18:10:32Z", "valueCodeableConcept": { "coding": [ { @@ -223,7 +223,7 @@ "display": "Sharon Decision" }, "effectiveDateTime": "2021-12-28T18:10:32Z", - "issued": "2021-12-28T18:10:32.000+00:00", + "issued": "2021-12-28T18:10:32Z", "valueCodeableConcept": { "coding": [ { @@ -283,7 +283,7 @@ "display": "Sharon Decision" }, "effectiveDateTime": "2021-12-28T18:10:32Z", - "issued": "2021-12-28T18:10:32.000+00:00", + "issued": "2021-12-28T18:10:32Z", "valueCodeableConcept": { "coding": [ { diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r4/input/tests/Bundle-extract-sdc-profile-example-multi-subject.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r4/input/tests/Bundle-extract-sdc-profile-example-multi-subject.json index fd154ab0c6..728585b574 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r4/input/tests/Bundle-extract-sdc-profile-example-multi-subject.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r4/input/tests/Bundle-extract-sdc-profile-example-multi-subject.json @@ -41,7 +41,7 @@ "reference": "http://example.org/fhir/Patient/12345" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 141, "system": "http://unitsofmeasure.org", @@ -95,7 +95,7 @@ "reference": "http://example.org/fhir/Patient/12345" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 42.3, "system": "http://unitsofmeasure.org", @@ -149,7 +149,7 @@ "reference": "http://example.org/fhir/Patient/123456" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 47, "system": "http://unitsofmeasure.org", @@ -203,7 +203,7 @@ "reference": "http://example.org/fhir/Patient/123456" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 8.7, "system": "http://unitsofmeasure.org", @@ -257,7 +257,7 @@ "reference": "http://example.org/fhir/Patient/123456" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 109, "system": "http://unitsofmeasure.org", @@ -311,7 +311,7 @@ "reference": "http://example.org/fhir/Patient/123456" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 27.3, "system": "http://unitsofmeasure.org", diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r5/input/tests/Bundle-extract-QRSharonDecision.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r5/input/tests/Bundle-extract-QRSharonDecision.json index 9b7492efc3..44a1d59330 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r5/input/tests/Bundle-extract-QRSharonDecision.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r5/input/tests/Bundle-extract-QRSharonDecision.json @@ -43,7 +43,7 @@ "display": "Sharon Decision" }, "effectiveDateTime": "2021-12-28T18:10:32Z", - "issued": "2021-12-28T18:10:32.000+00:00", + "issued": "2021-12-28T18:10:32Z", "valueCodeableConcept": { "coding": [ { @@ -103,7 +103,7 @@ "display": "Sharon Decision" }, "effectiveDateTime": "2021-12-28T18:10:32Z", - "issued": "2021-12-28T18:10:32.000+00:00", + "issued": "2021-12-28T18:10:32Z", "valueCodeableConcept": { "coding": [ { @@ -163,7 +163,7 @@ "display": "Sharon Decision" }, "effectiveDateTime": "2021-12-28T18:10:32Z", - "issued": "2021-12-28T18:10:32.000+00:00", + "issued": "2021-12-28T18:10:32Z", "valueCodeableConcept": { "coding": [ { @@ -223,7 +223,7 @@ "display": "Sharon Decision" }, "effectiveDateTime": "2021-12-28T18:10:32Z", - "issued": "2021-12-28T18:10:32.000+00:00", + "issued": "2021-12-28T18:10:32Z", "valueCodeableConcept": { "coding": [ { @@ -283,7 +283,7 @@ "display": "Sharon Decision" }, "effectiveDateTime": "2021-12-28T18:10:32Z", - "issued": "2021-12-28T18:10:32.000+00:00", + "issued": "2021-12-28T18:10:32Z", "valueCodeableConcept": { "coding": [ { diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r5/input/tests/Bundle-extract-sdc-profile-example-multi-subject.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r5/input/tests/Bundle-extract-sdc-profile-example-multi-subject.json index fd154ab0c6..728585b574 100644 --- a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r5/input/tests/Bundle-extract-sdc-profile-example-multi-subject.json +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/shared/r5/input/tests/Bundle-extract-sdc-profile-example-multi-subject.json @@ -41,7 +41,7 @@ "reference": "http://example.org/fhir/Patient/12345" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 141, "system": "http://unitsofmeasure.org", @@ -95,7 +95,7 @@ "reference": "http://example.org/fhir/Patient/12345" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 42.3, "system": "http://unitsofmeasure.org", @@ -149,7 +149,7 @@ "reference": "http://example.org/fhir/Patient/123456" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 47, "system": "http://unitsofmeasure.org", @@ -203,7 +203,7 @@ "reference": "http://example.org/fhir/Patient/123456" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 8.7, "system": "http://unitsofmeasure.org", @@ -257,7 +257,7 @@ "reference": "http://example.org/fhir/Patient/123456" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 109, "system": "http://unitsofmeasure.org", @@ -311,7 +311,7 @@ "reference": "http://example.org/fhir/Patient/123456" }, "effectiveDateTime": "2021-12-01T07:00:00Z", - "issued": "2021-12-01T07:00:00.000+00:00", + "issued": "2021-12-01T07:00:00Z", "valueQuantity": { "value": 27.3, "system": "http://unitsofmeasure.org", diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/BundleHelper.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/BundleHelper.java index fe09f09fcf..0c81b5d86e 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/BundleHelper.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/BundleHelper.java @@ -218,6 +218,36 @@ public static boolean isEntryRequestDelete(FhirVersionEnum fhirVersion, IBaseBac }; } + /** + * Checks if an entry has a request type of GET + * + * @param fhirVersion FhirVersionEnum + * @param entry IBaseBackboneElement type + * @return boolean + */ + public static boolean isEntryRequestGet(FhirVersionEnum fhirVersion, IBaseBackboneElement entry) { + return switch (fhirVersion) { + case DSTU3 -> + Optional.ofNullable(((Bundle.BundleEntryComponent) entry).getRequest()) + .map(Bundle.BundleEntryRequestComponent::getMethod) + .filter(r -> r == Bundle.HTTPVerb.GET) + .isPresent(); + case R4 -> + Optional.ofNullable(((BundleEntryComponent) entry).getRequest()) + .map(BundleEntryRequestComponent::getMethod) + .filter(r -> r == org.hl7.fhir.r4.model.Bundle.HTTPVerb.GET) + .isPresent(); + case R5 -> + Optional.ofNullable(((org.hl7.fhir.r5.model.Bundle.BundleEntryComponent) entry).getRequest()) + .map(org.hl7.fhir.r5.model.Bundle.BundleEntryRequestComponent::getMethod) + .filter(r -> r == org.hl7.fhir.r5.model.Bundle.HTTPVerb.GET) + .isPresent(); + default -> + throw new IllegalArgumentException( + UNSUPPORTED_VERSION_OF_FHIR.formatted(fhirVersion.getFhirVersionString())); + }; + } + /** * Returns the list of entries from the Bundle * diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/Constants.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/Constants.java index c548937098..6894e6740d 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/Constants.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/Constants.java @@ -93,6 +93,12 @@ public enum CPG_ACTIVITY_TYPE_CODE { public static final String CPG_RELATED_SUMMARY_DEFINITION = "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-relatedsummarydefinition"; + public static final String CPG_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT = + "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-questionnaire-definitionPopulationContext"; + + public static final String CPG_ADDITIONAL_LAUNCH_CONTEXT_SYSTEM = + "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-questionnaire-additionalLaunchContext"; + public static final String CQF_RESOURCETYPE = "http://hl7.org/fhir/StructureDefinition/cqf-resourceType"; // DSTU3 CQF Extensions @@ -101,6 +107,8 @@ public enum CPG_ACTIVITY_TYPE_CODE { public static final String CQF_APPLICABILITY_BEHAVIOR = "http://hl7.org/fhir/StructureDefinition/cqf-applicabilityBehavior"; + public static final String R6_PLAN_DEFINITION_ACTION_APPLICABILITY_BEHAVIOR = + "http://hl7.org/fhir/6.0/StructureDefinition/extension-PlanDefinition.action.applicabilityBehavior"; public enum CqfApplicabilityBehavior { ALL, @@ -167,6 +175,8 @@ public enum CqfApplicabilityBehavior { "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-definitionExtractValue"; public static final String SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT = "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemPopulationContext"; + public static final String SDC_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT = + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-definitionPopulationContext"; public static final String SDC_QUESTIONNAIRE_IS_SUBJECT = "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject"; public static final String SDC_QUESTIONNAIRE_PREPOPULATE_SUBJECT = @@ -193,6 +203,9 @@ public enum CqfApplicabilityBehavior { "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext"; public static final String SDC_QUESTIONNAIRE_ADAPTIVE = "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-questionnaireAdaptive"; + public static final String SDC_POPULATE_BEHAVIOR = + "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-populate-behavior"; + public static final String SDC_LAUNCH_CONTEXT_SYSTEM = "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext"; public enum SDC_QUESTIONNAIRE_LAUNCH_CONTEXT_CODE { PATIENT, @@ -203,6 +216,20 @@ public enum SDC_QUESTIONNAIRE_LAUNCH_CONTEXT_CODE { CLINICAL } + public enum SDC_POPULATE_BEHAVIOR_CODE { + ALWAYS("always"), + NEVER("never"), + IF_EMPTY("if-empty"), + IF_AVAILABLE("if-available"), + SUPPLEMENT("supplement"); + + public final String code; + + SDC_POPULATE_BEHAVIOR_CODE(String code) { + this.code = code; + } + } + public static final String SDC_QUESTIONNAIRE_SUB_QUESTIONNAIRE = "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire"; public static final String SDC_QUESTIONNAIRE_CALCULATED_EXPRESSION = diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/CqfExpression.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/CqfExpression.java index 9e9af2a6f4..30cc2250af 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/CqfExpression.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/CqfExpression.java @@ -6,6 +6,7 @@ import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.ICompositeType; +import org.opencds.cqf.fhir.utility.adapter.IAdapter; /** * This class is used to contain the various properties of a CqfExpression with an alternate so that @@ -44,6 +45,25 @@ public static CqfExpression of(IBaseExtension extension, Map expression, Map referencedLibraries) { + if (expression == null) { + return null; + } + var altExpressionExt = expression.getExtensionByUrl(Constants.ALT_EXPRESSION_EXT); + var altExpression = altExpressionExt == null + ? null + : expression.getAdapterFactory().createBase(altExpressionExt.getValue()); + return new CqfExpression( + expression.resolvePathString("language"), + expression.resolvePathString("expression"), + referencedLibraries, + expression.resolvePathString("reference"), + altExpression != null ? altExpression.resolvePathString("language") : null, + altExpression != null ? altExpression.resolvePathString("expression") : null, + altExpression != null ? altExpression.resolvePathString("reference") : null, + expression.resolvePathString("name")); + } + public static CqfExpression of( org.hl7.fhir.r4.model.Expression expression, Map referencedLibraries) { if (expression == null) { diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/SearchHelper.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/SearchHelper.java index 6cf19ccf36..026b9c76ed 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/SearchHelper.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/SearchHelper.java @@ -29,7 +29,7 @@ public class SearchHelper { private SearchHelper() {} @SuppressWarnings("unchecked") - protected static Class getBundleClass(IRepository repository) { + public static Class getBundleClass(IRepository repository) { return (Class) repository.fhirContext().getResourceDefinition("Bundle").getImplementingClass(); } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/VersionUtilities.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/VersionUtilities.java index 3b7929c5f8..15d85def88 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/VersionUtilities.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/VersionUtilities.java @@ -1,6 +1,9 @@ package org.opencds.cqf.fhir.utility; import ca.uhn.fhir.context.FhirVersionEnum; +import java.math.BigDecimal; +import java.util.Date; +import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IPrimitiveType; @@ -38,18 +41,13 @@ public static FhirVersionEnum enumForVersion(String fhirVersion) { } // This returns the closest matching major version - switch (fhirVersion.substring(0, 1)) { - case "2": - return FhirVersionEnum.DSTU2; - case "3": - return FhirVersionEnum.DSTU3; - case "4": - return FhirVersionEnum.R4; - case "5": - return FhirVersionEnum.R5; - default: - throw new IllegalArgumentException(UNSUPPORTED); - } + return switch (fhirVersion.substring(0, 1)) { + case "2" -> FhirVersionEnum.DSTU2; + case "3" -> FhirVersionEnum.DSTU3; + case "4" -> FhirVersionEnum.R4; + case "5" -> FhirVersionEnum.R5; + default -> throw new IllegalArgumentException(UNSUPPORTED); + }; } /** @@ -70,18 +68,13 @@ public static IPrimitiveType stringTypeForVersion(FhirVersionEnum fhirVe * @return the new StringType */ public static IPrimitiveType stringTypeForVersion(FhirVersionEnum fhirVersion, String string) { - switch (fhirVersion) { - case DSTU2: - return new org.hl7.fhir.dstu2.model.StringType(string); - case DSTU3: - return new org.hl7.fhir.dstu3.model.StringType(string); - case R4: - return new org.hl7.fhir.r4.model.StringType(string); - case R5: - return new org.hl7.fhir.r5.model.StringType(string); - default: - throw new IllegalArgumentException(UNSUPPORTED); - } + return switch (fhirVersion) { + case DSTU2 -> new org.hl7.fhir.dstu2.model.StringType(string); + case DSTU3 -> new org.hl7.fhir.dstu3.model.StringType(string); + case R4 -> new org.hl7.fhir.r4.model.StringType(string); + case R5 -> new org.hl7.fhir.r5.model.StringType(string); + default -> throw new IllegalArgumentException(UNSUPPORTED); + }; } /** @@ -102,18 +95,13 @@ public static IPrimitiveType uriTypeForVersion(FhirVersionEnum fhirVersi * @return the new UriType */ public static IPrimitiveType uriTypeForVersion(FhirVersionEnum fhirVersion, String uri) { - switch (fhirVersion) { - case DSTU2: - return new org.hl7.fhir.dstu2.model.UriType(uri); - case DSTU3: - return new org.hl7.fhir.dstu3.model.UriType(uri); - case R4: - return new org.hl7.fhir.r4.model.UriType(uri); - case R5: - return new org.hl7.fhir.r5.model.UriType(uri); - default: - throw new IllegalArgumentException(UNSUPPORTED); - } + return switch (fhirVersion) { + case DSTU2 -> new org.hl7.fhir.dstu2.model.UriType(uri); + case DSTU3 -> new org.hl7.fhir.dstu3.model.UriType(uri); + case R4 -> new org.hl7.fhir.r4.model.UriType(uri); + case R5 -> new org.hl7.fhir.r5.model.UriType(uri); + default -> throw new IllegalArgumentException(UNSUPPORTED); + }; } /** @@ -135,18 +123,13 @@ public static IPrimitiveType canonicalTypeForVersion(FhirVersionEnum fhi * @return the new CanonicalType */ public static IPrimitiveType canonicalTypeForVersion(FhirVersionEnum fhirVersion, String value) { - switch (fhirVersion) { - case DSTU2: - return new org.hl7.fhir.dstu2.model.UriType(value); - case DSTU3: - return new org.hl7.fhir.dstu3.model.UriType(value); - case R4: - return new org.hl7.fhir.r4.model.CanonicalType(value); - case R5: - return new org.hl7.fhir.r5.model.CanonicalType(value); - default: - throw new IllegalArgumentException(UNSUPPORTED); - } + return switch (fhirVersion) { + case DSTU2 -> new org.hl7.fhir.dstu2.model.UriType(value); + case DSTU3 -> new org.hl7.fhir.dstu3.model.UriType(value); + case R4 -> new org.hl7.fhir.r4.model.CanonicalType(value); + case R5 -> new org.hl7.fhir.r5.model.CanonicalType(value); + default -> throw new IllegalArgumentException(UNSUPPORTED); + }; } /** @@ -174,18 +157,13 @@ public static IPrimitiveType booleanTypeForVersion(FhirVersionEnum fhir * @return the new CodeType */ public static IPrimitiveType codeTypeForVersion(FhirVersionEnum fhirVersion, String code) { - switch (fhirVersion) { - case DSTU2: - return new org.hl7.fhir.dstu2.model.CodeType(code); - case DSTU3: - return new org.hl7.fhir.dstu3.model.CodeType(code); - case R4: - return new org.hl7.fhir.r4.model.CodeType(code); - case R5: - return new org.hl7.fhir.r5.model.CodeType(code); - default: - throw new IllegalArgumentException(UNSUPPORTED); - } + return switch (fhirVersion) { + case DSTU2 -> new org.hl7.fhir.dstu2.model.CodeType(code); + case DSTU3 -> new org.hl7.fhir.dstu3.model.CodeType(code); + case R4 -> new org.hl7.fhir.r4.model.CodeType(code); + case R5 -> new org.hl7.fhir.r5.model.CodeType(code); + default -> throw new IllegalArgumentException(UNSUPPORTED); + }; } /** @@ -196,17 +174,69 @@ public static IPrimitiveType codeTypeForVersion(FhirVersionEnum fhirVers * @return the new Reference */ public static IBaseReference referenceTypeForVersion(FhirVersionEnum fhirVersion, String value) { - switch (fhirVersion) { - case DSTU2: - return new org.hl7.fhir.dstu2.model.Reference().setReference(value); - case DSTU3: - return new org.hl7.fhir.dstu3.model.Reference().setReference(value); - case R4: - return new org.hl7.fhir.r4.model.Reference().setReference(value); - case R5: - return new org.hl7.fhir.r5.model.Reference().setReference(value); - default: - throw new IllegalArgumentException(UNSUPPORTED); - } + return switch (fhirVersion) { + case DSTU2 -> new org.hl7.fhir.dstu2.model.Reference().setReference(value); + case DSTU3 -> new org.hl7.fhir.dstu3.model.Reference().setReference(value); + case R4 -> new org.hl7.fhir.r4.model.Reference().setReference(value); + case R5 -> new org.hl7.fhir.r5.model.Reference().setReference(value); + default -> throw new IllegalArgumentException(UNSUPPORTED); + }; + } + + /** + * Returns a Reference for the supplied version with a value of the supplied value. + * + * @param fhirVersion the FHIR version to create a Reference for + * @param value the string value of the Reference + * @return the new Reference + */ + public static IBaseReference referenceTypeForVersion(FhirVersionEnum fhirVersion, IAnyResource value) { + return switch (fhirVersion) { + case DSTU2 -> new org.hl7.fhir.dstu2.model.Reference(value); + case DSTU3 -> new org.hl7.fhir.dstu3.model.Reference(value); + case R4 -> new org.hl7.fhir.r4.model.Reference(value); + case R5 -> new org.hl7.fhir.r5.model.Reference(value); + default -> throw new IllegalArgumentException(UNSUPPORTED); + }; + } + + public static IPrimitiveType integerTypeForVersion(FhirVersionEnum fhirVersion, Integer value) { + return switch (fhirVersion) { + case DSTU2 -> new org.hl7.fhir.dstu2.model.IntegerType(value); + case DSTU3 -> new org.hl7.fhir.dstu3.model.IntegerType(value); + case R4 -> new org.hl7.fhir.r4.model.IntegerType(value); + case R5 -> new org.hl7.fhir.r5.model.IntegerType(value); + default -> throw new IllegalArgumentException(UNSUPPORTED); + }; + } + + public static IPrimitiveType decimalTypeForVersion(FhirVersionEnum fhirVersion, BigDecimal value) { + return switch (fhirVersion) { + case DSTU2 -> new org.hl7.fhir.dstu2.model.DecimalType(value); + case DSTU3 -> new org.hl7.fhir.dstu3.model.DecimalType(value); + case R4 -> new org.hl7.fhir.r4.model.DecimalType(value); + case R5 -> new org.hl7.fhir.r5.model.DecimalType(value); + default -> throw new IllegalArgumentException(UNSUPPORTED); + }; + } + + public static IPrimitiveType dateTimeTypeForVersion(FhirVersionEnum fhirVersion, String value) { + return switch (fhirVersion) { + case DSTU2 -> new org.hl7.fhir.dstu2.model.DateTimeType(value); + case DSTU3 -> new org.hl7.fhir.dstu3.model.DateTimeType(value); + case R4 -> new org.hl7.fhir.r4.model.DateTimeType(value); + case R5 -> new org.hl7.fhir.r5.model.DateTimeType(value); + default -> throw new IllegalArgumentException(UNSUPPORTED); + }; + } + + public static IPrimitiveType instantTypeForVersion(FhirVersionEnum fhirVersion, String value) { + return switch (fhirVersion) { + case DSTU2 -> new org.hl7.fhir.dstu2.model.InstantType(value); + case DSTU3 -> new org.hl7.fhir.dstu3.model.InstantType(value); + case R4 -> new org.hl7.fhir.r4.model.InstantType(value); + case R5 -> new org.hl7.fhir.r5.model.InstantType(value); + default -> throw new IllegalArgumentException(UNSUPPORTED); + }; } } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapterFactory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapterFactory.java index cb8e7c5c41..3730d43a96 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapterFactory.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapterFactory.java @@ -284,4 +284,12 @@ IQuestionnaireResponseItemAnswerComponentAdapter createQuestionnaireResponseItem * @return an adapter exposing common api calls */ ITupleAdapter createTuple(IBase tuple); + + /** + * Creates an adapter that exposes common Observation operations across multiple versions of FHIR + * + * @param observation a HAPI FHIR Observation object + * @return an adapter exposing common api calls + */ + IObservationAdapter createObservation(IBaseResource observation); } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/ICodeableConceptAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/ICodeableConceptAdapter.java index 45ea866499..b2ff53b6ae 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/ICodeableConceptAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/ICodeableConceptAdapter.java @@ -1,7 +1,10 @@ package org.opencds.cqf.fhir.utility.adapter; +import static java.util.Collections.singletonList; + import java.util.List; import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseCoding; public interface ICodeableConceptAdapter extends IAdapter { @@ -15,4 +18,16 @@ default ICodingAdapter getCodingFirstRep() { List codings = getCoding(); return codings.isEmpty() ? null : codings.get(0); } + + ICodeableConceptAdapter addCoding(String system, String code, String display); + + default ICodeableConceptAdapter setCoding(List coding) { + setValue("coding", coding); + return this; + } + + default ICodeableConceptAdapter addCoding(IBaseCoding coding) { + setValue("coding", singletonList(coding)); + return this; + } } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/ICodingAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/ICodingAdapter.java index 7502fcbdb3..fea9744991 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/ICodingAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/ICodingAdapter.java @@ -1,10 +1,13 @@ package org.opencds.cqf.fhir.utility.adapter; import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseDatatype; public interface ICodingAdapter extends IAdapter { String getCode(); + IBaseDatatype getCodeType(); + boolean hasCode(); ICodingAdapter setCode(String code); diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IObservationAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IObservationAdapter.java new file mode 100644 index 0000000000..7944fc09c1 --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IObservationAdapter.java @@ -0,0 +1,102 @@ +package org.opencds.cqf.fhir.utility.adapter; + +import static org.opencds.cqf.fhir.utility.VersionUtilities.dateTimeTypeForVersion; +import static org.opencds.cqf.fhir.utility.VersionUtilities.instantTypeForVersion; + +import java.util.Date; +import java.util.List; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseDatatype; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.ICompositeType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +public interface IObservationAdapter extends IResourceAdapter { + default IObservationAdapter setBasedOn(List basedOn) { + setValue("basedOn", basedOn); + return this; + } + + default IObservationAdapter setPartOf(List partOf) { + setValue("partOf", partOf); + return this; + } + + default IObservationAdapter setDerivedFrom(List derivedFrom) { + setValue("derivedFrom", derivedFrom); + return this; + } + + default IObservationAdapter setStatus(String status) { + setValue("status", status); + return this; + } + + default String getStatus() { + return resolvePathString("status"); + } + + default IObservationAdapter setCode(ICompositeType codeableConcept) { + setValue("code", codeableConcept); + return this; + } + + default ICodeableConceptAdapter getCode() { + var code = resolvePath("code", IBase.class); + return code == null ? null : getAdapterFactory().createCodeableConcept(code); + } + + default IObservationAdapter setCategory(List category) { + setValue("category", category); + return this; + } + + default IObservationAdapter setSubject(IBaseReference subject) { + setValue("subject", subject); + return this; + } + + default IObservationAdapter setEncounter(IBaseReference encounter) { + setValue("encounter", encounter); + return this; + } + + default IObservationAdapter setEffective(String effective) { + return setEffective(dateTimeTypeForVersion(fhirVersion(), effective)); + } + + default IObservationAdapter setEffective(IBaseDatatype effective) { + if (effective instanceof IPrimitiveType primitive && primitive.getValue() instanceof Date) { + setValue("effectiveDateTime", primitive); + } + return this; + } + + default IObservationAdapter setEffectivePeriod(ICompositeType period) { + setValue(get(), "effectivePeriod", period); + return this; + } + + default IObservationAdapter setIssued(String issued) { + return setIssued(instantTypeForVersion(fhirVersion(), issued)); + } + + default IObservationAdapter setIssued(IBaseDatatype issued) { + if (issued instanceof IPrimitiveType primitive + && primitive.fhirType().equals("instant") + && primitive.getValue() instanceof Date) { + setValue("issued", primitive); + } + return this; + } + + default IObservationAdapter setPerformer(List performer) { + setValue("performer", performer); + return this; + } + + default IObservationAdapter setValue(IBaseDatatype value) { + setValue("value", value); + return this; + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IPlanDefinitionActionAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IPlanDefinitionActionAdapter.java index bdecc9b150..2bf0af248b 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IPlanDefinitionActionAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IPlanDefinitionActionAdapter.java @@ -67,12 +67,16 @@ public interface IPlanDefinitionActionAdapter extends IAdapter { // These will need to be overridden starting with R6 when this is introduced as an element on action default boolean hasApplicabilityBehavior() { - return hasExtension(Constants.CQF_APPLICABILITY_BEHAVIOR); + return hasExtension(Constants.CQF_APPLICABILITY_BEHAVIOR) + || hasExtension(Constants.R6_PLAN_DEFINITION_ACTION_APPLICABILITY_BEHAVIOR); } // These will need to be overridden starting with R6 when this is introduced as an element on action default CqfApplicabilityBehavior getApplicabilityBehavior() { var extension = getExtensionByUrl(Constants.CQF_APPLICABILITY_BEHAVIOR); + if (extension == null) { + extension = getExtensionByUrl(Constants.R6_PLAN_DEFINITION_ACTION_APPLICABILITY_BEHAVIOR); + } if (extension != null && extension.getValue() instanceof IPrimitiveType primitiveType) { try { return CqfApplicabilityBehavior.valueOf( diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IQuestionnaireItemComponentAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IQuestionnaireItemComponentAdapter.java index 96e01664b0..8043ed9e37 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IQuestionnaireItemComponentAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IQuestionnaireItemComponentAdapter.java @@ -1,5 +1,9 @@ package org.opencds.cqf.fhir.utility.adapter; +import static org.opencds.cqf.fhir.utility.Constants.CPG_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT; +import static org.opencds.cqf.fhir.utility.Constants.SDC_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT; +import static org.opencds.cqf.fhir.utility.Constants.SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT; + import java.util.List; import org.hl7.fhir.instance.model.api.IBaseCoding; import org.hl7.fhir.instance.model.api.IBaseDatatype; @@ -48,4 +52,10 @@ public interface IQuestionnaireItemComponentAdapter extends IItemComponentAdapte default ICompositeType newExpression(String expression) { return newExpression("text/cql-expression", expression); } + + default boolean isContextItem() { + return hasExtension(SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT) + || hasExtension(SDC_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT) + || hasExtension(CPG_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT); + } } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IQuestionnaireResponseAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IQuestionnaireResponseAdapter.java index 5d6a0c68ba..eae2652bcb 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IQuestionnaireResponseAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IQuestionnaireResponseAdapter.java @@ -3,6 +3,7 @@ import java.util.Date; import java.util.List; import java.util.stream.Collectors; +import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; @@ -21,16 +22,58 @@ public interface IQuestionnaireResponseAdapter extends IResourceAdapter { IQuestionnaireResponseAdapter setQuestionnaire(String canonical); + default List getBasedOn() { + return resolvePathList(get(), "basedOn", IBaseReference.class); + } + + default List getPartOf() { + return resolvePathList(get(), "partOf", IBaseReference.class); + } + boolean hasSubject(); IIdType getSubject(); IQuestionnaireResponseAdapter setSubject(IIdType subject); + default boolean hasEncounter() { + return getEncounter() != null; + } + + default IBaseReference getEncounter() { + return resolvePath("encounter", IBaseReference.class); + } + + default IQuestionnaireResponseAdapter setEncounter(IBaseReference encounter) { + setValue("encounter", encounter); + return this; + } + + default boolean hasAuthored() { + return getAuthored() != null; + } + + default Date getAuthored() { + var authored = resolvePath("authored", IPrimitiveType.class); + return authored == null ? null : (Date) authored.getValue(); + } + IQuestionnaireResponseAdapter setAuthored(Date date); + default boolean hasAuthor() { + return getAuthor() != null; + } + + default IBaseReference getAuthor() { + return resolvePath("author", IBaseReference.class); + } + IQuestionnaireResponseAdapter setStatus(String status); + default String getStatus() { + return resolvePathString("status"); + } + boolean hasItem(); default boolean hasItem(String linkId) { diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/AdapterFactory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/AdapterFactory.java index 6d31f589b1..15bbfd0bd9 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/AdapterFactory.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/AdapterFactory.java @@ -10,6 +10,7 @@ import org.hl7.fhir.dstu3.model.Library; import org.hl7.fhir.dstu3.model.Measure; import org.hl7.fhir.dstu3.model.MetadataResource; +import org.hl7.fhir.dstu3.model.Observation; import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.dstu3.model.PlanDefinition; @@ -41,6 +42,7 @@ import org.opencds.cqf.fhir.utility.adapter.IImplementationGuideAdapter; import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.IObservationAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersParameterComponentAdapter; import org.opencds.cqf.fhir.utility.adapter.IPlanDefinitionActionAdapter; @@ -69,6 +71,8 @@ public IResourceAdapter createResource(IBaseResource resource) { return createParameters(parameters); } else if (resource instanceof Group group) { return createGroup(group); + } else if (resource instanceof Observation observation) { + return createObservation(observation); } else { return new ResourceAdapter(resource); } @@ -263,4 +267,9 @@ public IImplementationGuideAdapter createImplementationGuide(IBaseResource imple public ITupleAdapter createTuple(IBase tuple) { throw new UnprocessableEntityException("No FHIR type Tuple exists in version DSTU3"); } + + @Override + public IObservationAdapter createObservation(IBaseResource observation) { + return new ObservationAdapter(observation); + } } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodeableConceptAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodeableConceptAdapter.java index 65298f159b..62b21ff1db 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodeableConceptAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodeableConceptAdapter.java @@ -3,6 +3,7 @@ import ca.uhn.fhir.context.FhirVersionEnum; import java.util.List; import org.hl7.fhir.dstu3.model.CodeableConcept; +import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.instance.model.api.IBase; import org.opencds.cqf.fhir.utility.adapter.BaseElementAdapter; import org.opencds.cqf.fhir.utility.adapter.ICodeableConceptAdapter; @@ -40,4 +41,10 @@ public List getCoding() { public boolean hasCoding(String code) { return get().getCoding().stream().anyMatch(coding -> coding.getCode().equals(code)); } + + @Override + public ICodeableConceptAdapter addCoding(String system, String code, String display) { + get().addCoding(new Coding().setSystem(system).setCode(code).setDisplay(display)); + return this; + } } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodingAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodingAdapter.java index e086ce811f..eea5af9148 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodingAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodingAdapter.java @@ -1,6 +1,7 @@ package org.opencds.cqf.fhir.utility.adapter.dstu3; import ca.uhn.fhir.context.FhirVersionEnum; +import org.hl7.fhir.dstu3.model.CodeType; import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.instance.model.api.IBase; import org.opencds.cqf.fhir.utility.adapter.BaseElementAdapter; @@ -29,6 +30,11 @@ public String getCode() { return get().getCode(); } + @Override + public CodeType getCodeType() { + return get().getCodeElement(); + } + @Override public boolean hasCode() { return get().hasCode(); diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/ObservationAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/ObservationAdapter.java new file mode 100644 index 0000000000..ff4be89246 --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/ObservationAdapter.java @@ -0,0 +1,21 @@ +package org.opencds.cqf.fhir.utility.adapter.dstu3; + +import org.hl7.fhir.dstu3.model.Observation; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.opencds.cqf.fhir.utility.adapter.IObservationAdapter; + +public class ObservationAdapter extends ResourceAdapter implements IObservationAdapter { + + public ObservationAdapter(IBaseResource observation) { + super(observation); + + if (!(observation instanceof Observation)) { + throw new IllegalArgumentException("resource passed as parameters argument is not an Observation resource"); + } + } + + @Override + public Observation get() { + return (Observation) resource; + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/AdapterFactory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/AdapterFactory.java index 713c3f5609..3d123fbafa 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/AdapterFactory.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/AdapterFactory.java @@ -14,6 +14,7 @@ import org.hl7.fhir.r4.model.Library; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MetadataResource; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r4.model.PlanDefinition; @@ -41,6 +42,7 @@ import org.opencds.cqf.fhir.utility.adapter.IImplementationGuideAdapter; import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.IObservationAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersParameterComponentAdapter; import org.opencds.cqf.fhir.utility.adapter.IPlanDefinitionActionAdapter; @@ -69,6 +71,8 @@ public IResourceAdapter createResource(IBaseResource resource) { return createParameters(parameters); } else if (resource instanceof Group group) { return createGroup(group); + } else if (resource instanceof Observation observation) { + return createObservation(observation); } else { return new ResourceAdapter(resource); } @@ -265,4 +269,9 @@ public IImplementationGuideAdapter createImplementationGuide(IBaseResource imple public ITupleAdapter createTuple(IBase tuple) { return new TupleAdapter(tuple); } + + @Override + public IObservationAdapter createObservation(IBaseResource observation) { + return new ObservationAdapter(observation); + } } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/CodeableConceptAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/CodeableConceptAdapter.java index 26d180e405..a62dce3fdc 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/CodeableConceptAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/CodeableConceptAdapter.java @@ -4,6 +4,7 @@ import java.util.List; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; import org.opencds.cqf.fhir.utility.adapter.BaseElementAdapter; import org.opencds.cqf.fhir.utility.adapter.ICodeableConceptAdapter; import org.opencds.cqf.fhir.utility.adapter.ICodingAdapter; @@ -40,4 +41,10 @@ public List getCoding() { public boolean hasCoding(String code) { return get().getCoding().stream().anyMatch(coding -> coding.getCode().equals(code)); } + + @Override + public ICodeableConceptAdapter addCoding(String system, String code, String display) { + get().addCoding(new Coding().setSystem(system).setCode(code).setDisplay(display)); + return this; + } } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/CodingAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/CodingAdapter.java index 24033a4b90..508c5b0f86 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/CodingAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/CodingAdapter.java @@ -2,6 +2,7 @@ import ca.uhn.fhir.context.FhirVersionEnum; import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.Coding; import org.opencds.cqf.fhir.utility.adapter.BaseElementAdapter; import org.opencds.cqf.fhir.utility.adapter.ICodingAdapter; @@ -28,6 +29,11 @@ public String getCode() { return get().getCode(); } + @Override + public CodeType getCodeType() { + return get().getCodeElement(); + } + @Override public boolean hasCode() { return get().hasCode(); diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/ObservationAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/ObservationAdapter.java new file mode 100644 index 0000000000..223e99c4ef --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/ObservationAdapter.java @@ -0,0 +1,21 @@ +package org.opencds.cqf.fhir.utility.adapter.r4; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Observation; +import org.opencds.cqf.fhir.utility.adapter.IObservationAdapter; + +public class ObservationAdapter extends ResourceAdapter implements IObservationAdapter { + + public ObservationAdapter(IBaseResource observation) { + super(observation); + + if (!(observation instanceof Observation)) { + throw new IllegalArgumentException("resource passed as parameters argument is not an Observation resource"); + } + } + + @Override + public Observation get() { + return (Observation) resource; + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/AdapterFactory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/AdapterFactory.java index f21df6d883..0126f297dd 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/AdapterFactory.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/AdapterFactory.java @@ -14,6 +14,7 @@ import org.hl7.fhir.r5.model.Library; import org.hl7.fhir.r5.model.Measure; import org.hl7.fhir.r5.model.MetadataResource; +import org.hl7.fhir.r5.model.Observation; import org.hl7.fhir.r5.model.Parameters; import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r5.model.PlanDefinition; @@ -41,6 +42,7 @@ import org.opencds.cqf.fhir.utility.adapter.IImplementationGuideAdapter; import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.IObservationAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersParameterComponentAdapter; import org.opencds.cqf.fhir.utility.adapter.IPlanDefinitionActionAdapter; @@ -69,6 +71,8 @@ public IResourceAdapter createResource(IBaseResource resource) { return createParameters(parameters); } else if (resource instanceof Group group) { return createGroup(group); + } else if (resource instanceof Observation observation) { + return createObservation(observation); } else { return new ResourceAdapter(resource); } @@ -264,4 +268,9 @@ public IImplementationGuideAdapter createImplementationGuide(IBaseResource imple public ITupleAdapter createTuple(IBase tuple) { return new TupleAdapter(tuple); } + + @Override + public IObservationAdapter createObservation(IBaseResource observation) { + return new ObservationAdapter(observation); + } } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/CodeableConceptAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/CodeableConceptAdapter.java index 99ebdbca49..5e3bf15e21 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/CodeableConceptAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/CodeableConceptAdapter.java @@ -5,6 +5,7 @@ import java.util.List; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.r5.model.CodeableConcept; +import org.hl7.fhir.r5.model.Coding; import org.opencds.cqf.fhir.utility.adapter.BaseElementAdapter; import org.opencds.cqf.fhir.utility.adapter.ICodeableConceptAdapter; import org.opencds.cqf.fhir.utility.adapter.ICodingAdapter; @@ -46,4 +47,10 @@ public List getCoding() { public boolean hasCoding(String code) { return get().getCoding().stream().anyMatch(coding -> coding.getCode().equals(code)); } + + @Override + public ICodeableConceptAdapter addCoding(String system, String code, String display) { + get().addCoding(new Coding().setSystem(system).setCode(code).setDisplay(display)); + return this; + } } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/CodingAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/CodingAdapter.java index b047034514..146a591140 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/CodingAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/CodingAdapter.java @@ -2,6 +2,7 @@ import ca.uhn.fhir.context.FhirVersionEnum; import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.r5.model.CodeType; import org.hl7.fhir.r5.model.Coding; import org.opencds.cqf.fhir.utility.adapter.BaseElementAdapter; import org.opencds.cqf.fhir.utility.adapter.ICodingAdapter; @@ -28,6 +29,11 @@ public String getCode() { return get().getCode(); } + @Override + public CodeType getCodeType() { + return get().getCodeElement(); + } + @Override public boolean hasCode() { return get().hasCode(); diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/ObservationAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/ObservationAdapter.java new file mode 100644 index 0000000000..143e3cf0a9 --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/ObservationAdapter.java @@ -0,0 +1,21 @@ +package org.opencds.cqf.fhir.utility.adapter.r5; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r5.model.Observation; +import org.opencds.cqf.fhir.utility.adapter.IObservationAdapter; + +public class ObservationAdapter extends ResourceAdapter implements IObservationAdapter { + + public ObservationAdapter(IBaseResource observation) { + super(observation); + + if (!(observation instanceof Observation)) { + throw new IllegalArgumentException("resource passed as parameters argument is not an Observation resource"); + } + } + + @Override + public Observation get() { + return (Observation) resource; + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepository.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepository.java index f39b3f929f..649acfdfd0 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepository.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepository.java @@ -38,6 +38,7 @@ import org.opencds.cqf.fhir.utility.repository.Repositories; import org.opencds.cqf.fhir.utility.repository.ig.EncodingBehavior.PreserveEncoding; import org.opencds.cqf.fhir.utility.repository.operations.IRepositoryOperationProvider; +import org.opencds.cqf.fhir.utility.search.Searches; /** * Provides access to FHIR resources stored in a directory structure following @@ -110,6 +111,7 @@ *
  • Auto-detects project conventions based on directory structure.
  • * */ +@SuppressWarnings("UnstableApiUsage") public class IgRepository implements IRepository { private final FhirContext fhirContext; private final Path root; @@ -745,8 +747,13 @@ public B transaction(B transaction, Map returnBundle, BundleHelper.newEntryWithResponse( version, BundleHelper.newResponseWithLocation(version, requestUrl))); + } else if (BundleHelper.isEntryRequestGet(version, e) + && BundleHelper.getEntryRequestUrl(version, e).contains("?")) { + var searchResult = executeSearchUrl(BundleHelper.getEntryRequestUrl(version, e), headers); + BundleHelper.addEntry(returnBundle, BundleHelper.newEntryWithResource(searchResult)); } else { - throw new NotImplementedOperationException("Transaction only supports PUT, POST, or DELETE"); + throw new NotImplementedOperationException( + "Transaction only supports PUT, POST, DELETE, or search-style GET"); } }); return returnBundle; @@ -759,4 +766,34 @@ protected R invokeOperation( } return operationProvider.invokeOperation(this, id, resourceType, operationName, parameters); } + + @SuppressWarnings("unchecked") + private IBaseBundle executeSearchUrl(String requestUrl, Map headers) { + var queryIdx = requestUrl.indexOf('?'); + var resourceType = queryIdx < 0 ? requestUrl : requestUrl.substring(0, queryIdx); + var queryString = queryIdx < 0 ? "" : requestUrl.substring(queryIdx + 1); + var resourceClass = (Class) + fhirContext.getResourceDefinition(resourceType).getImplementingClass(); + var bundleClass = + (Class) fhirContext.getResourceDefinition("Bundle").getImplementingClass(); + return this.search(bundleClass, resourceClass, parseSearchQuery(queryString), headers); + } + + private static Multimap> parseSearchQuery(String queryString) { + var builder = Searches.builder(); + if (queryString.isEmpty()) { + return builder.build(); + } + for (var pair : queryString.split("&")) { + var eq = pair.indexOf('='); + var key = eq < 0 ? pair : pair.substring(0, eq); + var value = eq < 0 ? "" : java.net.URLDecoder.decode(pair.substring(eq + 1), StandardCharsets.UTF_8); + switch (key) { + case "url" -> builder.withUriParam("url", value); + case "version" -> builder.withTokenParam("version", value); + default -> builder.withStringParam(key, value); + } + } + return builder.build(); + } } diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/BundleHelperTests.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/BundleHelperTests.java index 2fc1d632c5..4bfdd28ba4 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/BundleHelperTests.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/BundleHelperTests.java @@ -171,6 +171,37 @@ void r5() { assertFalse(getEntryResources(bundle).isEmpty()); } + @Test + void isEntryRequestGetDstu3() { + org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent bundle = + new org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent() + .setRequest(new org.hl7.fhir.dstu3.model.Bundle.BundleEntryRequestComponent() + .setMethod(org.hl7.fhir.dstu3.model.Bundle.HTTPVerb.GET)); + var res = BundleHelper.isEntryRequestGet(FhirVersionEnum.DSTU3, bundle); + + assertTrue(res); + } + + @Test + void isEntryRequestGetR4() { + BundleEntryComponent bundle = + new Bundle.BundleEntryComponent().setRequest(new BundleEntryRequestComponent().setMethod(HTTPVerb.GET)); + var res = BundleHelper.isEntryRequestGet(FhirVersionEnum.R4, bundle); + + assertTrue(res); + } + + @Test + void isEntryRequestGetR5() { + org.hl7.fhir.r5.model.Bundle.BundleEntryComponent bundle = + new org.hl7.fhir.r5.model.Bundle.BundleEntryComponent() + .setRequest(new org.hl7.fhir.r5.model.Bundle.BundleEntryRequestComponent() + .setMethod(org.hl7.fhir.r5.model.Bundle.HTTPVerb.GET)); + var res = BundleHelper.isEntryRequestGet(FhirVersionEnum.R5, bundle); + + assertTrue(res); + } + @Test void isEntryRequestDeleteDstu3() { org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent bundle = diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/CqfExpressionTests.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/CqfExpressionTests.java index 891ba7cf91..f7ab840a6e 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/CqfExpressionTests.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/CqfExpressionTests.java @@ -7,16 +7,16 @@ import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.r4.model.Expression; import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.utility.adapter.IAdapter; class CqfExpressionTests { @Test void testInvalidExtension() { - var cqfExpression = CqfExpression.of((IBaseExtension) null, null); - assertNull(cqfExpression); - var dstu2Expression = CqfExpression.of(new org.hl7.fhir.dstu2.model.Extension(), null); - assertNull(dstu2Expression); + assertNull(CqfExpression.of((IBaseExtension) null, null)); + assertNull(CqfExpression.of(new org.hl7.fhir.dstu2.model.Extension(), null)); assertNull(CqfExpression.of(new org.hl7.fhir.r4.model.Extension(), null)); assertNull(CqfExpression.of(new org.hl7.fhir.r5.model.Extension(), null)); + assertNull(CqfExpression.of((IAdapter) null, null)); } @Test diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/VersionUtilitiesTests.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/VersionUtilitiesTests.java index 51d80a48e5..fad0b1a794 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/VersionUtilitiesTests.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/VersionUtilitiesTests.java @@ -5,9 +5,18 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import ca.uhn.fhir.context.FhirVersionEnum; +import java.math.BigDecimal; import org.hl7.fhir.dstu3.model.BooleanType; +import org.hl7.fhir.dstu3.model.CodeType; +import org.hl7.fhir.dstu3.model.DateTimeType; +import org.hl7.fhir.dstu3.model.DecimalType; +import org.hl7.fhir.dstu3.model.InstantType; +import org.hl7.fhir.dstu3.model.IntegerType; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.Reference; import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.UriType; +import org.hl7.fhir.r4.model.CanonicalType; import org.junit.jupiter.api.Test; class VersionUtilitiesTests { @@ -21,7 +30,21 @@ void TestDstu3Versions() { assertEquals(FhirVersionEnum.DSTU3, VersionUtilities.enumForVersion("3.0.1")); assertInstanceOf(StringType.class, VersionUtilities.stringTypeForVersion(FhirVersionEnum.DSTU3)); assertInstanceOf(UriType.class, VersionUtilities.uriTypeForVersion(FhirVersionEnum.DSTU3)); + assertInstanceOf(UriType.class, VersionUtilities.canonicalTypeForVersion(FhirVersionEnum.DSTU3)); assertInstanceOf(BooleanType.class, VersionUtilities.booleanTypeForVersion(FhirVersionEnum.DSTU3, true)); + assertInstanceOf(Reference.class, VersionUtilities.referenceTypeForVersion(FhirVersionEnum.DSTU3, "ref")); + assertInstanceOf( + Reference.class, VersionUtilities.referenceTypeForVersion(FhirVersionEnum.DSTU3, new Patient())); + assertInstanceOf(CodeType.class, VersionUtilities.codeTypeForVersion(FhirVersionEnum.DSTU3, "code")); + assertInstanceOf(IntegerType.class, VersionUtilities.integerTypeForVersion(FhirVersionEnum.DSTU3, 1)); + assertInstanceOf( + DecimalType.class, VersionUtilities.decimalTypeForVersion(FhirVersionEnum.DSTU3, new BigDecimal(1))); + assertInstanceOf( + DateTimeType.class, + VersionUtilities.dateTimeTypeForVersion(FhirVersionEnum.DSTU3, "2026-01-01T12:12:12")); + assertInstanceOf( + InstantType.class, + VersionUtilities.instantTypeForVersion(FhirVersionEnum.DSTU3, "2026-01-01T12:12:12")); } @Test @@ -34,9 +57,29 @@ void TestR4Versions() { assertInstanceOf( org.hl7.fhir.r4.model.StringType.class, VersionUtilities.stringTypeForVersion(FhirVersionEnum.R4)); assertInstanceOf(org.hl7.fhir.r4.model.UriType.class, VersionUtilities.uriTypeForVersion(FhirVersionEnum.R4)); + assertInstanceOf(CanonicalType.class, VersionUtilities.canonicalTypeForVersion(FhirVersionEnum.R4)); assertInstanceOf( org.hl7.fhir.r4.model.BooleanType.class, VersionUtilities.booleanTypeForVersion(FhirVersionEnum.R4, true)); + assertInstanceOf( + org.hl7.fhir.r4.model.Reference.class, + VersionUtilities.referenceTypeForVersion(FhirVersionEnum.R4, "ref")); + assertInstanceOf( + org.hl7.fhir.r4.model.Reference.class, + VersionUtilities.referenceTypeForVersion(FhirVersionEnum.R4, new Patient())); + assertInstanceOf( + org.hl7.fhir.r4.model.CodeType.class, VersionUtilities.codeTypeForVersion(FhirVersionEnum.R4, "code")); + assertInstanceOf( + org.hl7.fhir.r4.model.IntegerType.class, VersionUtilities.integerTypeForVersion(FhirVersionEnum.R4, 1)); + assertInstanceOf( + org.hl7.fhir.r4.model.DecimalType.class, + VersionUtilities.decimalTypeForVersion(FhirVersionEnum.R4, new BigDecimal(1))); + assertInstanceOf( + org.hl7.fhir.r4.model.DateTimeType.class, + VersionUtilities.dateTimeTypeForVersion(FhirVersionEnum.R4, "2026-01-01T12:12:12")); + assertInstanceOf( + org.hl7.fhir.r4.model.InstantType.class, + VersionUtilities.instantTypeForVersion(FhirVersionEnum.R4, "2026-01-01T12:12:12")); } @Test @@ -49,9 +92,31 @@ void TestR5Versions() { assertInstanceOf( org.hl7.fhir.r5.model.StringType.class, VersionUtilities.stringTypeForVersion(FhirVersionEnum.R5)); assertInstanceOf(org.hl7.fhir.r5.model.UriType.class, VersionUtilities.uriTypeForVersion(FhirVersionEnum.R5)); + assertInstanceOf( + org.hl7.fhir.r5.model.CanonicalType.class, + VersionUtilities.canonicalTypeForVersion(FhirVersionEnum.R5)); assertInstanceOf( org.hl7.fhir.r5.model.BooleanType.class, VersionUtilities.booleanTypeForVersion(FhirVersionEnum.R5, true)); + assertInstanceOf( + org.hl7.fhir.r5.model.Reference.class, + VersionUtilities.referenceTypeForVersion(FhirVersionEnum.R5, "ref")); + assertInstanceOf( + org.hl7.fhir.r5.model.Reference.class, + VersionUtilities.referenceTypeForVersion(FhirVersionEnum.R5, new Patient())); + assertInstanceOf( + org.hl7.fhir.r5.model.CodeType.class, VersionUtilities.codeTypeForVersion(FhirVersionEnum.R5, "code")); + assertInstanceOf( + org.hl7.fhir.r5.model.IntegerType.class, VersionUtilities.integerTypeForVersion(FhirVersionEnum.R5, 1)); + assertInstanceOf( + org.hl7.fhir.r5.model.DecimalType.class, + VersionUtilities.decimalTypeForVersion(FhirVersionEnum.R5, new BigDecimal(1))); + assertInstanceOf( + org.hl7.fhir.r5.model.DateTimeType.class, + VersionUtilities.dateTimeTypeForVersion(FhirVersionEnum.R5, "2026-01-01T12:12:12")); + assertInstanceOf( + org.hl7.fhir.r5.model.InstantType.class, + VersionUtilities.instantTypeForVersion(FhirVersionEnum.R5, "2026-01-01T12:12:12")); } @Test @@ -79,8 +144,32 @@ void TestUnsupported() { assertThrows(IllegalArgumentException.class, () -> { VersionUtilities.uriTypeForVersion(FhirVersionEnum.R4B); }); + assertThrows(IllegalArgumentException.class, () -> { + VersionUtilities.canonicalTypeForVersion(FhirVersionEnum.R4B); + }); assertThrows(IllegalArgumentException.class, () -> { VersionUtilities.booleanTypeForVersion(FhirVersionEnum.R4B, true); }); + assertThrows(IllegalArgumentException.class, () -> { + VersionUtilities.referenceTypeForVersion(FhirVersionEnum.R4B, "test"); + }); + assertThrows(IllegalArgumentException.class, () -> { + VersionUtilities.referenceTypeForVersion(FhirVersionEnum.R4B, new org.hl7.fhir.r4b.model.Patient()); + }); + assertThrows(IllegalArgumentException.class, () -> { + VersionUtilities.codeTypeForVersion(FhirVersionEnum.R4B, "test"); + }); + assertThrows(IllegalArgumentException.class, () -> { + VersionUtilities.integerTypeForVersion(FhirVersionEnum.R4B, 1); + }); + assertThrows(IllegalArgumentException.class, () -> { + VersionUtilities.decimalTypeForVersion(FhirVersionEnum.R4B, new BigDecimal(1)); + }); + assertThrows(IllegalArgumentException.class, () -> { + VersionUtilities.dateTimeTypeForVersion(FhirVersionEnum.R4B, "test"); + }); + assertThrows(IllegalArgumentException.class, () -> { + VersionUtilities.instantTypeForVersion(FhirVersionEnum.R4B, "test"); + }); } } diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodeableConceptAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodeableConceptAdapterTest.java index 406973f73d..2041d5c898 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodeableConceptAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodeableConceptAdapterTest.java @@ -35,5 +35,8 @@ void testCoding() { var adapter = adapterFactory.createCodeableConcept(codeableConcept); assertTrue(adapter.hasCoding("test")); assertEquals(1, adapter.getCoding().size()); + adapter.addCoding("test.com", "test2", "Test2"); + assertTrue(adapter.hasCoding("test2")); + assertEquals(2, adapter.getCoding().size()); } } diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodingAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodingAdapterTest.java index b65f9093fe..301c0dac07 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodingAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/CodingAdapterTest.java @@ -1,11 +1,13 @@ package org.opencds.cqf.fhir.utility.adapter.dstu3; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import ca.uhn.fhir.context.FhirVersionEnum; +import org.hl7.fhir.dstu3.model.CodeType; import org.hl7.fhir.dstu3.model.CodeableConcept; import org.hl7.fhir.dstu3.model.Coding; import org.junit.jupiter.api.Test; @@ -37,6 +39,7 @@ void testCode() { var adapter = adapterFactory.createCoding(coding); assertTrue(adapter.hasCode()); assertEquals(code, adapter.getCode()); + assertInstanceOf(CodeType.class, adapter.getCodeType()); var newCode = "test2"; adapter.setCode(newCode); assertEquals(newCode, adapter.getCode()); diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/ObservationAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/ObservationAdapterTest.java new file mode 100644 index 0000000000..a5184d0627 --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/ObservationAdapterTest.java @@ -0,0 +1,79 @@ +package org.opencds.cqf.fhir.utility.adapter.dstu3; + +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Date; +import org.hl7.fhir.dstu3.model.BooleanType; +import org.hl7.fhir.dstu3.model.CodeableConcept; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.DateTimeType; +import org.hl7.fhir.dstu3.model.InstantType; +import org.hl7.fhir.dstu3.model.Library; +import org.hl7.fhir.dstu3.model.Observation; +import org.hl7.fhir.dstu3.model.Observation.ObservationStatus; +import org.hl7.fhir.dstu3.model.Period; +import org.hl7.fhir.dstu3.model.Reference; +import org.junit.jupiter.api.Test; + +class ObservationAdapterTest { + private final org.opencds.cqf.fhir.utility.adapter.IAdapterFactory adapterFactory = new AdapterFactory(); + + @Test + void invalid_object_fails() { + var library = new Library(); + assertThrows(IllegalArgumentException.class, () -> new ObservationAdapter(library)); + } + + @Test + void adapter_get_and_set() { + var obs = new Observation(); + obs.setStatus(ObservationStatus.FINAL); + var adapter = (ObservationAdapter) adapterFactory.createResource(obs); + assertEquals("final", adapter.getStatus()); + adapter.setStatus("amended"); + assertEquals("amended", adapter.getStatus()); + assertNull(adapter.getCode()); + adapter.setCode(new CodeableConcept().addCoding(new Coding("test.com", "test", "test"))); + assertInstanceOf(CodeableConcept.class, adapter.get().getCode()); + assertEquals("test", adapter.getCode().getCodingFirstRep().getCode()); + assertEquals("test.com", adapter.getCode().getCodingFirstRep().getSystem()); + var basedOn = new Reference("basedOn"); + var category = new CodeableConcept().addCoding(new Coding("test.com", "test", "Test")); + var subject = new Reference("subject"); + var performer = new Reference("performer"); + var value = new BooleanType(true); + adapter.setBasedOn(singletonList(basedOn)) + .setCategory(singletonList(category)) + .setSubject(subject) + .setPerformer(singletonList(performer)) + .setValue(value); + assertEquals(basedOn, obs.getBasedOnFirstRep()); + assertEquals(category, obs.getCategoryFirstRep()); + assertEquals(subject, obs.getSubject()); + assertEquals(performer, obs.getPerformerFirstRep()); + assertEquals(value, obs.getValue()); + } + + @Test + void adapter_effective_issued() { + var obs = new Observation(); + var adapter = (ObservationAdapter) adapterFactory.createResource(obs); + var dateTimeString = "2026-01-01T12:12:12-01:00"; + adapter.setEffective(dateTimeString); + assertEquals(dateTimeString, obs.getEffectiveDateTimeType().asStringValue()); + var date = new Date(); + adapter.setEffective(new DateTimeType(date)); + assertEquals(date, obs.getEffectiveDateTimeType().getValue()); + var period = new Period(); + adapter.setEffectivePeriod(period); + assertEquals(period, obs.getEffectivePeriod()); + adapter.setIssued(dateTimeString); + assertEquals(dateTimeString, obs.getIssuedElement().asStringValue()); + adapter.setIssued(new InstantType(date)); + assertEquals(date, obs.getIssued()); + } +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/PlanDefinitionActionAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/PlanDefinitionActionAdapterTest.java index dd2faffe21..1e92b6ff61 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/PlanDefinitionActionAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/PlanDefinitionActionAdapterTest.java @@ -1,11 +1,13 @@ package org.opencds.cqf.fhir.utility.adapter.dstu3; import static org.junit.jupiter.api.Assertions.*; +import static org.opencds.cqf.fhir.utility.Constants.CQF_APPLICABILITY_BEHAVIOR; import ca.uhn.fhir.context.FhirVersionEnum; import org.hl7.fhir.dstu3.model.CodeableConcept; import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.dstu3.model.DataRequirement; +import org.hl7.fhir.dstu3.model.Extension; import org.hl7.fhir.dstu3.model.PlanDefinition; import org.hl7.fhir.dstu3.model.PlanDefinition.ActionConditionKind; import org.hl7.fhir.dstu3.model.PlanDefinition.ActionRelationshipType; @@ -16,10 +18,12 @@ import org.hl7.fhir.dstu3.model.Reference; import org.hl7.fhir.dstu3.model.RelatedArtifact; import org.hl7.fhir.dstu3.model.RequestGroup.RequestGroupActionComponent; +import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.Timing; import org.hl7.fhir.dstu3.model.TriggerDefinition; import org.hl7.fhir.dstu3.model.TriggerDefinition.TriggerType; import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.utility.Constants.CqfApplicabilityBehavior; import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; class PlanDefinitionActionAdapterTest { @@ -169,6 +173,17 @@ void testSelectionBehavior() { assertEquals(selectionBehavior, adapter.getSelectionBehavior()); } + @Test + void testApplicabilityBehavior() { + var action = new PlanDefinition.PlanDefinitionActionComponent(); + var adapter = new PlanDefinitionActionAdapter(action); + assertFalse(adapter.hasApplicabilityBehavior()); + assertEquals(CqfApplicabilityBehavior.ALL, adapter.getApplicabilityBehavior()); + adapter.addExtension(new Extension(CQF_APPLICABILITY_BEHAVIOR, new StringType("one"))); + assertTrue(adapter.hasApplicabilityBehavior()); + assertThrows(IllegalArgumentException.class, adapter::getApplicabilityBehavior); + } + @Test void testDefinition() { var definition = new Reference("test"); diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/QuestionnaireItemComponentAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/QuestionnaireItemComponentAdapterTest.java index 87a13ef2c9..b08a49794f 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/QuestionnaireItemComponentAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/QuestionnaireItemComponentAdapterTest.java @@ -11,6 +11,7 @@ import java.util.List; import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.dstu3.model.Library; +import org.hl7.fhir.dstu3.model.Questionnaire; import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType; import org.junit.jupiter.api.Test; @@ -89,4 +90,11 @@ void testItem() { adapter.addItems(List.of(item2, item3, item4)); assertEquals(4, adapter.getItem().size()); } + + @Test + void testContextItemExtensions() { + var item1 = adapterFactory.createQuestionnaireItem( + new QuestionnaireItemComponent().setLinkId("1").setType(Questionnaire.QuestionnaireItemType.GROUP)); + assertFalse(item1.isContextItem()); + } } diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/QuestionnaireResponseAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/QuestionnaireResponseAdapterTest.java index 7abf53ee9c..e8cdc13678 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/QuestionnaireResponseAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/QuestionnaireResponseAdapterTest.java @@ -1,7 +1,9 @@ package org.opencds.cqf.fhir.utility.adapter.dstu3; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -12,6 +14,7 @@ import org.hl7.fhir.dstu3.model.Library; import org.hl7.fhir.dstu3.model.QuestionnaireResponse; import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; +import org.hl7.fhir.dstu3.model.Reference; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; @@ -43,8 +46,29 @@ void test() { adapter.setSubject(id); assertTrue(adapter.hasSubject()); assertEquals(id, adapter.getSubject()); - adapter.setAuthored(new Date()); - adapter.setStatus("in-progress"); + var status = "in-progress"; + adapter.setStatus(status); + assertEquals(status, adapter.getStatus()); + assertFalse(adapter.hasAuthored()); + assertNull(adapter.getAuthored()); + var newDate = new Date(); + adapter.setAuthored(newDate); + assertTrue(adapter.hasAuthored()); + assertEquals(newDate, adapter.getAuthored()); + var basedOn = new Reference("basedOn"); + response.addBasedOn(basedOn); + assertEquals(basedOn, adapter.getBasedOn().get(0)); + assertEquals(0, adapter.getPartOf().size()); + assertFalse(adapter.hasEncounter()); + var encounter = new Reference("encounter"); + assertThrows(IllegalArgumentException.class, () -> adapter.setEncounter(encounter)); + assertFalse(adapter.hasEncounter()); + assertNull(adapter.getEncounter()); + assertFalse(adapter.hasAuthor()); + var author = new Reference("author"); + response.setAuthor(author); + assertTrue(adapter.hasAuthor()); + assertEquals(author, adapter.getAuthor()); } @Test @@ -66,6 +90,7 @@ void testItem() { var adapter = adapterFactory.createQuestionnaireResponse(questionnaireResponse); assertTrue(adapter.hasItem()); assertTrue(adapter.hasItem("1")); + assertFalse(adapter.hasItem("5")); assertEquals(2, adapter.getItem().size()); adapter.addItem(item3); assertEquals(3, adapter.getItem().size()); diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/CodeableConceptAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/CodeableConceptAdapterTest.java index f6b6af05b1..6cc3a3d11d 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/CodeableConceptAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/CodeableConceptAdapterTest.java @@ -35,5 +35,8 @@ void testCoding() { var adapter = adapterFactory.createCodeableConcept(codeableConcept); assertTrue(adapter.hasCoding("test")); assertEquals(1, adapter.getCoding().size()); + adapter.addCoding("test.com", "test2", "Test2"); + assertTrue(adapter.hasCoding("test2")); + assertEquals(2, adapter.getCoding().size()); } } diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/CodingAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/CodingAdapterTest.java index 09cf618ffa..5579c1aede 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/CodingAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/CodingAdapterTest.java @@ -1,11 +1,13 @@ package org.opencds.cqf.fhir.utility.adapter.r4; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import ca.uhn.fhir.context.FhirVersionEnum; +import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.junit.jupiter.api.Test; @@ -37,6 +39,7 @@ void testCode() { var adapter = adapterFactory.createCoding(coding); assertTrue(adapter.hasCode()); assertEquals(code, adapter.getCode()); + assertInstanceOf(CodeType.class, adapter.getCodeType()); var newCode = "test2"; adapter.setCode(newCode); assertEquals(newCode, adapter.getCode()); diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/ObservationAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/ObservationAdapterTest.java new file mode 100644 index 0000000000..91e951a9b5 --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/ObservationAdapterTest.java @@ -0,0 +1,88 @@ +package org.opencds.cqf.fhir.utility.adapter.r4; + +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Date; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.InstantType; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.Period; +import org.hl7.fhir.r4.model.Reference; +import org.junit.jupiter.api.Test; + +class ObservationAdapterTest { + private final org.opencds.cqf.fhir.utility.adapter.IAdapterFactory adapterFactory = new AdapterFactory(); + + @Test + void invalid_object_fails() { + var library = new Library(); + assertThrows(IllegalArgumentException.class, () -> new ObservationAdapter(library)); + } + + @Test + void adapter_get_and_set() { + var obs = new Observation(); + obs.setStatus(ObservationStatus.FINAL); + var adapter = (ObservationAdapter) adapterFactory.createResource(obs); + assertEquals("final", adapter.getStatus()); + adapter.setStatus("amended"); + assertEquals("amended", adapter.getStatus()); + assertNull(adapter.getCode()); + adapter.setCode(new CodeableConcept().addCoding(new Coding("test.com", "test", "test"))); + assertInstanceOf(CodeableConcept.class, adapter.get().getCode()); + assertEquals("test", adapter.getCode().getCodingFirstRep().getCode()); + assertEquals("test.com", adapter.getCode().getCodingFirstRep().getSystem()); + var basedOn = new Reference("basedOn"); + var partOf = new Reference("partOf"); + var derivedFrom = new Reference("derivedFrom"); + var category = new CodeableConcept().addCoding(new Coding("test.com", "test", "Test")); + var subject = new Reference("subject"); + var encounter = new Reference("encounter"); + var performer = new Reference("performer"); + var value = new BooleanType(true); + adapter.setBasedOn(singletonList(basedOn)) + .setPartOf(singletonList(partOf)) + .setDerivedFrom(singletonList(derivedFrom)) + .setCategory(singletonList(category)) + .setSubject(subject) + .setEncounter(encounter) + .setPerformer(singletonList(performer)) + .setValue(value); + assertEquals(basedOn, obs.getBasedOnFirstRep()); + assertEquals(partOf, obs.getPartOfFirstRep()); + assertEquals(derivedFrom, obs.getDerivedFromFirstRep()); + assertEquals(category, obs.getCategoryFirstRep()); + assertEquals(subject, obs.getSubject()); + assertEquals(encounter, obs.getEncounter()); + assertEquals(performer, obs.getPerformerFirstRep()); + assertEquals(value, obs.getValue()); + } + + @Test + void adapter_effective_issued() { + var obs = new Observation(); + var adapter = (ObservationAdapter) adapterFactory.createResource(obs); + var dateTimeString = "2026-01-01T12:12:12-01:00"; + adapter.setEffective(dateTimeString); + assertEquals(dateTimeString, obs.getEffectiveDateTimeType().asStringValue()); + var date = new Date(); + adapter.setEffective(new DateTimeType(date)); + assertEquals(date, obs.getEffectiveDateTimeType().getValue()); + var period = new Period(); + adapter.setEffectivePeriod(period); + assertEquals(period, obs.getEffectivePeriod()); + adapter.setIssued(dateTimeString); + assertEquals(dateTimeString, obs.getIssuedElement().asStringValue()); + adapter.setIssued(new InstantType(date)); + assertEquals(date, obs.getIssued()); + } +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/PlanDefinitionActionAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/PlanDefinitionActionAdapterTest.java index bfed866975..a9136bd4a6 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/PlanDefinitionActionAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/PlanDefinitionActionAdapterTest.java @@ -1,10 +1,14 @@ package org.opencds.cqf.fhir.utility.adapter.r4; +import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opencds.cqf.fhir.utility.Constants.CQF_APPLICABILITY_BEHAVIOR; +import static org.opencds.cqf.fhir.utility.Constants.R6_PLAN_DEFINITION_ACTION_APPLICABILITY_BEHAVIOR; import ca.uhn.fhir.context.FhirVersionEnum; import org.hl7.fhir.r4.model.CanonicalType; @@ -12,6 +16,7 @@ import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.DataRequirement; import org.hl7.fhir.r4.model.Expression; +import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.PlanDefinition; import org.hl7.fhir.r4.model.PlanDefinition.ActionConditionKind; import org.hl7.fhir.r4.model.PlanDefinition.ActionRelationshipType; @@ -22,10 +27,12 @@ import org.hl7.fhir.r4.model.PlanDefinition.RequestPriority; import org.hl7.fhir.r4.model.RelatedArtifact; import org.hl7.fhir.r4.model.RequestGroup.RequestGroupActionComponent; +import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Timing; import org.hl7.fhir.r4.model.TriggerDefinition; import org.hl7.fhir.r4.model.TriggerDefinition.TriggerType; import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.utility.Constants.CqfApplicabilityBehavior; import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; class PlanDefinitionActionAdapterTest { @@ -182,6 +189,27 @@ void testSelectionBehavior() { assertEquals(selectionBehavior, adapter.getSelectionBehavior()); } + @Test + void testApplicabilityBehavior() { + var action = new PlanDefinitionActionComponent(); + var adapter = new PlanDefinitionActionAdapter(action); + assertFalse(adapter.hasApplicabilityBehavior()); + assertEquals(CqfApplicabilityBehavior.ALL, adapter.getApplicabilityBehavior()); + adapter.setExtension(singletonList(new Extension(CQF_APPLICABILITY_BEHAVIOR, new StringType("any")))); + assertTrue(adapter.hasApplicabilityBehavior()); + assertEquals(CqfApplicabilityBehavior.ANY, adapter.getApplicabilityBehavior()); + adapter.setExtension(singletonList(new Extension(CQF_APPLICABILITY_BEHAVIOR, new StringType("one")))); + assertTrue(adapter.hasApplicabilityBehavior()); + assertThrows(IllegalArgumentException.class, adapter::getApplicabilityBehavior); + adapter.setExtension(singletonList(new Extension(CQF_APPLICABILITY_BEHAVIOR, null))); + assertTrue(adapter.hasApplicabilityBehavior()); + assertEquals(CqfApplicabilityBehavior.ALL, adapter.getApplicabilityBehavior()); + adapter.setExtension( + singletonList(new Extension(R6_PLAN_DEFINITION_ACTION_APPLICABILITY_BEHAVIOR, new StringType("any")))); + assertTrue(adapter.hasApplicabilityBehavior()); + assertEquals(CqfApplicabilityBehavior.ANY, adapter.getApplicabilityBehavior()); + } + @Test void testDefinition() { var definition = new CanonicalType("test"); diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/QuestionnaireItemComponentAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/QuestionnaireItemComponentAdapterTest.java index 985c361ccc..ade5dec1ea 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/QuestionnaireItemComponentAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/QuestionnaireItemComponentAdapterTest.java @@ -5,10 +5,16 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opencds.cqf.fhir.utility.Constants.CPG_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT; +import static org.opencds.cqf.fhir.utility.Constants.SDC_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT; +import static org.opencds.cqf.fhir.utility.Constants.SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT; import ca.uhn.fhir.context.FhirVersionEnum; import java.util.List; +import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Expression; +import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Library; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType; @@ -89,4 +95,28 @@ void testItem() { adapter.addItems(List.of(item2, item3, item4)); assertEquals(4, adapter.getItem().size()); } + + @Test + void testContextItemExtensions() { + var profile = new CanonicalType("test.com/fhir/StructureDefinition/Test"); + var expression = new Expression().setExpression("TestExpression"); + var item1 = adapterFactory.createQuestionnaireItem( + new QuestionnaireItemComponent().setLinkId("1").setType(QuestionnaireItemType.GROUP)); + item1.addExtension(new Extension(SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT, expression)); + assertTrue(item1.isContextItem()); + var item2 = adapterFactory.createQuestionnaireItem( + new QuestionnaireItemComponent().setLinkId("2").setType(QuestionnaireItemType.BOOLEAN)); + var sdcDefPopExt = new Extension(SDC_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT); + sdcDefPopExt.addExtension(new Extension("definition", profile)); + sdcDefPopExt.addExtension(new Extension("expression", expression)); + item2.addExtension(sdcDefPopExt); + assertTrue(item2.isContextItem()); + var item3 = adapterFactory.createQuestionnaireItem( + new QuestionnaireItemComponent().setLinkId("3").setType(QuestionnaireItemType.BOOLEAN)); + var cpgDefPopExt = new Extension(CPG_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT); + cpgDefPopExt.addExtension(new Extension("definition", profile)); + cpgDefPopExt.addExtension(new Extension("expression", expression)); + item3.addExtension(cpgDefPopExt); + assertTrue(item3.isContextItem()); + } } diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/QuestionnaireResponseAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/QuestionnaireResponseAdapterTest.java index 8ed44a1d5e..45e6dd4eed 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/QuestionnaireResponseAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/QuestionnaireResponseAdapterTest.java @@ -1,17 +1,20 @@ package org.opencds.cqf.fhir.utility.adapter.r4; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import ca.uhn.fhir.context.FhirVersionEnum; import java.util.Date; import java.util.List; -import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Library; import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; +import org.hl7.fhir.r4.model.Reference; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; @@ -43,8 +46,31 @@ void test() { adapter.setSubject(id); assertTrue(adapter.hasSubject()); assertEquals(id.getValue(), adapter.getSubject().getValue()); - adapter.setAuthored(new Date()); - adapter.setStatus("in-progress"); + var status = "in-progress"; + adapter.setStatus(status); + assertEquals(status, adapter.getStatus()); + assertFalse(adapter.hasAuthored()); + assertNull(adapter.getAuthored()); + var newDate = new Date(); + adapter.setAuthored(newDate); + assertTrue(adapter.hasAuthored()); + assertEquals(newDate, adapter.getAuthored()); + var basedOn = new Reference("basedOn"); + response.addBasedOn(basedOn); + assertEquals(basedOn, adapter.getBasedOn().get(0)); + var partOf = new Reference("partOf"); + response.addPartOf(partOf); + assertEquals(partOf, adapter.getPartOf().get(0)); + assertFalse(adapter.hasEncounter()); + var encounter = new Reference("encounter"); + adapter.setEncounter(encounter); + assertTrue(adapter.hasEncounter()); + assertEquals(encounter, adapter.getEncounter()); + assertFalse(adapter.hasAuthor()); + var author = new Reference("author"); + response.setAuthor(author); + assertTrue(adapter.hasAuthor()); + assertEquals(author, adapter.getAuthor()); } @Test @@ -66,6 +92,7 @@ void testItem() { var adapter = adapterFactory.createQuestionnaireResponse(questionnaireResponse); assertTrue(adapter.hasItem()); assertTrue(adapter.hasItem("1")); + assertFalse(adapter.hasItem("5")); assertEquals(2, adapter.getItem().size()); adapter.addItem(item3); assertEquals(3, adapter.getItem().size()); diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/CodeableConceptAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/CodeableConceptAdapterTest.java index 18387448dc..1f2c5ed0ef 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/CodeableConceptAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/CodeableConceptAdapterTest.java @@ -35,5 +35,8 @@ void testCoding() { var adapter = adapterFactory.createCodeableConcept(codeableConcept); assertTrue(adapter.hasCoding("test")); assertEquals(1, adapter.getCoding().size()); + adapter.addCoding("test.com", "test2", "Test2"); + assertTrue(adapter.hasCoding("test2")); + assertEquals(2, adapter.getCoding().size()); } } diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/CodingAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/CodingAdapterTest.java index 82af78b927..05c7664e79 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/CodingAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/CodingAdapterTest.java @@ -1,11 +1,13 @@ package org.opencds.cqf.fhir.utility.adapter.r5; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import ca.uhn.fhir.context.FhirVersionEnum; +import org.hl7.fhir.r5.model.CodeType; import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; import org.junit.jupiter.api.Test; @@ -37,6 +39,7 @@ void testCode() { var adapter = adapterFactory.createCoding(coding); assertTrue(adapter.hasCode()); assertEquals(code, adapter.getCode()); + assertInstanceOf(CodeType.class, adapter.getCodeType()); var newCode = "test2"; adapter.setCode(newCode); assertEquals(newCode, adapter.getCode()); diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/ObservationAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/ObservationAdapterTest.java new file mode 100644 index 0000000000..f86d79e4ea --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/ObservationAdapterTest.java @@ -0,0 +1,88 @@ +package org.opencds.cqf.fhir.utility.adapter.r5; + +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Date; +import org.hl7.fhir.r5.model.BooleanType; +import org.hl7.fhir.r5.model.CodeableConcept; +import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.DateTimeType; +import org.hl7.fhir.r5.model.Enumerations.ObservationStatus; +import org.hl7.fhir.r5.model.InstantType; +import org.hl7.fhir.r5.model.Library; +import org.hl7.fhir.r5.model.Observation; +import org.hl7.fhir.r5.model.Period; +import org.hl7.fhir.r5.model.Reference; +import org.junit.jupiter.api.Test; + +class ObservationAdapterTest { + private final org.opencds.cqf.fhir.utility.adapter.IAdapterFactory adapterFactory = new AdapterFactory(); + + @Test + void invalid_object_fails() { + var library = new Library(); + assertThrows(IllegalArgumentException.class, () -> new ObservationAdapter(library)); + } + + @Test + void adapter_get_and_set() { + var obs = new Observation(); + obs.setStatus(ObservationStatus.FINAL); + var adapter = (ObservationAdapter) adapterFactory.createResource(obs); + assertEquals("final", adapter.getStatus()); + adapter.setStatus("amended"); + assertEquals("amended", adapter.getStatus()); + assertNull(adapter.getCode()); + adapter.setCode(new CodeableConcept().addCoding(new Coding("test.com", "test", "test"))); + assertInstanceOf(CodeableConcept.class, adapter.get().getCode()); + assertEquals("test", adapter.getCode().getCodingFirstRep().getCode()); + assertEquals("test.com", adapter.getCode().getCodingFirstRep().getSystem()); + var basedOn = new Reference("basedOn"); + var partOf = new Reference("partOf"); + var derivedFrom = new Reference("derivedFrom"); + var category = new CodeableConcept().addCoding(new Coding("test.com", "test", "Test")); + var subject = new Reference("subject"); + var encounter = new Reference("encounter"); + var performer = new Reference("performer"); + var value = new BooleanType(true); + adapter.setBasedOn(singletonList(basedOn)) + .setPartOf(singletonList(partOf)) + .setDerivedFrom(singletonList(derivedFrom)) + .setCategory(singletonList(category)) + .setSubject(subject) + .setEncounter(encounter) + .setPerformer(singletonList(performer)) + .setValue(value); + assertEquals(basedOn, obs.getBasedOnFirstRep()); + assertEquals(partOf, obs.getPartOfFirstRep()); + assertEquals(derivedFrom, obs.getDerivedFromFirstRep()); + assertEquals(category, obs.getCategoryFirstRep()); + assertEquals(subject, obs.getSubject()); + assertEquals(encounter, obs.getEncounter()); + assertEquals(performer, obs.getPerformerFirstRep()); + assertEquals(value, obs.getValue()); + } + + @Test + void adapter_effective_issued() { + var obs = new Observation(); + var adapter = (ObservationAdapter) adapterFactory.createResource(obs); + var dateTimeString = "2026-01-01T12:12:12-01:00"; + adapter.setEffective(dateTimeString); + assertEquals(dateTimeString, obs.getEffectiveDateTimeType().asStringValue()); + var date = new Date(); + adapter.setEffective(new DateTimeType(date)); + assertEquals(date, obs.getEffectiveDateTimeType().getValue()); + var period = new Period(); + adapter.setEffectivePeriod(period); + assertEquals(period, obs.getEffectivePeriod()); + adapter.setIssued(dateTimeString); + assertEquals(dateTimeString, obs.getIssuedElement().asStringValue()); + adapter.setIssued(new InstantType(date)); + assertEquals(date, obs.getIssued()); + } +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/PlanDefinitionActionAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/PlanDefinitionActionAdapterTest.java index 50fd63d438..4d9bd7eb2b 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/PlanDefinitionActionAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/PlanDefinitionActionAdapterTest.java @@ -1,6 +1,7 @@ package org.opencds.cqf.fhir.utility.adapter.r5; import static org.junit.jupiter.api.Assertions.*; +import static org.opencds.cqf.fhir.utility.Constants.CQF_APPLICABILITY_BEHAVIOR; import ca.uhn.fhir.context.FhirVersionEnum; import org.hl7.fhir.r5.model.CanonicalType; @@ -12,16 +13,19 @@ import org.hl7.fhir.r5.model.Enumerations.ActionSelectionBehavior; import org.hl7.fhir.r5.model.Enumerations.RequestPriority; import org.hl7.fhir.r5.model.Expression; +import org.hl7.fhir.r5.model.Extension; import org.hl7.fhir.r5.model.PlanDefinition.PlanDefinitionActionComponent; import org.hl7.fhir.r5.model.PlanDefinition.PlanDefinitionActionConditionComponent; import org.hl7.fhir.r5.model.PlanDefinition.PlanDefinitionActionInputComponent; import org.hl7.fhir.r5.model.PlanDefinition.PlanDefinitionActionRelatedActionComponent; import org.hl7.fhir.r5.model.RelatedArtifact; import org.hl7.fhir.r5.model.RequestOrchestration.RequestOrchestrationActionComponent; +import org.hl7.fhir.r5.model.StringType; import org.hl7.fhir.r5.model.Timing; import org.hl7.fhir.r5.model.TriggerDefinition; import org.hl7.fhir.r5.model.TriggerDefinition.TriggerType; import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.utility.Constants.CqfApplicabilityBehavior; import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; class PlanDefinitionActionAdapterTest { @@ -176,6 +180,17 @@ void testSelectionBehavior() { assertEquals(selectionBehavior, adapter.getSelectionBehavior()); } + @Test + void testApplicabilityBehavior() { + var action = new PlanDefinitionActionComponent(); + var adapter = new PlanDefinitionActionAdapter(action); + assertFalse(adapter.hasApplicabilityBehavior()); + assertEquals(CqfApplicabilityBehavior.ALL, adapter.getApplicabilityBehavior()); + adapter.addExtension(new Extension(CQF_APPLICABILITY_BEHAVIOR, new StringType("one"))); + assertTrue(adapter.hasApplicabilityBehavior()); + assertThrows(IllegalArgumentException.class, adapter::getApplicabilityBehavior); + } + @Test void testDefinition() { var definition = new CanonicalType("test"); diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/QuestionnaireItemComponentAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/QuestionnaireItemComponentAdapterTest.java index c38cc77439..c2c3000cdf 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/QuestionnaireItemComponentAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/QuestionnaireItemComponentAdapterTest.java @@ -5,10 +5,16 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opencds.cqf.fhir.utility.Constants.CPG_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT; +import static org.opencds.cqf.fhir.utility.Constants.SDC_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT; +import static org.opencds.cqf.fhir.utility.Constants.SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT; import ca.uhn.fhir.context.FhirVersionEnum; import java.util.List; +import org.hl7.fhir.r5.model.CanonicalType; import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.Expression; +import org.hl7.fhir.r5.model.Extension; import org.hl7.fhir.r5.model.Library; import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemType; @@ -89,4 +95,28 @@ void testItem() { adapter.addItems(List.of(item2, item3, item4)); assertEquals(4, adapter.getItem().size()); } + + @Test + void testContextItemExtensions() { + var profile = new CanonicalType("test.com/fhir/StructureDefinition/Test"); + var expression = new Expression().setExpression("TestExpression"); + var item1 = adapterFactory.createQuestionnaireItem( + new QuestionnaireItemComponent("1", QuestionnaireItemType.GROUP)); + item1.addExtension(new Extension(SDC_QUESTIONNAIRE_ITEM_POPULATION_CONTEXT, expression)); + assertTrue(item1.isContextItem()); + var item2 = adapterFactory.createQuestionnaireItem( + new QuestionnaireItemComponent("2", QuestionnaireItemType.BOOLEAN)); + var sdcDefPopExt = new Extension(SDC_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT); + sdcDefPopExt.addExtension(new Extension("definition", profile)); + sdcDefPopExt.addExtension(new Extension("expression", expression)); + item2.addExtension(sdcDefPopExt); + assertTrue(item2.isContextItem()); + var item3 = adapterFactory.createQuestionnaireItem( + new QuestionnaireItemComponent("3", QuestionnaireItemType.BOOLEAN)); + var cpgDefPopExt = new Extension(CPG_QUESTIONNAIRE_DEFINITION_POPULATION_CONTEXT); + cpgDefPopExt.addExtension(new Extension("definition", profile)); + cpgDefPopExt.addExtension(new Extension("expression", expression)); + item3.addExtension(cpgDefPopExt); + assertTrue(item3.isContextItem()); + } } diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/QuestionnaireResponseAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/QuestionnaireResponseAdapterTest.java index d02e46d34e..4ed119a437 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/QuestionnaireResponseAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/QuestionnaireResponseAdapterTest.java @@ -1,17 +1,20 @@ package org.opencds.cqf.fhir.utility.adapter.r5; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import ca.uhn.fhir.context.FhirVersionEnum; import java.util.Date; import java.util.List; -import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.r5.model.IdType; import org.hl7.fhir.r5.model.Library; import org.hl7.fhir.r5.model.QuestionnaireResponse; import org.hl7.fhir.r5.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; +import org.hl7.fhir.r5.model.Reference; import org.junit.jupiter.api.Test; import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; @@ -43,8 +46,31 @@ void test() { adapter.setSubject(id); assertTrue(adapter.hasSubject()); assertEquals(id.getValue(), adapter.getSubject().getValue()); - adapter.setAuthored(new Date()); - adapter.setStatus("in-progress"); + var status = "in-progress"; + adapter.setStatus(status); + assertEquals(status, adapter.getStatus()); + assertFalse(adapter.hasAuthored()); + assertNull(adapter.getAuthored()); + var newDate = new Date(); + adapter.setAuthored(newDate); + assertTrue(adapter.hasAuthored()); + assertEquals(newDate, adapter.getAuthored()); + var basedOn = new Reference("basedOn"); + response.addBasedOn(basedOn); + assertEquals(basedOn, adapter.getBasedOn().get(0)); + var partOf = new Reference("partOf"); + response.addPartOf(partOf); + assertEquals(partOf, adapter.getPartOf().get(0)); + assertFalse(adapter.hasEncounter()); + var encounter = new Reference("encounter"); + adapter.setEncounter(encounter); + assertTrue(adapter.hasEncounter()); + assertEquals(encounter, adapter.getEncounter()); + assertFalse(adapter.hasAuthor()); + var author = new Reference("author"); + response.setAuthor(author); + assertTrue(adapter.hasAuthor()); + assertEquals(author, adapter.getAuthor()); } @Test @@ -62,6 +88,7 @@ void testItem() { var adapter = adapterFactory.createQuestionnaireResponse(questionnaireResponse); assertTrue(adapter.hasItem()); assertTrue(adapter.hasItem("1")); + assertFalse(adapter.hasItem("5")); assertEquals(2, adapter.getItem().size()); adapter.addItem(item3); assertEquals(3, adapter.getItem().size()); diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepositoryTransactionTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepositoryTransactionTest.java index 650e4ffb7a..a468d58722 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepositoryTransactionTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepositoryTransactionTest.java @@ -28,6 +28,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.opencds.cqf.fhir.test.Resources; import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; import org.opencds.cqf.fhir.utility.search.Searches.SearchBuilder; class IgRepositoryTransactionTest { @@ -76,6 +77,26 @@ Resource createWithoutId(String urlSuffix) { }; } + Resource createWithVersion(String id, String urlSuffix, String version) { + return switch (label) { + case "Library" -> { + var lib = new Library(); + lib.setId(id); + lib.setUrl(urlPrefix + urlSuffix); + lib.setVersion(version); + yield lib; + } + case "SearchParameter" -> { + var sp = newSearchParameter(); + sp.setId(id); + sp.setUrl(urlPrefix + urlSuffix); + sp.setVersion(version); + yield sp; + } + default -> throw new IllegalArgumentException("Unknown fixture: " + label); + }; + } + @Override @Nonnull public String toString() { @@ -332,6 +353,28 @@ void transactionMixed_postThenPutThenDelete(ResourceFixture fixture) { assertNotNull(readRes3); } + @ParameterizedTest(name = "{0}") + @MethodSource("resourceFixtures") + void transactionGet_searchRetrievesResource(ResourceFixture fixture) { + var suffix = fixture.label; + var version = "1.0.0"; + var resource = fixture.createWithVersion("tx-search-" + suffix, "tx-search-" + suffix, version); + repository.create(resource, HEADERS_EMPTY); + + var adapter = IAdapterFactory.createAdapterForResource(resource); + var txBundle = new Bundle().setType(Bundle.BundleType.TRANSACTION); + txBundle.addEntry() + .getRequest() + .setMethod(Bundle.HTTPVerb.GET) + .setUrl(String.format("%s?url=%s&version=%s", suffix, adapter.resolvePathString("url"), version)); + + var result = repository.transaction(txBundle, HEADERS_EMPTY); + assertEquals(1, result.getEntry().size()); + var bundle1 = (Bundle) result.getEntryFirstRep().getResource(); + assertEquals(1, bundle1.getEntry().size()); + assertEquals(resource, bundle1.getEntryFirstRep().getResource()); + } + @Test void transactionUnsupportedMethod_throws() { var txBundle = new Bundle().setType(Bundle.BundleType.TRANSACTION);