Skip to content

Commit d0da3ba

Browse files
authored
Merge pull request #3918 from LinuxForHealth/issue-2964-followon
issue-2964 - adding validations for interaction, mode paramter and in…
2 parents bfdd5b8 + c25341c commit d0da3ba

4 files changed

Lines changed: 434 additions & 55 deletions

File tree

fhir-server-test/src/test/java/org/linuxforhealth/fhir/server/test/FHIROperationTest.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* (C) Copyright IBM Corp. 2017,2019
2+
* (C) Copyright IBM Corp. 2017, 2022
33
*
44
* SPDX-License-Identifier: Apache-2.0
55
*/
@@ -276,7 +276,7 @@ public void testGetHelloOperation() throws Exception {
276276
assertEquals(((org.linuxforhealth.fhir.model.type.String) output.getParameter().get(0).getValue()).getValue(),
277277
message);
278278
}
279-
279+
280280
@Test(groups = { "fhir-operation" })
281281
// Testcase for GET [baseUrl]/${operationName}?input-boolean=true&input-canonical="https://examples.com"&input-code...
282282
public void testGetHelloOperationWithPrimitives() throws Exception {
@@ -309,7 +309,7 @@ public void testGetHelloOperationWithPrimitives() throws Exception {
309309
assertFalse(output.getParameter().isEmpty());
310310
assertEquals(output.getParameter().size(), 16);
311311
}
312-
312+
313313
@Test(groups = { "fhir-operation" })
314314
// Testcase for GET [baseUrl]/${operationName}"
315315
public void testGetHelloOperationWithEmptyBody() throws Exception {
@@ -351,11 +351,7 @@ public void testGetRscValidateOperation() throws Exception {
351351
assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode());
352352
OperationOutcome operationOutcome = response.getResource(OperationOutcome.class);
353353
String text = operationOutcome.getIssue().get(0).getDetails().getText().getValue();
354-
if (text.contains("Input parameter 'resource' is required")) {
355-
assertTrue(true); // Force assertion to true
356-
} else {
357-
assertTrue(false); // Force assertion to false
358-
}
354+
assertTrue(text.contains("Input parameter 'resource' must be present"), "Issue detail must say 'resource' is required");
359355
}
360356

361357
@Test(groups = { "fhir-operation" }, dependsOnMethods = { "testCreatePractitioner", "testCreatePatient" })

operation/fhir-operation-validate/src/main/java/org/linuxforhealth/fhir/operation/validate/ValidateOperation.java

Lines changed: 149 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -51,49 +51,34 @@ protected Parameters doInvoke(FHIROperationContext operationContext, Class<? ext
5151
try {
5252
Parameter resourceParameter = getParameter(parameters, "resource");
5353
String resourceTypeName = resourceType != null ? resourceType.getSimpleName() : null;
54-
Resource resource;
55-
// If $validate is on resource-instance level, fetch the resource from database.
56-
if (operationContext != null && FHIROperationContext.Type.INSTANCE.equals(operationContext.getType()) && resourceParameter == null) {
57-
resource = resourceHelper.doRead(resourceTypeName, logicalId).getResource();
58-
if (resource == null) {
59-
throw buildExceptionWithIssue(resourceTypeName + " with id '" + logicalId + "' was not found", IssueType.NOT_FOUND);
60-
}
61-
} else if (resourceParameter == null) {
62-
throw buildExceptionWithIssue("Input parameter 'resource' is required for the $validate operation", IssueType.INVALID);
63-
} else {
64-
resource = resourceParameter.getResource();
65-
}
54+
Parameter modeParameter = getParameter(parameters, "mode");
55+
ModeType modeType = getModeType(modeParameter);
56+
// Validate the resource parameter
57+
Resource resource = validateResource(operationContext, logicalId, resourceHelper, resourceParameter, resourceTypeName, modeType);
6658

6759
List<OperationOutcome.Issue> issues;
6860
Parameter profileParameter = getParameter(parameters, "profile");
69-
Parameter modeParameter = getParameter(parameters, "mode");
7061

71-
// Only valid "mode" codes are accepted by the $validate operation.
72-
validateModeParameter(modeParameter);
62+
// Validate mode parameter.
63+
validateModeParameter(modeType, resourceHelper, resourceTypeName, operationContext, logicalId);
7364

7465
if (profileParameter != null && profileParameter.getValue() != null) {
7566
// If the 'profile' parameter is specified, always validate the resource against that profile.
7667
Uri profileUri = profileParameter.getValue().as(Uri.class);
7768
String profile = profileUri == null ? null : profileUri.getValue();
7869
issues = FHIRValidator.validator().validate(resource, profile);
79-
} else if (modeParameter != null && modeParameter.getValue() != null
80-
&& ("create".equals(modeParameter.getValue().as(Code.class).getValue())
81-
|| "update".equals(modeParameter.getValue().as(Code.class).getValue()))) {
70+
} else if (modeType == ModeType.CREATE || modeType == ModeType.UPDATE) {
8271
// If the 'mode' parameter is specified and its value is 'create' or 'update', validate the resource
8372
// against the FHIR server config's profile properties and the resource's asserted profiles.
8473
issues = resourceHelper.validateResource(resource);
85-
} else if (modeParameter != null
86-
&& modeParameter.getValue() != null
87-
&& ModeType.DELETE.value().equals(modeParameter.getValue().as(Code.class).getValue())
88-
&& FHIROperationContext.Type.INSTANCE.equals(operationContext.getType())) {
74+
} else if (modeType == ModeType.DELETE && operationContext.getType() == FHIROperationContext.Type.INSTANCE) {
8975
// If the 'mode' parameter is specified and its value is 'delete' and delete is invoked at the resource-instance level,
9076
// validate if the persistence layer implementation supports the "delete" operation
9177
issues = validateDeleteResource(resourceTypeName, logicalId, operationContext);
9278
} else {
9379
// Standard validation against the resource's asserted profiles.
9480
issues = FHIRValidator.validator().validate(resource);
95-
}
96-
81+
}
9782
return FHIROperationUtil.getOutputParameters(buildResourceValidOperationOutcome(issues));
9883
} catch (FHIROperationException e) {
9984
throw e;
@@ -131,21 +116,90 @@ private OperationOutcome buildResourceValidOperationOutcome(List<Issue> issues)
131116

132117

133118
/**
134-
* Check if a resource validation mode code is valid.
119+
* This method does the following validations if modeType(mode parameter) is not null. If the mode parameter is null then the below validations are skipped.
120+
* 1. Validate an interaction for a specified resource type.
121+
* 2. Validate if persistence layer implementation supports update/create mode if mode = create.
122+
* 3. Validate if persistence layer implementation supports update/create mode if mode = update but the resource doesnot exist yet in the DB.
123+
* 4. Validate if modes update and delete are only be used when the operation is invoked at the resource instance level
135124
*
136125
* @param modeParameter resource validation mode code to be validated
137-
* @throws FHIROperationException when the resource validation mode code is invalid
126+
* @param resourceHelper Resource operation provider for loading related Library resources
127+
* @param resourceType a valid resource type string
128+
* @param operationContext the FHIROperationContext associated with the request
129+
* @throws Exception
130+
*/
131+
private void validateModeParameter(ModeType modetype, FHIRResourceHelpers resourceHelper, String resourceType, FHIROperationContext operationContext, String logicalId) throws Exception {
132+
if (modetype == null) {
133+
return;
134+
}
135+
// Validate an interaction for a specified resource type.
136+
if ((modetype == ModeType.CREATE
137+
|| modetype == ModeType.UPDATE
138+
|| modetype == ModeType.DELETE)
139+
&& resourceType != null) {
140+
FHIRResourceHelpers.Interaction interaction = FHIRResourceHelpers.Interaction.from(modetype.value());
141+
resourceHelper.validateInteraction(interaction, resourceType);
142+
}
143+
// Validate if persistence layer implementation supports update/create mode if mode = create.
144+
if (operationContext != null && operationContext.getType() == FHIROperationContext.Type.INSTANCE
145+
&& modetype == ModeType.CREATE) {
146+
validateUpdateCreateEnabled(modetype, resourceType, operationContext);
147+
}
148+
// Validate if persistence layer implementation supports update/create mode if mode = update but the resource does not exist yet in the DB.
149+
if (operationContext != null && operationContext.getType() == FHIROperationContext.Type.INSTANCE
150+
&& modetype == ModeType.UPDATE) {
151+
Resource existingResource = resourceHelper.doRead(resourceType, logicalId).getResource();
152+
if (existingResource == null) {
153+
validateUpdateCreateEnabled(modetype, resourceType, operationContext);
154+
}
155+
}
156+
// Validate if modes update and delete are only be used when the operation is invoked at the resource instance level
157+
if (operationContext != null && operationContext.getType() == FHIROperationContext.Type.RESOURCE_TYPE &&
158+
(modetype == ModeType.UPDATE || modetype == ModeType.DELETE)) {
159+
160+
throw buildExceptionWithIssue(IssueSeverity.ERROR, IssueType.NOT_SUPPORTED, "Modes update and delete can only be used when the operation is invoked at the resource instance level.", null);
161+
}
162+
}
163+
164+
/**
165+
* This method when invoked checks if the persistence layer implementation supports update/create.
166+
* If the persistence layer implementation does not support update/create a FHIROperationException with issue is thrown.
167+
* @param modetype
168+
* @param resourceType
169+
* @param operationContext
170+
* @throws FHIROperationException
138171
*/
139-
private void validateModeParameter(Parameter modeParameter) throws FHIROperationException {
172+
private void validateUpdateCreateEnabled(ModeType modetype, String resourceType, FHIROperationContext operationContext) throws FHIROperationException {
173+
FHIRPersistence persistence =
174+
(FHIRPersistence) operationContext.getProperty(FHIROperationContext.PROPNAME_PERSISTENCE_IMPL);
175+
if (!persistence.isUpdateCreateEnabled()) {
176+
throw buildExceptionWithIssue(IssueSeverity.ERROR, IssueType.NOT_SUPPORTED, "Resource " + modetype.value() + ", of type '"
177+
+ resourceType + "', is not supported.", null);
178+
}
179+
}
180+
181+
/**
182+
* If the mode parameter is not null, this method validates if a resource validation mode code is valid and
183+
* returns valid resource validation mode code from ModeType enum.
184+
* If mode parameter is null this method returns null.
185+
* If mode parameter is not valid this method throws FHIROperationException with issues.
186+
* @param modeParameter resource validation mode code
187+
* @return The corresponding ModeType or null if a null value was passed
188+
* @throws FHIROperationException
189+
*/
190+
private ModeType getModeType(Parameter modeParameter) throws FHIROperationException {
140191
if (modeParameter != null && modeParameter.getValue() != null) {
192+
ModeType modetype;
141193
try {
142-
ModeType.from(modeParameter.getValue().as(Code.class).getValue());
194+
modetype = ModeType.from(modeParameter.getValue().as(Code.class).getValue());
143195
} catch (IllegalArgumentException e) {
144196
String msg = "'" + modeParameter.getValue().as(Code.class).getValue() + "' is not a valid resource validation mode";
145-
throw buildExceptionWithIssue(msg, IssueType.VALUE);
197+
throw buildExceptionWithIssue(IssueSeverity.ERROR, IssueType.NOT_SUPPORTED, msg, null);
146198
}
199+
return modetype;
200+
147201
}
148-
202+
return null;
149203
}
150204

151205

@@ -163,9 +217,72 @@ public List<Issue> validateDeleteResource(String type, String id, FHIROperationC
163217
FHIRPersistence persistence =
164218
(FHIRPersistence) operationContext.getProperty(FHIROperationContext.PROPNAME_PERSISTENCE_IMPL);
165219
if (!persistence.isDeleteSupported()) {
166-
warnings.add(FHIRUtil.buildOperationOutcomeIssue(IssueSeverity.WARNING, IssueType.NOT_SUPPORTED, "Resource deletion of type '"
167-
+ type + "' with id '" + id + "' is not supported."));
220+
warnings.add(FHIRUtil.buildOperationOutcomeIssue(IssueSeverity.ERROR, IssueType.NOT_SUPPORTED, "Resource deletion, of type '"
221+
+ type + "', with id '" + id + "', is not supported."));
168222
}
169223
return warnings;
170224
}
225+
226+
/**
227+
* Method to build the OperationOutcome error response
228+
* @param severity the issue severity
229+
* @param issueType describes the type of issue
230+
* @param msg describes the error message
231+
* @param cause the throwable that causes this OperationOutcome/Exception
232+
* @return FHIROperationException with issues
233+
* @throws FHIROperationException
234+
*/
235+
protected FHIROperationException buildExceptionWithIssue(IssueSeverity severity, IssueType issueType, String msg, Throwable cause) throws FHIROperationException {
236+
OperationOutcome.Issue ooi = FHIRUtil.buildOperationOutcomeIssue(severity, issueType, msg);
237+
return new FHIROperationException(msg, cause).withIssue(ooi);
238+
}
239+
240+
/**
241+
* Method to validate the resource parameter. The below validations are performed by this method.
242+
* 1. if $validate is invoked at instance level and mode = create, check if the resource already exists.
243+
* 2. resource parameter must be present unless the mode is "delete" (or "profile" per https://jira.hl7.org/browse/FHIR-37998)
244+
* 3. if mode=profile AND no resource parameter value is provided then the resource at this id is read and validated against the nominated profile
245+
* @param operationContext - the FHIROperationContext associated with the request
246+
* @param logicalId - the logical id of the FHIR resource
247+
* @param resourceHelper - Resource operation provider for loading related Library resources
248+
* @param resourceParameter - the input resource parameter
249+
* @param resourceType - a valid resource type string
250+
* @param modeType - a valid resource validation mode code
251+
* @return Resource - a FHIR Resource object
252+
* @throws Exception
253+
* @throws FHIROperationException
254+
*/
255+
private Resource validateResource(FHIROperationContext operationContext, String logicalId, FHIRResourceHelpers resourceHelper, Parameter resourceParameter,
256+
String resourceType, ModeType modeType) throws Exception, FHIROperationException {
257+
Resource resource = null;
258+
// if $validate is invoked at instance level and mode = create, check if the resource already exists.
259+
if (operationContext != null && operationContext.getType() == FHIROperationContext.Type.INSTANCE
260+
&& modeType == ModeType.CREATE) {
261+
Resource existingResource = resourceHelper.doRead(resourceType, logicalId).getResource();
262+
if (existingResource != null) {
263+
throw buildExceptionWithIssue(IssueSeverity.ERROR, IssueType.NOT_SUPPORTED, resourceType + " with id '" + logicalId + "' already exists", null);
264+
}
265+
}
266+
// resource parameter must be present unless the mode is "delete" (or "profile" per https://jira.hl7.org/browse/FHIR-37998)
267+
if (resourceParameter == null
268+
&& (modeType == null
269+
|| modeType == ModeType.CREATE
270+
|| modeType == ModeType.UPDATE)) {
271+
throw buildExceptionWithIssue(IssueSeverity.ERROR, IssueType.INVALID, "Input parameter 'resource' must be present unless the mode is 'delete' or 'profile'", null);
272+
}
273+
// if mode=profile AND no resource parameter value is provided then the resource at this id is read and validated against the nominated profile
274+
if (resourceParameter == null && operationContext != null && operationContext.getType() == FHIROperationContext.Type.INSTANCE
275+
&& modeType == ModeType.PROFILE) {
276+
resource = resourceHelper.doRead(resourceType, logicalId).getResource();
277+
if (resource == null) {
278+
throw buildExceptionWithIssue(IssueSeverity.ERROR, IssueType.INVALID, resourceType + " with id '" + logicalId + "' does not exist", null);
279+
}
280+
}
281+
282+
if (resourceParameter != null) {
283+
resource = resourceParameter.getResource();
284+
}
285+
return resource;
286+
}
287+
171288
}

operation/fhir-operation-validate/src/main/java/org/linuxforhealth/fhir/operation/validate/type/ModeType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public enum ModeType {
1414
CREATE("create"),
1515
UPDATE("update"),
1616
DELETE("delete"),
17-
PROFILE("PROFILE");
17+
PROFILE("profile");
1818

1919
private String value;
2020

0 commit comments

Comments
 (0)