Add support for returning subproperties for RemoteTerminologyService lookup (#5936)

* Add support for returning subproperties in the RemoteTerminologyService

* Add clarifying comments to code. Simplify logic to filter properties.

* Update sub-property iteration logic in the conversion code to make it more readable

* Fix spotless errors

* Add a few more test cases to test property filtering for the lookup
This commit is contained in:
Martha Mitran 2024-05-16 14:02:53 -07:00 committed by GitHub
parent 14c364dffd
commit bff59b6c50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1273 additions and 797 deletions

View File

@ -530,8 +530,13 @@ public interface IValidationSupport {
public abstract String getType();
}
// The reason these cannot be declared within an enum is because a Remote Terminology Service
// can support arbitrary types. We do not restrict against the types in the spec.
// Some of the types in the spec are not yet implemented as well.
// @see https://github.com/hapifhir/hapi-fhir/issues/5700
String TYPE_STRING = "string";
String TYPE_CODING = "Coding";
String TYPE_GROUP = "group";
class StringConceptProperty extends BaseConceptProperty {
private final String myValue;
@ -589,6 +594,31 @@ public interface IValidationSupport {
}
}
class GroupConceptProperty extends BaseConceptProperty {
public GroupConceptProperty(String thePropertyName) {
super(thePropertyName);
}
private List<BaseConceptProperty> subProperties;
public BaseConceptProperty addSubProperty(BaseConceptProperty theProperty) {
if (subProperties == null) {
subProperties = new ArrayList<>();
}
subProperties.add(theProperty);
return this;
}
public List<BaseConceptProperty> getSubProperties() {
return subProperties != null ? subProperties : Collections.emptyList();
}
@Override
public String getType() {
return TYPE_GROUP;
}
}
class CodeValidationResult {
public static final String SOURCE_DETAILS = "sourceDetails";
public static final String RESULT = "result";
@ -871,8 +901,15 @@ public interface IValidationSupport {
}
}
/**
* Converts the current LookupCodeResult instance into a IBaseParameters instance which is returned
* to the client of the $lookup operation.
* @param theContext the FHIR context used for running the operation
* @param thePropertyNamesToFilter the properties which are passed as parameter to filter the result.
* @return the output for the lookup operation.
*/
public IBaseParameters toParameters(
FhirContext theContext, List<? extends IPrimitiveType<String>> thePropertyNames) {
FhirContext theContext, List<? extends IPrimitiveType<String>> thePropertyNamesToFilter) {
IBaseParameters retVal = ParametersUtil.newInstance(theContext);
if (isNotBlank(getCodeSystemDisplayName())) {
@ -886,50 +923,29 @@ public interface IValidationSupport {
if (myProperties != null) {
Set<String> properties = Collections.emptySet();
if (thePropertyNames != null) {
properties = thePropertyNames.stream()
final List<BaseConceptProperty> propertiesToReturn;
if (thePropertyNamesToFilter != null && !thePropertyNamesToFilter.isEmpty()) {
// TODO MM: The logic to filter of properties could actually be moved to the lookupCode provider.
// That is where the rest of the lookupCode input parameter handling is done.
// This was left as is for now but can be done with next opportunity.
Set<String> propertyNameList = thePropertyNamesToFilter.stream()
.map(IPrimitiveType::getValueAsString)
.collect(Collectors.toSet());
propertiesToReturn = myProperties.stream()
.filter(p -> propertyNameList.contains(p.getPropertyName()))
.collect(Collectors.toList());
} else {
propertiesToReturn = myProperties;
}
for (BaseConceptProperty next : myProperties) {
String propertyName = next.getPropertyName();
if (!properties.isEmpty() && !properties.contains(propertyName)) {
continue;
}
for (BaseConceptProperty next : propertiesToReturn) {
IBase property = ParametersUtil.addParameterToParameters(theContext, retVal, "property");
ParametersUtil.addPartCode(theContext, property, "code", propertyName);
String propertyType = next.getType();
switch (propertyType) {
case TYPE_STRING:
StringConceptProperty stringConceptProperty = (StringConceptProperty) next;
ParametersUtil.addPartString(
theContext, property, "value", stringConceptProperty.getValue());
break;
case TYPE_CODING:
CodingConceptProperty codingConceptProperty = (CodingConceptProperty) next;
ParametersUtil.addPartCoding(
theContext,
property,
"value",
codingConceptProperty.getCodeSystem(),
codingConceptProperty.getCode(),
codingConceptProperty.getDisplay());
break;
default:
throw new IllegalStateException(
Msg.code(1739) + "Don't know how to handle " + next.getClass());
}
populateProperty(theContext, property, next);
}
}
if (myDesignations != null) {
for (ConceptDesignation next : myDesignations) {
IBase property = ParametersUtil.addParameterToParameters(theContext, retVal, "designation");
ParametersUtil.addPartCode(theContext, property, "language", next.getLanguage());
ParametersUtil.addPartCoding(
@ -941,6 +957,41 @@ public interface IValidationSupport {
return retVal;
}
private void populateProperty(
FhirContext theContext, IBase theProperty, BaseConceptProperty theConceptProperty) {
ParametersUtil.addPartCode(theContext, theProperty, "code", theConceptProperty.getPropertyName());
String propertyType = theConceptProperty.getType();
switch (propertyType) {
case TYPE_STRING:
StringConceptProperty stringConceptProperty = (StringConceptProperty) theConceptProperty;
ParametersUtil.addPartString(theContext, theProperty, "value", stringConceptProperty.getValue());
break;
case TYPE_CODING:
CodingConceptProperty codingConceptProperty = (CodingConceptProperty) theConceptProperty;
ParametersUtil.addPartCoding(
theContext,
theProperty,
"value",
codingConceptProperty.getCodeSystem(),
codingConceptProperty.getCode(),
codingConceptProperty.getDisplay());
break;
case TYPE_GROUP:
GroupConceptProperty groupConceptProperty = (GroupConceptProperty) theConceptProperty;
if (groupConceptProperty.getSubProperties().isEmpty()) {
break;
}
groupConceptProperty.getSubProperties().forEach(p -> {
IBase subProperty = ParametersUtil.addPart(theContext, theProperty, "subproperty", null);
populateProperty(theContext, subProperty, p);
});
break;
default:
throw new IllegalStateException(
Msg.code(1739) + "Don't know how to handle " + theConceptProperty.getClass());
}
}
public void setErrorMessage(String theErrorMessage) {
myErrorMessage = theErrorMessage;
}

View File

@ -433,7 +433,7 @@ public class ParametersUtil {
addPart(theContext, theParameter, theName, coding);
}
public static void addPart(FhirContext theContext, IBase theParameter, String theName, IBase theValue) {
public static IBase addPart(FhirContext theContext, IBase theParameter, String theName, @Nullable IBase theValue) {
BaseRuntimeElementCompositeDefinition<?> def =
(BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(theParameter.getClass());
BaseRuntimeChildDefinition partChild = def.getChildByName("part");
@ -448,12 +448,15 @@ public class ParametersUtil {
name.setValue(theName);
partChildElem.getChildByName("name").getMutator().addValue(part, name);
if (theValue != null) {
if (theValue instanceof IBaseResource) {
partChildElem.getChildByName("resource").getMutator().addValue(part, theValue);
} else {
partChildElem.getChildByName("value[x]").getMutator().addValue(part, theValue);
}
}
return part;
}
public static void addPartResource(
FhirContext theContext, IBase theParameter, String theName, IBaseResource theValue) {

View File

@ -0,0 +1,5 @@
---
type: add
issue: 5935
title: "Remote Terminology Service can now return subproperty fields for a CodeSystem lookup operation.
This can be done in DSTU3 and R4. R5 is not yet implemented."

View File

@ -216,7 +216,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
ParametersUtil.addParameterToParametersString(fhirContext, params, "language", displayLanguage);
}
for (String propertyName : theLookupCodeRequest.getPropertyNames()) {
ParametersUtil.addParameterToParametersString(fhirContext, params, "property", propertyName);
ParametersUtil.addParameterToParametersCode(fhirContext, params, "property", propertyName);
}
Class<? extends IBaseResource> codeSystemClass =
myCtx.getResourceDefinition("CodeSystem").getImplementingClass();
@ -229,7 +229,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
if (outcome != null && !outcome.isEmpty()) {
switch (fhirVersion) {
case DSTU3:
return generateLookupCodeResultDSTU3(
return generateLookupCodeResultDstu3(
code, system, (org.hl7.fhir.dstu3.model.Parameters) outcome);
case R4:
return generateLookupCodeResultR4(code, system, (Parameters) outcome);
@ -243,7 +243,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
return null;
}
private LookupCodeResult generateLookupCodeResultDSTU3(
private LookupCodeResult generateLookupCodeResultDstu3(
String theCode, String theSystem, org.hl7.fhir.dstu3.model.Parameters outcomeDSTU3) {
// NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding
// several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in
@ -257,13 +257,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
String parameterTypeAsString = Objects.toString(parameterComponent.getValue(), null);
switch (parameterComponent.getName()) {
case "property":
org.hl7.fhir.dstu3.model.Property part = parameterComponent.getChildByName("part");
// The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be
// saved
if (part == null || part.getValues().size() < 2) {
continue;
}
BaseConceptProperty conceptProperty = createBaseConceptPropertyDstu3(part.getValues());
BaseConceptProperty conceptProperty = createConceptPropertyDstu3(parameterComponent);
if (conceptProperty != null) {
result.getProperties().add(conceptProperty);
}
@ -289,36 +283,47 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
return result;
}
private static BaseConceptProperty createBaseConceptPropertyDstu3(List<org.hl7.fhir.dstu3.model.Base> theValues) {
org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent part1 =
(org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) theValues.get(0);
String propertyName = ((org.hl7.fhir.dstu3.model.CodeType) part1.getValue()).getValue();
private static BaseConceptProperty createConceptPropertyDstu3(
org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent theParameterComponent) {
org.hl7.fhir.dstu3.model.Property property = theParameterComponent.getChildByName("part");
BaseConceptProperty conceptProperty = null;
org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent part2 =
(org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) theValues.get(1);
// The assumption here is that we may at east 2 elements in this part
if (property == null || property.getValues().size() < 2) {
return null;
}
org.hl7.fhir.dstu3.model.Type value = part2.getValue();
if (value == null) {
return conceptProperty;
List<org.hl7.fhir.dstu3.model.Base> values = property.getValues();
org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent firstPart =
(org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(0);
String propertyName = ((org.hl7.fhir.dstu3.model.CodeType) firstPart.getValue()).getValue();
org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent secondPart =
(org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(1);
org.hl7.fhir.dstu3.model.Type value = secondPart.getValue();
if (value != null) {
return createConceptPropertyDstu3(propertyName, value);
}
String fhirType = value.fhirType();
switch (fhirType) {
case TYPE_STRING:
org.hl7.fhir.dstu3.model.StringType stringType = (org.hl7.fhir.dstu3.model.StringType) part2.getValue();
conceptProperty = new StringConceptProperty(propertyName, stringType.getValue());
break;
case TYPE_CODING:
org.hl7.fhir.dstu3.model.Coding coding = (org.hl7.fhir.dstu3.model.Coding) part2.getValue();
conceptProperty = new CodingConceptProperty(
propertyName, coding.getSystem(), coding.getCode(), coding.getDisplay());
break;
// TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699
default:
// other types will not fail for Remote Terminology
conceptProperty = new StringConceptProperty(propertyName, value.toString());
String groupName = secondPart.getName();
if (!"subproperty".equals(groupName)) {
return null;
}
return conceptProperty;
// handle property group (a property containing sub-properties)
GroupConceptProperty groupConceptProperty = new GroupConceptProperty(propertyName);
// we already retrieved the property name (group name) as first element, next will be the sub-properties.
// there is no dedicated value for a property group as it is an aggregate
for (int i = 1; i < values.size(); i++) {
org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent nextPart =
(org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(i);
BaseConceptProperty subProperty = createConceptPropertyDstu3(nextPart);
if (subProperty != null) {
groupConceptProperty.addSubProperty(subProperty);
}
}
return groupConceptProperty;
}
public static BaseConceptProperty createConceptProperty(final String theName, final IBaseDatatype theValue) {
@ -396,13 +401,7 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
String parameterTypeAsString = Objects.toString(parameterComponent.getValue(), null);
switch (parameterComponent.getName()) {
case "property":
Property part = parameterComponent.getChildByName("part");
// The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be
// saved
if (part == null || part.getValues().size() < 2) {
continue;
}
BaseConceptProperty conceptProperty = createBaseConceptPropertyR4(part.getValues());
BaseConceptProperty conceptProperty = createConceptPropertyR4(parameterComponent);
if (conceptProperty != null) {
result.getProperties().add(conceptProperty);
}
@ -428,34 +427,43 @@ public class RemoteTerminologyServiceValidationSupport extends BaseValidationSup
return result;
}
private static BaseConceptProperty createBaseConceptPropertyR4(List<Base> values) {
ParametersParameterComponent part1 = (ParametersParameterComponent) values.get(0);
String propertyName = ((CodeType) part1.getValue()).getValue();
private static BaseConceptProperty createConceptPropertyR4(ParametersParameterComponent thePropertyComponent) {
Property property = thePropertyComponent.getChildByName("part");
ParametersParameterComponent part2 = (ParametersParameterComponent) values.get(1);
Type value = part2.getValue();
if (value == null) {
// The assumption here is that we may at east 2 elements in this part
if (property == null || property.getValues().size() < 2) {
return null;
}
BaseConceptProperty conceptProperty;
String fhirType = value.fhirType();
switch (fhirType) {
case IValidationSupport.TYPE_STRING:
StringType stringType = (StringType) part2.getValue();
conceptProperty = new StringConceptProperty(propertyName, stringType.getValue());
break;
case IValidationSupport.TYPE_CODING:
Coding coding = (Coding) part2.getValue();
conceptProperty = new CodingConceptProperty(
propertyName, coding.getSystem(), coding.getCode(), coding.getDisplay());
break;
// TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699
default:
// other types will not fail for Remote Terminology
conceptProperty = new StringConceptProperty(propertyName, value.toString());
List<Base> values = property.getValues();
ParametersParameterComponent firstPart = (ParametersParameterComponent) values.get(0);
String propertyName = ((CodeType) firstPart.getValue()).getValue();
ParametersParameterComponent secondPart = (ParametersParameterComponent) values.get(1);
Type value = secondPart.getValue();
if (value != null) {
return createConceptPropertyR4(propertyName, value);
}
return conceptProperty;
String groupName = secondPart.getName();
if (!"subproperty".equals(groupName)) {
return null;
}
// handle property group (a property containing sub-properties)
GroupConceptProperty groupConceptProperty = new GroupConceptProperty(propertyName);
// we already retrieved the property name (group name) as first element, next will be the sub-properties.
// there is no dedicated value for a property group as it is an aggregate
for (int i = 1; i < values.size(); i++) {
ParametersParameterComponent nextPart = (ParametersParameterComponent) values.get(i);
BaseConceptProperty subProperty = createConceptPropertyR4(nextPart);
if (subProperty != null) {
groupConceptProperty.addSubProperty(subProperty);
}
}
return groupConceptProperty;
}
private static BaseConceptProperty createConceptPropertyR4(final String theName, final Type theValue) {

View File

@ -1,23 +1,26 @@
package org.hl7.fhir.common.hapi.validation;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport.BaseConceptProperty;
import ca.uhn.fhir.context.support.IValidationSupport.CodingConceptProperty;
import ca.uhn.fhir.context.support.IValidationSupport.ConceptDesignation;
import ca.uhn.fhir.context.support.IValidationSupport.GroupConceptProperty;
import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult;
import ca.uhn.fhir.context.support.IValidationSupport.StringConceptProperty;
import ca.uhn.fhir.context.support.LookupCodeRequest;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Collection;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_CODING;
import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_GROUP;
import static ca.uhn.fhir.context.support.IValidationSupport.TYPE_STRING;
import static java.util.stream.IntStream.range;
import static org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport.createConceptProperty;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -25,6 +28,17 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/**
* Contains basic test setup for the $lookup operation against a {@link org.hl7.fhir.dstu3.model.CodeSystem}.
* The provider for CodeSystem and the validation service needs to be configured by the implementing class.
* This test interface contains the following:
* (1) basic tests which are version independent, without any parameters, declared as default with @Test annotation
* e.g. lookupCode_forCodeSystemWithBlankCode_throwsException
* (2) test template methods for running version dependent tests with parameters, declared as default, without annotations
* e.g. @see #verifyLookupCodeResult
* (3) methods which help to assert part of the output (designation, property), declared as private
* e.g. assertEqualConceptProperty
*/
public interface ILookupCodeTest {
String DISPLAY = "DISPLAY";
String LANGUAGE = "en";
@ -33,26 +47,11 @@ public interface ILookupCodeTest {
String CODE_SYSTEM_NAME = "Code System";
String CODE = "CODE";
interface IValidationTest {
RemoteTerminologyServiceValidationSupport getService();
IResourceProvider getCodeSystemProvider();
}
@Nested
interface ILookupCodeSupportedPropertyTest extends IValidationTest {
IValidationSupport getService();
IMyCodeSystemProvider getCodeSystemProvider();
Stream<Arguments> getEmptyPropertyValues();
Stream<Arguments> getPropertyValues();
Stream<Arguments> getDesignations();
void verifyProperty(IValidationSupport.BaseConceptProperty theConceptProperty, String theExpectedPropertName, IBaseDatatype theExpectedValue);
@Test
default void testLookupCode_forCodeSystemWithBlankCode_throwsException() {
default void lookupCode_forCodeSystemWithBlankCode_throwsException() {
try {
getService().lookupCode(null, new LookupCodeRequest(CODE_SYSTEM, ""));
fail();
@ -62,45 +61,28 @@ public interface ILookupCodeTest {
}
@Test
default void testLookupCode_forCodeSystemWithPropertyInvalidType_throwsException() {
default void lookupCode_forCodeSystemWithPropertyInvalidType_throwsException() {
// test
LookupCodeResult result = new LookupCodeResult();
result.getProperties().add(new IValidationSupport.BaseConceptProperty("someProperty") {
result.getProperties().add(new BaseConceptProperty("someProperty") {
public String getType() {
return "someUnsupportedType";
}
});
getCodeSystemProvider().setLookupCodeResult(result);
// test and verify
try {
getService().lookupCode(null, new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, null));
LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, null);
getService().lookupCode(null, request);
fail();
} catch (InternalErrorException e) {
assertTrue(e.getMessage().contains("HAPI-1739: Don't know how to handle "));
}
}
@ParameterizedTest
@MethodSource(value = "getEmptyPropertyValues")
default void testLookupCode_forCodeSystemWithPropertyEmptyValue_returnsCorrectParameters(IBaseDatatype thePropertyValue) {
// setup
final String propertyName = "someProperty";
IValidationSupport.BaseConceptProperty property = createConceptProperty(propertyName, thePropertyValue);
LookupCodeResult result = new LookupCodeResult();
result.getProperties().add(property);
getCodeSystemProvider().setLookupCodeResult(result);
// test
LookupCodeResult outcome = getService().lookupCode(null, new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, List.of(propertyName)));
// verify
assertNotNull(outcome);
Optional<IValidationSupport.BaseConceptProperty> propertyOptional = outcome.getProperties().stream().findFirst().filter(a -> propertyName.equals(a.getPropertyName()));
assertFalse(propertyOptional.isPresent());
}
@Test
default void testLookupCode_forCodeSystemWithParameters_returnsCorrectParameters() {
// setup
default void lookupCode_forCodeSystem_returnsCorrectResult() {
LookupCodeResult result = new LookupCodeResult().setFound(true).setSearchedForCode(CODE).setSearchedForSystem(CODE_SYSTEM);
result.setCodeIsAbstract(false);
result.setCodeSystemVersion(CODE_SYSTEM_VERSION);
@ -108,111 +90,159 @@ public interface ILookupCodeTest {
result.setCodeDisplay(DISPLAY);
getCodeSystemProvider().setLookupCodeResult(result);
// test
LookupCodeResult outcome = getService().lookupCode(null, new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, null));
// verify
assertNotNull(outcome);
assertEquals(CODE, getCodeSystemProvider().getCode());
assertEquals(CODE_SYSTEM, getCodeSystemProvider().getSystem());
assertEquals(result.getCodeSystemDisplayName(), outcome.getCodeSystemDisplayName());
assertEquals(result.getCodeDisplay(), outcome.getCodeDisplay());
assertEquals(result.getCodeSystemVersion(), outcome.getCodeSystemVersion());
assertEquals(result.isCodeIsAbstract(), outcome.isCodeIsAbstract());
}
@ParameterizedTest
@MethodSource(value = "getPropertyValues")
default void testLookupCode_forCodeSystemWithProperty_returnsCorrectProperty(IBaseDatatype thePropertyValue) {
// setup
final String propertyName = "someProperty";
LookupCodeResult result = new LookupCodeResult()
.setFound(true).setSearchedForCode(CODE).setSearchedForSystem(CODE_SYSTEM);
result.setCodeIsAbstract(false);
result.setCodeSystemVersion(CODE_SYSTEM_VERSION);
result.setCodeSystemDisplayName(CODE_SYSTEM_NAME);
result.setCodeDisplay(DISPLAY);
IValidationSupport.BaseConceptProperty property = createConceptProperty(propertyName, thePropertyValue);
result.getProperties().add(property);
getCodeSystemProvider().setLookupCodeResult(result);
// test
LookupCodeResult outcome = getService().lookupCode(null, new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, List.of(propertyName)));
// verify
assertNotNull(outcome);
assertEquals(CODE, getCodeSystemProvider().getCode());
assertEquals(CODE_SYSTEM, getCodeSystemProvider().getSystem());
assertEquals(result.getCodeSystemDisplayName(), outcome.getCodeSystemDisplayName());
assertEquals(result.getCodeDisplay(), outcome.getCodeDisplay());
assertEquals(result.getCodeSystemVersion(), outcome.getCodeSystemVersion());
assertEquals(result.isCodeIsAbstract(), outcome.isCodeIsAbstract());
Optional<IValidationSupport.BaseConceptProperty> propertyOptional = outcome.getProperties().stream().findFirst().filter(a -> propertyName.equals(a.getPropertyName()));
assertTrue(propertyOptional.isPresent());
IValidationSupport.BaseConceptProperty outputProperty = propertyOptional.get();
verifyProperty(outputProperty, propertyName, thePropertyValue);
}
@ParameterizedTest
@MethodSource(value = "getDesignations")
default void testLookupCode_withCodeSystemWithDesignation_returnsCorrectDesignation(final IValidationSupport.ConceptDesignation theConceptDesignation) {
// setup
LookupCodeResult result = new LookupCodeResult();
result.getDesignations().add(theConceptDesignation);
getCodeSystemProvider().setLookupCodeResult(result);
// test
LookupCodeResult outcome = getService().lookupCode(null, new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, null));
// verify
assertNotNull(outcome);
Collection<IValidationSupport.ConceptDesignation> designations = outcome.getDesignations();
assertEquals(1, designations.size());
IValidationSupport.ConceptDesignation designation = designations.iterator().next();
assertEquals(theConceptDesignation.getValue(), designation.getValue());
assertEquals(theConceptDesignation.getLanguage(), designation.getLanguage());
assertEquals(theConceptDesignation.getUseCode(), designation.getUseCode());
assertEquals(theConceptDesignation.getUseSystem(), designation.getUseSystem());
assertEquals(theConceptDesignation.getUseDisplay(), designation.getUseDisplay());
// test and verify
LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, null);
verifyLookupCodeResult(request, result);
}
@Test
default void testLookupCode_withCodeSystemWithMultipleDesignations_returnsCorrectDesignations() {
default void lookupCode_withCodeSystemWithMultipleDesignations_returnsCorrectDesignations() {
// setup
final String code1 = "code1";
final String code2 = "code2";
IValidationSupport.ConceptDesignation designation1 = new IValidationSupport.ConceptDesignation().setUseCode(code1).setUseSystem("system1").setValue("value1").setLanguage("en");
IValidationSupport.ConceptDesignation designation2 = new IValidationSupport.ConceptDesignation().setUseCode(code2).setUseSystem("system2").setValue("value2").setLanguage("es");
ConceptDesignation designation1 = new ConceptDesignation().setUseCode(code1).setUseSystem("system1").setValue("value1").setLanguage("en");
ConceptDesignation designation2 = new ConceptDesignation().setUseCode(code2).setUseSystem("system2").setValue("value2").setLanguage("es");
LookupCodeResult result = new LookupCodeResult();
result.getDesignations().add(designation1);
result.getDesignations().add(designation2);
getCodeSystemProvider().setLookupCodeResult(result);
// test and verify
LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, null);
verifyLookupCodeResult(request, result);
}
default void verifyLookupWithEmptyPropertyValue(IBaseDatatype thePropertyValue) {
// setup
final String propertyName = "someProperty";
BaseConceptProperty property = createConceptProperty(propertyName, thePropertyValue);
LookupCodeResult result = new LookupCodeResult();
result.getProperties().add(property);
getCodeSystemProvider().setLookupCodeResult(result);
// test
LookupCodeResult outcome = getService().lookupCode(null, new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, null));
LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, List.of(propertyName));
LookupCodeResult outcome = getService().lookupCode(null, request);
// verify
assertNotNull(outcome);
Collection<IValidationSupport.ConceptDesignation> designations = outcome.getDesignations();
assertEquals(2, designations.size());
for (IValidationSupport.ConceptDesignation designation : designations) {
IValidationSupport.ConceptDesignation expectedDesignation = code1.equals(designation.getUseCode()) ? designation1 : designation2;
assertEquals(expectedDesignation.getValue(), designation.getValue());
assertEquals(expectedDesignation.getLanguage(), designation.getLanguage());
assertEquals(expectedDesignation.getUseCode(), designation.getUseCode());
assertEquals(expectedDesignation.getUseSystem(), designation.getUseSystem());
assertEquals(expectedDesignation.getUseDisplay(), designation.getUseDisplay());
Optional<BaseConceptProperty> propertyOptional = outcome.getProperties().stream().findFirst().filter(a -> propertyName.equals(a.getPropertyName()));
assertFalse(propertyOptional.isPresent());
}
default void verifyLookupWithProperty(List<IBaseDatatype> thePropertyValues, List<Integer> thePropertyIndexesToFilter) {
// setup
final String propertyName = "someProperty";
LookupCodeResult result = new LookupCodeResult().setFound(true).setSearchedForCode(CODE).setSearchedForSystem(CODE_SYSTEM);
result.setCodeIsAbstract(false);
result.setCodeSystemVersion(CODE_SYSTEM_VERSION);
result.setCodeSystemDisplayName(CODE_SYSTEM_NAME);
result.setCodeDisplay(DISPLAY);
List<String> propertyNamesToFilter = new ArrayList<>();
for (int i = 0; i < thePropertyValues.size(); i++) {
String currentPropertyName = propertyName + i;
result.getProperties().add(createConceptProperty(currentPropertyName, thePropertyValues.get(i)));
if (thePropertyIndexesToFilter.contains(i)) {
propertyNamesToFilter.add(currentPropertyName);
}
}
getCodeSystemProvider().setLookupCodeResult(result);
// test
LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, propertyNamesToFilter);
// verify
result.getProperties().removeIf(p -> !propertyNamesToFilter.contains(p.getPropertyName()));
verifyLookupCodeResult(request, result);
}
default void verifyLookupWithSubProperties(List<IBaseDatatype> thePropertyValues) {
// setup
final String groupName = "group";
LookupCodeResult result = new LookupCodeResult().setFound(true).setSearchedForCode(CODE).setSearchedForSystem(CODE_SYSTEM);
result.setCodeIsAbstract(false);
result.setCodeSystemVersion(CODE_SYSTEM_VERSION);
result.setCodeSystemDisplayName(CODE_SYSTEM_NAME);
result.setCodeDisplay(DISPLAY);
final String subPropertyName = "someSubProperty";
GroupConceptProperty group = new GroupConceptProperty(groupName);
for (int i = 0; i < thePropertyValues.size(); i++) {
group.addSubProperty(createConceptProperty(subPropertyName + i, thePropertyValues.get(i)));
}
result.getProperties().add(group);
getCodeSystemProvider().setLookupCodeResult(result);
// test and verify
LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, List.of(groupName));
verifyLookupCodeResult(request, result);
}
default void verifyLookupCodeResult(LookupCodeRequest theRequest, LookupCodeResult theExpectedResult) {
// test
LookupCodeResult outcome = getService().lookupCode(null, theRequest);
assertNotNull(outcome);
// verify
assertNotNull(outcome);
assertEquals(theRequest.getCode(), getCodeSystemProvider().getCode());
assertEquals(theRequest.getSystem(), getCodeSystemProvider().getSystem());
assertEquals(theExpectedResult.getCodeSystemDisplayName(), outcome.getCodeSystemDisplayName());
assertEquals(theExpectedResult.getCodeDisplay(), outcome.getCodeDisplay());
assertEquals(theExpectedResult.getCodeSystemVersion(), outcome.getCodeSystemVersion());
assertEquals(theExpectedResult.isCodeIsAbstract(), outcome.isCodeIsAbstract());
assertEquals(theExpectedResult.getProperties().size(), outcome.getProperties().size());
range(0, outcome.getProperties().size()).forEach(i -> assertEqualConceptProperty(theExpectedResult.getProperties().get(i), outcome.getProperties().get(i)));
assertEquals(theExpectedResult.getDesignations().size(), outcome.getDesignations().size());
range(0, outcome.getDesignations().size()).forEach(i -> assertEqualConceptDesignation(theExpectedResult.getDesignations().get(i), outcome.getDesignations().get(i)));
}
default void verifyLookupWithConceptDesignation(final ConceptDesignation theConceptDesignation) {
// setup
LookupCodeResult result = new LookupCodeResult();
result.getDesignations().add(theConceptDesignation);
getCodeSystemProvider().setLookupCodeResult(result);
// test and verify
LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, LANGUAGE, null);
verifyLookupCodeResult(request, result);
}
private void assertEqualConceptProperty(BaseConceptProperty theProperty, BaseConceptProperty theExpectedProperty) {
assertEquals(theExpectedProperty.getPropertyName(), theProperty.getPropertyName());
assertEquals(theExpectedProperty.getType(), theProperty.getType());
switch (theProperty.getType()) {
case TYPE_STRING -> {
StringConceptProperty expected = (StringConceptProperty) theExpectedProperty;
StringConceptProperty actual = (StringConceptProperty) theProperty;
assertEquals(expected.getValue(), actual.getValue());
}
case TYPE_CODING -> {
CodingConceptProperty expected = (CodingConceptProperty) theExpectedProperty;
CodingConceptProperty actual = (CodingConceptProperty) theProperty;
assertEquals(expected.getCode(), actual.getCode());
assertEquals(expected.getCodeSystem(), actual.getCodeSystem());
assertEquals(expected.getDisplay(), actual.getDisplay());
}
case TYPE_GROUP -> {
GroupConceptProperty expected = (GroupConceptProperty) theExpectedProperty;
GroupConceptProperty actual = (GroupConceptProperty) theProperty;
assertEquals(expected.getSubProperties().size(), actual.getSubProperties().size());
range(0, actual.getSubProperties().size()).forEach(i -> assertEqualConceptProperty(expected.getSubProperties().get(i), actual.getSubProperties().get(i)));
}
default -> fail();
}
}
private void assertEqualConceptDesignation(final ConceptDesignation theActualDesignation, final ConceptDesignation theExpectedDesignation) {
assertEquals(theActualDesignation.getValue(), theExpectedDesignation.getValue());
assertEquals(theActualDesignation.getLanguage(), theExpectedDesignation.getLanguage());
assertEquals(theActualDesignation.getUseCode(), theExpectedDesignation.getUseCode());
assertEquals(theActualDesignation.getUseSystem(), theExpectedDesignation.getUseSystem());
assertEquals(theActualDesignation.getUseDisplay(), theExpectedDesignation.getUseDisplay());
}
interface IMyCodeSystemProvider extends IResourceProvider {
String getCode();
@ -220,8 +250,4 @@ public interface ILookupCodeTest {
void setLookupCodeResult(LookupCodeResult theLookupCodeResult);
}
interface IMySimpleCodeSystemProvider extends IResourceProvider {
IBaseDatatype getPropertyValue();
}
}

View File

@ -1,251 +0,0 @@
package org.hl7.fhir.dstu3.hapi.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport.ConceptDesignation;
import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
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.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import jakarta.servlet.http.HttpServletRequest;
import org.hl7.fhir.common.hapi.validation.ILookupCodeTest.ILookupCodeSupportedPropertyTest;
import org.hl7.fhir.common.hapi.validation.ILookupCodeTest.IMyCodeSystemProvider;
import org.hl7.fhir.common.hapi.validation.ILookupCodeTest.IMySimpleCodeSystemProvider;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.dstu3.model.BooleanType;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.CodeType;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.Parameters;
import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.Type;
import org.hl7.fhir.dstu3.model.UriType;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.InstantType;
import org.hl7.fhir.r4.model.IntegerType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.provider.Arguments;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Stream;
import static ca.uhn.fhir.context.support.IValidationSupport.BaseConceptProperty;
import static ca.uhn.fhir.context.support.IValidationSupport.CodingConceptProperty;
import static ca.uhn.fhir.context.support.IValidationSupport.StringConceptProperty;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class LookupCodeDstu3Test {
private static final FhirContext ourCtx = FhirContext.forDstu3Cached();
@RegisterExtension
public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx);
private final RemoteTerminologyServiceValidationSupport mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx);
@BeforeEach
public void before() {
String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort();
mySvc.setBaseUrl(baseUrl);
mySvc.addClientInterceptor(new LoggingInterceptor(true));
}
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Nested
class ILookupCodeSupportedPropertyDstu3Test implements ILookupCodeSupportedPropertyTest {
private final MyCodeSystemProviderDstu3 myCodeSystemProvider = new MyCodeSystemProviderDstu3();
@Override
public IMyCodeSystemProvider getCodeSystemProvider() {
return myCodeSystemProvider;
}
@Override
public RemoteTerminologyServiceValidationSupport getService() {
return mySvc;
}
@BeforeEach
public void before() {
ourRestfulServerExtension.getRestfulServer().registerProvider(myCodeSystemProvider);
}
public void verifyProperty(BaseConceptProperty theConceptProperty, String theExpectedPropertName, IBaseDatatype theExpectedValue) {
assertEquals(theExpectedPropertName, theConceptProperty.getPropertyName());
String type = theConceptProperty.getType();
switch (type) {
case IValidationSupport.TYPE_STRING -> {
if (!(theExpectedValue instanceof StringType stringValue)) {
// TODO: remove this branch to test other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699
IValidationSupport.StringConceptProperty stringConceptProperty = (IValidationSupport.StringConceptProperty) theConceptProperty;
assertEquals(theExpectedValue.toString(), stringConceptProperty.getValue());
break;
}
// StringType stringValue = (StringType) theExpectedValue;
assertTrue(theConceptProperty instanceof StringConceptProperty);
StringConceptProperty stringConceptProperty = (StringConceptProperty) theConceptProperty;
assertEquals(stringValue.getValue(), stringConceptProperty.getValue());
}
case IValidationSupport.TYPE_CODING -> {
assertTrue(theExpectedValue instanceof Coding);
Coding coding = (Coding) theExpectedValue;
assertTrue(theConceptProperty instanceof CodingConceptProperty);
CodingConceptProperty codingConceptProperty = (CodingConceptProperty) theConceptProperty;
assertEquals(coding.getCode(), codingConceptProperty.getCode());
assertEquals(coding.getSystem(), codingConceptProperty.getCodeSystem());
assertEquals(coding.getDisplay(), codingConceptProperty.getDisplay());
}
default -> {
IValidationSupport.StringConceptProperty stringConceptProperty = (IValidationSupport.StringConceptProperty) theConceptProperty;
assertEquals(theExpectedValue.toString(), stringConceptProperty.getValue());
}
}
}
public Stream<Arguments> getEmptyPropertyValues() {
return Stream.of(
Arguments.arguments(new StringType()),
Arguments.arguments(new StringType("")),
Arguments.arguments(new Coding()),
Arguments.arguments(new Coding("", null, null)),
Arguments.arguments(new Coding("", "", null)),
Arguments.arguments(new Coding(null, "", null))
);
}
public Stream<Arguments> getPropertyValues() {
return Stream.of(
// FHIR DSTU3 spec types
Arguments.arguments(new StringType("value")),
Arguments.arguments(new Coding("code", "system", "display")),
Arguments.arguments(new CodeType("code")),
Arguments.arguments(new BooleanType(true)),
Arguments.arguments(new IntegerType(1)),
Arguments.arguments(new DateTimeType(Calendar.getInstance())),
// other types will also not fail for Remote Terminology
Arguments.arguments(new DecimalType(1.1)),
Arguments.arguments(new InstantType(Calendar.getInstance())),
Arguments.arguments(new Type() {
@Override
protected Type typedCopy() {
return this;
}
@Override
public String toString() {
return "randomType";
}
})
);
}
public Stream<Arguments> getDesignations() {
return Stream.of(
Arguments.arguments(new ConceptDesignation().setLanguage("en").setUseCode("code1").setUseSystem("system-1").setUseDisplay("display").setValue("some value")),
Arguments.arguments(new ConceptDesignation().setUseCode("code2").setUseSystem("system1").setUseDisplay("display").setValue("someValue")),
Arguments.arguments(new ConceptDesignation().setUseCode("code2").setUseSystem("system1").setValue("someValue")),
Arguments.arguments(new ConceptDesignation().setUseCode("code2").setUseSystem("system1")),
Arguments.arguments(new ConceptDesignation().setUseCode("code2"))
);
}
}
static class MySimplePropertyCodeSystemProviderDstu3 implements IMySimpleCodeSystemProvider {
String myPropertyName;
Type myPropertyValue;
@Override
public IBaseDatatype getPropertyValue() {
return myPropertyValue;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
@Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= {
@OperationParam(name = "name", type = StringType.class, min = 1),
@OperationParam(name = "version", type = StringType.class, min = 0),
@OperationParam(name = "display", type = StringType.class, min = 1),
@OperationParam(name = "abstract", type = BooleanType.class, min = 1),
@OperationParam(name = "property", min = 0, max = OperationParam.MAX_UNLIMITED)
})
public Parameters lookup(
HttpServletRequest theServletRequest,
@OperationParam(name = "code", max = 1) CodeType theCode,
@OperationParam(name = "system", max = 1) UriType theSystem,
@OperationParam(name = "coding", max = 1) Coding theCoding,
@OperationParam(name = "version", max = 1) StringType theVersion,
@OperationParam(name = "displayLanguage", max = 1) CodeType theDisplayLanguage,
@OperationParam(name= " property", max = OperationParam.MAX_UNLIMITED) List<CodeType> thePropertyNames,
RequestDetails theRequestDetails
) {
ParametersParameterComponent component = new ParametersParameterComponent();
component.setName("property");
component.addPart(new ParametersParameterComponent().setName("code").setValue(new CodeType(myPropertyName)));
component.addPart(new ParametersParameterComponent().setName("value").setValue(myPropertyValue));
return new Parameters().addParameter(component);
}
}
static class MyCodeSystemProviderDstu3 implements IMyCodeSystemProvider {
private UriType mySystemUrl;
private CodeType myCode;
private LookupCodeResult myLookupCodeResult;
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
@Override
public String getCode() {
return myCode != null ? myCode.getValueAsString() : null;
}
@Override
public String getSystem() {
return mySystemUrl != null ? mySystemUrl.getValueAsString() : null;
}
@Override
public void setLookupCodeResult(LookupCodeResult theLookupCodeResult) {
myLookupCodeResult = theLookupCodeResult;
}
@Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= {
@OperationParam(name = "name", type = StringType.class, min = 1),
@OperationParam(name = "version", type = StringType.class, min = 0),
@OperationParam(name = "display", type = StringType.class, min = 1),
@OperationParam(name = "abstract", type = BooleanType.class, min = 1),
@OperationParam(name = "property", min = 0, max = OperationParam.MAX_UNLIMITED)
})
public Parameters lookup(
HttpServletRequest theServletRequest,
@OperationParam(name = "code", max = 1) CodeType theCode,
@OperationParam(name = "system", max = 1) UriType theSystem,
@OperationParam(name = "coding", max = 1) Coding theCoding,
@OperationParam(name = "version", max = 1) StringType theVersion,
@OperationParam(name = "displayLanguage", max = 1) CodeType theDisplayLanguage,
@OperationParam(name= " property", max = OperationParam.MAX_UNLIMITED) List<CodeType> thePropertyNames,
RequestDetails theRequestDetails
) {
myCode = theCode;
mySystemUrl = theSystem;
return (Parameters)myLookupCodeResult.toParameters(theRequestDetails.getFhirContext(), thePropertyNames);
}
}
}

View File

@ -0,0 +1,202 @@
package org.hl7.fhir.dstu3.hapi.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport.ConceptDesignation;
import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
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.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import jakarta.servlet.http.HttpServletRequest;
import org.hl7.fhir.common.hapi.validation.ILookupCodeTest;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.dstu3.model.BooleanType;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.CodeType;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.Type;
import org.hl7.fhir.dstu3.model.UriType;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.InstantType;
import org.hl7.fhir.r4.model.IntegerType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Stream;
/**
* Version specific tests for CodeSystem $lookup against RemoteTerminologyValidationSupport.
* @see RemoteTerminologyServiceValidationSupport
*/
public class RemoteTerminologyLookupCodeDstu3Test implements ILookupCodeTest {
private static final FhirContext ourCtx = FhirContext.forDstu3Cached();
@RegisterExtension
public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx);
private final RemoteTerminologyServiceValidationSupport mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx);
@BeforeEach
public void before() {
String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort();
mySvc.setBaseUrl(baseUrl);
mySvc.addClientInterceptor(new LoggingInterceptor(true));
ourRestfulServerExtension.getRestfulServer().registerProvider(myCodeSystemProvider);
}
private final MyCodeSystemProviderDstu3 myCodeSystemProvider = new MyCodeSystemProviderDstu3();
@Override
public IMyCodeSystemProvider getCodeSystemProvider() {
return myCodeSystemProvider;
}
@Override
public RemoteTerminologyServiceValidationSupport getService() {
return mySvc;
}
public static Stream<Arguments> getEmptyPropertyValues() {
return Stream.of(
Arguments.of(new StringType()),
Arguments.of(new StringType("")),
Arguments.of(new Coding()),
Arguments.of(new Coding("", null, null)),
Arguments.of(new Coding("", "", null)),
Arguments.of(new Coding(null, "", null))
);
}
@ParameterizedTest
@MethodSource(value = "getEmptyPropertyValues")
public void lookupCode_forCodeSystemWithPropertyEmptyValue_returnsCorrectParameters(IBaseDatatype thePropertyValue) {
verifyLookupWithEmptyPropertyValue(thePropertyValue);
}
public static Stream<Arguments> getPropertyValueArguments() {
return Stream.of(
// FHIR DSTU3 spec types
Arguments.of(new StringType("value")),
Arguments.of(new Coding("code", "system", "display")),
Arguments.of(new CodeType("code")),
Arguments.of(new BooleanType(true)),
Arguments.of(new IntegerType(1)),
Arguments.of(new DateTimeType(Calendar.getInstance())),
// other types will also not fail for Remote Terminology
Arguments.of(new DecimalType(1.1)),
Arguments.of(new InstantType(Calendar.getInstance())),
Arguments.of(new Type() {
@Override
protected Type typedCopy() {
return this;
}
@Override
public String toString() {
return "randomType";
}
})
);
}
public static Stream<Arguments> getPropertyValueListArguments() {
return Stream.of(
Arguments.of(List.of(new StringType("value1")), new StringType("value2")),
Arguments.of(List.of(new StringType("value1")), new Coding("code", "system", "display"))
);
}
@ParameterizedTest
@MethodSource(value = "getPropertyValueArguments")
public void lookupCode_forCodeSystemWithProperty_returnsCorrectProperty(IBaseDatatype thePropertyValue) {
verifyLookupWithProperty(List.of(thePropertyValue), List.of());
}
@ParameterizedTest
@MethodSource(value = "getPropertyValueListArguments")
public void lookupCode_forCodeSystemWithPropertyFilter_returnsCorrectProperty(List<IBaseDatatype> thePropertyValues) {
verifyLookupWithProperty(thePropertyValues, List.of());
verifyLookupWithProperty(thePropertyValues, List.of(thePropertyValues.size() - 1));
}
@ParameterizedTest
@MethodSource(value = "getPropertyValueListArguments")
public void lookupCode_forCodeSystemWithPropertyGroup_returnsCorrectProperty(List<IBaseDatatype> thePropertyValues) {
verifyLookupWithSubProperties(thePropertyValues);
}
public static Stream<Arguments> getDesignations() {
return Stream.of(
Arguments.of(new ConceptDesignation().setLanguage("en").setUseCode("code1").setUseSystem("system-1").setUseDisplay("display").setValue("some value")),
Arguments.of(new ConceptDesignation().setUseCode("code2").setUseSystem("system1").setUseDisplay("display").setValue("someValue")),
Arguments.of(new ConceptDesignation().setUseCode("code2").setUseSystem("system1").setValue("someValue")),
Arguments.of(new ConceptDesignation().setUseCode("code2").setUseSystem("system1")),
Arguments.of(new ConceptDesignation().setUseCode("code2"))
);
}
@ParameterizedTest
@MethodSource(value = "getDesignations")
void lookupCode_withCodeSystemWithDesignation_returnsCorrectDesignation(final IValidationSupport.ConceptDesignation theConceptDesignation) {
verifyLookupWithConceptDesignation(theConceptDesignation);
}
static class MyCodeSystemProviderDstu3 implements IMyCodeSystemProvider {
private UriType mySystemUrl;
private CodeType myCode;
private LookupCodeResult myLookupCodeResult;
@Override
public void setLookupCodeResult(LookupCodeResult theLookupCodeResult) {
myLookupCodeResult = theLookupCodeResult;
}
@Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= {
@OperationParam(name = "name", type = StringType.class, min = 1),
@OperationParam(name = "version", type = StringType.class, min = 0),
@OperationParam(name = "display", type = StringType.class, min = 1),
@OperationParam(name = "abstract", type = BooleanType.class, min = 1),
@OperationParam(name = "property", min = 0, max = OperationParam.MAX_UNLIMITED)
})
public IBaseParameters lookup(
HttpServletRequest theServletRequest,
@OperationParam(name = "code", max = 1) CodeType theCode,
@OperationParam(name = "system", max = 1) UriType theSystem,
@OperationParam(name = "coding", max = 1) Coding theCoding,
@OperationParam(name = "version", max = 1) StringType theVersion,
@OperationParam(name = "displayLanguage", max = 1) CodeType theDisplayLanguage,
@OperationParam(name= " property", max = OperationParam.MAX_UNLIMITED) List<StringType> thePropertyNames,
RequestDetails theRequestDetails
) {
myCode = theCode;
mySystemUrl = theSystem;
return myLookupCodeResult.toParameters(theRequestDetails.getFhirContext(), thePropertyNames);
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
@Override
public String getCode() {
return myCode != null ? myCode.getValueAsString() : null;
}
@Override
public String getSystem() {
return mySystemUrl != null ? mySystemUrl.getValueAsString() : null;
}
}
}

View File

@ -0,0 +1,294 @@
package org.hl7.fhir.dstu3.hapi.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.LookupCodeRequest;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
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.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.ClasspathUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.dstu3.model.BooleanType;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.CodeType;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.Parameters;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.Type;
import org.hl7.fhir.dstu3.model.UriType;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import java.util.List;
import java.util.Optional;
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;
/**
* Version specific tests for validation using RemoteTerminologyValidationSupport.
* The tests in this class simulate the call to a remote server and therefore, only tests the code in
* the RemoteTerminologyServiceValidationSupport itself. The remote client call is simulated using the test providers.
* @see RemoteTerminologyServiceValidationSupport
*
* Other operations are tested separately.
* @see RemoteTerminologyLookupCodeDstu3Test
*/
public class RemoteTerminologyServiceResourceProviderDstu3Test {
private static final String DISPLAY = "DISPLAY";
private static final String CODE_SYSTEM = "CODE_SYS";
private static final String CODE = "CODE";
private static final String VALUE_SET_URL = "http://value.set/url";
private static final String SAMPLE_MESSAGE = "This is a sample message";
private static final FhirContext ourCtx = FhirContext.forDstu3Cached();
private static final MyCodeSystemProvider ourCodeSystemProvider = new MyCodeSystemProvider();
private static final MyValueSetProvider ourValueSetProvider = new MyValueSetProvider();
@RegisterExtension
public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx, ourCodeSystemProvider,
ourValueSetProvider);
private RemoteTerminologyServiceValidationSupport mySvc;
@BeforeEach
public void before_ConfigureService() {
String myBaseUrl = "http://localhost:" + ourRestfulServerExtension.getPort();
mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx, myBaseUrl);
mySvc.addClientInterceptor(new LoggingInterceptor(false).setLogRequestSummary(true).setLogResponseSummary(true));
}
@AfterEach
public void after_UnregisterProviders() {
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.ONCE);
ourRestfulServerExtension.getRestfulServer().getInterceptorService().unregisterAllInterceptors();
}
@Test
public void testValidateCodeInCodeSystem_BlankCode_ReturnsNull() {
IValidationSupport.CodeValidationResult outcome = mySvc
.validateCode(null, null, CODE_SYSTEM, null, DISPLAY, null);
assertNull(outcome);
}
@Test
public void testValidateCodeInCodeSystem_ProvidingMinimalInputs_ReturnsSuccess() {
createNextCodeSystemReturnParameters(true, null, null);
IValidationSupport.CodeValidationResult outcome = mySvc
.validateCode(null, null, CODE_SYSTEM, CODE, null, null);
assertNotNull(outcome);
assertEquals(CODE, outcome.getCode());
assertNull(outcome.getSeverity());
assertNull(outcome.getMessage());
assertEquals(CODE, ourCodeSystemProvider.myLastCode.getValue());
assertEquals(CODE_SYSTEM, ourCodeSystemProvider.myLastUrl.getValueAsString());
}
@Test
public void testValidateCodeInCodeSystem_WithMessageValue_ReturnsMessage() {
createNextCodeSystemReturnParameters(true, DISPLAY, SAMPLE_MESSAGE);
IValidationSupport.CodeValidationResult outcome = mySvc
.validateCode(null, null, CODE_SYSTEM, CODE, DISPLAY, null);
assertNotNull(outcome);
assertEquals(CODE, outcome.getCode());
assertEquals(DISPLAY, outcome.getDisplay());
assertNull(outcome.getSeverity());
assertNull(outcome.getMessage());
assertEquals(CODE, ourCodeSystemProvider.myLastCode.getValue());
assertEquals(DISPLAY, ourCodeSystemProvider.myLastDisplay.getValue());
assertEquals(CODE_SYSTEM, ourCodeSystemProvider.myLastUrl.getValueAsString());
assertEquals(SAMPLE_MESSAGE, getParameterValue(ourCodeSystemProvider.myNextReturnParams, "message").toString());
}
@Test
public void testValidateCodeInCodeSystem_AssumeFailure_ReturnsFailureCodeAndFailureMessage() {
createNextCodeSystemReturnParameters(false, null, SAMPLE_MESSAGE);
IValidationSupport.CodeValidationResult outcome = mySvc
.validateCode(null, null, CODE_SYSTEM, CODE, null, null);
assertNotNull(outcome);
assertEquals(IValidationSupport.IssueSeverity.ERROR, outcome.getSeverity());
assertEquals(SAMPLE_MESSAGE, outcome.getMessage());
assertFalse(((BooleanType) getParameterValue(ourCodeSystemProvider.myNextReturnParams, "result")).booleanValue());
}
@Test
public void testValidateCodeInValueSet_ProvidingMinimalInputs_ReturnsSuccess() {
ourValueSetProvider.myNextReturnParams = new Parameters();
ourValueSetProvider.myNextReturnParams.addParameter().setName("result").setValue(new BooleanType(true));
IValidationSupport.CodeValidationResult outcome = mySvc
.validateCode(null, null, CODE_SYSTEM, CODE, null, VALUE_SET_URL);
assertNotNull(outcome);
assertEquals(CODE, outcome.getCode());
assertNull(outcome.getSeverity());
assertNull(outcome.getMessage());
assertEquals(CODE, ourValueSetProvider.myLastCode.getValue());
assertEquals(VALUE_SET_URL, ourValueSetProvider.myLastUrl.getValueAsString());
}
@Test
public void testValidateCodeInValueSet_WithMessageValue_ReturnsMessage() {
ourValueSetProvider.myNextReturnParams = new Parameters();
ourValueSetProvider.myNextReturnParams.addParameter().setName("result").setValue(new BooleanType(true));
ourValueSetProvider.myNextReturnParams.addParameter().setName("display").setValue(new StringType(DISPLAY));
ourValueSetProvider.myNextReturnParams.addParameter().setName("message").setValue(new StringType(SAMPLE_MESSAGE));
IValidationSupport.CodeValidationResult outcome = mySvc
.validateCode(null, null, CODE_SYSTEM, CODE, DISPLAY, VALUE_SET_URL);
assertNotNull(outcome);
assertEquals(CODE, outcome.getCode());
assertEquals(DISPLAY, outcome.getDisplay());
assertNull(outcome.getSeverity());
assertNull(outcome.getMessage());
assertEquals(CODE, ourValueSetProvider.myLastCode.getValue());
assertEquals(DISPLAY, ourValueSetProvider.myLastDisplay.getValue());
assertEquals(VALUE_SET_URL, ourValueSetProvider.myLastUrl.getValueAsString());
assertEquals(SAMPLE_MESSAGE, getParameterValue(ourValueSetProvider.myNextReturnParams, "message").toString());
}
@Test
public void lookupCode_withParametersOutput_convertsCorrectly() {
String paramsAsString = ClasspathUtil.loadResource("/r4/CodeSystem-lookup-output-with-subproperties.json");
IBaseResource baseResource = ourCtx.newJsonParser().parseResource(paramsAsString);
assertTrue(baseResource instanceof Parameters);
Parameters resultParameters = (Parameters) baseResource;
ourCodeSystemProvider.myNextReturnParams = resultParameters;
LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, null, List.of("interfaces"));
// test
IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, request);
assertNotNull(outcome);
IBaseParameters theActualParameters = outcome.toParameters(ourCtx, request.getPropertyNames().stream().map(StringType::new).toList());
String actual = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(theActualParameters);
String expected = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resultParameters);
assertEquals(expected, actual);
}
private void createNextCodeSystemReturnParameters(boolean theResult, String theDisplay, String theMessage) {
ourCodeSystemProvider.myNextReturnParams = new Parameters();
ourCodeSystemProvider.myNextReturnParams.addParameter().setName("result").setValue(new BooleanType(theResult));
ourCodeSystemProvider.myNextReturnParams.addParameter().setName("display").setValue(new StringType(theDisplay));
if (theMessage != null) {
ourCodeSystemProvider.myNextReturnParams.addParameter().setName("message").setValue(new StringType(theMessage));
}
}
private Type getParameterValue(Parameters theParameters, String theParameterName) {
Optional<Parameters.ParametersParameterComponent> paramOpt = theParameters.getParameter()
.stream().filter(param -> param.getName().equals(theParameterName)).findFirst();
assertTrue(paramOpt.isPresent());
return paramOpt.get().getValue();
}
private static class MyCodeSystemProvider implements IResourceProvider {
private UriType myLastUrl;
private CodeType myLastCode;
private StringType myLastDisplay;
private Parameters myNextReturnParams;
@Operation(name = "validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = BooleanType.class, min = 1),
@OperationParam(name = "message", type = StringType.class),
@OperationParam(name = "display", type = StringType.class)
})
public Parameters validateCode(
HttpServletRequest theServletRequest,
@IdParam(optional = true) IdType theId,
@OperationParam(name = "url", min = 0, max = 1) UriType theCodeSystemUrl,
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay
) {
myLastUrl = theCodeSystemUrl;
myLastCode = theCode;
myLastDisplay = theDisplay;
return myNextReturnParams;
}
@Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= {
@OperationParam(name = "name", type = StringType.class, min = 1),
@OperationParam(name = "version", type = StringType.class),
@OperationParam(name = "display", type = StringType.class, min = 1),
@OperationParam(name = "abstract", type = BooleanType.class, min = 1),
@OperationParam(name = "property", type = StringType.class, min = 0, max = OperationParam.MAX_UNLIMITED)
})
public IBaseParameters lookup(
HttpServletRequest theServletRequest,
@OperationParam(name = "code", max = 1) CodeType theCode,
@OperationParam(name = "system",max = 1) UriType theSystem,
@OperationParam(name = "coding", max = 1) Coding theCoding,
@OperationParam(name = "version", max = 1) StringType theVersion,
@OperationParam(name = "displayLanguage", max = 1) CodeType theDisplayLanguage,
@OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List<CodeType> thePropertyNames,
RequestDetails theRequestDetails
) {
myLastCode = theCode;
return myNextReturnParams;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
}
private static class MyValueSetProvider implements IResourceProvider {
private Parameters myNextReturnParams;
private UriType myLastUrl;
private CodeType myLastCode;
private StringType myLastDisplay;
@Operation(name = "validate-code", idempotent = true, returnParameters = {
@OperationParam(name = "result", type = BooleanType.class, min = 1),
@OperationParam(name = "message", type = StringType.class),
@OperationParam(name = "display", type = StringType.class)
})
public Parameters validateCode(
HttpServletRequest theServletRequest,
@IdParam(optional = true) IdType theId,
@OperationParam(name = "url", min = 0, max = 1) UriType theValueSetUrl,
@OperationParam(name = "code", min = 0, max = 1) CodeType theCode,
@OperationParam(name = "system", min = 0, max = 1) UriType theSystem,
@OperationParam(name = "display", min = 0, max = 1) StringType theDisplay,
@OperationParam(name = "valueSet") ValueSet theValueSet
) {
myLastUrl = theValueSetUrl;
myLastCode = theCode;
myLastDisplay = theDisplay;
return myNextReturnParams;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return ValueSet.class;
}
}
}

View File

@ -1,242 +0,0 @@
package org.hl7.fhir.r4.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
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.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import jakarta.servlet.http.HttpServletRequest;
import org.hl7.fhir.common.hapi.validation.ILookupCodeTest.ILookupCodeSupportedPropertyTest;
import org.hl7.fhir.common.hapi.validation.ILookupCodeTest.IMyCodeSystemProvider;
import org.hl7.fhir.common.hapi.validation.ILookupCodeTest.IMySimpleCodeSystemProvider;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeType;
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.InstantType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Type;
import org.hl7.fhir.r4.model.UriType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.provider.Arguments;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class LookupCodeR4Test {
private static final FhirContext ourCtx = FhirContext.forR4Cached();
@RegisterExtension
public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx);
private final RemoteTerminologyServiceValidationSupport mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx);
@BeforeEach
public void before() {
String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort();
mySvc.setBaseUrl(baseUrl);
mySvc.addClientInterceptor(new LoggingInterceptor(true));
}
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Nested
class ILookupCodeSupportedPropertyR4Test implements ILookupCodeSupportedPropertyTest {
private final MyCodeSystemProviderR4 myCodeSystemProvider = new MyCodeSystemProviderR4();
@Override
public IMyCodeSystemProvider getCodeSystemProvider() {
return myCodeSystemProvider;
}
@Override
public RemoteTerminologyServiceValidationSupport getService() {
return mySvc;
}
@BeforeEach
public void before() {
ourRestfulServerExtension.getRestfulServer().registerProvider(myCodeSystemProvider);
}
@Override
public void verifyProperty(IValidationSupport.BaseConceptProperty theConceptProperty, String theExpectedPropertName, IBaseDatatype theExpectedValue) {
assertEquals(theExpectedPropertName, theConceptProperty.getPropertyName());
String type = theConceptProperty.getType();
switch (type) {
case IValidationSupport.TYPE_STRING -> {
if (!(theExpectedValue instanceof StringType stringValue)) {
// TODO: remove this branch to test other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699
IValidationSupport.StringConceptProperty stringConceptProperty = (IValidationSupport.StringConceptProperty) theConceptProperty;
assertEquals(theExpectedValue.toString(), stringConceptProperty.getValue());
break;
}
// StringType stringValue = (StringType) theExpectedValue;
assertTrue(theConceptProperty instanceof IValidationSupport.StringConceptProperty);
IValidationSupport.StringConceptProperty stringConceptProperty = (IValidationSupport.StringConceptProperty) theConceptProperty;
assertEquals(stringValue.getValue(), stringConceptProperty.getValue());
}
case IValidationSupport.TYPE_CODING -> {
assertTrue(theExpectedValue instanceof Coding);
Coding coding = (Coding) theExpectedValue;
assertTrue(theConceptProperty instanceof IValidationSupport.CodingConceptProperty);
IValidationSupport.CodingConceptProperty codingConceptProperty = (IValidationSupport.CodingConceptProperty) theConceptProperty;
assertEquals(coding.getCode(), codingConceptProperty.getCode());
assertEquals(coding.getSystem(), codingConceptProperty.getCodeSystem());
assertEquals(coding.getDisplay(), codingConceptProperty.getDisplay());
}
default -> {
IValidationSupport.StringConceptProperty stringConceptProperty = (IValidationSupport.StringConceptProperty) theConceptProperty;
assertEquals(theExpectedValue.toString(), stringConceptProperty.getValue());
}
}
}
public Stream<Arguments> getEmptyPropertyValues() {
return Stream.of(
Arguments.arguments(new StringType()),
Arguments.arguments(new StringType("")),
Arguments.arguments(new Coding()),
Arguments.arguments(new Coding("", null, null)),
Arguments.arguments(new Coding("", "", null)),
Arguments.arguments(new Coding(null, "", null))
);
}
public Stream<Arguments> getPropertyValues() {
return Stream.of(
// FHIR R4 spec types
Arguments.arguments(new StringType("value")),
Arguments.arguments(new Coding("code", "system", "display")),
Arguments.arguments(new CodeType("code")),
Arguments.arguments(new BooleanType(true)),
Arguments.arguments(new IntegerType(1)),
Arguments.arguments(new DateTimeType(Calendar.getInstance())),
Arguments.arguments(new DecimalType(1.1)),
// other types will also not fail for Remote Terminology
Arguments.arguments(new InstantType(Calendar.getInstance())),
Arguments.arguments(new Type() {
@Override
protected Type typedCopy() {
return this;
}
@Override
public String toString() {
return "randomType";
}
})
);
}
public Stream<Arguments> getDesignations() {
return Stream.of(
Arguments.arguments(new IValidationSupport.ConceptDesignation().setLanguage("en").setUseCode("code1").setUseSystem("system-1").setUseDisplay("display").setValue("some value")),
Arguments.arguments(new IValidationSupport.ConceptDesignation().setUseCode("code2").setUseSystem("system1").setUseDisplay("display").setValue("someValue")),
Arguments.arguments(new IValidationSupport.ConceptDesignation().setUseCode("code2").setUseSystem("system1").setValue("someValue")),
Arguments.arguments(new IValidationSupport.ConceptDesignation().setUseCode("code2").setUseSystem("system1")),
Arguments.arguments(new IValidationSupport.ConceptDesignation().setUseCode("code2"))
);
}
}
static class MySimplePropertyCodeSystemProviderR4 implements IMySimpleCodeSystemProvider {
String myPropertyName;
Type myPropertyValue;
@Override
public IBaseDatatype getPropertyValue() {
return myPropertyValue;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
@Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= {
@OperationParam(name = "name", type = StringType.class, min = 1),
@OperationParam(name = "version", type = StringType.class, min = 0),
@OperationParam(name = "display", type = StringType.class, min = 1),
@OperationParam(name = "abstract", type = BooleanType.class, min = 1),
@OperationParam(name = "property", min = 0, max = OperationParam.MAX_UNLIMITED)
})
public Parameters lookup(
HttpServletRequest theServletRequest,
@OperationParam(name = "code", max = 1) CodeType theCode,
@OperationParam(name = "system", max = 1) UriType theSystem,
@OperationParam(name = "coding", max = 1) Coding theCoding,
@OperationParam(name = "version", max = 1) StringType theVersion,
@OperationParam(name = "displayLanguage", max = 1) CodeType theDisplayLanguage,
@OperationParam(name= " property", max = OperationParam.MAX_UNLIMITED) List<CodeType> thePropertyNames,
RequestDetails theRequestDetails
) {
Parameters.ParametersParameterComponent component = new Parameters.ParametersParameterComponent();
component.setName("property");
component.addPart(new Parameters.ParametersParameterComponent().setName("code").setValue(new CodeType(myPropertyName)));
component.addPart(new Parameters.ParametersParameterComponent().setName("value").setValue(myPropertyValue));
return new Parameters().addParameter(component);
}
}
static class MyCodeSystemProviderR4 implements IMyCodeSystemProvider {
private UriType mySystemUrl;
private CodeType myCode;
private IValidationSupport.LookupCodeResult myLookupCodeResult;
@Override
public void setLookupCodeResult(IValidationSupport.LookupCodeResult theLookupCodeResult) {
myLookupCodeResult = theLookupCodeResult;
}
@Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= {
@OperationParam(name = "name", type = StringType.class, min = 1),
@OperationParam(name = "version", type = StringType.class),
@OperationParam(name = "display", type = StringType.class, min = 1),
@OperationParam(name = "abstract", type = BooleanType.class, min = 1),
@OperationParam(name = "property", type = StringType.class, min = 0, max = OperationParam.MAX_UNLIMITED)
})
public IBaseParameters lookup(
HttpServletRequest theServletRequest,
@OperationParam(name = "code", max = 1) CodeType theCode,
@OperationParam(name = "system",max = 1) UriType theSystem,
@OperationParam(name = "coding", max = 1) Coding theCoding,
@OperationParam(name = "version", max = 1) StringType theVersion,
@OperationParam(name = "displayLanguage", max = 1) CodeType theDisplayLanguage,
@OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List<StringType> thePropertyNames,
RequestDetails theRequestDetails
) {
myCode = theCode;
mySystemUrl = theSystem;
return myLookupCodeResult.toParameters(theRequestDetails.getFhirContext(), thePropertyNames);
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
@Override
public String getCode() {
return myCode != null ? myCode.getValueAsString() : null;
}
@Override
public String getSystem() {
return mySystemUrl != null ? mySystemUrl.getValueAsString() : null;
}
}
}

View File

@ -0,0 +1,201 @@
package org.hl7.fhir.r4.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
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.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import jakarta.servlet.http.HttpServletRequest;
import org.hl7.fhir.common.hapi.validation.ILookupCodeTest;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseDatatype;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeType;
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.InstantType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Type;
import org.hl7.fhir.r4.model.UriType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Stream;
import static ca.uhn.fhir.context.support.IValidationSupport.ConceptDesignation;
import static ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult;
/**
* Version specific tests for CodeSystem $lookup against RemoteTerminologyValidationSupport.
* @see RemoteTerminologyServiceValidationSupport
*/
public class RemoteTerminologyLookupCodeR4Test implements ILookupCodeTest {
private static final FhirContext ourCtx = FhirContext.forR4Cached();
@RegisterExtension
public static RestfulServerExtension ourRestfulServerExtension = new RestfulServerExtension(ourCtx);
private final RemoteTerminologyServiceValidationSupport mySvc = new RemoteTerminologyServiceValidationSupport(ourCtx);
private final MyCodeSystemProviderR4 myCodeSystemProvider = new MyCodeSystemProviderR4();
@BeforeEach
public void before() {
String baseUrl = "http://localhost:" + ourRestfulServerExtension.getPort();
mySvc.setBaseUrl(baseUrl);
mySvc.addClientInterceptor(new LoggingInterceptor(true));
ourRestfulServerExtension.getRestfulServer().registerProvider(myCodeSystemProvider);
}
@Override
public IMyCodeSystemProvider getCodeSystemProvider() {
return myCodeSystemProvider;
}
@Override
public RemoteTerminologyServiceValidationSupport getService() {
return mySvc;
}
public static Stream<Arguments> getEmptyPropertyValues() {
return Stream.of(
Arguments.of(new StringType()),
Arguments.of(new StringType("")),
Arguments.of(new Coding()),
Arguments.of(new Coding("", null, null)),
Arguments.of(new Coding("", "", null)),
Arguments.of(new Coding(null, "", null))
);
}
@ParameterizedTest
@MethodSource(value = "getEmptyPropertyValues")
public void lookupCode_forCodeSystemWithPropertyEmptyValue_returnsCorrectParameters(IBaseDatatype thePropertyValue) {
verifyLookupWithEmptyPropertyValue(thePropertyValue);
}
public static Stream<Arguments> getPropertyValueArguments() {
return Stream.of(
// FHIR R4 spec types
Arguments.of(new StringType("test-value")),
Arguments.of(new Coding("test-system", "test-code", "test-display")),
Arguments.of(new CodeType("test-code")),
Arguments.of(new BooleanType(true)),
Arguments.of(new IntegerType(1)),
Arguments.of(new DateTimeType(Calendar.getInstance())),
Arguments.of(new DecimalType(1.1)),
// other types will also not fail for Remote Terminology
Arguments.of(new InstantType(Calendar.getInstance())),
Arguments.of(new Type() {
@Override
protected Type typedCopy() {
return this;
}
@Override
public String toString() {
return "randomType";
}
})
);
}
public static Stream<Arguments> getPropertyValueListArguments() {
return Stream.of(
Arguments.of(List.of(new StringType("value1")), new StringType("value2")),
Arguments.of(List.of(new StringType("value1")), new Coding("code", "system", "display"))
);
}
@ParameterizedTest
@MethodSource(value = "getPropertyValueArguments")
public void lookupCode_forCodeSystemWithProperty_returnsCorrectProperty(IBaseDatatype thePropertyValue) {
verifyLookupWithProperty(List.of(thePropertyValue), List.of());
}
@ParameterizedTest
@MethodSource(value = "getPropertyValueListArguments")
public void lookupCode_forCodeSystemWithPropertyFilter_returnsCorrectProperty(List<IBaseDatatype> thePropertyValues) {
verifyLookupWithProperty(thePropertyValues, List.of());
verifyLookupWithProperty(thePropertyValues, List.of(thePropertyValues.size() - 1));
}
@ParameterizedTest
@MethodSource(value = "getPropertyValueListArguments")
public void lookupCode_forCodeSystemWithPropertyGroup_returnsCorrectProperty(List<IBaseDatatype> thePropertyValues) {
verifyLookupWithSubProperties(thePropertyValues);
}
public static Stream<Arguments> getDesignations() {
return Stream.of(
Arguments.of(new ConceptDesignation().setLanguage("en").setUseCode("code1").setUseSystem("system-1").setUseDisplay("display").setValue("some value")),
Arguments.of(new ConceptDesignation().setUseCode("code2").setUseSystem("system1").setUseDisplay("display").setValue("someValue")),
Arguments.of(new ConceptDesignation().setUseCode("code2").setUseSystem("system1").setValue("someValue")),
Arguments.of(new ConceptDesignation().setUseCode("code2").setUseSystem("system1")),
Arguments.of(new ConceptDesignation().setUseCode("code2"))
);
}
@ParameterizedTest
@MethodSource(value = "getDesignations")
void lookupCode_withCodeSystemWithDesignation_returnsCorrectDesignation(final IValidationSupport.ConceptDesignation theConceptDesignation) {
verifyLookupWithConceptDesignation(theConceptDesignation);
}
static class MyCodeSystemProviderR4 implements IMyCodeSystemProvider {
private UriType mySystemUrl;
private CodeType myCode;
private LookupCodeResult myLookupCodeResult;
@Override
public void setLookupCodeResult(LookupCodeResult theLookupCodeResult) {
myLookupCodeResult = theLookupCodeResult;
}
@Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= {
@OperationParam(name = "name", type = StringType.class, min = 1),
@OperationParam(name = "version", type = StringType.class),
@OperationParam(name = "display", type = StringType.class, min = 1),
@OperationParam(name = "abstract", type = BooleanType.class, min = 1),
@OperationParam(name = "property", type = StringType.class, min = 0, max = OperationParam.MAX_UNLIMITED)
})
public IBaseParameters lookup(
HttpServletRequest theServletRequest,
@OperationParam(name = "code", max = 1) CodeType theCode,
@OperationParam(name = "system",max = 1) UriType theSystem,
@OperationParam(name = "coding", max = 1) Coding theCoding,
@OperationParam(name = "version", max = 1) StringType theVersion,
@OperationParam(name = "displayLanguage", max = 1) CodeType theDisplayLanguage,
@OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List<CodeType> thePropertyNames,
RequestDetails theRequestDetails
) {
myCode = theCode;
mySystemUrl = theSystem;
return myLookupCodeResult.toParameters(theRequestDetails.getFhirContext(), thePropertyNames);
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;
}
@Override
public String getCode() {
return myCode != null ? myCode.getValueAsString() : null;
}
@Override
public String getSystem() {
return mySystemUrl != null ? mySystemUrl.getValueAsString() : null;
}
}
}

View File

@ -1,20 +1,26 @@
package ca.uhn.fhir.jpa.provider.r4;
package org.hl7.fhir.r4.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.LookupCodeRequest;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
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.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.ClasspathUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.StringType;
@ -25,15 +31,23 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import java.util.List;
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;
/*
* This set of Unit Tests simulates the call to a remote server and therefore, only tests the code in the
* {@link org.hl7.fhir.common.hapi.validation.support.RemoteTerminologyServiceValidationSupport#invokeRemoteValidateCode}
* method, before and after it makes that remote client call.
/**
* Version specific tests for validation using RemoteTerminologyValidationSupport.
* The tests in this class simulate the call to a remote server and therefore, only tests the code in
* the RemoteTerminologyServiceValidationSupport itself. The remote client call is simulated using the test providers.
* @see RemoteTerminologyServiceValidationSupport
*
* Other operations are tested separately.
* @see RemoteTerminologyLookupCodeR4Test
* @see RemoteTerminologyServiceValidationSupportR4Test
*/
public class RemoteTerminologyServiceResourceProviderR4Test {
private static final String DISPLAY = "DISPLAY";
@ -152,6 +166,27 @@ public class RemoteTerminologyServiceResourceProviderR4Test {
assertEquals(SAMPLE_MESSAGE, ourValueSetProvider.myNextReturnParams.getParameterValue("message").toString());
}
@Test
public void lookupCode_withParametersOutput_convertsCorrectly() {
String paramsAsString = ClasspathUtil.loadResource("/r4/CodeSystem-lookup-output-with-subproperties.json");
IBaseResource baseResource = ourCtx.newJsonParser().parseResource(paramsAsString);
assertTrue(baseResource instanceof Parameters);
Parameters resultParameters = (Parameters) baseResource;
ourCodeSystemProvider.myNextReturnParams = resultParameters;
LookupCodeRequest request = new LookupCodeRequest(CODE_SYSTEM, CODE, null, List.of("interfaces"));
// test
IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(null, request);
assertNotNull(outcome);
IBaseParameters theActualParameters = outcome.toParameters(ourCtx, request.getPropertyNames().stream().map(StringType::new).toList());
String actual = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(theActualParameters);
String expected = ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resultParameters);
assertEquals(expected, actual);
}
private void createNextCodeSystemReturnParameters(boolean theResult, String theDisplay, String theMessage) {
ourCodeSystemProvider.myNextReturnParams = new Parameters();
ourCodeSystemProvider.myNextReturnParams.addParameter("result", theResult);
@ -186,6 +221,27 @@ public class RemoteTerminologyServiceResourceProviderR4Test {
}
@Operation(name = JpaConstants.OPERATION_LOOKUP, idempotent = true, returnParameters= {
@OperationParam(name = "name", type = StringType.class, min = 1),
@OperationParam(name = "version", type = StringType.class),
@OperationParam(name = "display", type = StringType.class, min = 1),
@OperationParam(name = "abstract", type = BooleanType.class, min = 1),
@OperationParam(name = "property", type = StringType.class, min = 0, max = OperationParam.MAX_UNLIMITED)
})
public IBaseParameters lookup(
HttpServletRequest theServletRequest,
@OperationParam(name = "code", max = 1) CodeType theCode,
@OperationParam(name = "system",max = 1) UriType theSystem,
@OperationParam(name = "coding", max = 1) Coding theCoding,
@OperationParam(name = "version", max = 1) StringType theVersion,
@OperationParam(name = "displayLanguage", max = 1) CodeType theDisplayLanguage,
@OperationParam(name = "property", max = OperationParam.MAX_UNLIMITED) List<CodeType> thePropertyNames,
RequestDetails theRequestDetails
) {
myLastCode = theCode;
return myNextReturnParams;
}
@Override
public Class<? extends IBaseResource> getResourceType() {
return CodeSystem.class;

View File

@ -59,6 +59,16 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Version specific tests for validation using RemoteTerminologyValidationSupport.
* The tests in this class simulate the call to a remote server and therefore, only tests the code in
* the RemoteTerminologyServiceValidationSupport itself. The remote client call is simulated using the test providers.
* @see RemoteTerminologyServiceValidationSupport
*
* Other operations are tested separately.
* @see RemoteTerminologyLookupCodeR4Test
* @see RemoteTerminologyServiceResourceProviderR4Test
*/
public class RemoteTerminologyServiceValidationSupportR4Test extends BaseValidationTestWithInlineMocks {
private static final String DISPLAY = "DISPLAY";
private static final String CODE_SYSTEM = "CODE_SYS";

View File

@ -0,0 +1,113 @@
{
"resourceType": "Parameters",
"parameter": [
{
"name": "name",
"valueString": "Client_Config_CodeSystem"
},
{
"name": "display",
"valueString": "Value to display"
},
{
"name": "abstract",
"valueBoolean": false
},
{
"name": "property",
"part": [
{
"name": "code",
"valueCode": "interfaces"
},
{
"name": "subproperty",
"part": [
{
"name": "code",
"valueCode": "dataType"
},
{
"name": "value",
"valueString": "drugs"
}
]
},
{
"name": "subproperty",
"part": [
{
"name": "code",
"valueCode": "apiVersion"
},
{
"name": "value",
"valueString": "4.5.0"
}
]
},
{
"name": "subproperty",
"part": [
{
"name": "code",
"valueCode": "onboardStatus"
},
{
"name": "value",
"valueString": "unauthorized"
}
]
}
]
},
{
"name": "property",
"part": [
{
"name": "code",
"valueCode": "interfaces"
},
{
"name": "subproperty",
"part": [
{
"name": "code",
"valueCode": "dataType"
},
{
"name": "value",
"valueString": "ps"
}
]
},
{
"name": "subproperty",
"part": [
{
"name": "code",
"valueCode": "apiVersion"
},
{
"name": "value",
"valueString": "4.0.0"
}
]
},
{
"name": "subproperty",
"part": [
{
"name": "code",
"valueCode": "onboardStatus"
},
{
"name": "value",
"valueString": "contributor-testing"
}
]
}
]
}
]
}