diff --git a/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v311/ConformanceTest.java b/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v311/ConformanceTest.java index 95bcdc9811f..7f07de902b3 100644 --- a/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v311/ConformanceTest.java +++ b/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v311/ConformanceTest.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.List; +import org.testng.Assert; import org.testng.annotations.Test; import com.ibm.fhir.model.format.Format; @@ -340,9 +341,12 @@ public void testUSCoreEthnicityExtension4() throws Exception { issues.forEach(System.out::println); - assertEquals(countWarnings(issues), 0); - assertEquals(countErrors(issues), 2); - assertEquals(countInformation(issues), 1); + // one for generated-ext-1: Extension must conform to definition 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race' + // and one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countErrors(issues), 4); + Assert.assertEquals(countWarnings(issues), 0); + // one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countInformation(issues), 3); } /** @@ -523,9 +527,12 @@ public void testUSCoreRaceExtension4() throws Exception { issues.forEach(System.out::println); - assertEquals(countWarnings(issues), 1); - assertEquals(countErrors(issues), 2); - assertEquals(countInformation(issues), 1); + // one for generated-ext-1: Extension must conform to definition 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race' + // and one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countErrors(issues), 4); + Assert.assertEquals(countWarnings(issues), 1); + // one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countInformation(issues), 3); } } \ No newline at end of file diff --git a/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v400/ConformanceTest.java b/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v400/ConformanceTest.java index f3f4b1942e1..09d670decd6 100644 --- a/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v400/ConformanceTest.java +++ b/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v400/ConformanceTest.java @@ -333,9 +333,12 @@ public void testUSCoreEthnicityExtension4() throws Exception { issues.forEach(System.out::println); + // one for generated-ext-1: Extension must conform to definition 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race' + // and one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countErrors(issues), 4); Assert.assertEquals(countWarnings(issues), 1); - Assert.assertEquals(countErrors(issues), 2); - Assert.assertEquals(countInformation(issues), 1); + // one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countInformation(issues), 3); } /** @@ -516,9 +519,12 @@ public void testUSCoreRaceExtension4() throws Exception { issues.forEach(System.out::println); + // one for generated-ext-1: Extension must conform to definition 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race' + // and one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countErrors(issues), 4); Assert.assertEquals(countWarnings(issues), 1); - Assert.assertEquals(countErrors(issues), 2); - Assert.assertEquals(countInformation(issues), 1); + // one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countInformation(issues), 3); } @Test diff --git a/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v500/ConformanceTest.java b/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v500/ConformanceTest.java index d5748d13ec9..52b543ccc19 100644 --- a/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v500/ConformanceTest.java +++ b/conformance/fhir-ig-us-core/src/test/java/com/ibm/fhir/ig/us/core/test/v500/ConformanceTest.java @@ -333,9 +333,12 @@ public void testUSCoreEthnicityExtension4() throws Exception { issues.forEach(System.out::println); + // one for generated-ext-1: Extension must conform to definition 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race' + // and one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countErrors(issues), 4); Assert.assertEquals(countWarnings(issues), 1); - Assert.assertEquals(countErrors(issues), 2); - Assert.assertEquals(countInformation(issues), 1); + // one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countInformation(issues), 3); } /** @@ -516,9 +519,12 @@ public void testUSCoreRaceExtension4() throws Exception { issues.forEach(System.out::println); + // one for generated-ext-1: Extension must conform to definition 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race' + // and one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countErrors(issues), 4); Assert.assertEquals(countWarnings(issues), 1); - Assert.assertEquals(countErrors(issues), 2); - Assert.assertEquals(countInformation(issues), 1); + // one for each version of the extension due to the generated constraint "conformsTo('a'|1) or conformsTo('a'|2) or conformsTo('a'|3)..." + Assert.assertEquals(countInformation(issues), 3); } @Test diff --git a/fhir-model/src/main/java/com/ibm/fhir/model/type/Xhtml.java b/fhir-model/src/main/java/com/ibm/fhir/model/type/Xhtml.java index 98b3ee0c13a..3a471c99c1e 100644 --- a/fhir-model/src/main/java/com/ibm/fhir/model/type/Xhtml.java +++ b/fhir-model/src/main/java/com/ibm/fhir/model/type/Xhtml.java @@ -11,19 +11,19 @@ import javax.annotation.Generated; +import org.owasp.encoder.Encode; + import com.ibm.fhir.model.annotation.Required; import com.ibm.fhir.model.util.ValidationSupport; import com.ibm.fhir.model.visitor.Visitor; -import org.owasp.encoder.Encode; /** * XHTML */ @Generated("com.ibm.fhir.tools.CodeGenerator") public class Xhtml extends Element { - private static final java.lang.String DIV_OPEN = "
"; - - private static final java.lang.String DIV_CLOSE = "
"; + public static final java.lang.String DIV_OPEN = "
"; + public static final java.lang.String DIV_CLOSE = "
"; @Required private final java.lang.String value; @@ -35,7 +35,7 @@ private Xhtml(Builder builder) { /** * Actual xhtml - * + * * @return * An immutable object of type {@link java.lang.String} that is non-null. */ @@ -55,7 +55,7 @@ public boolean hasChildren() { /** * Factory method for creating Xhtml objects from an XHTML java.lang.String - * + * * @param value * A java.lang.String with valid XHTML content, not null */ @@ -66,7 +66,7 @@ public static Xhtml of(java.lang.String value) { /** * Factory method for creating Xhtml objects from an XHTML java.lang.String - * + * * @param value * A java.lang.String with valid XHTML content, not null */ @@ -77,10 +77,10 @@ public static Xhtml xhtml(java.lang.String value) { /** * Factory method for creating Xhtml objects from a plain text string - * + * *

This method will automatically encode the passed string for use within XHTML, * then wrap it in an XHTML {@code

} element with a namespace of {@code http://www.w3.org/1999/xhtml} - * + * * @param plainText * The text to encode and wrap for use within a Narrative, not null */ @@ -116,8 +116,8 @@ public boolean equals(Object obj) { return false; } Xhtml other = (Xhtml) obj; - return Objects.equals(id, other.id) && - Objects.equals(extension, other.extension) && + return Objects.equals(id, other.id) && + Objects.equals(extension, other.extension) && Objects.equals(value, other.value); } @@ -125,8 +125,8 @@ public boolean equals(Object obj) { public int hashCode() { int result = hashCode; if (result == 0) { - result = Objects.hash(id, - extension, + result = Objects.hash(id, + extension, value); hashCode = result; } @@ -151,10 +151,10 @@ private Builder() { /** * unique id for the element within a resource (for internal references) - * + * * @param id * xml:id (or equivalent in JSON) - * + * * @return * A reference to this Builder instance */ @@ -164,19 +164,19 @@ public Builder id(java.lang.String id) { } /** - * May be used to represent additional information that is not part of the basic definition of the resource. To make the - * use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of - * extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part + * May be used to represent additional information that is not part of the basic definition of the resource. To make the + * use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of + * extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part * of the definition of the extension. - * + * *

Adds new element(s) to the existing list. * If any of the elements are null, calling {@link #build()} will fail. - * + * *

This element is prohibited. - * + * * @param extension * Additional content defined by implementations - * + * * @return * A reference to this Builder instance */ @@ -186,22 +186,22 @@ public Builder extension(Extension... extension) { } /** - * May be used to represent additional information that is not part of the basic definition of the resource. To make the - * use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of - * extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part + * May be used to represent additional information that is not part of the basic definition of the resource. To make the + * use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of + * extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part * of the definition of the extension. - * + * *

Replaces the existing list with a new one containing elements from the Collection. * If any of the elements are null, calling {@link #build()} will fail. - * + * *

This element is prohibited. - * + * * @param extension * Additional content defined by implementations - * + * * @return * A reference to this Builder instance - * + * * @throws NullPointerException * If the passed collection is null */ @@ -212,12 +212,12 @@ public Builder extension(Collection extension) { /** * Actual xhtml - * + * *

This element is required. - * + * * @param value * Actual xhtml - * + * * @return * A reference to this Builder instance */ @@ -228,12 +228,12 @@ public Builder value(java.lang.String value) { /** * Build the {@link Xhtml} - * + * *

Required elements: *

- * + * * @return * An immutable object of type {@link Xhtml} * @throws IllegalStateException diff --git a/fhir-model/src/main/java/com/ibm/fhir/model/visitor/PathAwareCollectingVisitor.java b/fhir-model/src/main/java/com/ibm/fhir/model/visitor/PathAwareCollectingVisitor.java new file mode 100644 index 00000000000..ee55ebe090b --- /dev/null +++ b/fhir-model/src/main/java/com/ibm/fhir/model/visitor/PathAwareCollectingVisitor.java @@ -0,0 +1,99 @@ +/* + * (C) Copyright IBM Corp. 2022 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.ibm.fhir.model.visitor; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Visits a Resource or Element and collects all of its descendants of a given type into a collection + * of those elements, indexed by their simple FHIRPath path. + * + * @param The type of object to collect + * @implNote The order of the list will be consistent with a depth-first traversal of the visited object + */ +public class PathAwareCollectingVisitor extends PathAwareVisitor { + protected final Map result = new LinkedHashMap<>(); + protected final Class type; + + public PathAwareCollectingVisitor(Class type) { + super(); + this.type = type; + } + + protected void collect(Object object) { + if (type.isInstance(object)) { + result.put(getPath(), type.cast(object)); + } + } + + public Map getResult() { + return Collections.unmodifiableMap(result); + } + + @Override + public boolean visit(java.lang.String elementName, int elementIndex, Visitable visitable) { + collect(visitable); + return true; + } + + @Override + public void doVisit(java.lang.String elementName, byte[] value) { + collect(value); + } + + @Override + public void doVisit(java.lang.String elementName, BigDecimal value) { + collect(value); + } + + @Override + public void doVisit(java.lang.String elementName, java.lang.Boolean value) { + collect(value); + } + + @Override + public void doVisit(java.lang.String elementName, java.lang.Integer value) { + collect(value); + } + + @Override + public void doVisit(java.lang.String elementName, LocalDate value) { + collect(value); + } + + @Override + public void doVisit(java.lang.String elementName, LocalTime value) { + collect(value); + } + + @Override + public void doVisit(java.lang.String elementName, java.lang.String value) { + collect(value); + } + + @Override + public void doVisit(java.lang.String elementName, Year value) { + collect(value); + } + + @Override + public void doVisit(java.lang.String elementName, YearMonth value) { + collect(value); + } + + @Override + public void doVisit(java.lang.String elementName, ZonedDateTime value) { + collect(value); + } +} diff --git a/fhir-model/src/test/java/com/ibm/fhir/model/visitor/test/PathAwareCollectingVisitorTest.java b/fhir-model/src/test/java/com/ibm/fhir/model/visitor/test/PathAwareCollectingVisitorTest.java new file mode 100644 index 00000000000..5a4210fe166 --- /dev/null +++ b/fhir-model/src/test/java/com/ibm/fhir/model/visitor/test/PathAwareCollectingVisitorTest.java @@ -0,0 +1,62 @@ +/* + * (C) Copyright IBM Corp. 2022 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.model.visitor.test; + +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +import org.testng.annotations.Test; + +import com.ibm.fhir.model.resource.Patient; +import com.ibm.fhir.model.resource.ValueSet; +import com.ibm.fhir.model.resource.ValueSet.Expansion; +import com.ibm.fhir.model.type.DateTime; +import com.ibm.fhir.model.type.Extension; +import com.ibm.fhir.model.type.HumanName; +import com.ibm.fhir.model.type.Meta; +import com.ibm.fhir.model.type.Narrative; +import com.ibm.fhir.model.type.Xhtml; +import com.ibm.fhir.model.type.code.NarrativeStatus; +import com.ibm.fhir.model.type.code.PublicationStatus; +import com.ibm.fhir.model.visitor.PathAwareCollectingVisitor; + +public class PathAwareCollectingVisitorTest { + @Test + public void testPrimitiveSetterEquivalence() { + Patient p1 = Patient.builder() + .text(Narrative.builder() + .status(NarrativeStatus.ADDITIONAL) + .div(Xhtml.of(Xhtml.DIV_OPEN + "
this
is
a test
" + Xhtml.DIV_CLOSE)) + .build()) + .meta(Meta.builder() + .lastUpdated(ZonedDateTime.of(2021, 8, 19, 00, 59, 59, 0, ZoneOffset.of("-05:00"))) // Instant + .build()) + .extension(Extension.builder() + .url("test") + .value("string") + .build()) + .contained(ValueSet.builder() + .status(PublicationStatus.DRAFT) + .expansion(Expansion.builder() + .timestamp(DateTime.of("2021-08-19T00:59:59-05:00")) + .total(0) // Integer + .build()) + .build()) + .active(false) // Boolean + .birthDate(LocalDate.of(1984, 9, 4)) // Date + .multipleBirth(1) + .name(HumanName.builder() + .given("Lee") // String + .build()) + .build(); + + PathAwareCollectingVisitor extCollector = new PathAwareCollectingVisitor(Extension.class); + p1.accept(extCollector); + System.out.println(extCollector.getResult()); + } +} diff --git a/fhir-validation/src/main/java/com/ibm/fhir/validation/FHIRValidator.java b/fhir-validation/src/main/java/com/ibm/fhir/validation/FHIRValidator.java index 61722596fe3..8b924fe8aa4 100644 --- a/fhir-validation/src/main/java/com/ibm/fhir/validation/FHIRValidator.java +++ b/fhir-validation/src/main/java/com/ibm/fhir/validation/FHIRValidator.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2019, 2021 + * (C) Copyright IBM Corp. 2019, 2022 * * SPDX-License-Identifier: Apache-2.0 */ @@ -29,12 +29,17 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import com.ibm.fhir.model.annotation.Constraint; import com.ibm.fhir.model.annotation.Constraint.FHIRPathConstraintValidator; @@ -47,6 +52,7 @@ import com.ibm.fhir.model.type.code.IssueSeverity; import com.ibm.fhir.model.type.code.IssueType; import com.ibm.fhir.model.util.ModelSupport; +import com.ibm.fhir.model.visitor.PathAwareCollectingVisitor; import com.ibm.fhir.model.visitor.Visitable; import com.ibm.fhir.path.FHIRPathElementNode; import com.ibm.fhir.path.FHIRPathNode; @@ -58,6 +64,7 @@ import com.ibm.fhir.path.visitor.FHIRPathDefaultNodeVisitor; import com.ibm.fhir.profile.ProfileSupport; import com.ibm.fhir.registry.FHIRRegistry; +import com.ibm.fhir.registry.resource.FHIRRegistryResource; import com.ibm.fhir.validation.exception.FHIRValidationException; import net.jcip.annotations.NotThreadSafe; @@ -300,16 +307,18 @@ public void doVisit(FHIRPathResourceNode node) { private void validate(FHIRPathElementNode elementNode) { Class elementType = elementNode.element().getClass(); List constraints = new ArrayList<>(ModelSupport.getConstraints(elementType)); - if (Extension.class.equals(elementType)) { - String url = elementNode.element().as(Extension.class).getUrl(); - if (isAbsolute(url)) { - if (FHIRRegistry.getInstance().hasResource(url, StructureDefinition.class)) { - constraints.add(Constraint.Factory.createConstraint("generated-ext-1", Constraint.LEVEL_RULE, Constraint.LOCATION_BASE, "Extension must conform to definition '" + url + "'", "conformsTo('" + url + "')", SOURCE_VALIDATOR, false, true)); - } else { - issues.add(issue(IssueSeverity.WARNING, IssueType.NOT_SUPPORTED, "Extension definition '" + url + "' is not supported", elementNode)); - } - } - } +// if (Extension.class.equals(elementType)) { +// String url = elementNode.element().as(Extension.class).getUrl(); +// if (isAbsolute(url)) { +// Collection registryResources = FHIRRegistry.getInstance().getRegistryResources(StructureDefinition.class); +// if (FHIRRegistry.getInstance().hasResource(url, StructureDefinition.class)) { +// constraints.add(Constraint.Factory.createConstraint("generated-ext-1", Constraint.LEVEL_RULE, Constraint.LOCATION_BASE, +// "Extension must conform to definition '" + url + "'", "conformsTo('" + url + "')", SOURCE_VALIDATOR, false, true)); +// } else { +// issues.add(issue(IssueSeverity.WARNING, IssueType.NOT_SUPPORTED, "Extension definition '" + url + "' is not supported", elementNode)); +// } +// } +// } validate(elementNode, constraints); } @@ -334,9 +343,67 @@ private void validate(FHIRPathResourceNode resourceNode) { validateProfileReferences(resourceNode, profiles, false); constraints.addAll(ProfileSupport.getConstraints(profiles, resourceType)); } + + // add instance-specific extension constraints + PathAwareCollectingVisitor extCollector = new PathAwareCollectingVisitor(Extension.class); + resourceNode.resource().accept(extCollector); + Map pathToExtension = extCollector.getResult(); + + // for option A below: find all the versions of the extension in the registry + Map> profileVersions = collectProfileVersions(pathToExtension.values()); + + for (Entry entry : pathToExtension.entrySet()) { + String path = entry.getKey(); + Extension e = entry.getValue(); + String url = e.getUrl(); + if (isAbsolute(url)) { + // Option A: find all versions of the extension and pass validation if the instance conforms to at least one + // Option B: introspect the existing constraints and only add this one if the extension is not covered by profile constraints + + // Option A: conformance to any one version of the extension is sufficient + if (profileVersions.containsKey(url)) { + String constraint = profileVersions.get(url).stream() + .map(v -> "conformsTo('" + url + "|" + v + "')") + .collect(Collectors.joining(" or ")); + + constraints.add(Constraint.Factory.createConstraint("generated-ext-1", Constraint.LEVEL_RULE, path, + "Extension must conform to at least one definition of '" + url + "'", constraint, SOURCE_VALIDATOR, false, true)); + } else { + issues.add(issue(IssueSeverity.WARNING, IssueType.NOT_SUPPORTED, "Extension definition '" + url + "' is not supported", null, path)); + } + + // Option B: conditionally add a conformsTo constraint for the default/latest version of this extension +// if (FHIRRegistry.getInstance().hasResource(url, StructureDefinition.class)) { +// constraints.add(Constraint.Factory.createConstraint("generated-ext-2", Constraint.LEVEL_RULE, path, +// "Extension must conform to definition '" + url + "'", +// "conformsTo('" + url + "')", SOURCE_VALIDATOR, false, true)); +// } else { +// issues.add(issue(IssueSeverity.WARNING, IssueType.NOT_SUPPORTED, "Extension definition '" + url + "' is not supported", null, path)); +// } + } + } + validate(resourceNode, constraints); } + private Map> collectProfileVersions(Collection extensions) { + Map> profileVersions = new HashMap<>(); + + Set uniqueExtensionUrls = extensions.stream() + .map(e -> e.getUrl()) + .distinct() + .collect(Collectors.toSet()); + + // RegistryResourceProviders have a method thats basically just what we want, but unfortunately the registry does not + for (FHIRRegistryResource rr : FHIRRegistry.getInstance().getRegistryResources(StructureDefinition.class)) { + if (uniqueExtensionUrls.contains(rr.getUrl()) && rr.getVersion() != null) { + profileVersions.computeIfAbsent(rr.getUrl(), x -> new HashSet<>()).add(rr.getVersion().toString()); + } + } + + return profileVersions; + } + private void validateProfileReferences(FHIRPathResourceNode resourceNode, List profiles, boolean resourceAsserted) { Class resourceType = resourceNode.resource().getClass(); for (String url : profiles) { diff --git a/fhir-validation/src/test/java/com/ibm/fhir/validation/test/FHIRValidatorTest.java b/fhir-validation/src/test/java/com/ibm/fhir/validation/test/FHIRValidatorTest.java index 2ae862fce53..d820d96c746 100644 --- a/fhir-validation/src/test/java/com/ibm/fhir/validation/test/FHIRValidatorTest.java +++ b/fhir-validation/src/test/java/com/ibm/fhir/validation/test/FHIRValidatorTest.java @@ -17,6 +17,9 @@ import java.util.List; import java.util.UUID; +import org.testng.Assert; +import org.testng.annotations.Test; + import com.ibm.fhir.model.format.Format; import com.ibm.fhir.model.generator.FHIRGenerator; import com.ibm.fhir.model.parser.FHIRParser; @@ -42,9 +45,6 @@ import com.ibm.fhir.validation.FHIRValidator; import com.ibm.fhir.validation.exception.FHIRValidationException; -import org.testng.Assert; -import org.testng.annotations.Test; - public class FHIRValidatorTest { @Test public void testPatientValidation() throws Exception { @@ -99,9 +99,17 @@ public void testPatientValidation() throws Exception { } assertEquals(issues.size(), 3); assertEquals(issues.get(0).getSeverity(), IssueSeverity.WARNING); - assertTrue(issues.get(0).getDetails().getText().getValue().contains("dom-6: A resource should have narrative for robust management")); + assertTrue(issues.get(0).getDetails().getText().getValue().contains("Extension definition 'http://www.ibm.com/someExtension' is not supported")); assertTrue(issues.get(0).getExpression().size() == 1); - assertTrue(issues.get(0).getExpression().get(0).getValue().equals("Patient")); + assertTrue(issues.get(0).getExpression().get(0).getValue().equals("Patient.name[0].given[0].extension[0]")); + assertEquals(issues.get(1).getSeverity(), IssueSeverity.WARNING); + assertTrue(issues.get(1).getDetails().getText().getValue().contains("Extension definition 'http://www.ibm.com/someExtension' is not supported")); + assertTrue(issues.get(1).getExpression().size() == 1); + assertTrue(issues.get(1).getExpression().get(0).getValue().equals("Patient.name[0].given[1].extension[0]")); + assertEquals(issues.get(2).getSeverity(), IssueSeverity.WARNING); + assertTrue(issues.get(2).getDetails().getText().getValue().contains("dom-6: A resource should have narrative for robust management")); + assertTrue(issues.get(2).getExpression().size() == 1); + assertTrue(issues.get(2).getExpression().get(0).getValue().equals("Patient")); } @Test @@ -205,9 +213,9 @@ public void testCodingValidation() throws FHIRValidationException { List issues = FHIRValidator.validator().validate(endpoint); // 1. Profile 'http://hl7.org/fhir/us/davinci-pdex-plan-net/StructureDefinition/plannet-Endpoint' is not supported // 2. dom-6: A resource should have narrative for robust management - // 3. Code 'hl7-fhir-opn' in system 'http://hl7.org/fhir/us/davinci-pdex-plan-net/CodeSystem/EndpointConnectionTypeCS' + // 3. Code 'hl7-fhir-opn' in system 'http://hl7.org/fhir/us/davinci-pdex-plan-net/CodeSystem/EndpointConnectionTypeCS' // is not a valid member of ValueSet with URL=http://hl7.org/fhir/ValueSet/endpoint-connection-type and version=4.0.1 - // 4. endpoint-0: The concept in this element must be from the specified value set + // 4. endpoint-0: The concept in this element must be from the specified value set // 'http://hl7.org/fhir/ValueSet/endpoint-connection-type' if possible assertEquals(issues.size(), 4, "number of issues"); } diff --git a/term/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java b/term/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java index a1efd7097b4..47d64a05489 100644 --- a/term/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java +++ b/term/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java @@ -416,34 +416,74 @@ public LookupOutcome lookup(Coding coding, LookupParameters parameters) { if (!LookupParameters.EMPTY.equals(parameters)) { throw new UnsupportedOperationException("Lookup parameters are not suppored"); } + java.lang.String system = (coding.getSystem() != null) ? coding.getSystem().getValue() : null; + java.lang.String version = (coding.getVersion() != null) ? coding.getVersion().getValue() : null; + java.lang.String url = (version != null) ? system + "|" + version : system; + CodeSystem codeSystem = CodeSystemSupport.getCodeSystem(url); + if (codeSystem == null) { + LOGGER.fine(() -> "Unable to find CodeSystem with url: " + url); + return null; + } + return lookup(codeSystem, coding, parameters); + } + + /** + * Lookup the code system concept for the given coding within the passed CodeSystem + * @param codeSystem + * the code system to look in + * @param coding + * the coding to lookup + * @param parameters + * the lookup parameters + * + * @return + * the outcome of the lookup + */ + public LookupOutcome lookup(CodeSystem codeSystem, Coding coding, LookupParameters parameters) { + if (!LookupParameters.EMPTY.equals(parameters)) { + throw new UnsupportedOperationException("Lookup parameters are not suppored"); + } + Objects.requireNonNull(coding, "coding"); + Objects.requireNonNull(codeSystem, "codeSystem"); Uri system = coding.getSystem(); Code code = coding.getCode(); - if (system != null && code != null) { - java.lang.String version = (coding.getVersion() != null) ? coding.getVersion().getValue() : null; - java.lang.String url = (version != null) ? system.getValue() + "|" + version : system.getValue(); - CodeSystem codeSystem = CodeSystemSupport.getCodeSystem(url); - if (codeSystem != null) { - Concept concept = findProvider(codeSystem).getConcept(codeSystem, code); - if (concept != null) { - return LookupOutcome.builder() - .name((codeSystem.getName() != null) ? codeSystem.getName() : STRING_DATA_ABSENT_REASON_UNKNOWN) - .version(codeSystem.getVersion()) - .display((concept.getDisplay() != null) ? concept.getDisplay() : STRING_DATA_ABSENT_REASON_UNKNOWN) - .property(concept.getProperty().stream() - .map(property -> Property.builder() - .code(property.getCode()) - .value(property.getValue()) - .build()) - .collect(Collectors.toList())) - .designation(concept.getDesignation().stream() - .map(designation -> Designation.builder() - .language(designation.getLanguage()) - .use(designation.getUse()) - .value(designation.getValue()) - .build()) - .collect(Collectors.toList())) - .build(); + + if (coding.getVersion() != null) { + if (codeSystem.getVersion() != null && codeSystem.getVersion().hasValue()) { + java.lang.String systemVersion = codeSystem.getVersion().getValue(); + if (!systemVersion.equals(coding.getVersion().getValue())) { + java.lang.String systemUrl = (codeSystem.getUrl() == null) ? null : codeSystem.getUrl().getValue(); + LOGGER.info("Client code requested version " + coding.getVersion().getValue() + " but the" + + " passed CodeSystem (" + systemUrl + ") was version " + systemVersion); + return null; } + } else { + LOGGER.info("Client code requested version " + coding.getVersion().getValue() + " but using" + + " a CodeSystem (" + codeSystem.getUrl() + ") with no version info."); + } + } + + if (system != null && code != null) { + Concept concept = findProvider(codeSystem).getConcept(codeSystem, code); + if (concept != null) { + return LookupOutcome.builder() + .name((codeSystem.getName() != null) ? codeSystem.getName() : STRING_DATA_ABSENT_REASON_UNKNOWN) + .version(codeSystem.getVersion()) + .display((concept.getDisplay() != null) ? concept.getDisplay() : STRING_DATA_ABSENT_REASON_UNKNOWN) + .property(concept.getProperty().stream() + .map(property -> Property.builder() + .code(property.getCode()) + .value(property.getValue()) + .build()) + .collect(Collectors.toList())) + .designation(concept.getDesignation().stream() + .map(designation -> Designation.builder() + .language(designation.getLanguage()) + .use(designation.getUse()) + .value(designation.getValue()) + .build()) + .collect(Collectors.toList())) + .build(); } } return null; @@ -749,9 +789,9 @@ public ValidationOutcome validateCode(CodeSystem codeSystem, CodeableConcept cod .append(codeSystem.getVersion() == null ? null : codeSystem.getVersion().getValue()); LOGGER.fine(message.toString()); } - + // If we add a message to this ValidationOutcome, then it will create a new issue in the issue list; - // our assumption here is that the false result will instead bubble up to some other issue and so we + // our assumption here is that the false result will instead bubble up to some other issue and so we // chose not to create redundant issues. return buildValidationOutcome(false); } @@ -786,7 +826,7 @@ public ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding, Vali if (!ValidationParameters.EMPTY.equals(parameters)) { throw new UnsupportedOperationException("Validation parameters are not supported"); } - LookupOutcome outcome = lookup(coding, LookupParameters.EMPTY); + LookupOutcome outcome = lookup(codeSystem, coding, LookupParameters.EMPTY); if (outcome != null) { return validateDisplay(null, coding, outcome); } else { @@ -799,7 +839,7 @@ public ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding, Vali message.append(coding.getSystem().getValue()); } message.append("'"); - + return buildValidationOutcome(false, message.toString()); } } @@ -938,7 +978,7 @@ public ValidationOutcome validateCode(ValueSet valueSet, CodeableConcept codeabl return validateDisplay(null, coding, outcome); } } - + if (LOGGER.isLoggable(Level.FINE)) { StringBuilder message = new StringBuilder() .append("None of the Coding values in the CodeableConcept were found to be valid in ValueSet with URL=") @@ -949,7 +989,7 @@ public ValidationOutcome validateCode(ValueSet valueSet, CodeableConcept codeabl } // If we add a message to this ValidationOutcome, then it will create a new issue in the issue list; - // our assumption here is that the false result will instead bubble up to some other issue and so we + // our assumption here is that the false result will instead bubble up to some other issue and so we // chose not to create redundant issues. return buildValidationOutcome(false); } @@ -1109,11 +1149,11 @@ private ValidationOutcome validateDisplay(CodeSystem codeSystem, Coding coding, java.lang.String url = (version != null) ? system + "|" + version : system; caseSensitive = CodeSystemSupport.isCaseSensitive(url); } - boolean result = caseSensitive ? lookupOutcome.getDisplay().equals(coding.getDisplay()) : + boolean result = caseSensitive ? lookupOutcome.getDisplay().equals(coding.getDisplay()) : normalize(lookupOutcome.getDisplay().getValue()).equals(normalize(coding.getDisplay().getValue())); - java.lang.String message = !result ? java.lang.String.format("The display '%s' is incorrect for code '%s' from code system '%s'", + java.lang.String message = !result ? java.lang.String.format("The display '%s' is incorrect for code '%s' from code system '%s'", coding.getDisplay().getValue(), coding.getCode().getValue(), system) : null; - + return buildValidationOutcome(result, message, lookupOutcome); }