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 = "
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);
}