From 51a6ddad3ddd1bd77b15a70e8433a381925e9865 Mon Sep 17 00:00:00 2001 From: jmarchionatto <60409882+jmarchionatto@users.noreply.github.com> Date: Fri, 26 Jan 2024 15:06:20 -0500 Subject: [PATCH 1/5] Add setting to make broker not use JacksonMessageConverter (#5611) * Add setting to make broker not use JacksonMessageConverter * Add changelog * Implement suggestions --------- Co-authored-by: juan.marchionatto --- ...ow-broker-other-than-jacksonmessageconverter.yaml | 5 +++++ .../channel/api/BaseChannelSettings.java | 12 ++++++++++++ .../subscription/channel/api/IChannelSettings.java | 2 ++ 3 files changed, 19 insertions(+) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5610-allow-broker-other-than-jacksonmessageconverter.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5610-allow-broker-other-than-jacksonmessageconverter.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5610-allow-broker-other-than-jacksonmessageconverter.yaml new file mode 100644 index 00000000000..ac120967c6b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5610-allow-broker-other-than-jacksonmessageconverter.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 5610 +title: "Before, message broker always used `JacksonMessageConverter`. Added a configuration option that allows + to disable it so other converters can be configured depending on the message type." diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/channel/api/BaseChannelSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/channel/api/BaseChannelSettings.java index 29a4b6d086e..de72550f73c 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/channel/api/BaseChannelSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/channel/api/BaseChannelSettings.java @@ -26,6 +26,9 @@ public abstract class BaseChannelSettings implements IChannelSettings { private ChannelRetryConfiguration myRetryConfigurationParameters; + // init true to match previous behaviour + private boolean myUseJacksonMessageConverter = true; + /** * Default true. Used by IChannelNamer to decide how to qualify the channel name. */ @@ -48,4 +51,13 @@ public abstract class BaseChannelSettings implements IChannelSettings { public ChannelRetryConfiguration getRetryConfigurationParameters() { return myRetryConfigurationParameters; } + + @Override + public boolean isUseJacksonMessageConverter() { + return myUseJacksonMessageConverter; + } + + public void setUseJacksonMessageConverter(boolean theUseJacksonMessageConverter) { + myUseJacksonMessageConverter = theUseJacksonMessageConverter; + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/channel/api/IChannelSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/channel/api/IChannelSettings.java index 2a72d424bf5..f14f9f7a585 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/channel/api/IChannelSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/channel/api/IChannelSettings.java @@ -21,4 +21,6 @@ package ca.uhn.fhir.jpa.subscription.channel.api; public interface IChannelSettings { boolean isQualifyChannelName(); + + boolean isUseJacksonMessageConverter(); } From 250bc7b031443d0c296e71c9c571d883be6af023 Mon Sep 17 00:00:00 2001 From: Martha Mitran Date: Fri, 2 Feb 2024 10:45:46 -0800 Subject: [PATCH 2/5] Fix contentType request parameter in UploadTerminologyCommand (#5639) --- .../main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java | 3 ++- .../java/ca/uhn/fhir/cli/UploadTerminologyCommandTest.java | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java index 108d540b0c8..e073c2e2c60 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java @@ -36,6 +36,7 @@ import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.CountingInputStream; import org.apache.commons.lang3.StringUtils; @@ -268,7 +269,7 @@ public class UploadTerminologyCommand extends BaseRequestGeneratingCommand { byte[] bytes = theBytes; String fileName = theFileName; - String suffix = fileName.substring(fileName.lastIndexOf(".")); + String suffix = FilenameUtils.getExtension(fileName); if (bytes.length > ourTransferSizeLimit) { ourLog.info( diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/UploadTerminologyCommandTest.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/UploadTerminologyCommandTest.java index e7203deb3de..f0e08800425 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/UploadTerminologyCommandTest.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/UploadTerminologyCommandTest.java @@ -388,7 +388,6 @@ public class UploadTerminologyCommandTest { }, "-t", theIncludeTls, myBaseRestServerHelper )); - UploadTerminologyCommand uploadTerminologyCommand = new UploadTerminologyCommand(); verify(myTermLoaderSvc, times(1)).loadCustom(any(), myDescriptorListCaptor.capture(), any()); From cbd733c340c4e475bb668c5ca11ad211b68075cf Mon Sep 17 00:00:00 2001 From: Martha Mitran Date: Fri, 2 Feb 2024 14:20:43 -0800 Subject: [PATCH 3/5] Refactor `CommonCodeSystemTerminologyService` method `validateCode` (#5631) * Refactored CommonCodeSystemTerminologyService validateCode method and added more tests --- .../ca/uhn/fhir/i18n/hapi-messages.properties | 4 + ....java => JpaPackageCacheSearchR4Test.java} | 4 +- ...st.java => PackageInstallerSvcR4Test.java} | 8 +- .../CommonCodeSystemsTerminologyService.java | 332 +++++++------ ...mmonCodeSystemsTerminologyServiceTest.java | 444 ++++++++++++------ 5 files changed, 468 insertions(+), 324 deletions(-) rename hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/{NpmSearchR4Test.java => JpaPackageCacheSearchR4Test.java} (98%) rename hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/{NpmR4Test.java => PackageInstallerSvcR4Test.java} (99%) diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index fa53cd9effe..da21fd50e4f 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -1,6 +1,10 @@ org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport.displayMismatch=Concept Display "{0}" does not match expected "{1}" +org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.unknownCodeInSystem=Unknown code "{0}#{1}" +org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.invalidCodeInSystem=Code {0} is not valid for system: {1} +org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.mismatchCodeSystem=Inappropriate CodeSystem URL "{0}" for ValueSet: {1} +org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.codeNotFoundInValueSet=Code "{0}" is not in valueset: {1} ca.uhn.fhir.jpa.term.TermReadSvcImpl.expansionRefersToUnknownCs=Unknown CodeSystem URI "{0}" referenced from ValueSet ca.uhn.fhir.jpa.term.TermReadSvcImpl.valueSetNotYetExpanded=ValueSet "{0}" has not yet been pre-expanded. Performing in-memory expansion without parameters. Current status: {1} | {2} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmSearchR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheSearchR4Test.java similarity index 98% rename from hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmSearchR4Test.java rename to hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheSearchR4Test.java index aa06e958181..cf3ae56ad57 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmSearchR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/JpaPackageCacheSearchR4Test.java @@ -20,9 +20,9 @@ import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -public class NpmSearchR4Test extends BaseJpaR4Test { +public class JpaPackageCacheSearchR4Test extends BaseJpaR4Test { - private static final Logger ourLog = LoggerFactory.getLogger(NpmSearchR4Test.class); + private static final Logger ourLog = LoggerFactory.getLogger(JpaPackageCacheSearchR4Test.class); @Autowired public IPackageInstallerSvc igInstaller; @Autowired diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcR4Test.java similarity index 99% rename from hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java rename to hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcR4Test.java index a6a129549e0..1f8c1877082 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcR4Test.java @@ -24,15 +24,11 @@ import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import ca.uhn.fhir.test.utilities.JettyUtil; import ca.uhn.fhir.test.utilities.ProxyUtil; import ca.uhn.fhir.test.utilities.server.HttpServletExtension; import ca.uhn.fhir.util.ClasspathUtil; import ca.uhn.fhir.util.JsonUtil; import ca.uhn.fhir.validation.ValidationResult; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.ee10.servlet.ServletHandler; -import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Enumerations; @@ -82,9 +78,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @TestMethodOrder(MethodOrderer.MethodName.class) -public class NpmR4Test extends BaseJpaR4Test { +public class PackageInstallerSvcR4Test extends BaseJpaR4Test { - private static final Logger ourLog = LoggerFactory.getLogger(NpmR4Test.class); + private static final Logger ourLog = LoggerFactory.getLogger(PackageInstallerSvcR4Test.class); @Autowired @Qualifier("myImplementationGuideDaoR4") protected IFhirResourceDao myImplementationGuideDao; diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java index d463003bf9f..4d1f2323243 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyService.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; import org.fhir.ucum.UcumEssenceService; import org.fhir.ucum.UcumException; import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_30_40; @@ -30,16 +29,18 @@ import org.hl7.fhir.dstu2.model.ValueSet; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.io.InputStream; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.Optional; -import static org.apache.commons.lang3.StringUtils.defaultIfBlank; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -65,8 +66,8 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { public static final String UCUM_CODESYSTEM_URL = "http://unitsofmeasure.org"; public static final String UCUM_VALUESET_URL = "http://hl7.org/fhir/ValueSet/ucum-units"; public static final String ALL_LANGUAGES_VALUESET_URL = "http://hl7.org/fhir/ValueSet/all-languages"; - private static final String USPS_CODESYSTEM_URL = "https://www.usps.com/"; - private static final String USPS_VALUESET_URL = "http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state"; + public static final String USPS_CODESYSTEM_URL = "https://www.usps.com/"; + public static final String USPS_VALUESET_URL = "http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state"; private static final Logger ourLog = LoggerFactory.getLogger(CommonCodeSystemsTerminologyService.class); private static final Map USPS_CODES = Collections.unmodifiableMap(buildUspsCodes()); private static final Map ISO_4217_CODES = Collections.unmodifiableMap(buildIso4217Codes()); @@ -81,8 +82,7 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { * Constructor */ public CommonCodeSystemsTerminologyService(FhirContext theFhirContext) { - Validate.notNull(theFhirContext); - + Objects.requireNonNull(theFhirContext); myFhirContext = theFhirContext; myVersionCanonicalizer = new VersionCanonicalizer(theFhirContext); } @@ -108,136 +108,109 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { public CodeValidationResult validateCode( @Nonnull ValidationSupportContext theValidationSupportContext, @Nonnull ConceptValidationOptions theOptions, - String theCodeSystem, - String theCode, - String theDisplay, - String theValueSetUrl) { + final String theCodeSystem, + final String theCode, + final String theDisplay, + final String theValueSetUrl) { /* ************************************************************************************** - * NOTE: Update validation_support_modules.html if any of the support in this module + * NOTE: Update validation_support_modules.md if any of the support in this module * changes in any way! * **************************************************************************************/ - Map handlerMap = null; - String expectSystem = null; - switch (defaultString(theValueSetUrl)) { + String valueSet = defaultString(theValueSetUrl); + String system = defaultString(theCodeSystem); + + if (!isBlank(valueSet)) { + final String expectSystem = getCodeSystemForValueSet(valueSet); + if (!isBlank(system) && !system.equals(expectSystem)) { + return getValidateCodeResultInError("mismatchCodeSystem", system, valueSet); + } + system = expectSystem; + } + + switch (valueSet) { case USPS_VALUESET_URL: - handlerMap = USPS_CODES; - expectSystem = USPS_CODESYSTEM_URL; - break; - + return validateCodeUsingCodeMap(theCode, system, USPS_CODES); case CURRENCIES_VALUESET_URL: - handlerMap = ISO_4217_CODES; - expectSystem = CURRENCIES_CODESYSTEM_URL; - break; - + return validateCodeUsingCodeMap(theCode, system, ISO_4217_CODES); case LANGUAGES_VALUESET_URL: - expectSystem = LANGUAGES_CODESYSTEM_URL; - if (!expectSystem.equals(theCodeSystem) && !(theCodeSystem == null && theOptions.isInferSystem())) { - return new CodeValidationResult() - .setSeverity(IssueSeverity.ERROR) - .setMessage("Inappropriate CodeSystem URL \"" + theCodeSystem + "\" for ValueSet: " - + theValueSetUrl); - } - - IBaseResource languagesVs = myLanguagesVs; - if (languagesVs == null) { - languagesVs = theValidationSupportContext - .getRootValidationSupport() - .fetchValueSet("http://hl7.org/fhir/ValueSet/languages"); - myLanguagesVs = myVersionCanonicalizer.valueSetToValidatorCanonical(languagesVs); - } - Optional match = - myLanguagesVs.getCompose().getInclude().stream() - .flatMap(t -> t.getConcept().stream()) - .filter(t -> theCode.equals(t.getCode())) - .findFirst(); - if (match.isPresent()) { - return new CodeValidationResult() - .setCode(theCode) - .setDisplay(match.get().getDisplay()); - } else { - return new CodeValidationResult() - .setSeverity(IssueSeverity.ERROR) - .setMessage("Code \"" + theCode + "\" is not in valueset: " + theValueSetUrl); - } - + return validateLanguageCodeInValueSet(theValidationSupportContext, theCode); case ALL_LANGUAGES_VALUESET_URL: - expectSystem = LANGUAGES_CODESYSTEM_URL; - if (!expectSystem.equals(theCodeSystem) && !(theCodeSystem == null && theOptions.isInferSystem())) { - return new CodeValidationResult() - .setSeverity(IssueSeverity.ERROR) - .setMessage("Inappropriate CodeSystem URL \"" + theCodeSystem + "\" for ValueSet: " - + theValueSetUrl); - } - - LookupCodeResult outcome = lookupLanguageCode(theCode); - if (outcome.isFound()) { - return new CodeValidationResult().setCode(theCode).setDisplay(outcome.getCodeDisplay()); - } else { - return new CodeValidationResult() - .setSeverity(IssueSeverity.ERROR) - .setMessage("Code \"" + theCode + "\" is not in valueset: " + theValueSetUrl); - } - + return validateLanguageCode(theCode, valueSet); case MIMETYPES_VALUESET_URL: // This is a pretty naive implementation - Should be enhanced in future - return new CodeValidationResult().setCode(theCode).setDisplay(theDisplay); - + return getValidateCodeResultOk(theCode, theDisplay); case UCUM_VALUESET_URL: { - String system = theCodeSystem; - expectSystem = UCUM_CODESYSTEM_URL; - if (system == null && theOptions.isInferSystem()) { - system = expectSystem; - } - CodeValidationResult validationResult = - validateLookupCode(theValidationSupportContext, theCode, system); - if (validationResult != null) { - return validationResult; - } + return validateCodeUsingSystemLookup(theValidationSupportContext, theCode, system); } } - if (handlerMap != null) { - String display = handlerMap.get(theCode); - if (display != null) { - if (expectSystem.equals(theCodeSystem) || theOptions.isInferSystem()) { - return new CodeValidationResult().setCode(theCode).setDisplay(display); - } - } - - String actualSystem = defaultIfBlank(theCodeSystem, expectSystem); - String unknownCodeMessage = myFhirContext - .getLocalizer() - .getMessage("ca.uhn.fhir.jpa.term.TermReadSvcImpl.unknownCodeInSystem", actualSystem, theCode); - return new CodeValidationResult().setSeverity(IssueSeverity.ERROR).setMessage(unknownCodeMessage); - } - - if (isBlank(theValueSetUrl)) { - return validateLookupCode(theValidationSupportContext, theCode, theCodeSystem); + if (isBlank(valueSet)) { + return validateCodeUsingSystemLookup(theValidationSupportContext, theCode, system); } return null; } - @Nullable - public CodeValidationResult validateLookupCode( - ValidationSupportContext theValidationSupportContext, String theCode, String theSystem) { - LookupCodeResult lookupResult = - lookupCode(theValidationSupportContext, new LookupCodeRequest(theSystem, theCode)); - CodeValidationResult validationResult = null; - if (lookupResult != null) { - if (lookupResult.isFound()) { - validationResult = new CodeValidationResult() - .setCode(lookupResult.getSearchedForCode()) - .setDisplay(lookupResult.getCodeDisplay()); - } else if (lookupResult.getErrorMessage() != null) { - validationResult = new CodeValidationResult() - .setSeverity(IssueSeverity.ERROR) - .setMessage(lookupResult.getErrorMessage()); - } + private static String getCodeSystemForValueSet(final String theValueSetUrl) { + String theCodeSystem = null; + switch (defaultString(theValueSetUrl)) { + case USPS_VALUESET_URL: + theCodeSystem = USPS_CODESYSTEM_URL; + break; + case CURRENCIES_VALUESET_URL: + theCodeSystem = CURRENCIES_CODESYSTEM_URL; + break; + case LANGUAGES_VALUESET_URL: + case ALL_LANGUAGES_VALUESET_URL: + theCodeSystem = LANGUAGES_CODESYSTEM_URL; + break; + case MIMETYPES_VALUESET_URL: + theCodeSystem = MIMETYPES_CODESYSTEM_URL; + break; + case UCUM_VALUESET_URL: + theCodeSystem = UCUM_CODESYSTEM_URL; + break; } + return theCodeSystem; + } - return validationResult; + protected CodeValidationResult getValidateCodeResultInError( + final String errorCode, final String theFirstParam, final String theSecondParam) { + String message = getErrorMessage(errorCode, theFirstParam, theSecondParam); + return getValidateCodeResultError(message); + } + + protected CodeValidationResult getValidateCodeResultOk(final String theCode, final String theDisplay) { + return new CodeValidationResult().setCode(theCode).setDisplay(theDisplay); + } + + protected CodeValidationResult getValidateCodeResultError(final String theMessage) { + return new CodeValidationResult().setSeverity(IssueSeverity.ERROR).setMessage(theMessage); + } + + private CodeValidationResult validateCodeUsingCodeMap( + final String theCode, final String theSystem, final Map theCodeMap) { + if (theCodeMap.containsKey(theCode)) { + return getValidateCodeResultOk(theCode, theCodeMap.get(theCode)); + } else { + return getValidateCodeResultInError("unknownCodeInSystem", theSystem, theCode); + } + } + + @Nullable + public CodeValidationResult validateCodeUsingSystemLookup( + final ValidationSupportContext theValidationSupportContext, final String theCode, final String theSystem) { + LookupCodeResult result = lookupCode(theValidationSupportContext, new LookupCodeRequest(theSystem, theCode)); + if (result == null) { + return getValidateCodeResultInError("unknownCodeInSystem", theSystem, theCode); + } + if (result.isFound()) { + return getValidateCodeResultOk(theCode, result.getCodeDisplay()); + } else if (result.getErrorMessage() != null) { + return getValidateCodeResultError(result.getErrorMessage()); + } + return null; } @Override @@ -267,85 +240,96 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { return null; } - String display = map.get(code); - if (isNotBlank(display)) { - LookupCodeResult retVal = new LookupCodeResult(); - retVal.setSearchedForCode(code); - retVal.setSearchedForSystem(system); - retVal.setFound(true); - retVal.setCodeDisplay(display); - return retVal; - } - - // If we get here it means we know the codesystem but the code was bad LookupCodeResult retVal = new LookupCodeResult(); retVal.setSearchedForCode(code); retVal.setSearchedForSystem(system); - retVal.setFound(false); - retVal.setErrorMessage("Code '" + code + "' is not valid for system: " + system); + + String display = map.get(code); + if (isNotBlank(display)) { + retVal.setFound(true); + retVal.setCodeDisplay(display); + } else { + // If we get here it means we know the CodeSystem but the code was bad + retVal.setFound(false); + String invalidCodeMessage = getErrorMessage("invalidCodeInSystem", code, system); + retVal.setErrorMessage(invalidCodeMessage); + } + return retVal; } + private CodeValidationResult validateLanguageCode(final String theCode, final String theValueSetUrl) { + LookupCodeResult outcome = lookupLanguageCode(theCode); + if (outcome.isFound()) { + return getValidateCodeResultOk(theCode, outcome.getCodeDisplay()); + } else { + return getValidateCodeResultInError("codeNotFoundInValueSet", theCode, theValueSetUrl); + } + } + + private CodeValidationResult validateLanguageCodeInValueSet( + final ValidationSupportContext theValidationSupportContext, final String theCode) { + final String valueSet = LANGUAGES_VALUESET_URL; + if (myLanguagesVs == null) { + IBaseResource languagesVs = + theValidationSupportContext.getRootValidationSupport().fetchValueSet(valueSet); + myLanguagesVs = myVersionCanonicalizer.valueSetToValidatorCanonical(languagesVs); + } + Optional match = myLanguagesVs.getCompose().getInclude().stream() + .flatMap(t -> t.getConcept().stream()) + .filter(t -> theCode.equals(t.getCode())) + .findFirst(); + if (match.isPresent()) { + return getValidateCodeResultOk(theCode, match.get().getDisplay()); + } else { + return getValidateCodeResultInError("codeNotFoundInValueSet", theCode, valueSet); + } + } + private LookupCodeResult lookupLanguageCode(String theCode) { if (myLanguagesLanugageMap == null || myLanguagesRegionMap == null) { initializeBcp47LanguageMap(); } - int langRegionSeparatorIndex = StringUtils.indexOfAny(theCode, '-', '_'); - boolean hasRegionAndCodeSegments = langRegionSeparatorIndex > 0; - String language; - String region; + final LookupCodeResult lookupCodeResult = new LookupCodeResult(); + lookupCodeResult.setSearchedForSystem(LANGUAGES_CODESYSTEM_URL); + lookupCodeResult.setSearchedForCode(theCode); + + final int langRegionSeparatorIndex = StringUtils.indexOfAny(theCode, '-', '_'); + final boolean hasRegionAndCodeSegments = langRegionSeparatorIndex > 0; + + final boolean found; + final String display; if (hasRegionAndCodeSegments) { // we look for languages in lowercase only // this will allow case insensitivity for language portion of code - language = myLanguagesLanugageMap.get( + String language = myLanguagesLanugageMap.get( theCode.substring(0, langRegionSeparatorIndex).toLowerCase()); - region = myLanguagesRegionMap.get( + String region = myLanguagesRegionMap.get( theCode.substring(langRegionSeparatorIndex + 1).toUpperCase()); - if (language == null || region == null) { - // In case the user provides both a language and a region, they must both be valid for the lookup to - // succeed. + // In case the user provides both a language and a region, they must both be valid for the lookup to + // succeed. + found = language != null && region != null; + display = found ? language + " " + region : null; + if (!found) { ourLog.warn("Couldn't find a valid bcp47 language-region combination from code: {}", theCode); - return buildNotFoundLookupCodeResult(theCode); - } else { - return buildLookupResultForLanguageAndRegion(theCode, language, region); } } else { // In case user has only provided a language, we build the lookup from only that. // NB: we only use the lowercase version of the language - language = myLanguagesLanugageMap.get(theCode.toLowerCase()); - if (language == null) { + String language = myLanguagesLanugageMap.get(theCode.toLowerCase()); + found = language != null; + display = language; + if (!found) { ourLog.warn("Couldn't find a valid bcp47 language from code: {}", theCode); - return buildNotFoundLookupCodeResult(theCode); - } else { - return buildLookupResultForLanguage(theCode, language); } } - } - private LookupCodeResult buildLookupResultForLanguageAndRegion( - @Nonnull String theOriginalCode, @Nonnull String theLanguage, @Nonnull String theRegion) { - LookupCodeResult lookupCodeResult = buildNotFoundLookupCodeResult(theOriginalCode); - lookupCodeResult.setCodeDisplay(theLanguage + " " + theRegion); - lookupCodeResult.setFound(true); - return lookupCodeResult; - } + lookupCodeResult.setFound(found); + lookupCodeResult.setCodeDisplay(display); - private LookupCodeResult buildLookupResultForLanguage( - @Nonnull String theOriginalCode, @Nonnull String theLanguage) { - LookupCodeResult lookupCodeResult = buildNotFoundLookupCodeResult(theOriginalCode); - lookupCodeResult.setCodeDisplay(theLanguage); - lookupCodeResult.setFound(true); - return lookupCodeResult; - } - - private LookupCodeResult buildNotFoundLookupCodeResult(@Nonnull String theOriginalCode) { - LookupCodeResult lookupCodeResult = new LookupCodeResult(); - lookupCodeResult.setFound(false); - lookupCodeResult.setSearchedForSystem(LANGUAGES_CODESYSTEM_URL); - lookupCodeResult.setSearchedForCode(theOriginalCode); return lookupCodeResult; } @@ -404,24 +388,20 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { @Nonnull private LookupCodeResult lookupUcumCode(String theCode) { - InputStream input = ClasspathUtil.loadResourceAsStream("/ucum-essence.xml"); - String outcome; LookupCodeResult retVal = new LookupCodeResult(); retVal.setSearchedForCode(theCode); retVal.setSearchedForSystem(UCUM_CODESYSTEM_URL); - try { + try (InputStream input = ClasspathUtil.loadResourceAsStream("/ucum-essence.xml")) { UcumEssenceService svc = new UcumEssenceService(input); - outcome = svc.analyse(theCode); + String outcome = svc.analyse(theCode); if (outcome != null) { retVal.setFound(true); retVal.setCodeDisplay(outcome); } - } catch (UcumException e) { + } catch (UcumException | IOException e) { ourLog.debug("Failed parse UCUM code: {}", theCode, e); retVal.setErrorMessage(e.getMessage()); - } finally { - ClasspathUtil.close(input); } return retVal; } @@ -470,7 +450,7 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { break; } - Validate.notNull(normalized); + Objects.requireNonNull(normalized); return normalized; } @@ -1373,4 +1353,8 @@ public class CommonCodeSystemsTerminologyService implements IValidationSupport { codes.put("ZWE", "Zimbabwe"); return codes; } + + protected String getErrorMessage(String errorCode, String theFirstParam, String theSecondParam) { + return myFhirContext.getLocalizer().getMessage(getClass(), errorCode, theFirstParam, theSecondParam); + } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java index 21a2799eb16..9bb0e691dd1 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CommonCodeSystemsTerminologyServiceTest.java @@ -2,17 +2,35 @@ package org.hl7.fhir.common.hapi.validation.support; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.ConceptValidationOptions; -import ca.uhn.fhir.context.support.IValidationSupport; -import ca.uhn.fhir.context.support.ValidationSupportContext; +import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult; +import ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity; +import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult; import ca.uhn.fhir.context.support.LookupCodeRequest; +import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.fhirpath.BaseValidationTestWithInlineMocks; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.EncodingEnum; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.ValueSet; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.ALL_LANGUAGES_VALUESET_URL; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.CURRENCIES_CODESYSTEM_URL; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.CURRENCIES_VALUESET_URL; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.LANGUAGES_CODESYSTEM_URL; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.LANGUAGES_VALUESET_URL; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.MIMETYPES_CODESYSTEM_URL; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.MIMETYPES_VALUESET_URL; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.UCUM_CODESYSTEM_URL; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.UCUM_VALUESET_URL; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.USPS_CODESYSTEM_URL; +import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.USPS_VALUESET_URL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -31,214 +49,356 @@ public class CommonCodeSystemsTerminologyServiceTest extends BaseValidationTestW mySvc = new CommonCodeSystemsTerminologyService(myCtx); } - @Test - public void testUcum_LookupCode_Good() { - IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(newSupport(), new LookupCodeRequest("http://unitsofmeasure.org", "Cel")); - assert outcome != null; - assertTrue(outcome.isFound()); + @ParameterizedTest + @CsvSource({"Cel, (degree Celsius)", "kg/m2, (kilogram) / (meter ^ 2)"}) + public void testLookupCode_withUnitsOfMeasureWithKnownCode_returnsFound(final String theCode, final String theDisplay) { + final String system = UCUM_CODESYSTEM_URL; + LookupCodeResult outcome = mySvc.lookupCode(newSupport(), new LookupCodeRequest(system, theCode)); + lookupCodeResultOk(outcome, theCode, system, theDisplay); } @Test - public void testUcum_LookupCode_Good2() { - IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(newSupport(), new LookupCodeRequest("http://unitsofmeasure.org", "kg/m2")); - assert outcome != null; - assertTrue(outcome.isFound()); + public void testLookupCode_withUnitsOfMeasureWithUnknownCode_returnsNotFound() { + final String code = "someCode"; + LookupCodeResult outcome = mySvc.lookupCode(newSupport(), new LookupCodeRequest(UCUM_CODESYSTEM_URL, code)); + lookupCodeResultError(outcome, code, "Error processing unit '" + code +"': The unit '" + code + "' is unknown' at position 0"); } @Test - public void testUcum_LookupCode_Bad() { - IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(newSupport(), new LookupCodeRequest("http://unitsofmeasure.org", "AAAAA")); - assert outcome != null; - assertFalse(outcome.isFound()); - } - - @Test - public void testUcum_LookupCode_UnknownSystem() { - IValidationSupport.LookupCodeResult outcome = mySvc.lookupCode(newSupport(), new LookupCodeRequest("http://foo", "AAAAA")); + public void testLookupCode_withUnknownSystem_returnsNull() { + LookupCodeResult outcome = mySvc.lookupCode(newSupport(), new LookupCodeRequest("http://foo", "someCode")); assertNull(outcome); } - @Test - public void lookupCode_languageOnlyLookup_isCaseInsensitive() { - IValidationSupport.LookupCodeResult outcomeUpper = mySvc.lookupCode(newSupport(), new LookupCodeRequest("urn:ietf:bcp:47", "SGN", "Sign Languages", null)); - IValidationSupport.LookupCodeResult outcomeLower = mySvc.lookupCode(newSupport(), new LookupCodeRequest("urn:ietf:bcp:47", "sgn", "Sign Languages", null)); - assertNotNull(outcomeUpper); - assertNotNull(outcomeLower); - assertTrue(outcomeLower.isFound()); - assertTrue(outcomeUpper.isFound()); + @ParameterizedTest + @CsvSource({"SGN, Sign languages", "sgn, Sign languages", "EN-US, English United States", "en-us, English United States"}) + public void testLookupCode_withLanguageOnlyWithKnownCode_returnsFound(final String theCode, final String theDisplay) { + final String system = LANGUAGES_CODESYSTEM_URL; + LookupCodeResult outcome = mySvc.lookupCode(newSupport(), new LookupCodeRequest(system, theCode, null, null)); + lookupCodeResultOk(outcome, theCode, system, theDisplay); } @Test - public void lookupCode_languageAndRegionLookup_isCaseInsensitive() { - IValidationSupport.LookupCodeResult outcomeUpper = mySvc.lookupCode(newSupport(), new LookupCodeRequest("urn:ietf:bcp:47", "EN-US", "English", null)); - IValidationSupport.LookupCodeResult outcomeLower = mySvc.lookupCode(newSupport(), new LookupCodeRequest("urn:ietf:bcp:47", "en-us", "English", null)); - assertNotNull(outcomeUpper); - assertNotNull(outcomeLower); - assertTrue(outcomeLower.isFound()); - assertTrue(outcomeUpper.isFound()); + public void testValidateCode_withUnitsOfMeasureWithKnownCode_returnsValid() { + final String code = "mg"; + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), UCUM_CODESYSTEM_URL, code, null, UCUM_VALUESET_URL); + validateCodeResultOk(result, code, "(milligram)"); } @Test - public void testUcum_ValidateCode_Good() { - ValueSet vs = new ValueSet(); - vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); - IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(newSupport(), newOptions(), "http://unitsofmeasure.org", "mg", null, vs); - assert outcome != null; - assertTrue(outcome.isOk()); - assertEquals("(milligram)", outcome.getDisplay()); + public void testValidateCodeInValueSet_withUnitsOfMeasureWithKnownCode_returnsValid() { + final ValueSet vs = new ValueSet().setUrl(UCUM_VALUESET_URL); + final String code = "mg"; + CodeValidationResult result = mySvc.validateCodeInValueSet(newSupport(), newOptions(), UCUM_CODESYSTEM_URL, code, null, vs); + validateCodeResultOk(result, code, "(milligram)"); } @Test - public void testUcum_ValidateCode_Good_SystemInferred() { - ValueSet vs = new ValueSet(); - vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); - IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(newSupport(), newOptions().setInferSystem(true), null, "mg", null, vs); - assert outcome != null; - assertTrue(outcome.isOk()); - assertEquals("(milligram)", outcome.getDisplay()); + public void testValidateCodeInValueSet_withUnitsOfMeasureWithInferSystem_returnsValid() { + final ValueSet vs = new ValueSet().setUrl(UCUM_VALUESET_URL); + final String code = "mg"; + CodeValidationResult result = mySvc.validateCodeInValueSet(newSupport(), newOptions().setInferSystem(true), null, code, null, vs); + validateCodeResultOk(result, code, "(milligram)"); } @Test - public void testUcum_ValidateCode_Bad() { - ValueSet vs = new ValueSet(); - vs.setUrl("http://hl7.org/fhir/ValueSet/ucum-units"); - IValidationSupport.CodeValidationResult outcome = mySvc.validateCodeInValueSet(newSupport(), newOptions(), "http://unitsofmeasure.org", "aaaaa", null, vs); - assertNotNull(outcome); - assertFalse(outcome.isOk()); - assertEquals("Error processing unit 'aaaaa': The unit 'aaaaa' is unknown' at position 0", outcome.getMessage()); - assertEquals("error", outcome.getSeverityCode()); + public void testValidateCodeInValueSet_withUnitsOfMeasureWithUnknownCode_returnsInvalid() { + final String code = "FOO"; + final ValueSet vs = new ValueSet().setUrl(UCUM_VALUESET_URL); + CodeValidationResult result = mySvc.validateCodeInValueSet(newSupport(), newOptions(), UCUM_CODESYSTEM_URL, code, null, vs); + validateCodeResultError(result, "Error processing unit '" + code +"': The unit '" + code + "' is unknown' at position 0"); + } + + @ParameterizedTest + @CsvSource({"en-CA, English Canada", "en-US, English United States"}) + public void testValidateLookupCode_withLanguagesWithKnownCode_returnsValid(final String theCode, final String theDisplay) { + CodeValidationResult result = mySvc.validateCodeUsingSystemLookup(newSupport(), theCode, LANGUAGES_CODESYSTEM_URL); + validateCodeResultOk(result, theCode, theDisplay); + } + + @ParameterizedTest + @CsvSource({"en-CA, English (Canada)", "en-US, English (United States)"}) + public void testValidateCode_withLanguagesWithKnownCode_returnsValid(final String theCode, final String theDisplay) { + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), LANGUAGES_CODESYSTEM_URL, theCode, null, LANGUAGES_VALUESET_URL); + validateCodeResultOk(result, theCode, theDisplay); } @Test - public void testLanguagesLanguagesCs_GoodCode() { - IValidationSupport.CodeValidationResult outcome = mySvc.validateLookupCode(newSupport(), "en-CA", "urn:ietf:bcp:47"); - assert outcome != null; - assertTrue(outcome.isOk()); - assertEquals("English Canada", outcome.getDisplay()); + public void testValidateCode_withLanguagesWithUnknownCode_returnsInvalid() { + final String code = "FOO"; + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), LANGUAGES_CODESYSTEM_URL, code, null, LANGUAGES_VALUESET_URL); + validateCodeResultError(result, "Code \""+ code +"\" is not in valueset: " + LANGUAGES_VALUESET_URL); } @Test - public void testLanguagesLanguagesCs_BadCode() { - IValidationSupport.CodeValidationResult outcome = mySvc.validateLookupCode(newSupport(), "en-FOO", "urn:ietf:bcp:47"); - assertNull(outcome); + public void testValidateCode_withLanguagesWithIncorrectSystem_returnsInvalid() { + final String system = "FOO"; + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), system, "en-US", null, LANGUAGES_VALUESET_URL); + validateCodeResultError(result, "Inappropriate CodeSystem URL \"" + system + "\" for ValueSet: " + LANGUAGES_VALUESET_URL); + } + + @ParameterizedTest + @CsvSource({"en-CA, English Canada", "en-US, English United States"}) + public void testValidateCode_withAllLanguagesWithKnownCode_returnsValid(final String theCode, final String theDisplay) { + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), LANGUAGES_CODESYSTEM_URL, theCode, null, ALL_LANGUAGES_VALUESET_URL); + validateCodeResultOk(result, theCode, theDisplay); + } + + + @Test + public void testValidateCode_withAllLanguagesWithUnknownCode_returnsInvalid() { + final String code = "FOO"; + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), LANGUAGES_CODESYSTEM_URL, code, null, ALL_LANGUAGES_VALUESET_URL); + validateCodeResultError(result, "Code \"" + code + "\" is not in valueset: " + ALL_LANGUAGES_VALUESET_URL); } @Test - public void testLanguages_CommonLanguagesVs_GoodCode() { - IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(newSupport(), newOptions(), "urn:ietf:bcp:47", "en-US", null, "http://hl7.org/fhir/ValueSet/languages"); - assert outcome != null; - assertTrue(outcome.isOk()); - assertEquals("English (United States)", outcome.getDisplay()); + public void testValidateCode_withAllLanguagesWithIncorrectSystem_returnsInvalid() { + final String system = "FOO"; + final String valueSet = ALL_LANGUAGES_VALUESET_URL; + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), system, "en-US", null, valueSet); + validateCodeResultError(result, "Inappropriate CodeSystem URL \"" + system + "\" for ValueSet: " + valueSet); + } + + @ParameterizedTest + @CsvSource({"nl, Dutch", "nl-NL, Dutch Netherlands"}) + public void testLookupCode_withLanguagesWithKnownLanguageOnlyCode_returnsFound(final String theCode, final String theDisplay) { + final String system = LANGUAGES_CODESYSTEM_URL; + LookupCodeResult result = mySvc.lookupCode(newSupport(), new LookupCodeRequest(system, theCode)); + lookupCodeResultOk(result, theCode, system, theDisplay); } @Test - public void testLanguages_CommonLanguagesVs_OnlyLanguage_NoRegion() { - IValidationSupport.LookupCodeResult nl = mySvc.lookupCode(newSupport(), new LookupCodeRequest("urn:ietf:bcp:47", "nl")); - assertTrue(nl.isFound()); - assertEquals("Dutch", nl.getCodeDisplay()); - } - - @Test - public void testLanguages_CommonLanguagesVs_LanguageAndRegion() { - IValidationSupport.LookupCodeResult nl = mySvc.lookupCode(newSupport(), new LookupCodeRequest("urn:ietf:bcp:47", "nl-NL")); - assertTrue(nl.isFound()); - assertEquals("Dutch Netherlands", nl.getCodeDisplay()); - } - - @Test - public void testLanguages_CommonLanguagesVs_BadCode() { - IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(newSupport(), newOptions(), "urn:ietf:bcp:47", "FOO", null, "http://hl7.org/fhir/ValueSet/languages"); - assert outcome != null; - assertFalse(outcome.isOk()); - assertEquals("Code \"FOO\" is not in valueset: http://hl7.org/fhir/ValueSet/languages", outcome.getMessage()); - } - - @Test - public void testLanguages_CommonLanguagesVs_BadSystem() { - IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(newSupport(), newOptions(), "FOO", "en-US", null, "http://hl7.org/fhir/ValueSet/languages"); - assert outcome != null; - assertFalse(outcome.isOk()); - assertEquals("Inappropriate CodeSystem URL \"FOO\" for ValueSet: http://hl7.org/fhir/ValueSet/languages", outcome.getMessage()); - } - - @Test - public void testLanguages_AllLanguagesVs_GoodCode() { - IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(newSupport(), newOptions(), "urn:ietf:bcp:47", "en-US", null, "http://hl7.org/fhir/ValueSet/all-languages"); - assert outcome != null; - assertTrue(outcome.isOk()); - assertEquals("English United States", outcome.getDisplay()); - } - - @Test - public void testLanguages_AllLanguagesVs_BadCode() { - IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(newSupport(), newOptions(), "urn:ietf:bcp:47", "FOO", null, "http://hl7.org/fhir/ValueSet/all-languages"); - assert outcome != null; - assertFalse(outcome.isOk()); - assertEquals("Code \"FOO\" is not in valueset: http://hl7.org/fhir/ValueSet/all-languages", outcome.getMessage()); - } - - @Test - public void testLanguages_AllLanguagesVs_BadSystem() { - IValidationSupport.CodeValidationResult outcome = mySvc.validateCode(newSupport(), newOptions(), "FOO", "en-US", null, "http://hl7.org/fhir/ValueSet/all-languages"); - assert outcome != null; - assertFalse(outcome.isOk()); - assertEquals("Inappropriate CodeSystem URL \"FOO\" for ValueSet: http://hl7.org/fhir/ValueSet/all-languages", outcome.getMessage()); - } - - @Test - public void testFetchCodeSystemBuiltIn_Iso3166_R4() { - CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL); - assert cs != null; - assertEquals(498, cs.getConcept().size()); - } - - @Test - public void testFetchCodeSystemBuiltIn_Iso3166_DSTU3() { + public void testFetchCodeSystem_withCountriesForDSTU3_returnsOk() { CommonCodeSystemsTerminologyService svc = new CommonCodeSystemsTerminologyService(FhirContext.forDstu3Cached()); org.hl7.fhir.dstu3.model.CodeSystem cs = (org.hl7.fhir.dstu3.model.CodeSystem) svc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL); - assert cs != null; + assertNotNull(cs); assertEquals(498, cs.getConcept().size()); } @Test - public void testFetchCodeSystemBuiltIn_Iso3166_R5() { + public void testFetchCodeSystem_withCountriesForR4_returnsOk() { + CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL); + assertNotNull(cs); + assertEquals(498, cs.getConcept().size()); + } + + @Test + public void testFetchCodeSystem_withCountriesForR5_returnsOk() { CommonCodeSystemsTerminologyService svc = new CommonCodeSystemsTerminologyService(FhirContext.forR5Cached()); org.hl7.fhir.r5.model.CodeSystem cs = (org.hl7.fhir.r5.model.CodeSystem) svc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL); - assert cs != null; + assertNotNull(cs); assertEquals(498, cs.getConcept().size()); } @Test - public void testFetchCodeSystemBuiltIn_Iso3166_DSTU2() { + public void testFetchCodeSystem_withCountriesForDSTU2_returnsOk() { CommonCodeSystemsTerminologyService svc = new CommonCodeSystemsTerminologyService(FhirContext.forDstu2Cached()); IBaseResource cs = svc.fetchCodeSystem(CommonCodeSystemsTerminologyService.COUNTRIES_CODESYSTEM_URL); assertNull(cs); } + @ParameterizedTest + @CsvSource({"WA, Washington", "PR, Puerto Rico"}) + public void testValidateCode_withUSPostalWithKnownCode_returnsValid(final String theCode, final String theDisplay) { + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), USPS_CODESYSTEM_URL, theCode, null, null); + validateCodeResultOk(result, theCode, theDisplay); + } + + @ParameterizedTest + @CsvSource({"WA, Washington", "PR, Puerto Rico"}) + public void testValidateCode_withUSPostalValueSetWithKnownCode_returnsValid(final String theCode, final String theDisplay) { + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), USPS_CODESYSTEM_URL, theCode, null, USPS_VALUESET_URL); + validateCodeResultOk(result, theCode, theDisplay); + } + @Test - public void testFetchCodeSystemBuiltIn_Iso_R4() { - CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem(CommonCodeSystemsTerminologyService.CURRENCIES_CODESYSTEM_URL); - assert cs != null; + public void testValidateCode_withUSPostalValueSetWithUnknownCode_returnsInvalid() { + final String system = USPS_CODESYSTEM_URL; + final String code = "FOO"; + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), system, code, null, USPS_VALUESET_URL); + validateCodeResultError(result, "Unknown code \"" + system + "#" + code + "\""); + } + + @ParameterizedTest + @CsvSource({"WA, Washington", "PR, Puerto Rico"}) + public void testLookupCode_withUSPostalWithKnownCode_returnsValid(final String theCode, final String theDisplay) { + final String system = USPS_CODESYSTEM_URL; + LookupCodeResult result = mySvc.lookupCode(newSupport(), new LookupCodeRequest(system, theCode)); + lookupCodeResultOk(result, theCode, system, theDisplay); + } + + @Test + public void testLookupCode_withUSPostalWithUnknownCode_returnsNotFound() { + final String system = USPS_CODESYSTEM_URL; + final String code = "invalidUSPS"; + LookupCodeResult result = mySvc.lookupCode(newSupport(), new LookupCodeRequest(system, code)); + lookupCodeResultError(result, code, "Code " + code + " is not valid for system: " + system); + } + + @ParameterizedTest + @CsvSource({"USD, United States dollar", "CAD, Canadian dollar", "EUR, Euro"}) + public void testValidateCode_withCurrenciesWithKnownCode_returnsValid(final String theCode, final String theDisplay) { + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), CURRENCIES_CODESYSTEM_URL, theCode, null, null); + validateCodeResultOk(result, theCode, theDisplay); + } + + @ParameterizedTest + @CsvSource({"USD, United States dollar", "CAD, Canadian dollar", "EUR, Euro"}) + public void testValidateCode_withCurrenciesValueSetWithKnownCode_returnsValid(final String theCode, final String theDisplay) { + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), CURRENCIES_CODESYSTEM_URL, theCode, null, CURRENCIES_VALUESET_URL); + validateCodeResultOk(result, theCode, theDisplay); + } + + @Test + public void testValidateCode_withCurrenciesValueSetWithUnknownCode_returnsInvalid() { + final String system = CURRENCIES_CODESYSTEM_URL; + final String code = "invalidCurrency"; + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), system, code, null, CURRENCIES_VALUESET_URL); + validateCodeResultError(result, "Unknown code \"" + system + "#" + code + "\""); + } + + @ParameterizedTest + @CsvSource({"USD, United States dollar", "CAD, Canadian dollar", "EUR, Euro"}) + public void testLookupCode_withCurrenciesWithKnownCode_returnsValid(final String theCode, final String theDisplay) { + final String system = CURRENCIES_CODESYSTEM_URL; + LookupCodeResult result = mySvc.lookupCode(newSupport(), new LookupCodeRequest(system, theCode)); + lookupCodeResultOk(result, theCode, system, theDisplay); + } + + @Test + public void testLookupCode_withCurrenciesWithUnknownCode_returnsNotFound() { + final String system = CURRENCIES_CODESYSTEM_URL; + final String code = "FOO"; + LookupCodeResult result = mySvc.lookupCode(newSupport(), new LookupCodeRequest(system, code)); + lookupCodeResultError(result, code, "Code " + code + " is not valid for system: " + system); + } + + @Test + public void testFetchCodeSystem_withCurrencies_returnsOk() { + CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem(CURRENCIES_CODESYSTEM_URL); + assertNotNull(cs); assertEquals(182, cs.getConcept().size()); } @Test - public void testFetchCodeSystemBuiltIn_Unknown() { + public void testFetchCodeSystem_withUnknownSystem_returnsNull() { CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem("http://foo"); assertNull(cs); } @Test - public void testFetchCodeSystemUrlDstu3() { + public void testGetCodeSystemUrl_forDSTU3_throwsException() { try { CommonCodeSystemsTerminologyService.getCodeSystemUrl(myCtx, new org.hl7.fhir.dstu3.model.CodeSystem()); - fail(); } catch (IllegalArgumentException e) { assertEquals(Msg.code(696) + "Can not handle version: DSTU3", e.getMessage()); } } + @Test + public void testFetchCodeSystem_withMimeType_returnsOk() { + CodeSystem cs = (CodeSystem) mySvc.fetchCodeSystem(MIMETYPES_CODESYSTEM_URL); + assertNull(cs); + } + + @ParameterizedTest + @ValueSource(strings = { EncodingEnum.JSON_PLAIN_STRING, Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON }) + public void testValidateCode_withMimetypesValueSetWithStandardCode_returnsValid(String theCode) { + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), MIMETYPES_CODESYSTEM_URL, theCode, null, MIMETYPES_VALUESET_URL); + validateCodeResultOk(result, theCode, null); + } + + @ParameterizedTest + @ValueSource(strings = { EncodingEnum.JSON_PLAIN_STRING, Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON }) + public void testValidateCode_withMimetypesValueSetWithInferSystemWithStandardCode_returnsValid(String theCode) { + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions().setInferSystem(true), null, theCode, null, MIMETYPES_VALUESET_URL); + validateCodeResultOk(result, theCode, null); + } + + @Test + public void testValidateCode_withMimetypesValueSetWithMismatchSystem_returnsInvalid() { + final String system = "someSystem"; + final String valueSet = MIMETYPES_VALUESET_URL; + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), system, system, null, valueSet); + validateCodeResultError(result, "Inappropriate CodeSystem URL \"" + system + "\" for ValueSet: " + valueSet); + } + + @ParameterizedTest + @ValueSource(strings = { EncodingEnum.JSON_PLAIN_STRING, Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON }) + public void testValidateCode_withMimetypesWithStandardCode_returnsValid(String theCode) { + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), MIMETYPES_CODESYSTEM_URL, theCode, null, null); + validateCodeResultOk(result, theCode, null); + } + + @Test + public void testValidateCode_withMimetypeValueSetWithArbitraryCode_returnsValid() { + final String code = "someCode"; + final String display = "displayValue"; + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), MIMETYPES_CODESYSTEM_URL, code, display, MIMETYPES_VALUESET_URL); + validateCodeResultOk(result, code, display); + } + + @Test + public void testValidateCode_withMimetypesWithArbitraryCode_returnsValid() { + final String code = "someCode"; + final String display = "displayValue"; + CodeValidationResult result = mySvc.validateCode(newSupport(), newOptions(), MIMETYPES_CODESYSTEM_URL, code, display, null); + validateCodeResultOk(result, code, null); + + // the display null in result bug is reported here: https://github.com/hapifhir/hapi-fhir/issues/5643 + } + + @ParameterizedTest + @ValueSource(strings = { EncodingEnum.JSON_PLAIN_STRING, Constants.FORMAT_TURTLE, Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_JSON }) + public void testLookupCode_withMimetypesWithStandardCode_returnFound(String code) { + final String system = MIMETYPES_CODESYSTEM_URL; + LookupCodeResult result = mySvc.lookupCode(newSupport(), new LookupCodeRequest(system, code)); + lookupCodeResultOk(result, code, system, null); + } + + @Test + public void testLookupCode_withMimetypesWithArbitraryCode_returnsFound() { + final String system = MIMETYPES_CODESYSTEM_URL; + final String code = "someCode"; + LookupCodeResult result = mySvc.lookupCode(newSupport(), new LookupCodeRequest(system, code)); + lookupCodeResultOk(result, code, system, null); + } + + private void validateCodeResultOk(final CodeValidationResult theResult, final String theCode, final String theDisplay) { + assertNotNull(theResult); + assertTrue(theResult.isOk()); + assertEquals(theCode, theResult.getCode()); + assertEquals(theDisplay, theResult.getDisplay()); + assertNull(theResult.getSeverity()); + assertNull(theResult.getMessage()); + } + + private void validateCodeResultError(final CodeValidationResult theResult, final String theError) { + assertNotNull(theResult); + assertFalse(theResult.isOk()); + assertEquals(IssueSeverity.ERROR, theResult.getSeverity()); + assertEquals(theError, theResult.getMessage()); + } + + private void lookupCodeResultOk(final LookupCodeResult theResult, final String theCode, final String theSystem, final String theDisplay) { + assertNotNull(theResult); + assertEquals(theSystem, theResult.getSearchedForSystem()); + assertEquals(theCode, theResult.getSearchedForCode()); + assertTrue(theResult.isFound()); + assertEquals(theDisplay, theResult.getCodeDisplay()); + } + + private void lookupCodeResultError(final LookupCodeResult theResult, final String theCode, final String theMessage) { + assertNotNull(theResult); + assertEquals(theCode, theResult.getSearchedForCode()); + assertFalse(theResult.isFound()); + assertEquals(theMessage, theResult.getErrorMessage()); + assertNull(theResult.getCodeDisplay()); + } + + private ValidationSupportContext newSupport() { return new ValidationSupportContext(myCtx.getValidationSupport()); } From bc955b8539f79d38ee94b13cbc7c1a305472d339 Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Tue, 6 Feb 2024 02:26:20 +0100 Subject: [PATCH 4/5] introduced _list fix (#5664) * introduced fix * Add value * Added backing tests * Ignore _list as _has is done --- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 6 +- .../uhn/fhir/jpa/provider/r4/ListR4Test.java | 74 +++++++++++++++++++ ...rCapabilityStatementProviderJpaR4Test.java | 2 +- .../resources/vm/jpa_resource_provider.vm | 5 ++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ListR4Test.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 079f9dc8215..9fada93f0db 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -151,6 +151,7 @@ import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -1985,8 +1986,11 @@ public abstract class BaseHapiFhirResourceDao extends B private void translateListSearchParams(SearchParameterMap theParams) { + Set>>> entryHashSet = new HashSet<>(theParams.entrySet()); + // Translate _list=42 to _has=List:item:_id=42 - for (String key : theParams.keySet()) { + for (Map.Entry>> stringListEntry : entryHashSet) { + String key = stringListEntry.getKey(); if (Constants.PARAM_LIST.equals((key))) { List> andOrValues = theParams.get(key); theParams.remove(key); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ListR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ListR4Test.java new file mode 100644 index 00000000000..eade89cf397 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ListR4Test.java @@ -0,0 +1,74 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.ListResource; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.Reference; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ListR4Test extends BaseResourceProviderR4Test { + + + private IIdType orgInList; + private IIdType practitionerInList; + private IIdType list; + private String identifierSystem = "http://example123.com/identifier"; + + + @BeforeEach + @Override + public void before() throws Exception { + super.before(); + + orgInList = createOrganization(withActiveTrue(), withIdentifier(identifierSystem, "B")); + practitionerInList = createPractitioner(withActiveTrue()); + ListResource testList = new ListResource() + .addEntry(new ListResource.ListEntryComponent().setItem(new Reference(orgInList.toUnqualifiedVersionless().getValue()))) + .addEntry(new ListResource.ListEntryComponent().setItem(new Reference(practitionerInList.toUnqualifiedVersionless().getValue()))); + + list = doCreateResource(testList); + + } + + @Test + public void organizationSearchUsingListReturnsOnlyOrgInList() { + Bundle results = (Bundle) myClient.search() + .forResource(Organization.class) + .whereMap(Collections.singletonMap("_list", Collections.singletonList(list.getIdPart()))) + .execute(); + + assertEquals(1, results.getEntry().size()); + assertEquals(orgInList.toUnqualifiedVersionless().getValue(), results.getEntryFirstRep().getResource().getIdElement().toUnqualifiedVersionless().getValue()); + } + + @Test + public void organizationSearchUsingListWorksWithAnotherParameter() { + Bundle results = (Bundle) myClient.search() + .forResource(Organization.class) + .whereMap(Collections.singletonMap("_list", Collections.singletonList(list.getIdPart()))) + .and(Organization.IDENTIFIER.hasSystemWithAnyCode(identifierSystem)) + .execute(); + + assertEquals(1, results.getEntry().size()); + assertEquals(orgInList.toUnqualifiedVersionless().getValue(), results.getEntryFirstRep().getResource().getIdElement().toUnqualifiedVersionless().getValue()); + } + + @Test + public void practitionerSearchUsingListReturnsOnlyPractitionerInList() { + Bundle results = (Bundle) myClient.search() + .forResource(Practitioner.class) + .whereMap(Collections.singletonMap("_list", Collections.singletonList(list.getIdPart()))) + .execute(); + + assertEquals(1, results.getEntry().size()); + assertEquals(practitionerInList.toUnqualifiedVersionless().getValue(), results.getEntryFirstRep().getResource().getIdElement().toUnqualifiedVersionless().getValue()); + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java index e64c5ddd7d7..51632c5c249 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java @@ -346,7 +346,7 @@ public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProv CapabilityStatement cs = myClient.capabilities().ofType(CapabilityStatement.class).execute(); for (CapabilityStatement.CapabilityStatementRestResourceComponent nextResource : cs.getRestFirstRep().getResource()) { for (CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent nextSp : nextResource.getSearchParam()) { - if (nextSp.getName().equals("_has")) { + if (nextSp.getName().equals("_has") || nextSp.getName().equals("_list")) { if (nextSp.getDefinition() == null) { continue; } diff --git a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm index 469f943a495..53b9842104d 100644 --- a/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm +++ b/hapi-tinder-plugin/src/main/resources/vm/jpa_resource_provider.vm @@ -69,6 +69,10 @@ public class ${className}ResourceProvider extends @OptionalParam(name=ca.uhn.fhir.rest.api.Constants.PARAM_PROFILE) UriAndListParam theSearchForProfile, + @Description(shortDefinition="Search the contents of the resource's data using a list") + @OptionalParam(name=ca.uhn.fhir.rest.api.Constants.PARAM_LIST) + StringAndListParam theList, + @Description(shortDefinition="Search for resources which have the given source value (Resource.meta.source)") @OptionalParam(name=ca.uhn.fhir.rest.api.Constants.PARAM_SOURCE) UriAndListParam theSearchForSource, @@ -149,6 +153,7 @@ public class ${className}ResourceProvider extends paramMap.add(ca.uhn.fhir.rest.api.Constants.PARAM_SECURITY, theSearchForSecurity); paramMap.add(ca.uhn.fhir.rest.api.Constants.PARAM_PROFILE, theSearchForProfile); paramMap.add(ca.uhn.fhir.rest.api.Constants.PARAM_SOURCE, theSearchForSource); + paramMap.add(ca.uhn.fhir.rest.api.Constants.PARAM_LIST, theList); paramMap.add("_has", theHas); #foreach ( $param in $searchParams ) paramMap.add("${param.name}", the${param.nameCapitalized}); From 8c16f57c0558f015e386dd424cf21e83729e7988 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Tue, 6 Feb 2024 04:23:51 -0800 Subject: [PATCH 5/5] changelog (#5673) --- .../uhn/hapi/fhir/changelog/7_2_0/3761-fix-list-search.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/3761-fix-list-search.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/3761-fix-list-search.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/3761-fix-list-search.yaml new file mode 100644 index 00000000000..b9e1bb4243e --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/3761-fix-list-search.yaml @@ -0,0 +1,5 @@ +--- +type: fix +issue: 3761 +title: "Fixed behaviour of the `_list` query parameter. Now it returns exclusively resources that are members of the given list. Thanks to Jens Villadsen (@jkiddo) for the contribution!" +