diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java index eb2dbba07f9..91a622301cd 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/support/IValidationSupport.java @@ -32,9 +32,11 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -77,9 +79,9 @@ public interface IValidationSupport { * Expands the given portion of a ValueSet * * @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to - * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. - * @param theExpansionOptions If provided (may be null), contains options controlling the expansion - * @param theValueSetToExpand The valueset that should be expanded + * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. + * @param theExpansionOptions If provided (may be null), contains options controlling the expansion + * @param theValueSetToExpand The valueset that should be expanded * @return The expansion, or null */ default ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, @Nullable ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) { @@ -116,15 +118,33 @@ public interface IValidationSupport { * Loads a resource needed by the validation (a StructureDefinition, or a * ValueSet) * - * @param theClass The type of the resource to load + *

+ * Note: Since 5.3.0, {@literal theClass} can be {@literal null} + *

+ * + * @param theClass The type of the resource to load, or null to return any resource with the given canonical URI * @param theUri The resource URI * @return Returns the resource, or null if no resource with the * given URI can be found */ - default T fetchResource(Class theClass, String theUri) { - Validate.notNull(theClass, "theClass must not be null or blank"); + @SuppressWarnings("unchecked") + default T fetchResource(@Nullable Class theClass, String theUri) { Validate.notBlank(theUri, "theUri must not be null or blank"); + if (theClass == null) { + Supplier[] sources = new Supplier[]{ + () -> fetchStructureDefinition(theUri), + () -> fetchValueSet(theUri), + () -> fetchCodeSystem(theUri) + }; + return (T) Arrays + .stream(sources) + .map(t -> t.get()) + .filter(t -> t != null) + .findFirst() + .orElse(null); + } + switch (getFhirContext().getResourceType(theClass)) { case "StructureDefinition": return theClass.cast(fetchStructureDefinition(theUri)); @@ -150,8 +170,8 @@ public interface IValidationSupport { * or validated * * @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to - * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. - * @param theSystem The URI for the code system, e.g. "http://loinc.org" + * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. + * @param theSystem The URI for the code system, e.g. "http://loinc.org" * @return Returns true if codes in the given code system can be * validated */ @@ -172,11 +192,11 @@ public interface IValidationSupport { * binding fields (e.g. Observation.code in the default profile. * * @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to - * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. - * @param theOptions Provides options controlling the validation - * @param theCodeSystem The code system, e.g. "http://loinc.org" - * @param theCode The code, e.g. "1234-5" - * @param theDisplay The display name, if it should also be validated + * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. + * @param theOptions Provides options controlling the validation + * @param theCodeSystem The code system, e.g. "http://loinc.org" + * @param theCode The code, e.g. "1234-5" + * @param theDisplay The display name, if it should also be validated * @return Returns a validation result object */ default CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { @@ -189,11 +209,11 @@ public interface IValidationSupport { * binding fields (e.g. Observation.code in the default profile. * * @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to - * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. - * @param theCodeSystem The code system, e.g. "http://loinc.org" - * @param theCode The code, e.g. "1234-5" - * @param theDisplay The display name, if it should also be validated - * @param theValueSet The ValueSet to validate against. Must not be null, and must be a ValueSet resource. + * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. + * @param theCodeSystem The code system, e.g. "http://loinc.org" + * @param theCode The code, e.g. "1234-5" + * @param theDisplay The display name, if it should also be validated + * @param theValueSet The ValueSet to validate against. Must not be null, and must be a ValueSet resource. * @return Returns a validation result object, or null if this validation support module can not handle this kind of request */ default CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) { @@ -204,9 +224,9 @@ public interface IValidationSupport { * Look up a code using the system and code value * * @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to - * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. - * @param theSystem The CodeSystem URL - * @param theCode The code + * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. + * @param theSystem The CodeSystem URL + * @param theCode The code */ default LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) { return null; @@ -217,8 +237,8 @@ public interface IValidationSupport { * validation support module * * @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to - * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. - * @param theValueSetUrl The ValueSet canonical URL + * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. + * @param theValueSetUrl The ValueSet canonical URL */ default boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { return false; @@ -228,7 +248,7 @@ public interface IValidationSupport { * Generate a snapshot from the given differential profile. * * @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to - * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. + * other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter. * @return Returns null if this module does not know how to handle this request */ default IBaseResource generateSnapshot(ValidationSupportContext theValidationSupportContext, IBaseResource theInput, String theUrl, String theWebUrl, String theProfileName) { @@ -249,6 +269,25 @@ public interface IValidationSupport { } + enum IssueSeverity { + /** + * The issue caused the action to fail, and no further checking could be performed. + */ + FATAL, + /** + * The issue is sufficiently important to cause the action to fail. + */ + ERROR, + /** + * The issue is not important enough to cause the action to fail, but may cause it to be performed suboptimally or in a way that is not as desired. + */ + WARNING, + /** + * The issue has no relation to the degree of success of the action. + */ + INFORMATION + } + class ConceptDesignation { private String myLanguage; private String myUseSystem; @@ -365,25 +404,6 @@ public interface IValidationSupport { } } - enum IssueSeverity { - /** - * The issue caused the action to fail, and no further checking could be performed. - */ - FATAL, - /** - * The issue is sufficiently important to cause the action to fail. - */ - ERROR, - /** - * The issue is not important enough to cause the action to fail, but may cause it to be performed suboptimally or in a way that is not as desired. - */ - WARNING, - /** - * The issue has no relation to the degree of success of the action. - */ - INFORMATION - } - class CodeValidationResult { private String myCode; private String myMessage; diff --git a/hapi-fhir-docs/src/test/java/ca/uhn/hapi/fhir/docs/ChangelogFilesTest.java b/hapi-fhir-docs/src/test/java/ca/uhn/hapi/fhir/docs/ChangelogFilesTest.java index 399d889bfa3..579982577ce 100644 --- a/hapi-fhir-docs/src/test/java/ca/uhn/hapi/fhir/docs/ChangelogFilesTest.java +++ b/hapi-fhir-docs/src/test/java/ca/uhn/hapi/fhir/docs/ChangelogFilesTest.java @@ -66,6 +66,9 @@ public class ChangelogFilesTest { // this one is optional boolean haveIssue = fieldNames.remove("issue"); + // this one is optional + fieldNames.remove("backport"); + assertThat("Invalid element in " + next + ": " + fieldNames, fieldNames, empty()); if (haveIssue) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java index c5e177d7e45..d3a1fe2adf1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java @@ -46,9 +46,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.transaction.Transactional; +import java.util.Arrays; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -104,110 +107,13 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport @Override @SuppressWarnings({"unchecked", "unused"}) - public T fetchResource(Class theClass, String theUri) { + public T fetchResource(@Nullable Class theClass, String theUri) { if (isBlank(theUri)) { return null; } - String key = theClass.getSimpleName() + " " + theUri; - IBaseResource fetched = myLoadCache.get(key, t -> { - IdType id = new IdType(theUri); - boolean localReference = false; - if (id.hasBaseUrl() == false && id.hasIdPart() == true) { - localReference = true; - } - - String resourceName = myFhirContext.getResourceType(theClass); - IBundleProvider search; - switch (resourceName) { - case "ValueSet": - if (localReference) { - SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - params.add(IAnyResource.SP_RES_ID, new StringParam(theUri)); - search = myDaoRegistry.getResourceDao(resourceName).search(params); - if (search.size() == 0) { - params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - params.add(ValueSet.SP_URL, new UriParam(theUri)); - search = myDaoRegistry.getResourceDao(resourceName).search(params); - } - } else { - int versionSeparator = theUri.lastIndexOf('|'); - SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - if (versionSeparator != -1) { - params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); - params.add(ValueSet.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); - } else { - params.add(ValueSet.SP_URL, new UriParam(theUri)); - } - params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC)); - search = myDaoRegistry.getResourceDao(resourceName).search(params); - } - break; - case "StructureDefinition": { - // Don't allow the core FHIR definitions to be overwritten - if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) { - String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length()); - if (myFhirContext.getElementDefinition(typeName) != null) { - return myNoMatch; - } - } - SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - params.add(StructureDefinition.SP_URL, new UriParam(theUri)); - search = myDaoRegistry.getResourceDao("StructureDefinition").search(params); - break; - } - case "Questionnaire": { - SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) { - params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart())); - } else { - params.add(Questionnaire.SP_URL, new UriParam(id.getValue())); - } - search = myDaoRegistry.getResourceDao("Questionnaire").search(params); - break; - } - case "CodeSystem": { - int versionSeparator = theUri.lastIndexOf('|'); - SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - if (versionSeparator != -1) { - params.add(CodeSystem.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); - params.add(CodeSystem.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); - } else { - params.add(CodeSystem.SP_URL, new UriParam(theUri)); - } - params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC)); - search = myDaoRegistry.getResourceDao(resourceName).search(params); - break; - } - case "ImplementationGuide": - case "SearchParameter": { - SearchParameterMap params = new SearchParameterMap(); - params.setLoadSynchronousUpTo(1); - params.add(ImplementationGuide.SP_URL, new UriParam(theUri)); - search = myDaoRegistry.getResourceDao(resourceName).search(params); - break; - } - default: - throw new IllegalArgumentException("Can't fetch resource type: " + resourceName); - } - - Integer size = search.size(); - if (size == null || size == 0) { - return myNoMatch; - } - - if (size > 1) { - ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri); - } - - return search.getResources(0, 1).get(0); - }); + String key = theClass + " " + theUri; + IBaseResource fetched = myLoadCache.get(key, t -> doFetchResource(theClass, theUri)); if (fetched == myNoMatch) { return null; @@ -216,6 +122,119 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport return (T) fetched; } + private IBaseResource doFetchResource(@Nullable Class theClass, String theUri) { + if (theClass == null) { + Supplier[] fetchers = new Supplier[]{ + () -> doFetchResource(ValueSet.class, theUri), + () -> doFetchResource(CodeSystem.class, theUri), + () -> doFetchResource(StructureDefinition.class, theUri) + }; + return Arrays + .stream(fetchers) + .map(t -> t.get()) + .filter(t -> t != myNoMatch) + .findFirst() + .orElse(myNoMatch); + } + + IdType id = new IdType(theUri); + boolean localReference = false; + if (id.hasBaseUrl() == false && id.hasIdPart() == true) { + localReference = true; + } + + String resourceName = myFhirContext.getResourceType(theClass); + IBundleProvider search; + switch (resourceName) { + case "ValueSet": + if (localReference) { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + params.add(IAnyResource.SP_RES_ID, new StringParam(theUri)); + search = myDaoRegistry.getResourceDao(resourceName).search(params); + if (search.size() == 0) { + params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + params.add(ValueSet.SP_URL, new UriParam(theUri)); + search = myDaoRegistry.getResourceDao(resourceName).search(params); + } + } else { + int versionSeparator = theUri.lastIndexOf('|'); + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + if (versionSeparator != -1) { + params.add(ValueSet.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); + params.add(ValueSet.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); + } else { + params.add(ValueSet.SP_URL, new UriParam(theUri)); + } + params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC)); + search = myDaoRegistry.getResourceDao(resourceName).search(params); + } + break; + case "StructureDefinition": { + // Don't allow the core FHIR definitions to be overwritten + if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) { + String typeName = theUri.substring("http://hl7.org/fhir/StructureDefinition/".length()); + if (myFhirContext.getElementDefinition(typeName) != null) { + return myNoMatch; + } + } + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + params.add(StructureDefinition.SP_URL, new UriParam(theUri)); + search = myDaoRegistry.getResourceDao("StructureDefinition").search(params); + break; + } + case "Questionnaire": { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + if (localReference || myFhirContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU2)) { + params.add(IAnyResource.SP_RES_ID, new StringParam(id.getIdPart())); + } else { + params.add(Questionnaire.SP_URL, new UriParam(id.getValue())); + } + search = myDaoRegistry.getResourceDao("Questionnaire").search(params); + break; + } + case "CodeSystem": { + int versionSeparator = theUri.lastIndexOf('|'); + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + if (versionSeparator != -1) { + params.add(CodeSystem.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); + params.add(CodeSystem.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); + } else { + params.add(CodeSystem.SP_URL, new UriParam(theUri)); + } + params.setSort(new SortSpec("_lastUpdated").setOrder(SortOrderEnum.DESC)); + search = myDaoRegistry.getResourceDao(resourceName).search(params); + break; + } + case "ImplementationGuide": + case "SearchParameter": { + SearchParameterMap params = new SearchParameterMap(); + params.setLoadSynchronousUpTo(1); + params.add(ImplementationGuide.SP_URL, new UriParam(theUri)); + search = myDaoRegistry.getResourceDao(resourceName).search(params); + break; + } + default: + throw new IllegalArgumentException("Can't fetch resource type: " + resourceName); + } + + Integer size = search.size(); + if (size == null || size == 0) { + return myNoMatch; + } + + if (size > 1) { + ourLog.warn("Found multiple {} instances with URL search value of: {}", resourceName, theUri); + } + + return search.getResources(0, 1).get(0); + } + @Override public FhirContext getFhirContext() { return myFhirContext; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java index 9255acd1496..df30c410d11 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/ValidatorResourceFetcher.java @@ -32,13 +32,13 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.elementmodel.JsonParser; +import org.hl7.fhir.r5.model.CanonicalResource; import org.hl7.fhir.r5.utils.IResourceValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.PostConstruct; -import java.io.IOException; import java.util.Locale; public class ValidatorResourceFetcher implements IResourceValidator.IValidatorResourceFetcher { @@ -61,7 +61,6 @@ public class ValidatorResourceFetcher implements IResourceValidator.IValidatorRe } - @SuppressWarnings("ConstantConditions") @Override public Element fetch(Object appContext, String theUrl) throws FHIRException { @@ -94,13 +93,13 @@ public class ValidatorResourceFetcher implements IResourceValidator.IValidatorRe } @Override - public boolean resolveURL(Object appContext, String path, String url) throws IOException, FHIRException { + public boolean resolveURL(Object appContext, String path, String url, String type) throws FHIRException { return true; } @Override - public byte[] fetchRaw(String url) throws IOException { - return new byte[0]; + public byte[] fetchRaw(String url) { + throw new UnsupportedOperationException(); } @Override @@ -108,4 +107,14 @@ public class ValidatorResourceFetcher implements IResourceValidator.IValidatorRe // ignore } + @Override + public CanonicalResource fetchCanonicalResource(String url) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean fetchesCanonicalResource(String url) { + return false; + } + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java index cc6897acf1f..6b3ab511fe9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2ValidateTest.java @@ -144,7 +144,7 @@ public class FhirResourceDaoDstu2ValidateTest extends BaseJpaDstu2Test { } catch (PreconditionFailedException e) { String ooString = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(e.getOperationOutcome()); ourLog.info(ooString); - assertThat(ooString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' could not be resolved, so has not been checked")); + assertThat(ooString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' has not been checked because it is unknown")); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java index 8b50cd2c2ed..ab04c07c46f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3ValidateTest.java @@ -333,7 +333,7 @@ public class FhirResourceDaoDstu3ValidateTest extends BaseJpaDstu3Test { OperationOutcome oo = (OperationOutcome) e.getOperationOutcome(); String outputString = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo); ourLog.info(outputString); - assertThat(outputString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' could not be resolved, so has not been checked")); + assertThat(outputString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' has not been checked because it is unknown")); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java index 06e7d516992..72b16b4d25f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4ValidateTest.java @@ -1138,7 +1138,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { org.hl7.fhir.r4.model.OperationOutcome oo = (org.hl7.fhir.r4.model.OperationOutcome) e.getOperationOutcome(); String outputString = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo); ourLog.info(outputString); - assertThat(outputString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' could not be resolved, so has not been checked")); + assertThat(outputString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' has not been checked because it is unknown")); } } @@ -1174,7 +1174,7 @@ public class FhirResourceDaoR4ValidateTest extends BaseJpaR4Test { org.hl7.fhir.r4.model.OperationOutcome oo = (org.hl7.fhir.r4.model.OperationOutcome) e.getOperationOutcome(); String outputString = myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(oo); ourLog.info(outputString); - assertThat(outputString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' could not be resolved, so has not been checked")); + assertThat(outputString, containsString("Profile reference 'http://example.com/StructureDefinition/testValidateResourceContainingProfileDeclarationInvalid' has not been checked because it is unknown")); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index cb2f52d307c..f4f6e2852eb 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -2403,7 +2403,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { String respString = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8); ourLog.info(respString); assertEquals(412, resp.getStatusLine().getStatusCode()); - assertThat(respString, containsString("Profile reference 'http://foo/structuredefinition/myprofile' could not be resolved, so has not been checked")); + assertThat(respString, containsString("Profile reference 'http://foo/structuredefinition/myprofile' has not been checked because it is unknown")); } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptorTest.java index 07459ede46a..4f675fc6ac3 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/FhirPathFilterInterceptorTest.java @@ -13,6 +13,7 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -95,6 +96,19 @@ public class FhirPathFilterInterceptorTest { } + @Test + public void testFilteredResponse_ExpressionReturnsExtension() throws IOException { + createPatient(); + + HttpGet request = new HttpGet(myPatientId + "?_fhirpath=Patient.extension('http://hl7.org/fhir/us/core/StructureDefinition/us-core-race')&_pretty=true"); + try (CloseableHttpResponse response = myHttpClient.execute(request)) { + String responseText = IOUtils.toString(response.getEntity().getContent(), Charsets.UTF_8); + ourLog.info("Response:\n{}", responseText); + assertThat(responseText, containsString("\"url\": \"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race\"")); + } + + } + @Test public void testFilteredResponse_ExpressionReturnsResource() throws IOException { createPatient(); @@ -159,6 +173,11 @@ public class FhirPathFilterInterceptorTest { private void createPatient() { Patient p = new Patient(); + p.addExtension() + .setUrl("http://hl7.org/fhir/us/core/StructureDefinition/us-core-race") + .addExtension() + .setUrl("ombCategory") + .setValue(new Coding("urn:oid:2.16.840.1.113883.6.238", "2106-3", "White")); p.setActive(true); p.addIdentifier().setSystem("http://identifiers/1").setValue("value-1"); p.addIdentifier().setSystem("http://identifiers/2").setValue("value-2"); diff --git a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java index b0da9754b83..52fe6b9e482 100644 --- a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java +++ b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java @@ -287,6 +287,11 @@ public final class HapiWorkerContext extends I18nBase implements IWorkerContext return myCtx.getVersion().getVersion().getFhirVersionString(); } + @Override + public String getSpecUrl() { + throw new UnsupportedOperationException(); + } + @Override public UcumService getUcumService() { throw new UnsupportedOperationException(); @@ -302,6 +307,11 @@ public final class HapiWorkerContext extends I18nBase implements IWorkerContext return false; } + @Override + public Set getCodeSystemsUsed() { + throw new UnsupportedOperationException(); + } + @Override public TranslationServices translator() { throw new UnsupportedOperationException(); diff --git a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/fhirpath/FhirPathR5.java b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/fhirpath/FhirPathR5.java index d82be431bc1..1e8d3de7dc8 100644 --- a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/fhirpath/FhirPathR5.java +++ b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/fhirpath/FhirPathR5.java @@ -30,9 +30,9 @@ public class FhirPathR5 implements IFhirPath { result = myEngine.evaluate((Base) theInput, thePath); } catch (FHIRException e) { throw new FhirPathExecutionException(e); - } + } - for (Base next : result) { + for (Base next : result) { if (!theReturnType.isAssignableFrom(next.getClass())) { throw new FhirPathExecutionException("FluentPath expression \"" + thePath + "\" returned unexpected type " + next.getClass().getSimpleName() + " - Expected " + theReturnType.getName()); } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java index 829b9977ad7..b6515f00d64 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java @@ -6,14 +6,13 @@ import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.context.support.ValidationSupportContext; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -64,8 +63,8 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple } @Override - public T fetchResource(Class theClass, String theUri) { - return loadFromCache(myCache, "fetchResource " + theClass.getName() + " " + theUri, + public T fetchResource(@Nullable Class theClass, String theUri) { + return loadFromCache(myCache, "fetchResource " + theClass + " " + theUri, t -> super.fetchResource(theClass, theUri)); } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/FhirInstanceValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/FhirInstanceValidator.java index 8b8175683d9..f673e2fda01 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/FhirInstanceValidator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/FhirInstanceValidator.java @@ -32,7 +32,7 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IInsta private boolean noExtensibleWarnings = false; private boolean noBindingMsgSuppressed = false; private volatile VersionSpecificWorkerContextWrapper myWrappedWorkerContext; - private boolean errorForUnknownProfiles; + private boolean errorForUnknownProfiles = true; private boolean assumeValidRestReferences; private List myExtensionDomains = Collections.emptyList(); private IResourceValidator.IValidatorResourceFetcher validatorResourceFetcher; diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/HapiToHl7OrgDstu2ValidatingSupportWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/HapiToHl7OrgDstu2ValidatingSupportWrapper.java index 743d3dfaf1c..922f1f4c328 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/HapiToHl7OrgDstu2ValidatingSupportWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/HapiToHl7OrgDstu2ValidatingSupportWrapper.java @@ -61,6 +61,9 @@ public class HapiToHl7OrgDstu2ValidatingSupportWrapper extends BaseValidationSup } private Class translateTypeToHapi(Class theCodeSystemType) { + if (theCodeSystemType == null) { + return null; + } String resName = getFhirContext().getResourceType(theCodeSystemType); return myHapiCtx.getResourceDefinition(resName).getImplementingClass(); } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java index 33817480b03..8c0614cab5b 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java @@ -17,6 +17,7 @@ import org.hl7.fhir.r5.elementmodel.Manager; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.utils.FHIRPathEngine; import org.hl7.fhir.r5.utils.IResourceValidator; +import org.hl7.fhir.r5.utils.XVerExtensionManager; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.validation.instance.InstanceValidator; import org.slf4j.Logger; @@ -103,8 +104,9 @@ class ValidatorWrapper { public List validate(IWorkerContext theWorkerContext, IValidationContext theValidationContext) { InstanceValidator v; FHIRPathEngine.IEvaluationContext evaluationCtx = new FhirInstanceValidator.NullEvaluationContext(); + XVerExtensionManager xverManager = new XVerExtensionManager(theWorkerContext); try { - v = new InstanceValidator(theWorkerContext, evaluationCtx); + v = new InstanceValidator(theWorkerContext, evaluationCtx, xverManager); } catch (Exception e) { throw new ConfigurationException(e); } @@ -193,7 +195,12 @@ class ValidatorWrapper { i--; } - if (message.endsWith("' could not be resolved, so has not been checked") && next.getLevel() == ValidationMessage.IssueSeverity.WARNING) { + if ( + myErrorForUnknownProfiles && + next.getLevel() == ValidationMessage.IssueSeverity.WARNING && + message.contains("Profile reference '") && + message.contains("' has not been checked because it is unknown") + ) { next.setLevel(ValidationMessage.IssueSeverity.ERROR); } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java index 30d79928b53..72269d6e3da 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/VersionSpecificWorkerContextWrapper.java @@ -81,13 +81,15 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo fetchResourceName = "ValueSet"; } } - Class fetchResourceType = myValidationSupportContext.getRootValidationSupport().getFhirContext().getResourceDefinition(fetchResourceName).getImplementingClass(); - IBaseResource fetched = myValidationSupportContext.getRootValidationSupport().fetchResource(fetchResourceType, key.getUri()); - if (fetched == null) { - return null; + Class fetchResourceType; + if (fetchResourceName.equals("Resource")) { + fetchResourceType = null; + } else { + fetchResourceType = myValidationSupportContext.getRootValidationSupport().getFhirContext().getResourceDefinition(fetchResourceName).getImplementingClass(); } + IBaseResource fetched = myValidationSupportContext.getRootValidationSupport().fetchResource(fetchResourceType, key.getUri()); Resource canonical = myModelConverter.toCanonical(fetched); @@ -425,6 +427,11 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo return myValidationSupportContext.getRootValidationSupport().getFhirContext().getVersion().getVersion().getFhirVersionString(); } + @Override + public String getSpecUrl() { + throw new UnsupportedOperationException(); + } + @Override public boolean hasCache() { throw new UnsupportedOperationException(); @@ -440,6 +447,11 @@ public class VersionSpecificWorkerContextWrapper extends I18nBase implements IWo return false; } + @Override + public Set getCodeSystemsUsed() { + throw new UnsupportedOperationException(); + } + @Override public List listTransforms() { throw new UnsupportedOperationException(); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java index f6f7fff31b7..e954e526591 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidatorDstu3Test.java @@ -664,6 +664,8 @@ public class FhirInstanceValidatorDstu3Test { } else if (t.getMessage().contains("ValueSet as a URI SHALL start with http:// or https:// or urn:")) { // Some DSTU3 structures have missing binding information return false; + } else if (t.getMessage().contains("The valueSet reference http://www.rfc-editor.org/bcp/bcp13.txt on element")) { + return false; } else { return true; } @@ -1135,7 +1137,7 @@ public class FhirInstanceValidatorDstu3Test { myInstanceVal.setValidationSupport(myValidationSupport); ValidationResult output = myVal.validateWithResult(input); List errors = logResultsAndReturnNonInformationalOnes(output); - assertThat(errors.toString(), containsString("Profile reference 'http://foo/structuredefinition/myprofile' could not be resolved, so has not been checked")); + assertThat(errors.toString(), containsString("Profile reference 'http://foo/structuredefinition/myprofile' has not been checked because it is unknown")); } @Test @@ -1314,7 +1316,7 @@ public class FhirInstanceValidatorDstu3Test { myInstanceVal.setValidatorResourceFetcher(resourceFetcher); myVal.validateWithResult(input); - verify(resourceFetcher, times(3)).resolveURL(any(), anyString(), anyString()); + verify(resourceFetcher, times(3)).resolveURL(any(), anyString(), anyString(), anyString()); verify(resourceFetcher, times(4)).validationPolicy(any(), anyString(), anyString()); verify(resourceFetcher, times(4)).fetch(any(), anyString()); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java index b514e0c311f..19634f234e8 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java @@ -254,7 +254,7 @@ public class QuestionnaireResponseValidatorDstu3Test { errors = myVal.validateWithResult(qa); errors = stripBindingHasNoSourceMessage(errors); ourLog.info(errors.toString()); - assertThat(errors.toString(), containsString("Unknown code: http://codesystems.com/system / code1 - QuestionnaireResponse.item[0].answer[0].value.ofType(Coding)")); + assertThat(errors.toString(), containsString("Unknown code: http://codesystems.com/system / code1 for 'http://codesystems.com/system#code1'")); assertThat(errors.toString(), containsString("QuestionnaireResponse.item[0].answer[0]")); // Unhandled system diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java index 3d5c728fced..53f066dce92 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java @@ -1152,7 +1152,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { List errors = logResultsAndReturnNonInformationalOnes(output); assertEquals(1, errors.size()); - assertEquals("Profile reference 'http://foo/structuredefinition/myprofile' could not be resolved, so has not been checked", errors.get(0).getMessage()); + assertEquals("Profile reference 'http://foo/structuredefinition/myprofile' has not been checked because it is unknown", errors.get(0).getMessage()); assertEquals(ResultSeverityEnum.ERROR, errors.get(0).getSeverity()); } @@ -1402,7 +1402,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { myInstanceVal.setValidatorResourceFetcher(resourceFetcher); myVal.validateWithResult(encoded); - verify(resourceFetcher, times(15)).resolveURL(any(), anyString(), anyString()); + verify(resourceFetcher, times(15)).resolveURL(any(), anyString(), anyString(), anyString()); verify(resourceFetcher, times(12)).validationPolicy(any(), anyString(), anyString()); verify(resourceFetcher, times(12)).fetch(any(), anyString()); } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java index 6c58df50ffa..f6e17f0bf65 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r5/validation/FhirInstanceValidatorR5Test.java @@ -432,8 +432,8 @@ public class FhirInstanceValidatorR5Test { myInstanceVal.setValidatorResourceFetcher(resourceFetcher); myVal.validateWithResult(input); - verify(resourceFetcher, times(13)).resolveURL(any(), anyString(), anyString()); - verify(resourceFetcher, times(3)).validationPolicy(any(), anyString(), anyString()); + verify(resourceFetcher, times(13)).resolveURL(any(), anyString(), anyString(), anyString()); + verify(resourceFetcher, times(4)).validationPolicy(any(), anyString(), anyString()); verify(resourceFetcher, times(3)).fetch(any(), anyString()); } @@ -794,7 +794,7 @@ public class FhirInstanceValidatorR5Test { myInstanceVal.setValidationSupport(myValidationSupport); ValidationResult output = myVal.validateWithResult(input); List errors = logResultsAndReturnNonInformationalOnes(output); - assertThat(errors.toString(), containsString("Profile reference 'http://foo/structuredefinition/myprofile' could not be resolved, so has not been checked")); + assertThat(errors.toString(), containsString("Profile reference 'http://foo/structuredefinition/myprofile' has not been checked because it is unknown")); } @Test diff --git a/pom.xml b/pom.xml index 11601b5e988..fe1062d7e26 100644 --- a/pom.xml +++ b/pom.xml @@ -712,7 +712,7 @@ - 5.2.0 + 5.2.16 1.0.3 -Dfile.encoding=UTF-8 -Xmx2048m