diff --git a/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv40_50/Questionnaire40_50Test.java b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv40_50/Questionnaire40_50Test.java new file mode 100644 index 000000000..229cb435b --- /dev/null +++ b/org.hl7.fhir.convertors/src/test/java/org/hl7/fhir/convertors/conv40_50/Questionnaire40_50Test.java @@ -0,0 +1,37 @@ +package org.hl7.fhir.convertors.conv40_50; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50; +import org.hl7.fhir.utilities.TextFile; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class Questionnaire40_50Test { + + @Test + @DisplayName("Test r5 -> r4 Questionnaire conversion.") + public void testR5_R4() throws IOException { + InputStream r4_input = this.getClass().getResourceAsStream("/q_open_40.json"); + String source = TextFile.streamToString(r4_input); + System.out.println(source); + + org.hl7.fhir.r4.model.Questionnaire r4_actual = (org.hl7.fhir.r4.model.Questionnaire) new org.hl7.fhir.r4.formats.JsonParser().parse(source); + org.hl7.fhir.r5.model.Resource r5_conv = VersionConvertorFactory_40_50.convertResource(r4_actual); + + org.hl7.fhir.r5.formats.JsonParser r5_parser = new org.hl7.fhir.r5.formats.JsonParser(); + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + r5_parser.compose(stream, r5_conv); + + org.hl7.fhir.r5.model.Resource r5_streamed = (org.hl7.fhir.r5.model.Questionnaire) new org.hl7.fhir.r5.formats.JsonParser().parse(new ByteArrayInputStream(stream.toByteArray())); + org.hl7.fhir.r4.model.Resource r4_conv = VersionConvertorFactory_40_50.convertResource(r5_streamed); + + assertTrue(r4_actual.equalsDeep(r4_conv), "should be the same"); + } +} diff --git a/org.hl7.fhir.convertors/src/test/resources/q_open_40.json b/org.hl7.fhir.convertors/src/test/resources/q_open_40.json new file mode 100644 index 000000000..ee9556bf6 --- /dev/null +++ b/org.hl7.fhir.convertors/src/test/resources/q_open_40.json @@ -0,0 +1,53 @@ +{"resourceType": "Questionnaire", + "id": "ed364266b937bb3bd73082b1", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "code": "editableDropdown" + } + ] + } + } + ], + "id": "specimen-source", + "answerOption": [ + { + "valueCoding": { + "code": "U", + "display": "Urine" + } + }, + { + "valueCoding": { + "code": "B", + "display": "Blood" + } + }, + { + "valueCoding": { + "code": "S", + "display": "Saliva" + } + } + ], + "code": [ + { + "code": "specimen-source" + } + ], + "linkId": "specimen-source", + "text": "Source of specimen", + "type": "open-choice" + } + ], + "name": "Test Open Choice question", + "status": "active", + "subjectType": [ + "Patient" + ] +} \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java index a4bfb0e1b..b1e80f184 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/profile/ProfileUtilities.java @@ -294,17 +294,28 @@ public class ProfileUtilities { public static class SourcedChildDefinitions { private StructureDefinition source; private List list; + private String path; public SourcedChildDefinitions(StructureDefinition source, List list) { super(); this.source = source; this.list = list; } + public SourcedChildDefinitions(StructureDefinition source, List list, String path) { + super(); + this.source = source; + this.list = list; + this.path = path; + } public StructureDefinition getSource() { return source; } public List getList() { return list; } + public String getPath() { + return path; + } + } public class ElementDefinitionResolution { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/DataRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/DataRenderer.java index 839648e4d..79980da72 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/DataRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/DataRenderer.java @@ -41,6 +41,7 @@ import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent; +import org.hl7.fhir.r5.renderers.Renderer.RenderingStatus; import org.hl7.fhir.r5.renderers.utils.RenderingContext; import org.hl7.fhir.r5.renderers.utils.RenderingContext.GenerationRules; import org.hl7.fhir.r5.renderers.utils.RenderingContext.ResourceRendererMode; @@ -486,24 +487,45 @@ public class DataRenderer extends Renderer implements CodeResolver { // } // } // - public void renderExtensionsInText(RenderingStatus status, XhtmlNode div, ResourceWrapper element, String sep) throws FHIRFormatError, DefinitionException, IOException { + public void renderExtensionsInText(RenderingStatus status, XhtmlNode x, ResourceWrapper element, String sep) throws FHIRFormatError, DefinitionException, IOException { boolean first = true; for (ResourceWrapper ext : element.extensions()) { if (canRender(ext)) { + status.setExtensions(true); if (first) { first = false; } else { - div.tx(sep); - div.tx(" "); + x.tx(sep); + x.tx(" "); } String lbl = getExtensionLabel(ext); - div.tx(lbl); - div.tx(": "); - renderDataType(status, div, ext.child("value")); + x.tx(lbl); + x.tx(": "); + renderDataType(status, x, ext.child("value")); } } } + + + protected void checkRenderExtensions(RenderingStatus status, XhtmlNode x, ResourceWrapper element) throws FHIRFormatError, DefinitionException, IOException { + if (element.has("extension")) { + boolean someCanRender = false; + for (ResourceWrapper ext : element.children("extension")) { + ResourceWrapper value = ext.child("value"); + if (canRender(ext) && value.isPrimitive()) { + someCanRender = true; + } + } + if (someCanRender) { + status.setExtensions(true); + x.tx(" ("); + renderExtensionsInText(status, x, element, ", "); + x.tx(")"); + } + } + + } // public void renderExtensionsInList(XhtmlNode div, BackboneType element, String sep) throws FHIRFormatError, DefinitionException, IOException { // boolean first = true; @@ -742,13 +764,14 @@ public class DataRenderer extends Renderer implements CodeResolver { } public boolean canRenderDataType(String type) { - return context.getContextUtilities().isPrimitiveType(type) || Utilities.existsInList(type, "Annotation", "Coding", "CodeableConcept", "Identifier", "HumanName", "Address", + return context.getContextUtilities().isPrimitiveType(type) || Utilities.existsInList(type, "Annotation", "Coding", "CodeableConcept", "Identifier", "HumanName", "Address", "Dosage", "Expression", "Money", "ContactPoint", "Quantity", "Range", "Period", "Timing", "SampledData", "Reference", "UsageContext", "ContactDetail", "Ratio", "Attachment", "CodeableReference"); } public boolean renderDataType(RenderingStatus status, XhtmlNode x, ResourceWrapper type) throws FHIRFormatError, DefinitionException, IOException { return renderDataType(status, null, x, type); } + public boolean renderDataType(RenderingStatus status, XhtmlNode parent, XhtmlNode x, ResourceWrapper type) throws FHIRFormatError, DefinitionException, IOException { if (type == null) { return false; @@ -863,10 +886,11 @@ public class DataRenderer extends Renderer implements CodeResolver { renderUri(status, x, type); } - private void renderRatio(RenderingStatus status, XhtmlNode x, ResourceWrapper type) { + private void renderRatio(RenderingStatus status, XhtmlNode x, ResourceWrapper type) throws FHIRFormatError, DefinitionException, IOException { renderQuantity(status, x, type.child("numerator")); x.tx("/"); - renderQuantity(status, x, type.child("denominator")); + renderQuantity(status, x, type.child("denominator")); + checkRenderExtensions(status, x, type); } private void renderAttachment(RenderingStatus status, XhtmlNode x, ResourceWrapper att) { @@ -892,6 +916,7 @@ public class DataRenderer extends Renderer implements CodeResolver { private void renderDateTime(RenderingStatus status, XhtmlNode x, ResourceWrapper type) throws FHIRFormatError, DefinitionException, IOException { if (!renderPrimitiveWithNoValue(status, x, type)) { x.tx(displayDateTime(type)); + checkRenderExtensions(status, x, type); } } @@ -1002,7 +1027,11 @@ public class DataRenderer extends Renderer implements CodeResolver { return true; } - private String tail(String url) { + protected String tail(String url) { + return url.substring(url.lastIndexOf(".")+1); + } + + protected String utail(String url) { return url.contains("/") ? url.substring(url.lastIndexOf("/")+1) : url; } @@ -1038,7 +1067,8 @@ public class DataRenderer extends Renderer implements CodeResolver { } } } - } + } + checkRenderExtensions(status, x, uri); } protected void renderAnnotation(RenderingStatus status, XhtmlNode x, ResourceWrapper a) throws FHIRException { @@ -1270,7 +1300,7 @@ public class DataRenderer extends Renderer implements CodeResolver { return new CodeResolution(null, null, null, code.getText(), code.getText()); } } - protected void renderCodingWithDetails(RenderingStatus status, XhtmlNode x, ResourceWrapper c) { + protected void renderCodingWithDetails(RenderingStatus status, XhtmlNode x, ResourceWrapper c) throws FHIRFormatError, DefinitionException, IOException { String s = ""; if (c.has("display")) s = context.getTranslated(c.child("display")); @@ -1296,6 +1326,7 @@ public class DataRenderer extends Renderer implements CodeResolver { if (c.has("version")) { x.tx(" "+context.formatPhrase(RenderingContext.DATA_REND_VERSION, c.primitiveValue("version"), ")")); } + checkRenderExtensions(status, x, c); } protected void renderCoding(RenderingStatus status, XhtmlNode x, ResourceWrapper c) { @@ -1434,7 +1465,8 @@ public class DataRenderer extends Renderer implements CodeResolver { } x.span(null, context.formatPhrase(RenderingContext.DATA_REND_CODES) +b.toString()).addText(s); - } + } + checkRenderExtensions(status, x, cc); } protected String displayIdentifier(ResourceWrapper ii) { @@ -1477,7 +1509,7 @@ public class DataRenderer extends Renderer implements CodeResolver { return s; } - protected void renderIdentifier(RenderingStatus status, XhtmlNode x, ResourceWrapper ii) { + protected void renderIdentifier(RenderingStatus status, XhtmlNode x, ResourceWrapper ii) throws FHIRFormatError, DefinitionException, IOException { if (ii.has("type")) { ResourceWrapper type = ii.child("type"); if (type.has("text")) { @@ -1527,7 +1559,8 @@ public class DataRenderer extends Renderer implements CodeResolver { x.tx(displayPeriod(ii.child("period"))); } x.tx(")"); - } + } + checkRenderExtensions(status, x, ii); } public static String displayHumanName(ResourceWrapper name) { @@ -1550,7 +1583,7 @@ public class DataRenderer extends Renderer implements CodeResolver { } - protected void renderHumanName(RenderingStatus status, XhtmlNode x, ResourceWrapper name) { + protected void renderHumanName(RenderingStatus status, XhtmlNode x, ResourceWrapper name) throws FHIRFormatError, DefinitionException, IOException { StringBuilder s = new StringBuilder(); if (name.has("text")) s.append(context.getTranslated(name.child("text"))); @@ -1567,7 +1600,8 @@ public class DataRenderer extends Renderer implements CodeResolver { if (name.has("use") && !name.primitiveValue("use").equals("usual")) { s.append("("+context.getTranslatedCode(name.primitiveValue("use"), "http://hl7.org/fhir/name-use")+")"); } - x.addText(s.toString()); + x.addText(s.toString()); + checkRenderExtensions(status, x, name); } private String displayAddress(ResourceWrapper address) { @@ -1604,8 +1638,9 @@ public class DataRenderer extends Renderer implements CodeResolver { return s.toString(); } - protected void renderAddress(RenderingStatus status, XhtmlNode x, ResourceWrapper address) { - x.addText(displayAddress(address)); + protected void renderAddress(RenderingStatus status, XhtmlNode x, ResourceWrapper address) throws FHIRFormatError, DefinitionException, IOException { + x.addText(displayAddress(address)); + checkRenderExtensions(status, x, address); } @@ -1788,7 +1823,7 @@ public class DataRenderer extends Renderer implements CodeResolver { return s.toString(); } - protected void renderQuantity(RenderingStatus status, XhtmlNode x, ResourceWrapper q) { + protected void renderQuantity(RenderingStatus status, XhtmlNode x, ResourceWrapper q) throws FHIRFormatError, DefinitionException, IOException { if (q.has("comparator")) x.addText(q.primitiveValue("comparator")); if (q.has("value")) { @@ -1805,7 +1840,8 @@ public class DataRenderer extends Renderer implements CodeResolver { } if (context.isTechnicalMode() && q.has("code")) { x.span("background: LightGoldenRodYellow", null).tx(" "+ (context.formatPhrase(RenderingContext.DATA_REND_DETAILS, displaySystem(q.primitiveValue("system")))) +q.primitiveValue("code")+" = '"+lookupCode(q.primitiveValue("system"), null, q.primitiveValue("code"))+"')"); - } + } + checkRenderExtensions(status, x, q); } @@ -2073,7 +2109,7 @@ public class DataRenderer extends Renderer implements CodeResolver { st = st + "-"+rep.primitiveValue("frequencyMax"); } if (rep.has("period")) { - st = st + " "+ (context.formatPhrase(RenderingContext.DATA_REND_PER))+rep.primitiveValue("period"); + st = st + " "+ (context.formatPhrase(RenderingContext.DATA_REND_PER))+" "+rep.primitiveValue("period"); if (rep.has("periodMax")) st = st + "-"+rep.primitiveValue("periodMax"); st = st + " "+displayTimeUnits(rep.primitiveValue("periodUnit")); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ProfileDrivenRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ProfileDrivenRenderer.java index 0e0227efb..90b3d55ac 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ProfileDrivenRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ProfileDrivenRenderer.java @@ -3,6 +3,7 @@ package org.hl7.fhir.r5.renderers; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -19,6 +20,7 @@ import org.hl7.fhir.r5.model.ElementDefinition; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; +import org.hl7.fhir.r5.renderers.StructureDefinitionRenderer.SourcedElementDefinition; import org.hl7.fhir.r5.renderers.utils.RenderingContext; import org.hl7.fhir.r5.renderers.utils.ResourceWrapper; import org.hl7.fhir.r5.renderers.utils.ResourceWrapper.NamedResourceWrapperList; @@ -49,7 +51,7 @@ public class ProfileDrivenRenderer extends ResourceRenderer { } else { ElementDefinition ed = sd.getSnapshot().getElement().get(0); containedIds.clear(); - generateByProfile(status, r, sd, r, sd.getSnapshot().getElement(), ed, context.getProfileUtilities().getChildList(sd, ed), x, r.fhirType(), context.isTechnicalMode(), 0); + generateByProfile(status, r, sd, r, ed, context.getProfileUtilities().getChildList(sd, ed), x, r.fhirType(), context.isTechnicalMode(), 0); } } catch (Exception e) { if (DEBUG) { @@ -159,7 +161,7 @@ public class ProfileDrivenRenderer extends ResourceRenderer { return null; } - private void renderLeaf(RenderingStatus status, ResourceWrapper res, ResourceWrapper ew, StructureDefinition sd, ElementDefinition defn, XhtmlNode parent, XhtmlNode x, boolean title, boolean showCodeDetails, Map displayHints, String path, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome { + private void renderLeaf(RenderingStatus status, ResourceWrapper res, ResourceWrapper ew, StructureDefinition sd, ElementDefinition defn, XhtmlNode parent, XhtmlNode x, boolean title, boolean showCodeDetails, Map displayHints, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome { if (ew == null) return; @@ -173,7 +175,7 @@ public class ProfileDrivenRenderer extends ResourceRenderer { } else if (!renderDataType(status, parent, x, ew)) { // well, we have a cell (x) to render this thing, whatever it is // it's not a data type for which we have a built rendering, so we're going to get a list of it's renderable datatype properties, and render them in a list - SourcedChildDefinitions childDefs = context.getProfileUtilities().getChildMap(sd, defn); + // SourcedChildDefinitions childDefs = context.getProfileUtilities().getChildMap(sd, defn); boolean first = true; x.tx(" ("); for (ResourceWrapper child : ew.children()) { @@ -199,24 +201,24 @@ public class ProfileDrivenRenderer extends ResourceRenderer { } } - private boolean displayLeaf(ResourceWrapper res, ResourceWrapper ew, ElementDefinition defn, XhtmlNode x, String name, boolean showCodeDetails, boolean allowLinks) throws FHIRException, UnsupportedEncodingException, IOException { - if (ew == null) - return false; - - Map displayHints = readDisplayHints(defn); - - if (name.endsWith("[x]")) - name = name.substring(0, name.length() - 3); - - if (!showCodeDetails && ew.isPrimitive() && isDefault(displayHints, ew)) { - return false; - } else if (Utilities.existsInList(ew.fhirType(), "Extension")) { - return false; - } else { - x.addText(name+": "+ displayDataType(ew)); - return true; - } - } +// private boolean displayLeaf(ResourceWrapper res, ResourceWrapper ew, ElementDefinition defn, XhtmlNode x, String name, boolean showCodeDetails, boolean allowLinks) throws FHIRException, UnsupportedEncodingException, IOException { +// if (ew == null) +// return false; +// +// Map displayHints = readDisplayHints(defn); +// +// if (name.endsWith("[x]")) +// name = name.substring(0, name.length() - 3); +// +// if (!showCodeDetails && ew.isPrimitive() && isDefault(displayHints, ew)) { +// return false; +// } else if (Utilities.existsInList(ew.fhirType(), "Extension")) { +// return false; +// } else { +// x.addText(name+": "+ displayDataType(ew)); +// return true; +// } +// } @@ -250,7 +252,8 @@ public class ProfileDrivenRenderer extends ResourceRenderer { return code != null && (code.equals("Element") || code.equals("BackboneElement")); } - private List getChildrenForPath(StructureDefinition profile, List elements, String path) throws DefinitionException { + private SourcedChildDefinitions getChildrenForPath(StructureDefinition profile, String path) throws DefinitionException { + var elements = profile.getSnapshot().getElement(); // do we need to do a name reference substitution? for (ElementDefinition e : elements) { if (e.getPath().equals(path) && e.hasContentReference()) { @@ -279,45 +282,45 @@ public class ProfileDrivenRenderer extends ResourceRenderer { } if (results.isEmpty() && t != null && t.getType().size() == 1) { StructureDefinition tsd = context.getWorker().fetchTypeDefinition(t.getTypeFirstRep().getWorkingCode()); - return getChildrenForPath(tsd, tsd.getSnapshot().getElement(), tsd.getType()); + return getChildrenForPath(tsd, tsd.getType()); } - return results; + return new SourcedChildDefinitions(profile, results, path); } - private void generateByProfile(RenderingStatus status, ResourceWrapper res, StructureDefinition profile, ResourceWrapper e, List allElements, ElementDefinition defn, List children, XhtmlNode x, String path, boolean showCodeDetails, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome { + private void generateByProfile(RenderingStatus status, ResourceWrapper res, StructureDefinition profile, ResourceWrapper e, ElementDefinition defn, List children, XhtmlNode x, String path, boolean showCodeDetails, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome { if (children.isEmpty()) { StructureDefinition sdt = context.getWorker().fetchTypeDefinition(e.fhirType()); if (sdt != null && (sdt.getKind() == StructureDefinitionKind.COMPLEXTYPE || sdt.getKind() == StructureDefinitionKind.PRIMITIVETYPE)) { - renderLeaf(status, res, e, profile, defn, x, x, false, showCodeDetails, readDisplayHints(defn), path, indent); + renderLeaf(status, res, e, profile, defn, x, x, false, showCodeDetails, readDisplayHints(defn), indent); } else { // we don't have anything to render? } } else { List pl = splitExtensions(profile, e.childrenInGroups()); for (NamedResourceWrapperList p : pl) { - generateForProperty(status, res, profile, allElements, children, x, path, showCodeDetails, indent, false, p); + generateForProperty(status, res, profile, children, x, path, showCodeDetails, indent, false, p); } for (NamedResourceWrapperList p : pl) { - generateForProperty(status, res, profile, allElements, children, x, path, showCodeDetails, indent, true, p); + generateForProperty(status, res, profile, children, x, path, showCodeDetails, indent, true, p); } } } private void generateForProperty(RenderingStatus status, ResourceWrapper res, StructureDefinition profile, - List allElements, List children, XhtmlNode x, String path, + List children, XhtmlNode x, String path, boolean showCodeDetails, int indent, boolean round2, NamedResourceWrapperList p) throws UnsupportedEncodingException, IOException, EOperationOutcome { if (!p.getValues().isEmpty()) { ElementDefinition child = getElementDefinition(children, path+"."+p.getName()); if (child != null) { if (!child.getBase().hasPath() || !child.getBase().getPath().startsWith("Resource.")) { - generateElementByProfile(status, res, profile, allElements, x, path, showCodeDetails, indent, p, child, round2); + generateElementByProfile(status, res, profile, x, path, showCodeDetails, indent, p, child, round2); } } } } - public void generateElementByProfile(RenderingStatus status, ResourceWrapper res, StructureDefinition profile, List allElements, XhtmlNode x, String path, + public void generateElementByProfile(RenderingStatus status, ResourceWrapper res, StructureDefinition profile, XhtmlNode x, String path, boolean showCodeDetails, int indent, NamedResourceWrapperList p, ElementDefinition child, boolean round2) throws UnsupportedEncodingException, IOException, EOperationOutcome { Map displayHints = readDisplayHints(child); if ("DomainResource.contained".equals(child.getBase().getPath())) { @@ -336,8 +339,8 @@ public class ProfileDrivenRenderer extends ResourceRenderer { if (isExt) { status.setExtensions(true); } - List grandChildren = getChildrenForPath(profile, allElements, path+"."+p.getName()); - filterGrandChildren(grandChildren, path+"."+p.getName(), p); + SourcedChildDefinitions grandChildren = getChildrenForPath(profile, path+"."+p.getName()); + filterGrandChildren(grandChildren.getList(), path+"."+p.getName(), p); if (p.getValues().size() > 0) { if (isSimple(child) && !isExt) { XhtmlNode para = x.isPara() ? para = x : x.para(); @@ -351,7 +354,7 @@ public class ProfileDrivenRenderer extends ResourceRenderer { if (renderAsList(child) && p.getValues().size() > 1) { XhtmlNode list = x.ul(); for (ResourceWrapper v : p.getValues()) - renderLeaf(status, res, v, profile, child, x, list.li(), false, showCodeDetails, displayHints, path, indent); + renderLeaf(status, res, v, profile, child, x, list.li(), false, showCodeDetails, displayHints, indent); } else { boolean first = true; for (ResourceWrapper v : p.getValues()) { @@ -360,23 +363,23 @@ public class ProfileDrivenRenderer extends ResourceRenderer { } else { para.tx(", "); } - renderLeaf(status, res, v, profile, child, x, para, false, showCodeDetails, displayHints, path, indent); + renderLeaf(status, res, v, profile, child, x, para, false, showCodeDetails, displayHints, indent); } } } - } else if (canDoTable(path, p, grandChildren, x)) { + } else if (canDoTable(path, p, grandChildren.getList(), x)) { XhtmlNode xn = new XhtmlNode(NodeType.Element, getHeader()); xn.addText(Utilities.capitalize(Utilities.camelCase(Utilities.pluralizeMe(p.getName())))); XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table"); tbl.setAttribute("class", "grid"); XhtmlNode tr = tbl.tr(); tr.td().style("display: none").tx("-"); // work around problem with empty table rows - boolean add = addColumnHeadings(tr, grandChildren); + boolean add = addColumnHeadings(tr, grandChildren.getList()); for (ResourceWrapper v : p.getValues()) { if (v != null) { tr = tbl.tr(); tr.td().style("display: none").tx("*"); // work around problem with empty table rows - add = addColumnValues(status, res, tr, profile, grandChildren, v, showCodeDetails, displayHints, path, indent) || add; + add = addColumnValues(status, res, tr, profile, grandChildren.getList(), v, showCodeDetails, displayHints, indent) || add; } } if (add) { @@ -393,7 +396,7 @@ public class ProfileDrivenRenderer extends ResourceRenderer { XhtmlNode para = x.para(); para.b().addText(labelforExtension(sd, p.getUrl())); para.tx(": "); - renderLeaf(status, res, vp, profile, child, x, para, false, showCodeDetails, displayHints, path, indent); + renderLeaf(status, res, vp, profile, child, x, para, false, showCodeDetails, displayHints, indent); } else if (!ev.isEmpty()) { XhtmlNode bq = x.addTag("blockquote"); bq.para().b().addText(labelforExtension(sd, p.getUrl())); @@ -410,13 +413,13 @@ public class ProfileDrivenRenderer extends ResourceRenderer { XhtmlNode li = ul.li(); li.tx(labelForSubExtension(vv.primitiveValue("url"), sd)); li.tx(": "); - renderLeaf(status, res, vv.child("value"), sd, child, x, li, isExt, showCodeDetails, displayHints, path, indent); + renderLeaf(status, res, vv.child("value"), sd, child, x, li, isExt, showCodeDetails, displayHints, indent); } } else { for (ResourceWrapper vv : ev) { StructureDefinition ex = context.getWorker().fetchTypeDefinition("Extension"); - List children = getChildrenForPath(profile, ex.getSnapshot().getElement(), "Extension"); - generateByProfile(status, res, ex, vv, allElements, child, children, bq, "Extension", showCodeDetails, indent+1); + SourcedChildDefinitions children = getChildrenForPath(ex, "Extension"); + generateByProfile(status, res, ex, vv, child, children.getList(), bq, "Extension", showCodeDetails, indent+1); } } } @@ -427,7 +430,7 @@ public class ProfileDrivenRenderer extends ResourceRenderer { if (v != null) { XhtmlNode bq = x.addTag("blockquote"); bq.para().b().addText(p.getName()); - generateByProfile(status, res, profile, v, allElements, child, grandChildren, bq, path+"."+p.getName(), showCodeDetails, indent+1); + generateByProfile(status, res, grandChildren.getSource(), v, child, grandChildren.getList(), bq, grandChildren.getPath(), showCodeDetails, indent+1); } } } @@ -435,6 +438,29 @@ public class ProfileDrivenRenderer extends ResourceRenderer { } } +// +// private String getGrandChildBase(List grandChildren) { +// if (grandChildren == null || grandChildren.isEmpty()) { +// return null; +// } +// String[] path = grandChildren.get(0).getPath().split("\\."); +// for (int i = 1; i < grandChildren.size(); i++) { +// path = getSharedString(path, grandChildren.get(1).getPath().split("\\.")); +// } +// return CommaSeparatedStringBuilder.join(".", path); +// } +// +// private String[] getSharedString(String[] path, String[] path2) { +// int m = -1; +// for (int i = 0; i < Integer.min(path.length, path2.length); i++) { +// if (path[i].equals(path2[i])) { +// m = i; +// } else { +// break; +// } +// } +// return m == -1 ? new String[0] : Arrays.copyOfRange(path, 0, m+1); +// } private String labelForSubExtension(String url, StructureDefinition sd) { return url; @@ -530,7 +556,7 @@ public class ProfileDrivenRenderer extends ResourceRenderer { return b; } - private boolean addColumnValues(RenderingStatus status, ResourceWrapper res, XhtmlNode tr, StructureDefinition profile, List grandChildren, ResourceWrapper v, boolean showCodeDetails, Map displayHints, String path, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome { + private boolean addColumnValues(RenderingStatus status, ResourceWrapper res, XhtmlNode tr, StructureDefinition profile, List grandChildren, ResourceWrapper v, boolean showCodeDetails, Map displayHints, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome { boolean b = false; for (ElementDefinition e : grandChildren) { List p = v.children(e.getPath().substring(e.getPath().lastIndexOf(".")+1)); @@ -542,7 +568,7 @@ public class ProfileDrivenRenderer extends ResourceRenderer { for (ResourceWrapper vv : p) { b = true; td.sep(", "); - renderLeaf(status, res, vv, profile, e, td, td, false, showCodeDetails, displayHints, path, indent); + renderLeaf(status, res, vv, profile, e, td, td, false, showCodeDetails, displayHints, indent); } } } @@ -656,6 +682,10 @@ public class ProfileDrivenRenderer extends ResourceRenderer { return path.substring(path.lastIndexOf(".")+1); } + protected String utail(String path) { + return path.contains("/") ? path.substring(path.lastIndexOf("/")+1) : path; + } + public boolean canRender(Resource resource) { return context.getWorker().getResourceNames().contains(resource.fhirType()); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java index ace526204..5bfafc50e 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java @@ -35,7 +35,6 @@ import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; import org.hl7.fhir.r5.utils.EOperationOutcome; import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.r5.utils.XVerExtensionManager; -import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece; @@ -369,8 +368,10 @@ public abstract class ResourceRenderer extends DataRenderer { } else { x.tx("??"); } + checkRenderExtensions(status, x, type); } + public void renderReference(ResourceWrapper res, HierarchicalTableGenerator gen, List pieces, Reference r, boolean allowLinks) throws UnsupportedEncodingException, IOException { if (r == null) { pieces.add(gen.new Piece(null, "null!", null)); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientManager.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientManager.java index a7bbc4fc8..6ca4050f0 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientManager.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/client/TerminologyClientManager.java @@ -83,6 +83,42 @@ public class TerminologyClientManager { ITerminologyClient makeClient(String id, String url, String userAgent, ToolingClientLogger logger) throws URISyntaxException; String getVersion(); } + + public class InternalLogEvent { + private String message; + private String server; + private String vs; + private String systems; + private String choices; + protected InternalLogEvent(String message, String server, String vs, String systems, String choices) { + super(); + this.message = message; + this.server = server; + this.vs = vs; + this.systems = systems; + this.choices = choices; + } + protected InternalLogEvent(String message) { + super(); + this.message = message; + } + public String getMessage() { + return message; + } + public String getVs() { + return vs; + } + public String getSystems() { + return systems; + } + public String getChoices() { + return choices; + } + public String getServer() { + return server; + } + + } public static final String UNRESOLVED_VALUESET = "--unknown--"; @@ -93,7 +129,7 @@ public class TerminologyClientManager { private List serverList = new ArrayList<>(); // clients by server address private Map serverMap = new HashMap<>(); // clients by server address private Map resMap = new HashMap<>(); // client resolution list - private List internalLog = new ArrayList<>(); + private List internalLog = new ArrayList<>(); protected Parameters expParameters; private TerminologyCache cache; @@ -124,6 +160,7 @@ public class TerminologyClientManager { monitorServiceURL = other.monitorServiceURL; factory = other.factory; usage = other.usage; + internalLog = other.internalLog; } @@ -150,6 +187,7 @@ public class TerminologyClientManager { } } if (ok) { + log(vs, s, systems, choices, "Found authoritative server "+s); return findClient(s, systems, expand); } } @@ -165,6 +203,7 @@ public class TerminologyClientManager { } } if (ok) { + log(vs, s, systems, choices, "Found partially authoritative server "+s); return findClient(s, systems, expand); } } @@ -180,6 +219,7 @@ public class TerminologyClientManager { } } if (ok) { + log(vs, s, systems, choices, "Found candidate server "+s); return findClient(s, systems, expand); } } @@ -194,6 +234,7 @@ public class TerminologyClientManager { "urn:ietf:rfc:3986", "http://www.ama-assn.org/go/cpt", "urn:oid:1.2.36.1.2001.1005.17", "urn:iso:std:iso:3166", "http://varnomen.hgvs.org", "http://unstats.un.org/unsd/methods/m49/m49.htm", "urn:iso:std:iso:4217", "http://hl7.org/fhir/sid/ndc", "http://fhir.ohdsi.org/CodeSystem/concepts", "http://fdasis.nlm.nih.gov", "https://www.usps.com/")) { + log(vs, serverList.get(0).getAddress(), systems, choices, "Use primary server for "+uri); return serverList.get(0); } } @@ -206,29 +247,37 @@ public class TerminologyClientManager { el = getMaster().getAddress(); } if (systems.size() == 1) { - internalLog.add(vs.getVersionedUrl()+" uses the system "+systems.toString()+" not handled by any servers. Using source @ '"+el+"'"); + log(vs, el, systems, choices, "Not handled by any servers. Using source @ '"+el+"'"); } else { - internalLog.add(vs.getVersionedUrl()+" includes multiple systems "+systems.toString()+" best handled by multiple servers: "+choices.toString()+". Using source @ '"+el+"'"); + log(vs, el, systems, choices, "Handled by multiple servers. Using source @ '"+el+"'"); } return findClient(el, systems, expand); } else { if (systems.size() == 1) { - internalLog.add(vs.getVersionedUrl()+" uses the system "+systems.toString()+" not handled by any servers. Using master @ '"+serverList.get(0)+"'"); + log(vs, serverList.get(0).getAddress(), systems, choices, "System not handled by any servers. Using primary server"); } else { - internalLog.add(vs.getVersionedUrl()+" includes multiple systems "+systems.toString()+" best handled by multiple servers: "+choices.toString()+". Using master @ '"+serverList.get(0)+"'"); + log(vs, serverList.get(0).getAddress(), systems, choices, "Systems handled by multiple servers. Using primary server"); } return findClient(serverList.get(0).getAddress(), systems, expand); } } else { if (systems.size() == 1) { - internalLog.add("Request for system "+systems.toString()+" not handled by any servers. Using master @ '"+serverList.get(0)+"'"); + log(vs, serverList.get(0).getAddress(), systems, choices, "System not handled by any servers. Using primary server"); } else { - internalLog.add("Request for multiple systems "+systems.toString()+" best handled by multiple servers: "+choices.toString()+". Using master @ '"+serverList.get(0)+"'"); + log(vs, serverList.get(0).getAddress(), systems, choices, "Systems handled by multiple servers. Using primary server"); } + log(vs, serverList.get(0).getAddress(), systems, choices, "Fallback: primary server"); return findClient(serverList.get(0).getAddress(), systems, expand); } } + private void log(ValueSet vs, String server, Set systems, List choices, String message) { + String svs = (vs == null ? "null" : vs.getVersionedUrl()); + String sys = systems.isEmpty() ? "--" : systems.size() == 1 ? systems.iterator().next() : systems.toString(); + String sch = choices.isEmpty() ? "--" : choices.size() == 1 ? choices.iterator().next().toString() : choices.toString(); + internalLog.add(new InternalLogEvent(message, server, svs, sys, sch)); + } + private TerminologyClientContext findClient(String server, Set systems, boolean expand) { TerminologyClientContext client = serverMap.get(server); if (client == null) { @@ -297,8 +346,8 @@ public class TerminologyClientManager { return ret; } catch (Exception e) { String msg = "Error resolving system "+url+": "+e.getMessage()+" ("+request+")"; - if (!internalLog.contains(msg)) { - internalLog.add(msg); + if (!hasMessage(msg)) { + internalLog.add(new InternalLogEvent(msg)); } if (!monitorServiceURL.contains("tx.fhir.org")) { e.printStackTrace(); @@ -308,6 +357,15 @@ public class TerminologyClientManager { } + private boolean hasMessage(String msg) { + for (InternalLogEvent log : internalLog) { + if (msg.equals(log.message)) { + return true; + } + } + return false; + } + public List serverList() { return serverList; } @@ -410,10 +468,6 @@ public class TerminologyClientManager { } } - public List getInternalLog() { - return internalLog; - } - public List getServerList() { return serverList; } @@ -518,8 +572,8 @@ public class TerminologyClientManager { } catch (Exception e) { e.printStackTrace(); String msg = "Error resolving valueSet "+canonical+": "+e.getMessage()+" ("+request+")"; - if (!internalLog.contains(msg)) { - internalLog.add(msg); + if (!hasMessage(msg)) { + internalLog.add(new InternalLogEvent(msg)); } e.printStackTrace(); return null; @@ -598,8 +652,8 @@ public class TerminologyClientManager { } catch (Exception e) { e.printStackTrace(); String msg = "Error resolving valueSet "+canonical+": "+e.getMessage()+" ("+request+")"; - if (!internalLog.contains(msg)) { - internalLog.add(msg); + if (!hasMessage(msg)) { + internalLog.add(new InternalLogEvent(msg)); } e.printStackTrace(); return null; @@ -614,5 +668,8 @@ public class TerminologyClientManager { } return false; } - + + public List getInternalLog() { + return internalLog; + } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyCache.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyCache.java index 3a60a33bc..8340cb092 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyCache.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/terminologies/utilities/TerminologyCache.java @@ -364,7 +364,7 @@ public class TerminologyCache { csCache.clear(); } - private void clear() throws IOException { + public void clear() throws IOException { if (folder != null) { Utilities.clearDirectory(folder); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/IResourceValidator.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/IResourceValidator.java index 691c4fa73..4a938b2fe 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/IResourceValidator.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/IResourceValidator.java @@ -80,7 +80,7 @@ public interface IResourceValidator { IResourceValidator setFetcher(IValidatorResourceFetcher value); IValidationPolicyAdvisor getPolicyAdvisor(); - IResourceValidator setPolicyAdvisor(IValidationPolicyAdvisor advisor); + void setPolicyAdvisor(IValidationPolicyAdvisor advisor); IValidationProfileUsageTracker getTracker(); IResourceValidator setTracker(IValidationProfileUsageTracker value); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/IValidationPolicyAdvisor.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/IValidationPolicyAdvisor.java index 5af265edc..c87508dcd 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/IValidationPolicyAdvisor.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/validation/IValidationPolicyAdvisor.java @@ -16,6 +16,21 @@ import org.hl7.fhir.r5.utils.validation.constants.BindingKind; public interface IValidationPolicyAdvisor { + /** + * Internal use, for chaining advisors + * + * @return + */ + ReferenceValidationPolicy getReferencePolicy(); + + /** + * + * @param path - the current path of the element + * @param messageId - the message id (from messages.properties) + * @return true if the validator should ignore the message + */ + boolean suppressMessageId(String path, String messageId); + /** * * @param validator @@ -169,5 +184,6 @@ public interface IValidationPolicyAdvisor { boolean valid, IMessagingServices msgServices, List messages); + } \ No newline at end of file diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index d9efb0e6e..55f05a570 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -514,6 +514,7 @@ public class I18nConstants { public static final String QUESTIONNAIRE_QR_ITEM_ONLYONEI = "Questionnaire_QR_Item_OnlyOneI"; public static final String QUESTIONNAIRE_QR_ITEM_ORDER = "Questionnaire_QR_Item_Order"; public static final String QUESTIONNAIRE_QR_ITEM_STRINGNOOPTIONS = "Questionnaire_QR_Item_StringNoOptions"; + public static final String QUESTIONNAIRE_QR_ITEM_STRING_IN_CODING = "QUESTIONNAIRE_QR_ITEM_STRING_IN_CODING"; public static final String QUESTIONNAIRE_QR_ITEM_TEXT = "Questionnaire_QR_Item_Text"; public static final String QUESTIONNAIRE_QR_ITEM_TIMENOOPTIONS = "Questionnaire_QR_Item_TimeNoOptions"; public static final String QUESTIONNAIRE_QR_ITEM_WRONGTYPE = "Questionnaire_QR_Item_WrongType"; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index df16be4b8..298726e66 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -524,6 +524,7 @@ Questionnaire_QR_Item_NoOptionsCoding = Option list has no option values of type Questionnaire_QR_Item_NoOptionsDate = Option list has no option values of type date Questionnaire_QR_Item_NoOptionsInteger = Option list has no option values of type integer Questionnaire_QR_Item_NoOptionsString = Option list has no option values of type string +QUESTIONNAIRE_QR_ITEM_STRING_IN_CODING = The string value ''{0}'' matches an entry in the list of valid Codings, so this is probably an error Questionnaire_QR_Item_NoOptionsTime = Option list has no option values of type time Questionnaire_QR_Item_NoString = The string {0} is not a valid option Questionnaire_QR_Item_NoTime = The time {0} is not a valid option diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java index ab76c2d1f..cb6f23fdd 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/BaseValidator.java @@ -69,9 +69,11 @@ import org.hl7.fhir.r5.utils.XVerExtensionManager; import org.hl7.fhir.r5.utils.XVerExtensionManager.XVerExtensionStatus; import org.hl7.fhir.r5.utils.validation.IMessagingServices; import org.hl7.fhir.r5.utils.validation.IResourceValidator; +import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.IValidationContextResourceLoader; import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel; +import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.FhirPublication; import org.hl7.fhir.utilities.StandardsStatus; @@ -85,6 +87,7 @@ import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; import org.hl7.fhir.utilities.validation.ValidationMessage.Source; import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.hl7.fhir.validation.cli.utils.ValidationLevel; +import org.hl7.fhir.validation.instance.advisor.BasePolicyAdvisorForFullValidation; import org.hl7.fhir.validation.instance.utils.IndexedElement; import org.hl7.fhir.validation.instance.utils.NodeStack; @@ -165,25 +168,31 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi protected final String BUNDLE = "Bundle"; protected final String LAST_UPDATED = "lastUpdated"; + protected String sessionId; protected BaseValidator parent; - protected Source source; protected IWorkerContext context; protected ValidationTimeTracker timeTracker = new ValidationTimeTracker(); protected XVerExtensionManager xverManager; - protected List trackedMessages = new ArrayList<>(); - protected List messagesToRemove = new ArrayList<>(); - protected ValidationLevel level = ValidationLevel.HINTS; - protected Coding jurisdiction; - protected boolean allowExamples; - protected boolean forPublication; - protected boolean debug; - protected boolean warnOnDraftOrExperimental; - protected Set statusWarnings = new HashSet<>(); - protected BestPracticeWarningLevel bpWarnings = BestPracticeWarningLevel.Warning; - protected String sessionId = Utilities.makeUuidLC(); - protected List usageContexts = new ArrayList(); - protected ValidationOptions baseOptions = new ValidationOptions(FhirPublication.R5); protected IValidatorResourceFetcher fetcher; + protected IValidationPolicyAdvisor policyAdvisor; + + // these two related to removing warnings on extensible bindings in structures that have derivatives that replace their bindings + protected List trackedMessages = new ArrayList<>(); + protected List messagesToRemove = new ArrayList<>(); + + // don't repeatedly raise the same warnings all the time + protected Set statusWarnings = new HashSet<>(); + + protected Source source; // @configuration + protected ValidationLevel level = ValidationLevel.HINTS; // @configuration + protected Coding jurisdiction; // @configuration + protected boolean allowExamples; // @configuration + protected boolean forPublication; // @configuration + protected boolean debug; // @configuration + protected boolean warnOnDraftOrExperimental; // @configuration + protected BestPracticeWarningLevel bpWarnings = BestPracticeWarningLevel.Warning; // @configuration + protected List usageContexts = new ArrayList(); // @configuration + protected ValidationOptions baseOptions = new ValidationOptions(FhirPublication.R5); // @configuration public BaseValidator(IWorkerContext context, XVerExtensionManager xverManager, boolean debug) { super(); @@ -193,6 +202,8 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi this.xverManager = new XVerExtensionManager(context); } this.debug = debug; + sessionId = Utilities.makeUuidLC(); + policyAdvisor = new BasePolicyAdvisorForFullValidation(ReferenceValidationPolicy.CHECK_VALID); urlRegex = Constants.URI_REGEX_XVER.replace("$$", CommaSeparatedStringBuilder.join("|", context.getResourceNames())); } @@ -217,6 +228,8 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi this.usageContexts.addAll(parent.usageContexts); this.baseOptions = parent.baseOptions; this.fetcher = parent.fetcher; + this.sessionId = parent.sessionId; + this.policyAdvisor = parent.policyAdvisor; } private boolean doingLevel(IssueSeverity error) { @@ -261,75 +274,22 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi protected String urlRegex; - /** - * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - @Deprecated - protected boolean fail(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String msg) { - if (!thePass && doingErrors()) { - addValidationMessage(errors, ruleDate, type, line, col, path, msg, IssueSeverity.FATAL, null); + private boolean suppressMsg(String path, String theMessage) { + if (policyAdvisor == null) { + return false; + } else { + return policyAdvisor.suppressMessageId(path, theMessage); } - return thePass; } protected boolean fail(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingErrors()) { + if (!thePass && doingErrors() && !suppressMsg(path, theMessage)) { String msg = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, line, col, path, msg, IssueSeverity.FATAL, theMessage); } return thePass; } - /** - * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - @Deprecated - protected boolean fail(List errors, String ruleDate, IssueType type, List pathParts, boolean thePass, String msg) { - if (!thePass && doingErrors()) { - String path = toPath(pathParts); - addValidationMessage(errors, ruleDate, type, -1, -1, path, msg, IssueSeverity.FATAL, null); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - @Deprecated - protected boolean fail(List errors, String ruleDate, IssueType type, List pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingErrors()) { - String path = toPath(pathParts); - addValidationMessage(errors, ruleDate, type, -1, -1, path, context.formatMessage(theMessage, theMessageArguments), IssueSeverity.FATAL, theMessage); - } - return thePass; - } - - /** - * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails - * - * @param thePass - * Set this parameter to false if the validation does not pass - * @return Returns thePass (in other words, returns true if the rule did not fail validation) - */ - @Deprecated - protected boolean fail(List errors, String ruleDate, IssueType type, String path, boolean thePass, String msg) { - if (!thePass && doingErrors()) { - addValidationMessage(errors, ruleDate, type, -1, -1, path, msg, IssueSeverity.FATAL, null); - } - return thePass; - } //TODO: i18n protected boolean grammarWord(String w) { return w.equals("and") || w.equals("or") || w.equals("a") || w.equals("the") || w.equals("for") || w.equals("this") || w.equals("that") || w.equals("of"); @@ -343,7 +303,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean hint(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String msg) { - if (!thePass && doingHints()) { + if (!thePass && doingHints() && !suppressMsg(path, msg)) { String message = context.formatMessage(msg); addValidationMessage(errors, ruleDate, type, line, col, path, message, IssueSeverity.INFORMATION, msg); } @@ -358,7 +318,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean hintInv(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String msg, String invId) { - if (!thePass && doingHints()) { + if (!thePass && doingHints() && !suppressMsg(path, invId)) { String message = context.formatMessage(msg); addValidationMessage(errors, ruleDate, type, line, col, path, message, IssueSeverity.INFORMATION, msg).setInvId(invId); } @@ -397,7 +357,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean hint(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingHints()) { + if (!thePass && doingHints() && !suppressMsg(path, theMessage)) { String message = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, line, col, path, message, IssueSeverity.INFORMATION, theMessage); } @@ -405,7 +365,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean hintPlural(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, int num, String theMessage, Object... theMessageArguments) { - if (!thePass && doingHints()) { + if (!thePass && doingHints() && !suppressMsg(path, theMessage)) { String message = context.formatMessagePlural(num, theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, line, col, path, message, IssueSeverity.INFORMATION, theMessage); } @@ -418,7 +378,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean txHint(List errors, String ruleDate, String txLink, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingHints()) { + if (!thePass && doingHints() && !suppressMsg(path, theMessage)) { String message = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, line, col, path, message, IssueSeverity.INFORMATION, Source.TerminologyEngine, theMessage).setTxLink(txLink); } @@ -433,7 +393,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean hint(List errors, String ruleDate, IssueType type, List pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingHints()) { + if (!thePass && doingHints() && !suppressMsg(CommaSeparatedStringBuilder.join(".", pathParts), theMessage)) { String path = toPath(pathParts); String message = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, -1, -1, path, message, IssueSeverity.INFORMATION, theMessage); @@ -449,7 +409,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean hint(List errors, String ruleDate, IssueType type, String path, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingHints()) { + if (!thePass && doingHints() && !suppressMsg(path, theMessage)) { String message = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, -1, -1, path, message, IssueSeverity.INFORMATION, null); } @@ -464,7 +424,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean rule(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingErrors()) { + if (!thePass && doingErrors() && !suppressMsg(path, theMessage)) { String message = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, line, col, path, message, IssueSeverity.ERROR, theMessage); } @@ -472,7 +432,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean ruleInv(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String theMessage, String invId, Object... theMessageArguments) { - if (!thePass && doingErrors()) { + if (!thePass && doingErrors() && !suppressMsg(path, theMessage)) { String message = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, line, col, path, message, IssueSeverity.ERROR, invId).setInvId(invId); } @@ -480,7 +440,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean rule(List errors, String ruleDate, IssueType type, NodeStack stack, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingErrors()) { + if (!thePass && doingErrors() && !suppressMsg(stack.getLiteralPath(), theMessage)) { String message = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, stack.line(), stack.col(), stack.getLiteralPath(), message, IssueSeverity.ERROR, theMessage); } @@ -492,7 +452,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean rulePlural(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, int num, String theMessage, Object... theMessageArguments) { - if (!thePass && doingErrors()) { + if (!thePass && doingErrors() && !suppressMsg(path, theMessage)) { String message = context.formatMessagePlural(num, theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, line, col, path, message, IssueSeverity.ERROR, theMessage); } @@ -500,7 +460,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean txRule(List errors, String ruleDate, String txLink, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingErrors()) { + if (!thePass && doingErrors() && !suppressMsg(path, theMessage)) { String message = context.formatMessage(theMessage, theMessageArguments); ValidationMessage vm = new ValidationMessage(Source.TerminologyEngine, type, line, col, path, message, IssueSeverity.ERROR).setMessageId(idForMessage(theMessage, message)); vm.setRuleDate(ruleDate); @@ -538,7 +498,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean rule(List errors, String ruleDate, IssueType type, List pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingErrors()) { + if (!thePass && doingErrors() && !suppressMsg(CommaSeparatedStringBuilder.join(".", pathParts), theMessage)) { String path = toPath(pathParts); String message = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, -1, -1, path, message, IssueSeverity.ERROR, theMessage); @@ -556,7 +516,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi protected boolean rule(List errors, String ruleDate, IssueType type, String path, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingErrors()) { + if (!thePass && doingErrors() && !suppressMsg(path, theMessage)) { String message = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, -1, -1, path, message, IssueSeverity.ERROR, theMessage); } @@ -564,7 +524,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean rulePlural(List errors, String ruleDate, IssueType type, String path, boolean thePass, int num, String theMessage, Object... theMessageArguments) { - if (!thePass && doingErrors()) { + if (!thePass && doingErrors() && !suppressMsg(path, theMessage)) { String message = context.formatMessagePlural(num, theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, -1, -1, path, message, IssueSeverity.ERROR, theMessage); } @@ -630,7 +590,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean warning(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(path, msg)) { String nmsg = context.formatMessage(msg, theMessageArguments); IssueSeverity severity = IssueSeverity.WARNING; addValidationMessage(errors, ruleDate, type, line, col, path, nmsg, severity, msg); @@ -640,7 +600,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean warning(List errors, String ruleDate, IssueType type, int line, int col, String path, String id, boolean thePass, String msg, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(path, msg)) { String nmsg = context.formatMessage(msg, theMessageArguments); IssueSeverity severity = IssueSeverity.WARNING; addValidationMessage(errors, ruleDate, type, line, col, path, nmsg, severity, id); @@ -650,7 +610,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean warningInv(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String msg, String invId, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(path, invId)) { String nmsg = context.formatMessage(msg, theMessageArguments); IssueSeverity severity = IssueSeverity.WARNING; String id = idForMessage(msg, nmsg); @@ -669,7 +629,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean warningPlural(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, int num, String msg, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(path, msg)) { String nmsg = context.formatMessagePlural(num, msg, theMessageArguments); IssueSeverity severity = IssueSeverity.WARNING; addValidationMessage(errors, ruleDate, type, line, col, path, nmsg, severity, msg); @@ -711,7 +671,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean txWarning(List errors, String ruleDate, String txLink, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(path, msg)) { String nmsg = context.formatMessage(msg, theMessageArguments); ValidationMessage vmsg = new ValidationMessage(Source.TerminologyEngine, type, line, col, path, nmsg, IssueSeverity.WARNING).setTxLink(txLink).setMessageId(idForMessage(msg, nmsg)); vmsg.setRuleDate(ruleDate); @@ -744,7 +704,9 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi vmsg.setMessageId(issue.getExtensionString(ToolingExtensions.EXT_ISSUE_MSG_ID)); } - errors.add(vmsg); + if (!suppressMsg(path, vmsg.getMessageId())) { + errors.add(vmsg); + } return vmsg; } @@ -760,7 +722,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean txWarningForLaterRemoval(Object location, List errors, String ruleDate, String txLink, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(path, msg)) { String nmsg = context.formatMessage(msg, theMessageArguments); ValidationMessage vmsg = new ValidationMessage(Source.TerminologyEngine, type, line, col, path, nmsg, IssueSeverity.WARNING).setTxLink(txLink).setMessageId(msg); vmsg.setRuleDate(ruleDate); @@ -789,7 +751,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean warningOrError(boolean isError, List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { - if (!thePass) { + if (!thePass && !suppressMsg(path, msg)) { String nmsg = context.formatMessage(msg, theMessageArguments); IssueSeverity lvl = isError ? IssueSeverity.ERROR : IssueSeverity.WARNING; if (doingLevel(lvl)) { @@ -805,7 +767,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi } protected boolean hintOrError(boolean isError, List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { - if (!thePass) { + if (!thePass && !suppressMsg(path, msg)) { String nmsg = context.formatMessage(msg, theMessageArguments); IssueSeverity lvl = isError ? IssueSeverity.ERROR : IssueSeverity.INFORMATION; if (doingLevel(lvl)) { @@ -824,7 +786,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean warning(List errors, String ruleDate, IssueType type, List pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(CommaSeparatedStringBuilder.join(".", pathParts), theMessage)) { String path = toPath(pathParts); String message = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, -1, -1, path, message, IssueSeverity.WARNING, theMessage); @@ -840,7 +802,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean warning(List errors, String ruleDate, IssueType type, String path, boolean thePass, String msg, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(path, msg)) { String message = context.formatMessage(msg, theMessageArguments); addValidationMessage(errors, ruleDate, type, -1, -1, path, message, IssueSeverity.WARNING, null); } @@ -855,7 +817,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean warningOrHint(List errors, String ruleDate, IssueType type, String path, boolean thePass, boolean warning, String msg, Object... theMessageArguments) { - if (!thePass) { + if (!thePass && !suppressMsg(path, msg)) { String message = context.formatMessage(msg, theMessageArguments); IssueSeverity lvl = warning ? IssueSeverity.WARNING : IssueSeverity.INFORMATION; if (doingLevel(lvl)) { @@ -873,7 +835,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean warningHtml(List errors, String ruleDate, IssueType type, String path, boolean thePass, String msg, String html) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(path, msg)) { addValidationMessage(errors, ruleDate, type, path, msg, html, IssueSeverity.WARNING, null); } return thePass; @@ -887,7 +849,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean warningHtml(List errors, String ruleDate, IssueType type, String path, boolean thePass, String msg, String html, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(path, msg)) { String nmsg = context.formatMessage(msg, theMessageArguments); addValidationMessage(errors, ruleDate, type, path, nmsg, html, IssueSeverity.WARNING, msg); } @@ -903,7 +865,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean suppressedwarning(List errors, String ruleDate, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(path, msg)) { String nmsg = context.formatMessage(msg, theMessageArguments); addValidationMessage(errors, ruleDate, type, line, col, path, nmsg, IssueSeverity.INFORMATION, msg); } @@ -919,7 +881,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean suppressedwarning(List errors, String ruleDate, IssueType type, List pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(CommaSeparatedStringBuilder.join(".", pathParts), theMessage)) { String path = toPath(pathParts); String message = context.formatMessage(theMessage, theMessageArguments); addValidationMessage(errors, ruleDate, type, -1, -1, path, message, IssueSeverity.INFORMATION, theMessage); @@ -974,7 +936,7 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi * @return Returns thePass (in other words, returns true if the rule did not fail validation) */ protected boolean suppressedwarning(List errors, String ruleDate, IssueType type, String path, boolean thePass, String msg, String html, Object... theMessageArguments) { - if (!thePass && doingWarnings()) { + if (!thePass && doingWarnings() && !suppressMsg(path, msg)) { String nmsg = context.formatMessage(msg, theMessageArguments); addValidationMessage(errors, ruleDate, type, path, nmsg, html, IssueSeverity.INFORMATION, msg); } @@ -1681,4 +1643,17 @@ public class BaseValidator implements IValidationContextResourceLoader, IMessagi return false; } + public IValidationPolicyAdvisor getPolicyAdvisor() { + return policyAdvisor; + } + + public void setPolicyAdvisor(IValidationPolicyAdvisor advisor) { + if (advisor == null) { + throw new Error("Cannot set advisor to null"); + } + this.policyAdvisor = advisor; + } + + + } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java index 72808e09c..545670b98 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java @@ -1288,4 +1288,14 @@ public class ValidationEngine implements IValidatorResourceFetcher, IValidationP definition, structure, resource, valid, msgServices, messages); } + @Override + public boolean suppressMessageId(String path, String messageId) { + return policyAdvisor.suppressMessageId(path, messageId); + } + + @Override + public ReferenceValidationPolicy getReferencePolicy() { + return ReferenceValidationPolicy.IGNORE; + } + } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java index 2e8631384..86b461fbe 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java @@ -376,6 +376,9 @@ public class ValidatorCli { ((StandaloneTask) cliTask).executeTask(cliContext,params,tt,tts); } + if (cliContext.getAdvisorFile() != null) { + System.out.println("Note: Some validation issues might be hidden by the advisor settings in the file "+cliContext.getAdvisorFile()); + } System.out.println("Done. " + tt.report()+". Max Memory = "+Utilities.describeSize(Runtime.getRuntime().maxMemory())); SystemExitManager.finish(); } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java index 6d5e0cb4a..37a4b9c5a 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java @@ -132,6 +132,12 @@ public class CliContext { @JsonProperty("showTimes") private boolean showTimes = false; + @JsonProperty("showTerminologyRouting") + private boolean showTerminologyRouting = false; + + @JsonProperty("clearTxCache") + private boolean clearTxCache = false; + @JsonProperty("locale") private String locale = Locale.ENGLISH.toLanguageTag(); @@ -172,6 +178,9 @@ public class CliContext { @JsonProperty("noExperimentalContent") private boolean noExperimentalContent; + @JsonProperty("advisorFile") + private String advisorFile; + @JsonProperty("baseEngine") public String getBaseEngine() { return baseEngine; @@ -754,6 +763,22 @@ public class CliContext { this.showTimes = showTimes; } + public boolean isShowTerminologyRouting() { + return showTerminologyRouting; + } + + public void setShowTerminologyRouting(boolean showTerminologyRouting) { + this.showTerminologyRouting = showTerminologyRouting; + } + + public boolean isClearTxCache() { + return clearTxCache; + } + + public void setClearTxCache(boolean clearTxCache) { + this.clearTxCache = clearTxCache; + } + public String getOutputStyle() { return outputStyle; } @@ -852,6 +877,7 @@ public class CliContext { Objects.equals(watchScanDelay, that.watchScanDelay) && Objects.equals(unknownCodeSystemsCauseErrors, that.unknownCodeSystemsCauseErrors) && Objects.equals(noExperimentalContent, that.noExperimentalContent) && + Objects.equals(advisorFile, that.advisorFile) && Objects.equals(watchSettleTime, that.watchSettleTime) ; } @@ -860,7 +886,7 @@ public class CliContext { return Objects.hash(baseEngine, doNative, extensions, hintAboutNonMustSupport, recursive, doDebug, assumeValidRestReferences, canDoNative, noInternalCaching, noExtensibleBindingMessages, noInvariants, displayWarnings, wantInvariantsInMessages, map, output, outputSuffix, htmlOutput, txServer, sv, txLog, txCache, mapLog, lang, srcLang, tgtLang, fhirpath, snomedCT, targetVer, igs, questionnaireMode, level, profiles, sources, inputs, mode, locale, locations, crumbTrails, showMessageIds, forPublication, showTimes, allowExampleUrls, outputStyle, jurisdiction, noUnicodeBiDiControlChars, - watchMode, watchScanDelay, watchSettleTime, bestPracticeLevel, unknownCodeSystemsCauseErrors, noExperimentalContent, htmlInMarkdownCheck, allowDoubleQuotesInFHIRPath, checkIPSCodes); + watchMode, watchScanDelay, watchSettleTime, bestPracticeLevel, unknownCodeSystemsCauseErrors, noExperimentalContent, advisorFile, htmlInMarkdownCheck, allowDoubleQuotesInFHIRPath, checkIPSCodes); } @Override @@ -922,6 +948,7 @@ public class CliContext { ", watchScanDelay=" + watchScanDelay + ", unknownCodeSystemsCauseErrors=" + unknownCodeSystemsCauseErrors + ", noExperimentalContent=" + noExperimentalContent + + ", advisorFile=" + advisorFile + '}'; } @@ -1002,5 +1029,15 @@ public class CliContext { this.noExperimentalContent = noExperimentalContent; } + @JsonProperty("advisorFile") + public String getAdvisorFile() { + return advisorFile; + } + + @JsonProperty("advisorFile") + public void setAdvisorFile(String advisorFile) { + this.advisorFile = advisorFile; + } + } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java index a239fd2d4..20beab9a8 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java @@ -17,12 +17,14 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.context.IWorkerContextManager; import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.elementmodel.Element.SpecialElement; import org.hl7.fhir.r5.model.CanonicalResource; import org.hl7.fhir.r5.model.ElementDefinition; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.terminologies.client.ITerminologyClient; +import org.hl7.fhir.r5.utils.validation.IMessagingServices; import org.hl7.fhir.r5.utils.validation.IResourceValidator; import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; @@ -41,13 +43,14 @@ import org.hl7.fhir.utilities.json.model.JsonObject; import org.hl7.fhir.utilities.json.parser.JsonParser; import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; import org.hl7.fhir.utilities.npm.NpmPackage; +import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.validation.cli.utils.Common; import org.hl7.fhir.validation.instance.advisor.BasePolicyAdvisorForFullValidation; import javax.annotation.Nonnull; -public class StandAloneValidatorFetcher extends BasePolicyAdvisorForFullValidation implements IValidatorResourceFetcher, IValidationPolicyAdvisor, IWorkerContextManager.ICanonicalResourceLocator { +public class StandAloneValidatorFetcher implements IValidatorResourceFetcher, IValidationPolicyAdvisor, IWorkerContextManager.ICanonicalResourceLocator { List mappingsUris = new ArrayList<>(); private FilesystemPackageCacheManager pcm; @@ -56,12 +59,13 @@ public class StandAloneValidatorFetcher extends BasePolicyAdvisorForFullValidati private Map urlList = new HashMap<>(); private Map pidList = new HashMap<>(); private Map pidMap = new HashMap<>(); - + private IValidationPolicyAdvisor policyAdvisor; + public StandAloneValidatorFetcher(FilesystemPackageCacheManager pcm, IWorkerContext context, IPackageInstaller installer) { - super(ReferenceValidationPolicy.IGNORE); this.pcm = pcm; this.context = context; this.installer = installer; + this.policyAdvisor = new BasePolicyAdvisorForFullValidation(ReferenceValidationPolicy.IGNORE); } @Override @@ -297,4 +301,55 @@ public class StandAloneValidatorFetcher extends BasePolicyAdvisorForFullValidati return new HashSet<>(); } + @Override + public boolean suppressMessageId(String path, String messageId) { + return policyAdvisor.suppressMessageId(path, messageId); + } + + @Override + public ContainedReferenceValidationPolicy policyForContained(IResourceValidator validator, Object appContext, + StructureDefinition structure, ElementDefinition element, String containerType, String containerId, + SpecialElement containingResourceType, String path, String url) { + return policyAdvisor.policyForContained(validator, appContext, structure, element, containerType, containerId, containingResourceType, path, url); + } + + @Override + public EnumSet policyForResource(IResourceValidator validator, Object appContext, + StructureDefinition type, String path) { + return policyAdvisor.policyForResource(validator, appContext, type, path); + } + + @Override + public EnumSet policyForElement(IResourceValidator validator, Object appContext, + StructureDefinition structure, ElementDefinition element, String path) { + return policyAdvisor.policyForElement(validator, appContext, structure, element, path); + } + + @Override + public EnumSet policyForCodedContent(IResourceValidator validator, Object appContext, + String stackPath, ElementDefinition definition, StructureDefinition structure, BindingKind kind, + AdditionalBindingPurpose purpose, ValueSet valueSet, List systems) { + return policyAdvisor.policyForCodedContent(validator, appContext, stackPath, definition, structure, kind, purpose, valueSet, systems); + } + + @Override + public List getImpliedProfilesForResource(IResourceValidator validator, Object appContext, + String stackPath, ElementDefinition definition, StructureDefinition structure, Element resource, boolean valid, + IMessagingServices msgServices, List messages) { + return policyAdvisor.getImpliedProfilesForResource(validator, appContext, stackPath, definition, structure, resource, valid, msgServices, messages); + } + + @Override + public ReferenceValidationPolicy getReferencePolicy() { + return policyAdvisor.getReferencePolicy(); + } + + public IValidationPolicyAdvisor getPolicyAdvisor() { + return policyAdvisor; + } + + public void setPolicyAdvisor(IValidationPolicyAdvisor policyAdvisor) { + this.policyAdvisor = policyAdvisor; + } + } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java index 644926361..b06bcdd55 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java @@ -40,6 +40,7 @@ import org.hl7.fhir.r5.renderers.spreadsheets.ConceptMapSpreadsheetGenerator; import org.hl7.fhir.r5.renderers.spreadsheets.StructureDefinitionSpreadsheetGenerator; import org.hl7.fhir.r5.renderers.spreadsheets.ValueSetSpreadsheetGenerator; import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; +import org.hl7.fhir.r5.terminologies.client.TerminologyClientManager.InternalLogEvent; import org.hl7.fhir.r5.terminologies.utilities.TerminologyCache; import org.hl7.fhir.utilities.FhirPublication; import org.hl7.fhir.utilities.SystemExitManager; @@ -69,6 +70,8 @@ import org.hl7.fhir.validation.cli.renderers.ValidationOutputRenderer; import org.hl7.fhir.validation.cli.utils.Common; import org.hl7.fhir.validation.cli.utils.EngineMode; import org.hl7.fhir.validation.cli.utils.VersionSourceInformation; +import org.hl7.fhir.validation.instance.advisor.JsonDrivenPolicyAdvisor; +import org.hl7.fhir.validation.instance.advisor.TextDrivenPolicyAdvisor; public class ValidationService { @@ -155,6 +158,7 @@ public class ValidationService { if (request.getCliContext().isShowTimes()) { response.getValidationTimes().put(fileToValidate.getFileName(), validatedFragments.getValidationTime()); } + } } @@ -289,6 +293,19 @@ public class ValidationService { TextFile.stringToFile(html, cliContext.getHtmlOutput()); System.out.println("HTML Summary in " + cliContext.getHtmlOutput()); } + + if (cliContext.isShowTerminologyRouting()) { + System.out.println(""); + System.out.println("Terminology Routing Dump ---------------------------------------"); + if (validator.getContext().getTxClientManager().getInternalLog().isEmpty()) { + System.out.println("(nothing happened)"); + } else { + for (InternalLogEvent log : validator.getContext().getTxClientManager().getInternalLog()) { + System.out.println(log.getMessage()+" -> "+log.getServer()+" (for VS "+log.getVs()+" with systems '"+log.getSystems()+"', choices = '"+log.getChoices()+"')"); + } + } + validator.getContext().getTxClientManager().getInternalLog().clear(); + } } if (watch != ValidatorWatchMode.NONE) { if (statusNeeded) { @@ -545,6 +562,10 @@ public class ValidationService { System.out.println(" No Terminology Cache"); } else { System.out.println(" Terminology Cache at "+validationEngine.getContext().getTxCache().getFolder()); + if (cliContext.isClearTxCache()) { + System.out.println(" Terminology Cache Entries Cleaned out"); + validationEngine.getContext().getTxCache().clear(); + } } System.out.print(" Get set... "); validationEngine.setQuestionnaireMode(cliContext.getQuestionnaireMode()); @@ -584,6 +605,13 @@ public class ValidationService { validationEngine.setFetcher(fetcher); validationEngine.getContext().setLocator(fetcher); validationEngine.setPolicyAdvisor(fetcher); + if (cliContext.getAdvisorFile() != null) { + if (cliContext.getAdvisorFile().endsWith(".json")) { + fetcher.setPolicyAdvisor(new JsonDrivenPolicyAdvisor(fetcher.getPolicyAdvisor(), new File(cliContext.getAdvisorFile()))); + } else { + fetcher.setPolicyAdvisor(new TextDrivenPolicyAdvisor(fetcher.getPolicyAdvisor(), new File(cliContext.getAdvisorFile()))); + } + } } validationEngine.getBundleValidationRules().addAll(cliContext.getBundleValidationRules()); validationEngine.setJurisdiction(CodeSystemUtilities.readCoding(cliContext.getJurisdiction())); diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java index 8f646e86b..675acd332 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/utils/Params.java @@ -54,6 +54,8 @@ public class Params { public static final String TERMINOLOGY = "-tx"; public static final String TERMINOLOGY_LOG = "-txLog"; public static final String TERMINOLOGY_CACHE = "-txCache"; + public static final String TERMINOLOGY_ROUTING = "-tx-routing"; + public static final String TERMINOLOGY_CACHE_CLEAR = "-clear-tx-cache"; public static final String LOG = "-log"; public static final String LANGUAGE = "-language"; public static final String IMPLEMENTATION_GUIDE = "-ig"; @@ -86,6 +88,7 @@ public class Params { public static final String SHOW_TIMES = "-show-times"; public static final String ALLOW_EXAMPLE_URLS = "-allow-example-urls"; public static final String OUTPUT_STYLE = "-output-style"; + public static final String ADVSIOR_FILE = "-advisor-file"; public static final String DO_IMPLICIT_FHIRPATH_STRING_CONVERSION = "-implicit-fhirpath-string-conversions"; public static final String JURISDICTION = "-jurisdiction"; public static final String HTML_IN_MARKDOWN = "-html-in-markdown"; @@ -339,10 +342,22 @@ public class Params { } else { throw new Error("Value for "+ALLOW_EXAMPLE_URLS+" not understood: "+bl); } + } else if (args[i].equals(TERMINOLOGY_ROUTING)) { + cliContext.setShowTerminologyRouting(true); + } else if (args[i].equals(TERMINOLOGY_CACHE_CLEAR)) { + cliContext.setClearTxCache(true); } else if (args[i].equals(SHOW_TIMES)) { cliContext.setShowTimes(true); } else if (args[i].equals(OUTPUT_STYLE)) { cliContext.setOutputStyle(args[++i]); + } else if (args[i].equals(ADVSIOR_FILE)) { + cliContext.setAdvisorFile(args[++i]); + File f = new File(cliContext.getAdvisorFile()); + if (!f.exists()) { + throw new Error("Cannot find advisor file "+cliContext.getAdvisorFile()); + } else if (!Utilities.existsInList(Utilities.getFileExtension(f.getName()), "json", "txt")) { + throw new Error("Advisor file "+cliContext.getAdvisorFile()+" must be a .json or a .txt file"); + } } else if (args[i].equals(SCAN)) { cliContext.setMode(EngineMode.SCAN); } else if (args[i].equals(TERMINOLOGY)) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index 2d511bf6a..0028218ed 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -574,7 +574,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private boolean noBindingMsgSuppressed; private Map fetchCache = new HashMap<>(); private HashMap resourceTracker = new HashMap<>(); - private IValidationPolicyAdvisor policyAdvisor = new BasePolicyAdvisorForFullValidation(ReferenceValidationPolicy.CHECK_VALID); long time = 0; long start = 0; long lastlog = 0; @@ -664,21 +663,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return this; } - - @Override - public IValidationPolicyAdvisor getPolicyAdvisor() { - return policyAdvisor; - } - - @Override - public IResourceValidator setPolicyAdvisor(IValidationPolicyAdvisor advisor) { - if (advisor == null) { - throw new Error("Cannot set advisor to null"); - } - this.policyAdvisor = advisor; - return this; - } - public IValidationProfileUsageTracker getTracker() { return this.tracker; } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/BasePolicyAdvisorForFullValidation.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/BasePolicyAdvisorForFullValidation.java index 4fd00c536..79b63eecb 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/BasePolicyAdvisorForFullValidation.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/BasePolicyAdvisorForFullValidation.java @@ -169,5 +169,15 @@ public class BasePolicyAdvisorForFullValidation implements IValidationPolicyAdvi return false; } + @Override + public boolean suppressMessageId(String path, String messageId) { + return false; + } + + @Override + public ReferenceValidationPolicy getReferencePolicy() { + return refpol; + } + } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/JsonDrivenPolicyAdvisor.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/JsonDrivenPolicyAdvisor.java index abaed6596..8121f9d62 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/JsonDrivenPolicyAdvisor.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/JsonDrivenPolicyAdvisor.java @@ -18,60 +18,35 @@ import org.hl7.fhir.r5.utils.validation.constants.BindingKind; import org.hl7.fhir.r5.utils.validation.constants.ContainedReferenceValidationPolicy; import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; import org.hl7.fhir.utilities.json.JsonException; +import org.hl7.fhir.utilities.json.model.JsonElement; import org.hl7.fhir.utilities.json.model.JsonObject; import org.hl7.fhir.utilities.json.parser.JsonParser; import org.hl7.fhir.utilities.validation.ValidationMessage; -public class JsonDrivenPolicyAdvisor implements IValidationPolicyAdvisor { +public class JsonDrivenPolicyAdvisor extends RulesDrivenPolicyAdvisor { - private JsonObject config; - private IValidationPolicyAdvisor base; - - public JsonDrivenPolicyAdvisor(IValidationPolicyAdvisor base, JsonObject config) { - this.base = base; - this.config = config; + public JsonDrivenPolicyAdvisor(IValidationPolicyAdvisor base, File source) throws JsonException, IOException { + super(base); + load(source); } - public JsonDrivenPolicyAdvisor(String config) throws JsonException, IOException { - this.config = JsonParser.parseObject(config, true); + public JsonDrivenPolicyAdvisor(ReferenceValidationPolicy refpol, File source) throws JsonException, IOException { + super(refpol); + load(source); } - public JsonDrivenPolicyAdvisor(File config) throws JsonException, IOException { - this.config = JsonParser.parseObject(new FileInputStream(config), true); - } - - public JsonDrivenPolicyAdvisor(FileInputStream config) throws JsonException, IOException { - this.config = JsonParser.parseObject(config, true); - } - - @Override - public ReferenceValidationPolicy policyForReference(IResourceValidator validator, Object appContext, String path, String url) { - return base.policyForReference(validator, appContext, path, url); - } - - @Override - public ContainedReferenceValidationPolicy policyForContained(IResourceValidator validator, Object appContext, StructureDefinition structure, ElementDefinition element, String containerType, String containerId, SpecialElement containingResourceType, String path, String url) { - return base.policyForContained(validator, appContext, structure, element, containerType, containerId, containingResourceType, path, url); - } - - @Override - public EnumSet policyForResource(IResourceValidator validator, Object appContext, StructureDefinition type, String path) { - return base.policyForResource(validator, appContext, type, path); - } - - @Override - public EnumSet policyForElement(IResourceValidator validator, Object appContext, StructureDefinition structure, ElementDefinition element, String path) { - return base.policyForElement(validator, appContext, structure, element, path); - } - - @Override - public EnumSet policyForCodedContent(IResourceValidator validator, Object appContext, String stackPath, ElementDefinition definition, StructureDefinition structure, BindingKind kind, AdditionalBindingPurpose purpose, ValueSet valueSet, List systems) { - return base.policyForCodedContent(validator, appContext, stackPath, definition, structure, kind, purpose, valueSet, systems); - } - - @Override - public List getImpliedProfilesForResource(IResourceValidator validator, Object appContext, String stackPath, ElementDefinition definition, StructureDefinition structure, Element resource, boolean valid, IMessagingServices msgServices, List messages) { - return base.getImpliedProfilesForResource(validator, appContext, stackPath, definition, structure, resource, valid, msgServices, messages); + private void load(File source) throws JsonException, IOException { + JsonObject json = JsonParser.parseObject(source); + for (JsonElement e : json.forceArray("suppress").getItems()) { + String s = e.asString(); + String id = s; + String path = null; + if (s.contains("@")) { + id = s.substring(0, s.indexOf("@")); + path = s.substring(s.indexOf("@")+1); + } + addSuppressMessageRule(id, path); + } } } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/RulesDrivenPolicyAdvisor.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/RulesDrivenPolicyAdvisor.java new file mode 100644 index 000000000..fe5a9f4ad --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/RulesDrivenPolicyAdvisor.java @@ -0,0 +1,176 @@ +package org.hl7.fhir.validation.instance.advisor; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.model.ElementDefinition; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.utils.validation.IMessagingServices; +import org.hl7.fhir.r5.utils.validation.IResourceValidator; +import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; +import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor.AdditionalBindingPurpose; +import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor.CodedContentValidationAction; +import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor.ElementValidationAction; +import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor.ResourceValidationAction; +import org.hl7.fhir.r5.utils.validation.constants.BindingKind; +import org.hl7.fhir.r5.utils.validation.constants.CodedContentValidationPolicy; +import org.hl7.fhir.r5.utils.validation.constants.ContainedReferenceValidationPolicy; +import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; +import org.hl7.fhir.utilities.validation.ValidationMessage; + +public class RulesDrivenPolicyAdvisor extends BasePolicyAdvisorForFullValidation { + + private IValidationPolicyAdvisor base; + + public RulesDrivenPolicyAdvisor(ReferenceValidationPolicy refpol) { + super(refpol); + base = null; + } + + public RulesDrivenPolicyAdvisor(IValidationPolicyAdvisor base) { + super(base.getReferencePolicy()); + this.base = base; + } + + private class SuppressMessageRule { + private String id; + private String path; + protected SuppressMessageRule(String id, String path) { + super(); + this.id = id; + this.path = path; + } + public String getId() { + return id; + } + public String getPath() { + return path; + } + public boolean matches(String mid, String p) { + if (((id == null) || id.equals(mid)) && ((path == null) || path.equals(p))) { + suppressed++; + return true; + } else if (((id == null) || mid.matches(id)) && ((path == null) || p.matches(path))) { + suppressed++; + return true; + } else { + return false; + } + } + } + + private List suppressMessageRules = new ArrayList<>(); + private int suppressed = 0; + + protected void addSuppressMessageRule(String id, String path) { + suppressMessageRules.add(new SuppressMessageRule(id, path)); + } + + @Override + public boolean suppressMessageId(String path, String messageId) { + for (SuppressMessageRule rule : suppressMessageRules) { + if (rule.matches(messageId, path)) { + return true; + } + } + if (base != null) { + return base.suppressMessageId(path, messageId); + } else { + return super.suppressMessageId(path, messageId); + } + } + + + @Override + public ReferenceValidationPolicy policyForReference(IResourceValidator validator, + Object appContext, + String path, + String url) { + if (base != null) { + return base.policyForReference(validator, appContext, path, url); + } else { + return super.policyForReference(validator, appContext, path, url); + } + } + + @Override + public ContainedReferenceValidationPolicy policyForContained(IResourceValidator validator, + Object appContext, + StructureDefinition structure, + ElementDefinition element, + String containerType, + String containerId, + Element.SpecialElement containingResourceType, + String path, + String url) { + if (base != null) { + return base.policyForContained(validator, appContext, structure, element, containerType, containerId, containingResourceType, path, url); + } else { + return super.policyForContained(validator, appContext, structure, element, containerType, containerId, containingResourceType, path, url); + } + } + + @Override + public EnumSet policyForResource(IResourceValidator validator, + Object appContext, + StructureDefinition type, + String path) { + if (base != null) { + return base.policyForResource(validator, appContext, type, path); + } else { + return super.policyForResource(validator, appContext, type, path); + } + } + + @Override + public EnumSet policyForElement(IResourceValidator validator, + Object appContext, + StructureDefinition structure, + ElementDefinition element, + String path) { + if (base != null) { + return base.policyForElement(validator, appContext, structure, element, path); + } else { + return super.policyForElement(validator, appContext, structure, element, path); + } + } + + @Override + public EnumSet policyForCodedContent(IResourceValidator validator, + Object appContext, + String stackPath, + ElementDefinition definition, + StructureDefinition structure, + BindingKind kind, + AdditionalBindingPurpose purpose, + ValueSet valueSet, + List systems) { + if (base != null) { + return base.policyForCodedContent(validator, appContext, stackPath, definition, structure, kind, purpose, valueSet, systems); + } else { + return super.policyForCodedContent(validator, appContext, stackPath, definition, structure, kind, purpose, valueSet, systems); + } + } + + @Override + public List getImpliedProfilesForResource(IResourceValidator validator, + Object appContext, + String stackPath, + ElementDefinition definition, + StructureDefinition structure, + Element resource, + boolean valid, + IMessagingServices msgServices, + List messages) { + if (base != null) { + return base.getImpliedProfilesForResource(validator, appContext, stackPath, definition, structure, resource, valid, msgServices, messages); + } else { + return super.getImpliedProfilesForResource(validator, appContext, stackPath, definition, structure, resource, valid, msgServices, messages); + } + } + + +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/TextDrivenPolicyAdvisor.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/TextDrivenPolicyAdvisor.java new file mode 100644 index 000000000..2920d51ae --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/advisor/TextDrivenPolicyAdvisor.java @@ -0,0 +1,54 @@ +package org.hl7.fhir.validation.instance.advisor; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; +import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.json.JsonException; + +public class TextDrivenPolicyAdvisor extends RulesDrivenPolicyAdvisor { + + public TextDrivenPolicyAdvisor(IValidationPolicyAdvisor base, File source) throws JsonException, IOException { + super(base); + load(source); + } + + public TextDrivenPolicyAdvisor(ReferenceValidationPolicy refpol, File source) throws JsonException, IOException { + super(refpol); + load(source); + } + + private void load(File source) throws JsonException, IOException { + BufferedReader reader = new BufferedReader(new FileReader(source)); + String line = reader.readLine(); + while (line != null) { + processLine(line); + line = reader.readLine(); + } + reader.close(); + } + + private void processLine(String line) { + line = line.trim(); + if (Utilities.noString(line) || line.startsWith("#")) { + return; + } + if (line.startsWith("-")) { + String s = line.substring(1).trim(); + String id = s; + String path = null; + if (s.contains("@")) { + id = s.substring(0, s.indexOf("@")); + path = s.substring(s.indexOf("@")+1); + } + addSuppressMessageRule(id, path); + } else { + // ignore it for now + } + + } +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java index be2131a7a..14b71ecc6 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/QuestionnaireValidator.java @@ -16,6 +16,7 @@ import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.DateType; import org.hl7.fhir.r5.model.IntegerType; import org.hl7.fhir.r5.model.Questionnaire; +import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireAnswerConstraint; import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemAnswerOptionComponent; import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemType; @@ -812,7 +813,7 @@ public class QuestionnaireValidator extends BaseValidator { } private boolean checkOption(List errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, String type) { - return checkOption(errors, answer, stack, qSrc, qItem, type, false); + return checkOption(errors, answer, stack, qSrc, qItem, type, qItem.getAnswerConstraint() == QuestionnaireAnswerConstraint.OPTIONSORSTRING); } private boolean checkOption(List errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, String type, boolean openChoice) { @@ -928,31 +929,29 @@ public class QuestionnaireValidator extends BaseValidator { Element v = answer.getNamedChild("valueString", false); NodeStack ns = stack.push(v, -1, null, null); if (qItem.getAnswerOption().size() > 0) { - List list = new ArrayList(); + boolean found = false; + boolean empty = true; for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { - try { - if (components.getValue() != null) { - list.add(components.getValueStringType()); - } - } catch (FHIRException e) { - // If it's the wrong type, just keep going + if (components.getValue() != null && components.hasValueStringType()) { + empty = false; + found = found || components.getValue().primitiveValue().equals((v.primitiveValue())); } } if (!openChoice) { - if (list.isEmpty()) { + if (empty) { ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSSTRING) && ok; } else { - boolean found = false; - for (StringType item : list) { - if (item.getValue().equals((v.primitiveValue()))) { - found = true; - break; - } - } - if (!found) { - ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOSTRING, v.primitiveValue()) && ok; + ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOSTRING, v.primitiveValue()) && ok; + } + } else { + found = false; + for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) { + if (components.getValue() != null && components.hasValueCoding()) { + Coding c = components.getValueCoding(); + found = found || (c.hasDisplay() && c.getDisplay().equalsIgnoreCase(v.primitiveValue())) || (c.hasCode() && c.getCode().equalsIgnoreCase(v.primitiveValue())); } } + ok = warning(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), !found, I18nConstants.QUESTIONNAIRE_QR_ITEM_STRING_IN_CODING, v.primitiveValue()) && ok; } } else { hint(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_STRINGNOOPTIONS); diff --git a/org.hl7.fhir.validation/src/main/resources/help/advisor.txt b/org.hl7.fhir.validation/src/main/resources/help/advisor.txt index c86340d44..ac48436f5 100644 --- a/org.hl7.fhir.validation/src/main/resources/help/advisor.txt +++ b/org.hl7.fhir.validation/src/main/resources/help/advisor.txt @@ -34,7 +34,7 @@ Some notes on this approach: * the messageId is directly tied to the format of the message. For some messages, there are multiple variants of the message, each with their own id, and these all need to be suppressed * message ids are stable (including across languages), but sometimes an existing message is improved in some contexts, and new message ids are introduced in later versions * the underlying validation still happens, even when the messages are suppressed - +* for invariants in profiles, the message id is the profile URL and the invariant id e.g. http://hl7.org/fhir/StructureDefinition/DomainResource#dom-6 ## Controlling what validation runs diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTests.java index 929b97395..cb155d739 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTests.java @@ -920,4 +920,14 @@ public class ValidationTests implements IEvaluationContext, IValidatorResourceFe return null; } } + + @Override + public boolean suppressMessageId(String path, String messageId) { + return false; + } + + @Override + public ReferenceValidationPolicy getReferencePolicy() { + return ReferenceValidationPolicy.IGNORE; + } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7679b0e5a..267b57fc7 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 2.17.0 32.0.1-jre 6.4.1 - 1.5.26 + 1.5.27-SNAPSHOT 2.17.0 5.9.2 1.8.2