Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +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 org.hl7.fhir.exceptions.FHIRException;
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;
Expand All @@ -19,7 +22,10 @@

@SuppressWarnings("UnstableApiUsage")
public class ProcessDefinition {

private static final Logger logger = LoggerFactory.getLogger(ProcessDefinition.class);
private static final List<String> SUPPORTED_DEFINITION_TYPES =
List.of("Questionnaire", "ActivityDefinition", "PlanDefinition");

final IRepository repository;
final ApplyProcessor applyProcessor;
Expand Down Expand Up @@ -74,15 +80,74 @@ protected IBaseResource resolveDefinition(ApplyRequest request, IPrimitiveType<S
requireNonNull(definition);
logger.debug("Resolving definition {}", definition.getValue());

var resourceName = resolveResourceName(request, definition);
return switch (FHIRTypes.fromCode(requireNonNull(resourceName))) {
case PLANDEFINITION -> 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) {
return null;
}
return switch (resource.fhirType()) {
case "PlanDefinition" -> applyNestedPlanDefinition(request, resource);
case "ActivityDefinition" -> applyActivityDefinition(request, resource);
default -> resource;
};
}

/**
* 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.
*
* <p>Resolution rules:
* <ul>
* <li>If exactly one resource matches (with or without version) — return it.</li>
* <li>If no resources match — return null.</li>
* <li>If multiple resources match — throw an {@link IllegalStateException}.</li>
* </ul>
*/
private IBaseResource resolveCanonicalByType(ApplyRequest request, IPrimitiveType<String> definition) {
var canonical = definition.getValue();
var url = getUrl(canonical);
var version = getVersion(canonical);
var hasVersion = version != null && !version.isEmpty();
var fhirVersion = request.getFhirVersion();

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);
var requestEntry = newRequest(fhirVersion, "GET", searchUrl);
var entry = setEntryRequest(fhirVersion, newEntry(fhirVersion), requestEntry);
addEntry(transaction, entry);
}

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<IBaseResource> collectMatchesFromResponse(IBaseBundle response) {
var matches = new ArrayList<IBaseResource>();
for (var resource : getEntryResources(response)) {
if (resource instanceof IBaseBundle resultBundle) {
matches.addAll(getEntryResources(resultBundle));
}
}
return matches;
}

protected Boolean isDefinitionCanonical(ApplyRequest request, IBase definition) {
requireNonNull(request);
return switch (request.getFhirVersion()) {
Expand All @@ -101,35 +166,20 @@ protected Boolean isDefinitionUri(ApplyRequest request, IBase definition) {
};
}

protected IBaseResource applyQuestionnaireDefinition(ApplyRequest request, IPrimitiveType<String> definition) {
protected IBaseResource applyActivityDefinition(ApplyRequest request, IPrimitiveType<String> 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;
var referenceToContained = definition.getValue().startsWith("#");
var activityDefinition = (referenceToContained
? resolveContained(request, definition.getValue())
: resolveRepository(definition));
return applyActivityDefinition(request, activityDefinition);
}

protected IBaseResource applyActivityDefinition(ApplyRequest request, IPrimitiveType<String> definition) {
requireNonNull(definition);
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
Expand All @@ -147,7 +197,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);
}
Expand All @@ -156,12 +206,16 @@ protected IBaseResource applyActivityDefinition(ApplyRequest request, IPrimitive

protected IBaseResource applyNestedPlanDefinition(ApplyRequest request, IPrimitiveType<String> 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());
Expand All @@ -170,7 +224,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;
Expand All @@ -181,21 +235,6 @@ protected IBaseResource resolveRepository(IPrimitiveType<String> definition) {
return searchRepositoryByCanonical(repository, definition);
}

protected String resolveResourceName(ApplyRequest request, IPrimitiveType<String> 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);
Expand Down
Loading
Loading