Skip to content

Commit e4db770

Browse files
authored
Improved support for adaptive Questionnaire in the $apply operation (#950)
* Improve handling of adaptive questionnaires in $apply * comment * review comments * Fix issue with null being passed in Either * cleanup * fix npe * cleanup unused references to spring security core
1 parent b87ddde commit e4db770

16 files changed

Lines changed: 295 additions & 91 deletions

File tree

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/common/InputParameterResolver.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
* This class provides functionality to resolve parameters passed into an operation as CQL Resource parameters
3535
* for evaluation. e.g. "%subject"
3636
*/
37+
@SuppressWarnings("UnstableApiUsage")
3738
public class InputParameterResolver implements IInputParameterResolver {
3839
private static final Logger logger = LoggerFactory.getLogger(InputParameterResolver.class);
3940

@@ -62,7 +63,7 @@ public InputParameterResolver(
6263
}
6364

6465
protected final IRepository resolveRepository(IRepository serverRepository, IBaseBundle data) {
65-
return data == null
66+
return data == null || BundleHelper.getEntry(data).isEmpty()
6667
? serverRepository
6768
: new FederatedRepository(
6869
serverRepository, new InMemoryFhirRepository(serverRepository.fhirContext(), data));

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ApplyProcessor.java

Lines changed: 71 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.opencds.cqf.fhir.cr.common.ExtensionBuilders.buildReferenceExt;
44
import static org.opencds.cqf.fhir.cr.common.ExtensionBuilders.pertainToGoalExtension;
5+
import static org.opencds.cqf.fhir.cr.questionnaire.Helpers.getQuestionnaireFromContained;
56
import static org.opencds.cqf.fhir.utility.BundleHelper.addEntry;
67
import static org.opencds.cqf.fhir.utility.BundleHelper.getEntry;
78
import static org.opencds.cqf.fhir.utility.BundleHelper.getEntryResources;
@@ -15,6 +16,7 @@
1516
import java.util.Collections;
1617
import java.util.Date;
1718
import java.util.List;
19+
import org.apache.commons.lang3.StringUtils;
1820
import org.hl7.fhir.instance.model.api.IBaseBundle;
1921
import org.hl7.fhir.instance.model.api.IBaseResource;
2022
import org.opencds.cqf.fhir.cr.common.ExtensionProcessor;
@@ -24,6 +26,7 @@
2426
import org.opencds.cqf.fhir.cr.questionnaireresponse.QuestionnaireResponseProcessor;
2527
import org.opencds.cqf.fhir.utility.Constants;
2628
import org.opencds.cqf.fhir.utility.Ids;
29+
import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireResponseAdapter;
2730
import org.opencds.cqf.fhir.utility.monad.Eithers;
2831
import org.slf4j.Logger;
2932
import org.slf4j.LoggerFactory;
@@ -116,63 +119,94 @@ public IBaseBundle applyR5(ApplyRequest request) {
116119
}
117120

118121
protected void initApply(ApplyRequest request) {
122+
var questionnaireResponses = request.getQuestionnaireResponses();
119123
var url = request.getPlanDefinitionAdapter().getUrl();
120124
// If the PlanDefinition has no URL we will not generate a Questionnaire
121125
// We will also add a warning to the result informing the user
122126
if (url != null) {
123127
var questionnaireUrl = url.replace("/PlanDefinition/", "/Questionnaire/");
124-
List<IBaseResource> entryResources =
125-
request.getData() == null ? List.of() : getEntryResources(request.getData());
126-
var questionnaire = entryResources.stream()
127-
.filter(r -> r.fhirType().equals("Questionnaire"))
128-
.map(q -> request.getAdapterFactory().createQuestionnaire(q))
129-
.filter(q -> q.getUrl().equals(questionnaireUrl))
128+
// In the case of an adaptive Questionnaire it should be contained within the QuestionnaireResponse
129+
// We are assuming a single QuestionnaireResponse in this instance
130+
var questionnaireResponse = questionnaireResponses.stream()
131+
.filter(r -> r.hasQuestionnaire() && r.getQuestionnaire().contains("#"))
130132
.findFirst()
131-
.orElse(request.getAdapterFactory()
132-
.createQuestionnaire(generateProcessor.generate(
133-
request.getPlanDefinition().getIdElement().getIdPart())));
134-
questionnaire.setUrl(questionnaireUrl);
133+
.orElse(null);
134+
var containedQuestionnaire = getQuestionnaireFromContained(questionnaireResponse);
135+
var questionnaire = containedQuestionnaire == null
136+
? null
137+
: request.getAdapterFactory().createQuestionnaire(containedQuestionnaire);
138+
// Otherwise if we have any Questionnaire in the data Bundle
139+
// with a url that matches the PlanDefinition we will use it
140+
if (questionnaire == null) {
141+
questionnaire = getEntryResources(request.getData()).stream()
142+
.filter(r -> r.fhirType().equals("Questionnaire"))
143+
.map(q -> request.getAdapterFactory().createQuestionnaire(q))
144+
.filter(q -> q.getUrl().equals(questionnaireUrl))
145+
.findFirst()
146+
.orElse(null);
147+
}
148+
// If we still don't have a Questionnaire we will generate one and give it the correct url
149+
if (questionnaire == null) {
150+
questionnaire = request.getAdapterFactory()
151+
.createQuestionnaire(generateProcessor.generate(
152+
request.getPlanDefinition().getIdElement().getIdPart()));
153+
questionnaire.setUrl(questionnaireUrl);
154+
}
155+
// Update the version
135156
var version = request.getPlanDefinitionAdapter().getVersion();
136157
if (version != null) {
137158
var formatter = new SimpleDateFormat("yyyy-MM-dd-hh.mm.ss");
138159
questionnaire.setVersion(version.concat(
139160
"-%s-%s".formatted(request.getSubjectId().getIdPart(), formatter.format(new Date()))));
140161
}
162+
// If we don't have a questionnaireResponse check for one in the data bundle
163+
if (questionnaireResponse == null) {
164+
var canonical = questionnaire.getCanonical();
165+
questionnaireResponse = questionnaireResponses.stream()
166+
.filter(IQuestionnaireResponseAdapter::hasQuestionnaire)
167+
.filter(r -> r.getQuestionnaire().equals(canonical))
168+
.findFirst()
169+
.orElse(null);
170+
}
141171
request.setQuestionnaire(questionnaire);
172+
request.setQuestionnaireResponse(questionnaireResponse);
142173
request.addCqlLibraryExtension();
143174
} else {
144175
request.logException("PlanDefinition %s is missing a canonical url."
145176
.formatted(request.getPlanDefinition().getIdElement().getValue()));
146177
}
147-
extractQuestionnaireResponse(request);
178+
extractQuestionnaireResponse(request, questionnaireResponses);
148179
}
149180

150-
protected void extractQuestionnaireResponse(ApplyRequest request) {
151-
if (request.getData() != null) {
152-
getEntryResources(request.getData()).stream()
153-
.filter(r -> r.fhirType().equals("QuestionnaireResponse"))
154-
.map(qr -> request.getAdapterFactory().createQuestionnaireResponse(qr))
155-
.forEach(questionnaireResponse -> {
156-
try {
157-
var extractBundle = extractProcessor.extract(
158-
Eithers.forRight(questionnaireResponse.get()),
159-
Eithers.forRight(request.getQuestionnaire()),
160-
request.getParameters(),
161-
request.getData(),
162-
request.getLibraryEngine());
163-
for (var entry : getEntry(extractBundle)) {
164-
addEntry(request.getData(), entry);
165-
// Not adding extracted resources back into the response to reduce size of payload
166-
// $extract can be called on the QuestionnaireResponse if these are desired
167-
// addEntry(request.getExtractedResources(), getEntryResource(request.getFhirVersion(),
168-
// entry))
169-
}
170-
} catch (Exception e) {
171-
request.logException("Error encountered extracting %s: %s"
172-
.formatted(questionnaireResponse.getId().getIdPart(), e.getMessage()));
173-
}
174-
});
175-
}
181+
protected void extractQuestionnaireResponse(ApplyRequest request, List<IQuestionnaireResponseAdapter> responses) {
182+
var questionnaireUrl = request.getQuestionnaireAdapter() != null
183+
? request.getQuestionnaireAdapter().getUrl()
184+
: null;
185+
responses.forEach(questionnaireResponse -> {
186+
try {
187+
var questionnaire = StringUtils.isNotBlank(questionnaireUrl)
188+
&& questionnaireResponse.hasQuestionnaire()
189+
&& questionnaireResponse.getQuestionnaire().equals(questionnaireUrl)
190+
? request.getQuestionnaire()
191+
: null;
192+
var extractBundle = extractProcessor.extract(
193+
Eithers.forRight(questionnaireResponse.get()),
194+
questionnaire == null ? null : Eithers.forRight(questionnaire),
195+
request.getParameters(),
196+
request.getData(),
197+
request.getLibraryEngine());
198+
for (var entry : getEntry(extractBundle)) {
199+
addEntry(request.getData(), entry);
200+
// Not adding extracted resources back into the response to reduce size of payload
201+
// $extract can be called on the QuestionnaireResponse if these are desired
202+
// addEntry(request.getExtractedResources(), getEntryResource(request.getFhirVersion(),
203+
// entry))
204+
}
205+
} catch (Exception e) {
206+
request.logException("Error encountered extracting %s: %s"
207+
.formatted(questionnaireResponse.getId().getIdPart(), e.getMessage()));
208+
}
209+
});
176210
}
177211

178212
public IBaseResource applyPlanDefinition(ApplyRequest request) {

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ApplyRequest.java

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static com.google.common.base.Preconditions.checkNotNull;
44
import static org.opencds.cqf.fhir.cr.common.IInputParameterResolver.createResolver;
5+
import static org.opencds.cqf.fhir.utility.BundleHelper.getEntryResources;
56
import static org.opencds.cqf.fhir.utility.BundleHelper.newBundle;
67
import static org.opencds.cqf.fhir.utility.Constants.APPLY_PARAMETER_ACTIVITY_DEFINITION;
78
import static org.opencds.cqf.fhir.utility.Constants.APPLY_PARAMETER_DATA;
@@ -44,6 +45,7 @@
4445
import org.opencds.cqf.fhir.utility.adapter.IPlanDefinitionAdapter;
4546
import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireAdapter;
4647
import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireItemComponentAdapter;
48+
import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireResponseAdapter;
4749

4850
public class ApplyRequest implements ICpgRequest {
4951
private static final String ACTIVITY_DEFINITION = "ActivityDefinition";
@@ -68,6 +70,7 @@ public class ApplyRequest implements ICpgRequest {
6870
private final Collection<IBaseResource> extractedResources;
6971
private IBaseOperationOutcome operationOutcome;
7072
private IQuestionnaireAdapter questionnaireAdapter;
73+
private IQuestionnaireResponseAdapter questionnaireResponseAdapter;
7174
private Boolean containResources;
7275
private Set<String> questionnaireDefinitions;
7376
// actionId is used to ensure all actions have an Id so they can be mapped
@@ -105,10 +108,10 @@ public ApplyRequest(
105108
this.setting = setting;
106109
this.settingContext = settingContext;
107110
this.parameters = parameters;
111+
if (data == null) {
112+
data = newBundle(fhirVersion);
113+
}
108114
if (prefetchData != null && !prefetchData.isEmpty()) {
109-
if (data == null) {
110-
data = newBundle(fhirVersion);
111-
}
112115
resolvePrefetchData(data, prefetchData);
113116
}
114117
this.data = data;
@@ -149,6 +152,7 @@ public ApplyRequest copy(IBaseResource planDefinition) {
149152
modelResolver,
150153
inputParameterResolver)
151154
.setQuestionnaire(getQuestionnaireAdapter())
155+
.setQuestionnaireResponse(getQuestionnaireResponseAdapter())
152156
.setContainResources(containResources);
153157
}
154158

@@ -232,7 +236,15 @@ public PopulateRequest toPopulateRequest() {
232236
}
233237
});
234238
return new PopulateRequest(
235-
questionnaireAdapter.get(), subjectId, context, null, data, libraryEngine, modelResolver);
239+
questionnaireAdapter.get(),
240+
// Not yet ready to pass QR to $populate
241+
// getQuestionnaireResponse(),
242+
subjectId,
243+
context,
244+
null,
245+
data,
246+
libraryEngine,
247+
modelResolver);
236248
}
237249

238250
public IBaseResource getPlanDefinition() {
@@ -373,6 +385,28 @@ public ApplyRequest setQuestionnaire(IQuestionnaireAdapter questionnaire) {
373385
return this;
374386
}
375387

388+
public ApplyRequest setQuestionnaireResponse(IQuestionnaireResponseAdapter questionnaireResponse) {
389+
questionnaireResponseAdapter = questionnaireResponse;
390+
return this;
391+
}
392+
393+
public IBaseResource getQuestionnaireResponse() {
394+
return questionnaireResponseAdapter == null ? null : questionnaireResponseAdapter.get();
395+
}
396+
397+
public IQuestionnaireResponseAdapter getQuestionnaireResponseAdapter() {
398+
return questionnaireResponseAdapter;
399+
}
400+
401+
public List<IQuestionnaireResponseAdapter> getQuestionnaireResponses() {
402+
return data == null
403+
? new ArrayList<>()
404+
: getEntryResources(data).stream()
405+
.filter(r -> r.fhirType().equals("QuestionnaireResponse"))
406+
.map(qr -> getAdapterFactory().createQuestionnaireResponse(qr))
407+
.toList();
408+
}
409+
376410
public ApplyRequest setData(IBaseBundle bundle) {
377411
data = bundle;
378412
return this;

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/Helpers.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import ca.uhn.fhir.context.FhirVersionEnum;
44
import org.apache.commons.lang3.StringUtils;
5+
import org.hl7.fhir.instance.model.api.IBaseResource;
6+
import org.opencds.cqf.fhir.utility.adapter.IQuestionnaireResponseAdapter;
57

68
public class Helpers {
9+
public static final String QUESTIONNAIRE = "Questionnaire";
710
private static final String CHOICE = "choice";
811
private static final String QUESTION = "question";
912
private static final String GROUP = "group";
@@ -50,4 +53,20 @@ public static String getSliceName(String elementId) {
5053
}
5154
return sliceName;
5255
}
56+
57+
public static IBaseResource getQuestionnaireFromContained(IQuestionnaireResponseAdapter questionnaireResponse) {
58+
return questionnaireResponse == null || !questionnaireResponse.hasQuestionnaire()
59+
? null
60+
: questionnaireResponse.getContained().stream()
61+
.filter(r -> r.fhirType().equals(QUESTIONNAIRE)
62+
&& questionnaireResponse
63+
.getQuestionnaire()
64+
.equals(getContainedId(r.getIdElement().getIdPart())))
65+
.findFirst()
66+
.orElse(null);
67+
}
68+
69+
private static String getContainedId(String id) {
70+
return id.startsWith("#") ? id : "#" + id;
71+
}
5372
}

0 commit comments

Comments
 (0)