From 004c1d8a0892ef7db6d06e282e88862907c9d230 Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Mon, 18 May 2026 23:11:39 +0200 Subject: [PATCH 1/3] Resolves definitionCanonical by fetching the resource first and routing by fhirType() instead of parsing the URL for the resource type name --- .../apply/ProcessDefinition.java | 116 +++++++++++++++--- 1 file changed, 100 insertions(+), 16 deletions(-) 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 218a5b3208..943bec332b 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 @@ -6,11 +6,14 @@ import ca.uhn.fhir.repository.IRepository; import java.util.Collections; +import java.util.List; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r5.model.Enumerations.FHIRTypes; +import org.opencds.cqf.fhir.utility.Canonicals; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.adapter.IPlanDefinitionActionAdapter; import org.opencds.cqf.fhir.utility.adapter.IRequestActionAdapter; @@ -19,7 +22,10 @@ @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"); final IRepository repository; final ApplyProcessor applyProcessor; @@ -74,15 +80,85 @@ protected IBaseResource resolveDefinition(ApplyRequest request, IPrimitiveType applyNestedPlanDefinition(request, definition); - case ACTIVITYDEFINITION -> applyActivityDefinition(request, definition); + var referenceToContained = definition.getValue().startsWith("#"); + var resource = referenceToContained + ? resolveContained(request, definition.getValue()) + : resolveCanonicalByType(definition); + if (resource == null) { + return null; + } + return switch (FHIRTypes.fromCode(resource.fhirType())) { + case PLANDEFINITION -> applyNestedPlanDefinition(request, resource); + case ACTIVITYDEFINITION -> applyActivityDefinition(request, resource); case QUESTIONNAIRE -> applyQuestionnaireDefinition(request, definition); - default -> throw new FHIRException("Unknown action definition: %s".formatted(definition.getValue())); + default -> resource; }; } + // Class shadow: Added method to resolve canonical reference by searching across all supported definition resource + // types using transaction Bundle. + /** + * Resolves a canonical reference by searching across all supported definition resource + * types using a batch Bundle. This avoids the upstream URL-based type detection which + * is case-sensitive and defaults to CodeSystem for unrecognized URL patterns (HAAS-1930). + * + *

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}.
  • + *
+ */ + private IBaseResource resolveCanonicalByType(IPrimitiveType definition) { + var canonical = definition.getValue(); + var url = Canonicals.getUrl(canonical); + var version = Canonicals.getVersion(canonical); + var hasVersion = version != null && !version.isEmpty(); + + var transaction = new org.hl7.fhir.r4.model.Bundle(); + transaction.setType(Bundle.BundleType.TRANSACTION); + + for (var type : SUPPORTED_DEFINITION_TYPES) { + var searchUrl = hasVersion + ? "%s?url=%s&version=%s".formatted(type, url, version) + : "%s?url=%s".formatted(type, url); + transaction + .addEntry() + .getRequest() + .setMethod(org.hl7.fhir.r4.model.Bundle.HTTPVerb.GET) + .setUrl(searchUrl); + } + + var response = repository.transaction(transaction); + var matches = collectMatchesFromResponse(response); + + if (matches.isEmpty()) { + return null; + } + if (matches.size() == 1) { + return matches.get(0); + } + var errorHint = hasVersion + ? "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)); + } + + private List collectMatchesFromResponse(Bundle response) { + var matches = new java.util.ArrayList(); + for (var entry : response.getEntry()) { + if (entry.getResource() instanceof org.hl7.fhir.r4.model.Bundle resultBundle) { + for (var resultEntry : resultBundle.getEntry()) { + if (resultEntry.getResource() != null) { + matches.add(resultEntry.getResource()); + } + } + } + } + return matches; + } + protected Boolean isDefinitionCanonical(ApplyRequest request, IBase definition) { requireNonNull(request); return switch (request.getFhirVersion()) { @@ -122,14 +198,18 @@ protected IBaseResource applyQuestionnaireDefinition(ApplyRequest request, IPrim protected IBaseResource applyActivityDefinition(ApplyRequest request, IPrimitiveType definition) { requireNonNull(definition); + var referenceToContained = definition.getValue().startsWith("#"); + var activityDefinition = (referenceToContained + ? resolveContained(request, definition.getValue()) + : resolveRepository(definition)); + return applyActivityDefinition(request, activityDefinition); + } + + private 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 @@ -147,7 +227,7 @@ protected IBaseResource applyActivityDefinition(ApplyRequest request, IPrimitive activityRequest.resolveOperationOutcome(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); } @@ -156,12 +236,16 @@ protected IBaseResource applyActivityDefinition(ApplyRequest request, IPrimitive protected IBaseResource applyNestedPlanDefinition(ApplyRequest request, IPrimitiveType definition) { requireNonNull(definition); + var referenceToContained = definition.getValue().startsWith("#"); + var nextPlanDefinition = (referenceToContained + ? resolveContained(request, definition.getValue()) + : resolveRepository(definition)); + return applyNestedPlanDefinition(request, nextPlanDefinition); + } + + private 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(result); request.getRequestResources().addAll(nestedRequest.getRequestResources()); @@ -170,7 +254,7 @@ 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; From c474511c35f48aa7b639cef717f40f15e632980b Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Tue, 19 May 2026 00:55:37 +0200 Subject: [PATCH 2/3] reviewing ... --- .../apply/ProcessDefinition.java | 91 ++------ .../apply/ProcessDefinitionTests.java | 203 +++++++++++++++--- 2 files changed, 197 insertions(+), 97 deletions(-) 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 943bec332b..6baf52e957 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 @@ -2,18 +2,18 @@ import static java.util.Objects.requireNonNull; import static org.opencds.cqf.fhir.cr.common.ExtensionBuilders.buildReference; +import static org.opencds.cqf.fhir.utility.BundleHelper.*; +import static org.opencds.cqf.fhir.utility.Canonicals.*; import static org.opencds.cqf.fhir.utility.SearchHelper.searchRepositoryByCanonical; import ca.uhn.fhir.repository.IRepository; +import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.hl7.fhir.exceptions.FHIRException; 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.r4.model.Bundle; -import org.hl7.fhir.r5.model.Enumerations.FHIRTypes; -import org.opencds.cqf.fhir.utility.Canonicals; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.adapter.IPlanDefinitionActionAdapter; import org.opencds.cqf.fhir.utility.adapter.IRequestActionAdapter; @@ -83,24 +83,20 @@ protected IBaseResource resolveDefinition(ApplyRequest request, IPrimitiveType applyNestedPlanDefinition(request, resource); - case ACTIVITYDEFINITION -> applyActivityDefinition(request, resource); - case QUESTIONNAIRE -> applyQuestionnaireDefinition(request, definition); + return switch (resource.fhirType()) { + case "PlanDefinition" -> applyNestedPlanDefinition(request, resource); + case "ActivityDefinition" -> applyActivityDefinition(request, resource); default -> resource; }; } - // Class shadow: Added method to resolve canonical reference by searching across all supported definition resource - // types using transaction Bundle. /** - * Resolves a canonical reference by searching across all supported definition resource - * types using a batch Bundle. This avoids the upstream URL-based type detection which - * is case-sensitive and defaults to CodeSystem for unrecognized URL patterns (HAAS-1930). + * Resolves a canonical reference by issuing a single transaction Bundle that searches every + * supported definition resource type in parallel, instead of inferring the type from the URL. * *

Resolution rules: *

    @@ -109,24 +105,21 @@ protected IBaseResource resolveDefinition(ApplyRequest request, IPrimitiveTypeIf multiple resources match — throw an {@link IllegalStateException}. *
*/ - private IBaseResource resolveCanonicalByType(IPrimitiveType definition) { + private IBaseResource resolveCanonicalByType(ApplyRequest request, IPrimitiveType definition) { var canonical = definition.getValue(); - var url = Canonicals.getUrl(canonical); - var version = Canonicals.getVersion(canonical); + var url = getUrl(canonical); + var version = getVersion(canonical); var hasVersion = version != null && !version.isEmpty(); + var fhirVersion = request.getFhirVersion(); - var transaction = new org.hl7.fhir.r4.model.Bundle(); - transaction.setType(Bundle.BundleType.TRANSACTION); - + var transaction = newBundle(fhirVersion, "transaction"); for (var type : SUPPORTED_DEFINITION_TYPES) { var searchUrl = hasVersion ? "%s?url=%s&version=%s".formatted(type, url, version) : "%s?url=%s".formatted(type, url); - transaction - .addEntry() - .getRequest() - .setMethod(org.hl7.fhir.r4.model.Bundle.HTTPVerb.GET) - .setUrl(searchUrl); + var requestEntry = newRequest(fhirVersion, "GET", searchUrl); + var entry = setEntryRequest(fhirVersion, newEntry(fhirVersion), requestEntry); + addEntry(transaction, entry); } var response = repository.transaction(transaction); @@ -145,15 +138,11 @@ private IBaseResource resolveCanonicalByType(IPrimitiveType definition) "Multiple resources (%d) found for canonical '%s'. %s".formatted(matches.size(), canonical, errorHint)); } - private List collectMatchesFromResponse(Bundle response) { - var matches = new java.util.ArrayList(); - for (var entry : response.getEntry()) { - if (entry.getResource() instanceof org.hl7.fhir.r4.model.Bundle resultBundle) { - for (var resultEntry : resultBundle.getEntry()) { - if (resultEntry.getResource() != null) { - matches.add(resultEntry.getResource()); - } - } + private List collectMatchesFromResponse(IBaseBundle response) { + var matches = new ArrayList(); + for (var resource : getEntryResources(response)) { + if (resource instanceof IBaseBundle resultBundle) { + matches.addAll(getEntryResources(resultBundle)); } } return matches; @@ -177,25 +166,6 @@ 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 applyActivityDefinition(ApplyRequest request, IPrimitiveType definition) { requireNonNull(definition); var referenceToContained = definition.getValue().startsWith("#"); @@ -265,21 +235,6 @@ 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.resolvePathList(request.getPlanDefinition(), "contained", IBaseResource.class); 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 52d4257c9c..c376273941 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,35 @@ 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.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 +44,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; @@ -60,7 +67,9 @@ void setup() { void applyActivityDefinitionShouldReturnNullOnException() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine, null); var definition = new CanonicalType(ACTIVITYDEFINITION); - doReturn(fhirContextR4).when(repository).fhirContext(); + var activityDef = new ActivityDefinition().setUrl(ACTIVITYDEFINITION); + activityDef.setId("test"); + doReturn(activityDef).when(fixture).resolveRepository(definition); var result = fixture.applyActivityDefinition(request, definition); assertNull(result); var oc = (OperationOutcome) request.getOperationOutcome(); @@ -71,7 +80,10 @@ void applyActivityDefinitionShouldReturnNullOnException() { void applyNestedPlanDefinitionShouldReturnNullOnException() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine, null); var definition = new CanonicalType(PLANDEFINITION); - doReturn(fhirContextR4).when(repository).fhirContext(); + var nestedPlanDef = new PlanDefinition().setUrl(PLANDEFINITION); + nestedPlanDef.setId("nested"); + doReturn(nestedPlanDef).when(fixture).resolveRepository(definition); + doThrow(new RuntimeException("boom")).when(applyProcessor).applyPlanDefinition(any()); var result = fixture.applyNestedPlanDefinition(request, definition); assertNull(result); var oc = (OperationOutcome) request.getOperationOutcome(); @@ -83,7 +95,10 @@ void resolveDefinitionShouldReturnQuestionnaire() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine, null); var definition = new CanonicalType(QUESTIONNAIRE); var expectedQuestionnaire = new Questionnaire().setUrl(QUESTIONNAIRE); - doReturn(expectedQuestionnaire).when(fixture).resolveRepository(definition); + expectedQuestionnaire.setId("q1"); + doReturn(buildTransactionResponse(List.of(expectedQuestionnaire), List.of(), List.of())) + .when(repository) + .transaction(any(Bundle.class)); var result = fixture.resolveDefinition(request, definition); assertEquals(expectedQuestionnaire, result); var oc = request.getOperationOutcome(); @@ -91,43 +106,173 @@ void resolveDefinitionShouldReturnQuestionnaire() { } @Test - void applyQuestionnaireDefinitionShouldReturnContainedQuestionnaire() { + void resolveDefinitionReturnsTaskResourceForUnknownFhirType() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine, null); - 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); - assertEquals(expectedQuestionnaire, result); - var oc = request.getOperationOutcome(); - assertNull(oc); + var definition = new CanonicalType(TASK); + var task = new Task(); + task.setId("t1"); + doReturn(buildTransactionResponse(List.of(), List.of(), List.of(task))) + .when(repository) + .transaction(any(Bundle.class)); + var result = fixture.resolveDefinition(request, definition); + assertEquals(task, result); } @Test - void applyQuestionnaireDefinitionShouldReturnNullOnException() { + void resolveDefinitionReturnsNullWhenCanonicalNotFound() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine, null); - 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())) + .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, null); - 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))) + .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 resolveDefinitionAmbiguityErrorMentionsVersionWhenVersionSpecified() { + var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine, null); + 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))) + .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 resolveResourceNameShouldFailIfCanonicalHasNoValue() { + void resolveDefinitionSendsTransactionWithSearchEntriesForAllSupportedTypes() { var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine, null); - final CanonicalType canonical = new CanonicalType(); - assertThrows(FHIRException.class, () -> fixture.resolveResourceName(request, canonical)); + 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())) + .when(repository) + .transaction(captor.capture()); + + fixture.resolveDefinition(request, definition); + + var sent = captor.getValue(); + assertEquals(Bundle.BundleType.TRANSACTION, sent.getType()); + assertEquals(3, 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")); + sent.getEntry() + .forEach(e -> assertEquals(Bundle.HTTPVerb.GET, e.getRequest().getMethod())); + } + + @Test + void resolveDefinitionIncludesVersionInSearchUrlsWhenProvided() { + var request = newPDApplyRequestForVersion(FhirVersionEnum.R4, libraryEngine, null); + 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())) + .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, null); + 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())) + .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, null); + 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))) + .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, null); + 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))) + .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) { + var response = new Bundle(); + response.addEntry().setResource(toSearchBundle(questionnaireMatches)); + response.addEntry().setResource(toSearchBundle(activityDefinitionMatches)); + response.addEntry().setResource(toSearchBundle(planDefinitionMatches)); + 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 From 7d83e5eb795fae695413be510d721f31ec30fb6f Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Tue, 19 May 2026 01:13:53 +0200 Subject: [PATCH 3/3] fixed last test --- .../cqf/fhir/utility/BundleHelper.java | 30 +++++++++++++++ .../utility/repository/ig/IgRepository.java | 38 ++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) 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/repository/ig/IgRepository.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepository.java index f39b3f929f..2530e7cb1d 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 @@ -745,8 +746,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 +765,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(); + } }