From ced8a36f3707fc50a3664381a4019b69fe92d71b Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Tue, 15 Aug 2023 19:33:39 +1000 Subject: [PATCH 1/6] Work on comparison and rendering for showing differences properly --- .../comparison/CanonicalResourceComparer.java | 4 + .../r5/comparison/CodeSystemComparer.java | 4 +- .../fhir/r5/comparison/ComparisonSession.java | 2 + .../fhir/r5/comparison/ProfileComparer.java | 25 +- .../fhir/r5/comparison/ValueSetComparer.java | 42 +- .../VersionComparisonAnnotation.java | 108 +- .../fhir/r5/renderers/CodeSystemRenderer.java | 8 +- .../StructureDefinitionRenderer.java | 1512 ++++++++++++++++- .../fhir/r5/renderers/ValueSetRenderer.java | 102 +- .../r5/renderers/utils/RenderingContext.java | 2 + .../fhir/r5/utils/DefinitionNavigator.java | 4 +- 11 files changed, 1711 insertions(+), 102 deletions(-) diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CanonicalResourceComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CanonicalResourceComparer.java index 2bc03621c..7beb420dc 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CanonicalResourceComparer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CanonicalResourceComparer.java @@ -232,6 +232,10 @@ public abstract class CanonicalResourceComparer extends ResourceComparer { } return b.toString(); } + + public boolean noUpdates() { + return !(changedMetadata.noteable() || changedDefinitions.noteable() || !changedContent.noteable() || !changedContentInterpretation.noteable()); + } } public CanonicalResourceComparer(ComparisonSession session) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CodeSystemComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CodeSystemComparer.java index ad19c6dea..9689e3aa3 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CodeSystemComparer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/CodeSystemComparer.java @@ -33,8 +33,8 @@ public class CodeSystemComparer extends CanonicalResourceComparer { public class CodeSystemComparison extends CanonicalResourceComparison { - private StructuralMatch properties; - private StructuralMatch filters; + private StructuralMatch properties = new StructuralMatch(); + private StructuralMatch filters = new StructuralMatch(); private StructuralMatch combined; private Map propMap = new HashMap<>(); // right to left; left retains it's name public CodeSystemComparison(CodeSystem left, CodeSystem right) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonSession.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonSession.java index 5d85b5298..2c7b9f253 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonSession.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ComparisonSession.java @@ -115,6 +115,7 @@ public class ComparisonSession { return csc; } } else if (left != null) { + VersionComparisonAnnotation.markDeleted(null, forVersion, left.fhirType(), left); // todo: waht? String key = key(left.getUrl(), left.getVersion(), left.getUrl(), left.getVersion()); if (compares.containsKey(key)) { return compares.get(key); @@ -123,6 +124,7 @@ public class ComparisonSession { compares.put(key, csc); return csc; } else { + VersionComparisonAnnotation.markAdded(right, forVersion); String key = key(right.getUrl(), right.getVersion(), right.getUrl(), right.getVersion()); if (compares.containsKey(key)) { return compares.get(key); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ProfileComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ProfileComparer.java index 8d5127216..ca6d9ebdf 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ProfileComparer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ProfileComparer.java @@ -173,7 +173,7 @@ public class ProfileComparer extends CanonicalResourceComparer implements Profil res.combined = sm; ln = new DefinitionNavigator(session.getContextLeft(), left, true); rn = new DefinitionNavigator(session.getContextRight(), right, true); - ch = compareDiff(ln.path(), null, ln, rn) || ch; + ch = compareDiff(ln.path(), null, ln, rn, res) || ch; // we don't preserve the differences - we only want the annotations } res.updateDefinitionsState(ch); @@ -389,7 +389,7 @@ public class ProfileComparer extends CanonicalResourceComparer implements Profil } - private boolean compareDiff(String path, String sliceName, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, FHIRFormatError, IOException { + private boolean compareDiff(String path, String sliceName, DefinitionNavigator left, DefinitionNavigator right, ProfileComparison res) throws DefinitionException, FHIRFormatError, IOException { assert(path != null); assert(left != null); assert(right != null); @@ -414,7 +414,7 @@ public class ProfileComparer extends CanonicalResourceComparer implements Profil def = comparePrimitivesWithTracking("max", left.current().getMaxElement(), right.current().getMaxElement(), null, IssueSeverity.INFORMATION, null, right.current(), session.getForVersion()) || def; // add the children - def = compareDiffChildren(path, left, right) || def; + def = compareDiffChildren(path, left, right, right.current(), res) || def; // // // now process the slices // if (left.current().hasSlicing() || right.current().hasSlicing()) { @@ -478,31 +478,26 @@ public class ProfileComparer extends CanonicalResourceComparer implements Profil } - private boolean compareDiffChildren(String path, DefinitionNavigator left, DefinitionNavigator right) throws DefinitionException, IOException, FHIRFormatError { + private boolean compareDiffChildren(String path, DefinitionNavigator left, DefinitionNavigator right, Base parent, ProfileComparison res) throws DefinitionException, IOException, FHIRFormatError { boolean def = false; List lc = left.children(); List rc = right.children(); - // it's possible that one of these profiles walks into a data type and the other doesn't - // if it does, we have to load the children for that data into the profile that doesn't - // walk into it - if (lc.isEmpty() && !rc.isEmpty() && right.current().getType().size() == 1 && left.hasTypeChildren(right.current().getType().get(0), left.getStructure())) - lc = left.childrenFromType(right.current().getType().get(0), right.getStructure()); - if (rc.isEmpty() && !lc.isEmpty() && left.current().getType().size() == 1 && right.hasTypeChildren(left.current().getType().get(0), right.getStructure())) - rc = right.childrenFromType(left.current().getType().get(0), left.getStructure()); - + List matchR = new ArrayList<>(); for (DefinitionNavigator l : lc) { DefinitionNavigator r = findInList(rc, l); if (r == null) { - // todo + VersionComparisonAnnotation.markDeleted(parent, session.getForVersion(), "element", l.current()); + res.updateContentState(true); } else { - def = compareDiff(l.path(), null, l, r) || def; + def = compareDiff(l.path(), null, l, r, res) || def; } } for (DefinitionNavigator r : rc) { if (!matchR.contains(r)) { - // todo + VersionComparisonAnnotation.markAdded(r.current(), session.getForVersion()); + res.updateContentState(true); } } return def; diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ValueSetComparer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ValueSetComparer.java index 7b6914218..5394f14fe 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ValueSetComparer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/ValueSetComparer.java @@ -142,7 +142,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { def = compareCompose(left.getCompose(), right.getCompose(), res, res.getUnion().getCompose(), res.getIntersection().getCompose()) || def; res.updateDefinitionsState(def); - compareExpansions(left, right, res); +// compareExpansions(left, right, res); VersionComparisonAnnotation.annotate(right, session.getForVersion(), res); return res; } @@ -166,7 +166,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { intersection.getInclude().add(csI); StructuralMatch sm = new StructuralMatch(l, r); res.getIncludes().getChildren().add(sm); - def = compareDefinitions(l, r, sm, csM, csI, res) || def; + def = compareDefinitions("ValueSet.compose.exclude["+right.getInclude().indexOf(r)+"]", l, r, sm, csM, csI, res) || def; } } for (ConceptSetComponent r : right.getInclude()) { @@ -194,7 +194,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { intersection.getExclude().add(csI); StructuralMatch sm = new StructuralMatch(l, r); res.getExcludes().getChildren().add(sm); - def = compareDefinitions(l, r, sm, csM, csI, res) || def; + def = compareDefinitions("ValueSet.compose.exclude["+right.getExclude().indexOf(r)+"]", l, r, sm, csM, csI, res) || def; } } for (ConceptSetComponent r : right.getExclude()) { @@ -241,7 +241,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { } - private boolean compareDefinitions(ConceptSetComponent left, ConceptSetComponent right, StructuralMatch combined, ConceptSetComponent union, ConceptSetComponent intersection, ValueSetComparison res) { + private boolean compareDefinitions(String path, ConceptSetComponent left, ConceptSetComponent right, StructuralMatch combined, ConceptSetComponent union, ConceptSetComponent intersection, ValueSetComparison res) { boolean def = false; // system must match, but the rest might not. we're going to do the full comparison whatever, so the outcome looks consistent to the user List matchVSR = new ArrayList<>(); @@ -250,7 +250,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { if (r == null) { union.getValueSet().add(l); res.updateContentState(true); - combined.getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.INFORMATION, "Removed ValueSet", "ValueSet.compose.include.valueSet"))); + combined.getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.ERROR, "Removed ValueSet", "ValueSet.compose.include.valueSet"))); if (session.isAnnotate()) { VersionComparisonAnnotation.markDeleted(right, session.getForVersion(), "valueset", l); } @@ -266,7 +266,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { union.getValueSet().add(l); union.getValueSet().add(r); res.updateContentState(true); - StructuralMatch sm = new StructuralMatch(l, r, vmI(IssueSeverity.INFORMATION, "Values are different", "ValueSet.compose.include.valueSet")); + StructuralMatch sm = new StructuralMatch(l, r, vmI(IssueSeverity.WARNING, "Values are different", "ValueSet.compose.include.valueSet")); combined.getChildren().add(sm); if (session.isAnnotate()) { VersionComparisonAnnotation.markChanged(r, session.getForVersion()); @@ -279,7 +279,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { if (!matchVSR.contains(r)) { union.getValueSet().add(r); res.updateContentState(true); - combined.getChildren().add(new StructuralMatch(vmI(IssueSeverity.INFORMATION, "Add ValueSet", "ValueSet.compose.include.valueSet"), r)); + combined.getChildren().add(new StructuralMatch(vmI(IssueSeverity.ERROR, "Add ValueSet", "ValueSet.compose.include.valueSet"), r)); VersionComparisonAnnotation.markAdded(r, session.getForVersion()); } } @@ -290,7 +290,8 @@ public class ValueSetComparer extends CanonicalResourceComparer { if (r == null) { union.getConcept().add(l); res.updateContentState(true); - combined.getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.INFORMATION, "Removed this Concept", "ValueSet.compose.include.concept"))); + combined.getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.ERROR, "Removed this Concept", "ValueSet.compose.include.concept"))); + res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" removed", IssueSeverity.ERROR)); VersionComparisonAnnotation.markDeleted(right, session.getForVersion(), "concept", l); } else { matchCR.add(r); @@ -301,15 +302,15 @@ public class ValueSetComparer extends CanonicalResourceComparer { intersection.getConcept().add(ci); StructuralMatch sm = new StructuralMatch(l, r); combined.getChildren().add(sm); - def = compareConcepts(l, r, sm, cu, ci) || def; + def = compareConcepts(path+".concept["+right.getConcept().indexOf(r)+"]", l, r, sm, cu, ci, res) || def; } else { // not that it's possible to get here? union.getConcept().add(l); union.getConcept().add(r); - StructuralMatch sm = new StructuralMatch(l, r, vmI(IssueSeverity.INFORMATION, "Concepts are different", "ValueSet.compose.include.concept")); + StructuralMatch sm = new StructuralMatch(l, r, vmI(IssueSeverity.WARNING, "Concepts are different", "ValueSet.compose.include.concept")); combined.getChildren().add(sm); res.updateContentState(true); - compareConcepts(l, r, sm, null, null); + compareConcepts(path+".concept["+right.getConcept().indexOf(r)+"]", l, r, sm, null, null, res); VersionComparisonAnnotation.markChanged(r, session.getForVersion()); } } @@ -318,7 +319,8 @@ public class ValueSetComparer extends CanonicalResourceComparer { if (!matchCR.contains(r)) { union.getConcept().add(r); res.updateContentState(true); - combined.getChildren().add(new StructuralMatch(vmI(IssueSeverity.INFORMATION, "Added this Concept", "ValueSet.compose.include.concept"), r)); + combined.getChildren().add(new StructuralMatch(vmI(IssueSeverity.ERROR, "Added this Concept", "ValueSet.compose.include.concept"), r)); + res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+r.getCode()+" added", IssueSeverity.ERROR)); VersionComparisonAnnotation.markAdded(r, session.getForVersion()); } } @@ -329,7 +331,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { if (r == null) { union.getFilter().add(l); res.updateContentState(true); - combined.getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.INFORMATION, "Removed this item", "ValueSet.compose.include.filter"))); + combined.getChildren().add(new StructuralMatch(l, vmI(IssueSeverity.ERROR, "Removed this item", "ValueSet.compose.include.filter"))); VersionComparisonAnnotation.markDeleted(right, session.getForVersion(), "filter", l); } else { matchFR.add(r); @@ -347,7 +349,7 @@ public class ValueSetComparer extends CanonicalResourceComparer { } else { union.getFilter().add(l); union.getFilter().add(r); - StructuralMatch sm = new StructuralMatch(l, r, vmI(IssueSeverity.INFORMATION, "Codes are different", "ValueSet.compose.include.filter")); + StructuralMatch sm = new StructuralMatch(l, r, vmI(IssueSeverity.WARNING, "Codes are different", "ValueSet.compose.include.filter")); res.updateContentState(true); combined.getChildren().add(sm); compareFilters(l, r, sm, null, null); @@ -358,14 +360,14 @@ public class ValueSetComparer extends CanonicalResourceComparer { if (!matchFR.contains(r)) { union.getFilter().add(r); res.updateContentState(true); - combined.getChildren().add(new StructuralMatch(vmI(IssueSeverity.INFORMATION, "Added this item", "ValueSet.compose.include.filter"), r)); + combined.getChildren().add(new StructuralMatch(vmI(IssueSeverity.ERROR, "Added this item", "ValueSet.compose.include.filter"), r)); VersionComparisonAnnotation.markAdded(r, session.getForVersion()); } } return def; } - private boolean compareConcepts(ConceptReferenceComponent l, ConceptReferenceComponent r, StructuralMatch sm, ConceptReferenceComponent cu, ConceptReferenceComponent ci) { + private boolean compareConcepts(String path, ConceptReferenceComponent l, ConceptReferenceComponent r, StructuralMatch sm, ConceptReferenceComponent cu, ConceptReferenceComponent ci, ValueSetComparison res) { boolean def = false; sm.getChildren().add(new StructuralMatch(l.getCodeElement(), r.getCodeElement(), l.getCode().equals(r.getCode()) ? null : vmI(IssueSeverity.INFORMATION, "Codes do not match", "ValueSet.compose.include.concept"))); if (ci != null) { @@ -379,7 +381,13 @@ public class ValueSetComparer extends CanonicalResourceComparer { cu.setDisplay(r.getDisplay()); } def = !l.getDisplay().equals(r.getDisplay()); + if (def) { + res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" display changed from '"+l.getDisplay()+"' to '"+r.getDisplay()+"'", IssueSeverity.WARNING)); + VersionComparisonAnnotation.markChanged(r.getDisplayElement(), session.getForVersion()); + } } else if (l.hasDisplay()) { + VersionComparisonAnnotation.markDeleted(r, session.getForVersion(), "display", l.getDisplayElement()); + res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" display '"+l.getDisplay()+"' removed", IssueSeverity.WARNING)); sm.getChildren().add(new StructuralMatch(l.getDisplayElement(), null, vmI(IssueSeverity.INFORMATION, "Display Removed", "ValueSet.compose.include.concept"))); if (ci != null) { ci.setDisplay(l.getDisplay()); @@ -387,6 +395,8 @@ public class ValueSetComparer extends CanonicalResourceComparer { } def = true; } else if (r.hasDisplay()) { + VersionComparisonAnnotation.markAdded(r.getDisplayElement(), session.getForVersion()); + res.getMessages().add(new ValidationMessage(Source.ProfileComparer, IssueType.INFORMATIONAL, path, "Code "+l.getCode()+" display '"+r.getDisplay()+"' added", IssueSeverity.WARNING)); sm.getChildren().add(new StructuralMatch(null, r.getDisplayElement(), vmI(IssueSeverity.INFORMATION, "Display added", "ValueSet.compose.include.concept"))); if (ci != null) { ci.setDisplay(r.getDisplay()); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/VersionComparisonAnnotation.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/VersionComparisonAnnotation.java index 31760be76..3df357510 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/VersionComparisonAnnotation.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/comparison/VersionComparisonAnnotation.java @@ -9,13 +9,17 @@ import org.hl7.fhir.r5.comparison.CanonicalResourceComparer.CanonicalResourceCom import org.hl7.fhir.r5.comparison.CanonicalResourceComparer.ChangeAnalysisState; import org.hl7.fhir.r5.model.Base; import org.hl7.fhir.r5.model.CanonicalResource; +import org.hl7.fhir.r5.model.CodeSystem; +import org.hl7.fhir.r5.model.StringType; import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; +import org.hl7.fhir.utilities.xhtml.NodeType; import org.hl7.fhir.utilities.xhtml.XhtmlNode; public class VersionComparisonAnnotation { public enum AnotationType { - NoChange, Added, Changed, Deleted; + NoChange, Added, Changed, ChildrenDeleted, Deleted; } public static final String USER_DATA_NAME = "version-annotation"; @@ -37,7 +41,7 @@ public class VersionComparisonAnnotation { public static void annotate(Base base, String version, CanonicalResourceComparison comp) { if (version != null) { - VersionComparisonAnnotation vca = new VersionComparisonAnnotation(comp.noChange() ? AnotationType.NoChange : AnotationType.Added, version); + VersionComparisonAnnotation vca = new VersionComparisonAnnotation(comp.noUpdates() ? AnotationType.NoChange : AnotationType.Changed, version); vca.comp = comp; base.setUserData(USER_DATA_NAME, vca); } @@ -57,13 +61,13 @@ public class VersionComparisonAnnotation { } public static void markDeleted(Base parent, String version, String name, Base other) { - if (version != null) { + if (version != null && other != null) { VersionComparisonAnnotation vca = null; if (parent.hasUserData(USER_DATA_NAME)) { vca = (VersionComparisonAnnotation) parent.getUserData(USER_DATA_NAME); assert vca.type != AnotationType.Added; } else { - vca = new VersionComparisonAnnotation(AnotationType.Changed, version); + vca = new VersionComparisonAnnotation(AnotationType.ChildrenDeleted, version); parent.setUserData(USER_DATA_NAME, vca); } if (vca.deletedChildren == null) { @@ -103,13 +107,13 @@ public class VersionComparisonAnnotation { return spanOuter; case Changed: spanOuter = x.span("border: solid 1px #dddddd; margin: 2px; padding: 2px", null); - spanInner = spanOuter.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been added since "+version); + spanInner = spanOuter.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been changed since "+version); spanInner.img("icon-change-edit.png", "icon"); spanInner.tx(" Changed:"); return spanOuter; case Deleted: spanOuter = x.span("border: solid 1px #dddddd; margin: 2px; padding: 2px", null); - spanInner = spanOuter.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been added since "+version); + spanInner = spanOuter.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been removed since "+version); spanInner.img("icon-change-remove.png", "icon"); spanInner.tx(" Removed:"); return spanOuter.strikethrough(); @@ -137,13 +141,13 @@ public class VersionComparisonAnnotation { return divOuter; case Changed: divOuter = x.div("border: solid 1px #dddddd; margin: 2px; padding: 2px"); - spanInner = divOuter.para().style("margin: 0").span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been added since "+version); + spanInner = divOuter.para().style("margin: 0").span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been changed since "+version); spanInner.img("icon-change-edit.png", "icon"); spanInner.tx(" Changed:"); return divOuter; case Deleted: divOuter = x.div("border: solid 1px #dddddd; margin: 2px; padding: 2px"); - spanInner = divOuter.para().style("margin: 0").span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been added since "+version); + spanInner = divOuter.para().style("margin: 0").span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been removed since "+version); spanInner.img("icon-change-remove.png", "icon"); spanInner.tx(" Removed:"); return divOuter.strikethrough(); @@ -151,6 +155,51 @@ public class VersionComparisonAnnotation { return x; } } + + + public static XhtmlNode renderRow(Base b, XhtmlNode tbl, XhtmlNode tr) { + if (b.hasUserData(USER_DATA_NAME)) { + VersionComparisonAnnotation self = (VersionComparisonAnnotation) b.getUserData(USER_DATA_NAME); + return self.renderRow(tbl, tr); + } else { + return tr.td(); + } + } + + private XhtmlNode renderRow(XhtmlNode tbl, XhtmlNode tr) { + switch (type) { + case Added: + if (tbl.isClass("grid")) { + tr.style("border: solid 1px #dddddd; margin: 2px; padding: 2px"); + } + XhtmlNode td = tr.td(); + XhtmlNode span = td.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This row of content has been added since "+version); + span.img("icon-change-add.png", "icon"); + span.tx(" Added:"); + XhtmlNode x = new XhtmlNode(NodeType.Element, "holder"); + x.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This row of content has been added since "+version).tx(" "); + tr.styleCells(x); + return td; + case Changed: + td = tr.td(); + span = td.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This row of content has been changed since "+version); + span.img("icon-change-edit.png", "icon"); + span.tx(" Changed:"); + return td; + case Deleted: + tr.style("text-decoration: line-through"); + td = tr.td(); + span = td.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been removed since "+version); + span.img("icon-change-remove.png", "icon"); + span.tx(" Removed:"); + x = new XhtmlNode(NodeType.Element, "holder"); + x.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px; text-decoration: none", "This row of content has been added since "+version).tx(" "); + tr.styleCells(x); + return td; + default: + return tr.td(); + } + } public static boolean hasDeleted(Base base, String... names) { boolean result = false; @@ -178,6 +227,18 @@ public class VersionComparisonAnnotation { return result; } + public static Base getDeletedItem(Base base, String name) { + List result = new ArrayList<>(); + if (base.hasUserData(USER_DATA_NAME)) { + VersionComparisonAnnotation self = (VersionComparisonAnnotation) base.getUserData(USER_DATA_NAME); + if (self.deletedChildren != null && self.deletedChildren.containsKey(name)) { + result.addAll(self.deletedChildren.get(name)); + } + } + return result.isEmpty() ? null : result.get(0); + } + + public static CanonicalResourceComparison artifactComparison(Base base) { if (base.hasUserData(USER_DATA_NAME)) { VersionComparisonAnnotation self = (VersionComparisonAnnotation) base.getUserData(USER_DATA_NAME); @@ -186,5 +247,34 @@ public class VersionComparisonAnnotation { return null; } } - + + public static void renderSummary(Base base, XhtmlNode x, String version) { + if (base.hasUserData(USER_DATA_NAME)) { + VersionComparisonAnnotation self = (VersionComparisonAnnotation) base.getUserData(USER_DATA_NAME); + switch (self.type) { + case Added: + XhtmlNode spanInner = x.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been added since "+version); + spanInner.img("icon-change-add.png", "icon"); + spanInner.tx(" Added"); + return; + case Changed: + spanInner = x.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been added since "+version); + spanInner.img("icon-change-edit.png", "icon"); + spanInner.tx(" Changed"); + return; + case Deleted: + spanInner = x.span("background-color: #fff2ff; border-left: solid 3px #ffa0ff; margin: 2px; padding: 2px", "This content has been added since "+version); + spanInner.img("icon-change-remove.png", "icon"); + spanInner.tx(" Removed"); + return; + default: + x.span("color: #eeeeee").tx("n/c"); + return; + } + } else { + x.span("color: #eeeeee").tx("--"); + } + } + + } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/CodeSystemRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/CodeSystemRenderer.java index 7d5305b4a..16c232e39 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/CodeSystemRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/CodeSystemRenderer.java @@ -398,7 +398,7 @@ public class CodeSystemRenderer extends TerminologyRenderer { tr.setAttribute("style", "background-color: #ffeeee"); } - XhtmlNode td = tr.td(); + XhtmlNode td = VersionComparisonAnnotation.renderRow(c, t, tr); if (hasHierarchy) { td.addText(Integer.toString(level+1)); td = tr.td(); @@ -407,9 +407,9 @@ public class CodeSystemRenderer extends TerminologyRenderer { } String link = isSupplement ? getLinkForCode(cs.getSupplements(), null, c.getCode()) : null; if (link != null) { - td.ah(link).attribute("style", "white-space:nowrap").addText(c.getCode()); + td.ah(link).style( "white-space:nowrap").addText(c.getCode()); } else { - VersionComparisonAnnotation.render(c, td.attribute("style", "white-space:nowrap")).addText(c.getCode()); + td.style("white-space:nowrap").addText(c.getCode()); } XhtmlNode a; if (c.hasCodeElement()) { @@ -579,7 +579,7 @@ public class CodeSystemRenderer extends TerminologyRenderer { td = tr.td(); String s = Utilities.padLeft("", '\u00A0', (level+1)*2); td.addText(s); - td.attribute("style", "white-space:nowrap"); + td.style("white-space:nowrap"); a = td.ah("#"+cs.getId()+"-" + Utilities.nmtokenize(cc.getCode())); a.addText(cc.getCode()); if (hasDisplay) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/StructureDefinitionRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/StructureDefinitionRenderer.java index 7bad517a2..1fddc8b06 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/StructureDefinitionRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/StructureDefinitionRenderer.java @@ -1,28 +1,35 @@ package org.hl7.fhir.r5.renderers; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.comparison.VersionComparisonAnnotation; import org.hl7.fhir.r5.conformance.profile.BindingResolution; -import org.hl7.fhir.r5.conformance.profile.ProfileKnowledgeProvider; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities.ElementChoiceGroup; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities.ExtensionContext; -import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult; import org.hl7.fhir.r5.formats.IParser; +import org.hl7.fhir.r5.formats.IParser.OutputStyle; import org.hl7.fhir.r5.formats.JsonParser; +import org.hl7.fhir.r5.formats.XmlParser; import org.hl7.fhir.r5.model.ActorDefinition; import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.r5.model.BooleanType; +import org.hl7.fhir.r5.model.CanonicalResource; import org.hl7.fhir.r5.model.CanonicalType; +import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.CodeType; import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; @@ -43,6 +50,7 @@ import org.hl7.fhir.r5.model.ElementDefinition.PropertyRepresentation; import org.hl7.fhir.r5.model.ElementDefinition.SlicingRules; import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; import org.hl7.fhir.r5.model.Enumeration; +import org.hl7.fhir.r5.model.Enumerations.BindingStrength; import org.hl7.fhir.r5.model.Extension; import org.hl7.fhir.r5.model.IdType; import org.hl7.fhir.r5.model.IntegerType; @@ -55,10 +63,9 @@ import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionMappingComponent; import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule; import org.hl7.fhir.r5.model.UriType; -import org.hl7.fhir.r5.model.UsageContext; +import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.renderers.utils.BaseWrappers.ResourceWrapper; -import org.hl7.fhir.r5.renderers.CodeResolver.CodeResolution; -import org.hl7.fhir.r5.renderers.ObligationsRenderer.ObligationDetail; +import org.hl7.fhir.r5.renderers.StructureDefinitionRenderer.InternalMarkdownProcessor; 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.KnownLinkType; @@ -78,10 +85,9 @@ import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece; import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row; import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableGenerationMode; import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel; -import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Title; import org.hl7.fhir.utilities.xhtml.NodeType; import org.hl7.fhir.utilities.xhtml.XhtmlNode; -import org.hl7.fhir.utilities.xhtml.XhtmlNodeList; +import org.hl7.fhir.utilities.xhtml.XhtmlParser; public class StructureDefinitionRenderer extends ResourceRenderer { @@ -148,25 +154,189 @@ public class StructureDefinitionRenderer extends ResourceRenderer { // // } + public class InternalMarkdownProcessor implements IMarkdownProcessor { + @Override + public String processMarkdown(String location, PrimitiveType md) throws FHIRException { + return context.getMarkdown().process(md.primitiveValue(), location); + } + + @Override + public String processMarkdown(String location, String text) throws FHIRException { + return context.getMarkdown().process(text, location); + } + } + + private enum ListItemStatus { New, Unchanged, Removed}; + + private abstract class ItemWithStatus { + ListItemStatus status = ListItemStatus.New; // new, unchanged, removed + + protected abstract void renderDetails(XhtmlNode f); + protected abstract boolean matches(ItemWithStatus other); + + public void render(XhtmlNode x) { + XhtmlNode f = x; + if (status == ListItemStatus.Unchanged) { + f = unchanged(f); + } else if (status == ListItemStatus.Removed) { + f = removed(f); + } + renderDetails(f); + } + } + + protected class StatusList extends ArrayList implements List { + + public boolean merge(T item) { + if (item == null) { + return false; + } + boolean found = false; + for (T t : this) { + if (t.matches(item)) { + found = true; + t.status = ListItemStatus.Unchanged; + } + } + if (!found) { + item.status = ListItemStatus.Removed; + return add(item); + } else { + return false; + } + } + + public boolean add(T item) { + if (item != null) { + return super.add(item); + } else { + return false; + } + } + } + + private class ResolvedCanonical extends ItemWithStatus { + String url; // what we used to resolve + CanonicalResource cr; // what we resolved + + public ResolvedCanonical(String url, CanonicalResource cr) { + this.url = url; + this.cr = cr; + } + public void renderDetails(XhtmlNode f) { + if (cr != null && cr.hasWebPath()) { + f.ah(cr.getWebPath()).tx(cr.present()); + } else { + f.code().tx(url); + } + } + protected boolean matches(ItemWithStatus other) { + return ((ResolvedCanonical) other).url.equals(url); + } + } + + private class InvariantWithStatus extends ItemWithStatus { + ElementDefinitionConstraintComponent value; + protected InvariantWithStatus(ElementDefinitionConstraintComponent value) { + this.value = value; + } + + protected boolean matches(ItemWithStatus other) { + return ((InvariantWithStatus) other).value.equalsDeep(value); + } + + public void renderDetails(XhtmlNode f) { + f.b().attribute("title", "Formal Invariant Identifier").tx(value.getKey()); + f.tx(": "); + f.tx(value.getHuman()); + f.tx(" ("); + if (status == ListItemStatus.New) { + f.code().tx(value.getExpression()); + } else { + f.tx(value.getExpression()); + } + f.tx(")"); + } + } + + private class DiscriminatorWithStatus extends ItemWithStatus { + ElementDefinitionSlicingDiscriminatorComponent value; + protected DiscriminatorWithStatus(ElementDefinitionSlicingDiscriminatorComponent value) { + this.value = value; + } + + protected boolean matches(ItemWithStatus other) { + return ((DiscriminatorWithStatus) other).value.equalsDeep(value); + } + + public void renderDetails(XhtmlNode f) { + f.tx(value.getType().toCode()); + f.tx(" @ "); + f.tx(value.getPath()); + } + } + + private class ValueWithStatus extends ItemWithStatus { + PrimitiveType value; + protected ValueWithStatus(PrimitiveType value) { + this.value = value; + } + + protected boolean matches(ItemWithStatus other) { + return ((ValueWithStatus) other).value.equalsDeep(value); + } + + public void renderDetails(XhtmlNode f) { + if (value.hasUserData("render.link")) { + f = f.ah(value.getUserString("render.link")); + } + f.tx(value.asStringValue()); + } + } + private List keyRows = new ArrayList<>(); + private Map> sdMapCache = new HashMap<>(); + private IMarkdownProcessor hostMd; public StructureDefinitionRenderer(RenderingContext context) { super(context); + hostMd = new InternalMarkdownProcessor(); } public StructureDefinitionRenderer(RenderingContext context, ResourceContext rcontext) { super(context, rcontext); } + + public Map> getSdMapCache() { + return sdMapCache; + } + + public void setSdMapCache(Map> sdMapCache) { + this.sdMapCache = sdMapCache; + } + + public IMarkdownProcessor getHostMd() { + return hostMd; + } + + public void setHostMd(IMarkdownProcessor hostMd) { + this.hostMd = hostMd; + } + public boolean render(XhtmlNode x, Resource dr) throws FHIRFormatError, DefinitionException, IOException { return render(x, (StructureDefinition) dr); } public boolean render(XhtmlNode x, StructureDefinition sd) throws FHIRFormatError, DefinitionException, IOException { - x.getChildNodes().add(generateTable(context.getDefinitionsTarget(), sd, true, context.getDestDir(), false, sd.getId(), false, + if (context.getStructureMode() == StructureDefinitionRendererMode.DATA_DICT) { + renderDict(sd, sd.getDifferential().getElement(), x.table("dict"), false, GEN_MODE_DIFF, ""); + } else { + x.getChildNodes().add(generateTable(context.getDefinitionsTarget(), sd, true, context.getDestDir(), false, sd.getId(), false, context.getLink(KnownLinkType.SPEC), "", sd.getKind() == StructureDefinitionKind.LOGICAL, false, null, false, context, "")); + } return true; } @@ -200,9 +370,19 @@ public class StructureDefinitionRenderer extends ResourceRenderer { // private static final boolean TABLE_FORMAT_FOR_FIXED_VALUES = false; public static final String CONSTRAINT_CHAR = "C"; public static final String CONSTRAINT_STYLE = "padding-left: 3px; padding-right: 3px; border: 1px maroon solid; font-weight: bold; color: #301212; background-color: #fdf4f4;"; + public static final int GEN_MODE_SNAP = 1; + public static final int GEN_MODE_DIFF = 2; + public static final int GEN_MODE_MS = 3; + public static final int GEN_MODE_KEY = 4; + public static final String RIM_MAPPING = "http://hl7.org/v3"; + public static final String v2_MAPPING = "http://hl7.org/v2"; + public static final String LOINC_MAPPING = "http://loinc.org"; + public static final String SNOMED_MAPPING = "http://snomed.info"; + private final boolean ADD_REFERENCE_TO_TABLE = true; private boolean useTableForFixedValues = true; + private String corePath; public static class UnusedTracker { private boolean used; @@ -2227,6 +2407,8 @@ public class StructureDefinitionRenderer extends ResourceRenderer { + + protected boolean isPrimitive(String value) { StructureDefinition sd = context.getWorker().fetchTypeDefinition(value); if (sd == null) // might be running before all SDs are available @@ -2727,7 +2909,7 @@ public class StructureDefinitionRenderer extends ResourceRenderer { return ed.getPath().substring(ed.getPath().indexOf(".")+1); } - public XhtmlNode formatTypeSpecifiers(IWorkerContext context, ElementDefinition d) { + public XhtmlNode formatTypeSpecifiers(ElementDefinition d) { XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); boolean first = true; for (Extension e : d.getExtensionsByUrl(ToolingExtensions.EXT_TYPE_SPEC)) { @@ -2737,7 +2919,7 @@ public class StructureDefinitionRenderer extends ResourceRenderer { x.tx("If "); x.code().tx(cond); x.tx(" then the type is "); - StructureDefinition sd = context.fetchTypeDefinition(type); + StructureDefinition sd = context.getContext().fetchTypeDefinition(type); if (sd == null) { x.code().tx(type); } else { @@ -2890,6 +3072,1316 @@ public class StructureDefinitionRenderer extends ResourceRenderer { return null; } + public void renderDict(StructureDefinition sd, List elements, XhtmlNode t, boolean incProfiledOut, int mode, String anchorPrefix) throws FHIRException, IOException { + int i = 0; + Map allAnchors = new HashMap<>(); + List excluded = new ArrayList<>(); + List stack = new ArrayList<>(); // keeps track of parents, for anchor generation + + for (ElementDefinition ec : elements) { + addToStack(stack, ec); + generateAnchors(stack, allAnchors); + checkInScope(stack, excluded); + } + for (ElementDefinition ec : elements) { + if ((incProfiledOut || !"0".equals(ec.getMax())) && !excluded.contains(ec)) { + ElementDefinition compareElement = null; + if (mode==GEN_MODE_DIFF) + compareElement = getBaseElement(ec, sd.getBaseDefinition()); + else if (mode==GEN_MODE_KEY) + compareElement = getRootElement(ec); + List anchors = makeAnchors(ec, anchorPrefix); + String title = ec.getId(); + XhtmlNode tr = t.tr(); + XhtmlNode sp = tr.td("structure").colspan(2).spanClss("self-link-parent"); + for (String s : anchors) { + sp.an(s).tx(" "); + } + sp.span("color: grey", null).tx(Integer.toString(i++)); + sp.b().tx(". "+title); + link(sp, ec.getId(), anchorPrefix); + if (isProfiledExtension(ec)) { + StructureDefinition extDefn = context.getContext().fetchResource(StructureDefinition.class, ec.getType().get(0).getProfile().get(0).getValue()); + if (extDefn == null) { + generateElementInner(t, sd, ec, 1, null, compareElement, null); + } else { + ElementDefinition valueDefn = getExtensionValueDefinition(extDefn); + ElementDefinition compareValueDefn = null; + try { + StructureDefinition compareExtDefn = context.getContext().fetchResource(StructureDefinition.class, compareElement.getType().get(0).getProfile().get(0).getValue()); + compareValueDefn = getExtensionValueDefinition(extDefn); + } catch (Exception except) {} + generateElementInner(t, sd, ec, valueDefn == null || valueDefn.prohibited() ? 2 : 3, valueDefn, compareElement, compareValueDefn); + // generateElementInner(b, extDefn, extDefn.getSnapshot().getElement().get(0), valueDefn == null ? 2 : 3, valueDefn); + } + } else { + generateElementInner(t, sd, ec, mode, null, compareElement, null); + if (ec.hasSlicing()) { + generateSlicing(t, sd, ec, ec.getSlicing(), compareElement, mode); + } + } + } + t.tx("\r\n"); + i++; + } + } + + public ElementDefinition getElementById(String url, String id) { + Map sdCache = sdMapCache.get(url); + + if (sdCache == null) { + StructureDefinition sd = (StructureDefinition) context.getContext().fetchResource(StructureDefinition.class, url); + if (sd == null) { + if (url.equals("http://hl7.org/fhir/StructureDefinition/Base")) { + sd = (StructureDefinition) context.getContext().fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/Element"); + } + if (sd == null) { + throw new FHIRException("Unable to retrieve StructureDefinition with URL " + url); + } + } + sdCache = new HashMap(); + sdMapCache.put(url, sdCache); + String webroot = sd.getUserString("webroot"); + for (ElementDefinition e : sd.getSnapshot().getElement()) { + context.getProfileUtilities().updateURLs(sd.getUrl(), webroot, e); + sdCache.put(e.getId(), e); + } + } + return sdCache.get(id); + } + + + // Returns the ElementDefinition for the 'parent' of the current element + private ElementDefinition getBaseElement(ElementDefinition e, String url) { + if (e.hasUserData(ProfileUtilities.UD_DERIVATION_POINTER)) { + return getElementById(url, e.getUserString(ProfileUtilities.UD_DERIVATION_POINTER)); + } + return null; + } + + // Returns the ElementDefinition for the 'root' ancestor of the current element + private ElementDefinition getRootElement(ElementDefinition e) { + if (!e.hasBase()) + return null; + String basePath = e.getBase().getPath(); + String url = "http://hl7.org/fhir/StructureDefinition/" + (basePath.contains(".") ? basePath.substring(0, basePath.indexOf(".")) : basePath); + try { + return getElementById(url, basePath); + } catch (FHIRException except) { + // Likely a logical model, so this is ok + return null; + } + } + private void checkInScope(List stack, List excluded) { + if (stack.size() > 2) { + ElementDefinition parent = stack.get(stack.size()-2); + ElementDefinition focus = stack.get(stack.size()-1); + + if (excluded.contains(parent) || "0".equals(parent.getMax())) { + excluded.add(focus); + } + } + } + + private void generateAnchors(List stack, Map allAnchors) { + List list = new ArrayList<>(); + list.add(stack.get(0).getId()); // initialise + for (int i = 1; i < stack.size(); i++) { + ElementDefinition ed = stack.get(i); + List aliases = new ArrayList<>(); + String name = tail(ed.getPath()); + if (name.endsWith("[x]")) { + aliases.add(name); + Set tl = new HashSet(); // guard against duplicate type names - can happn in some versions + for (TypeRefComponent tr : ed.getType()) { + String tc = tr.getWorkingCode(); + if (!tl.contains(tc)) { + aliases.add(name.replace("[x]", Utilities.capitalize(tc))); + aliases.add(name+":"+name.replace("[x]", Utilities.capitalize(tc))); + tl.add(tc); + } + } + } else if (ed.hasSliceName()) { + aliases.add(name+":"+ed.getSliceName()); + // names.add(name); no good generating this? + } else { + aliases.add(name); + } + List generated = new ArrayList<>(); + for (String l : list) { + for (String a : aliases) { + generated.add(l+"."+a); + } + } + list.clear(); + list.addAll(generated); + } + ElementDefinition ed = stack.get(stack.size()-1); + + // now we have all the possible names, but some of them might be inappropriate if we've + // already generated a type slicer. On the other hand, if we've already done that, we're + // going to steal any type specific ones off it. + List removed = new ArrayList<>(); + for (String s : list) { + if (!allAnchors.containsKey(s)) { + allAnchors.put(s, ed); + } else if (s.endsWith("[x]")) { + // that belongs on the earlier element + removed.add(s); + } else { + // we delete it from the other + @SuppressWarnings("unchecked") + List other = (List) allAnchors.get(s).getUserData("dict.generator.anchors"); + other.remove(s); + allAnchors.put(s, ed); + } + } + list.removeAll(removed); + ed.setUserData("dict.generator.anchors", list); + } + + private void addToStack(List stack, ElementDefinition ec) { + while (!stack.isEmpty() && !isParent(stack.get(stack.size()-1), ec)) { + stack.remove(stack.size()-1); + } + stack.add(ec); + } + + private boolean isParent(ElementDefinition ed, ElementDefinition ec) { + return ec.getPath().startsWith(ed.getPath()+"."); + } + + private List makeAnchors(ElementDefinition ed, String anchorPrefix) { + List list = (List) ed.getUserData("dict.generator.anchors"); + List res = new ArrayList<>(); + res.add(anchorPrefix + ed.getId()); + for (String s : list) { + if (!s.equals(ed.getId())) { + res.add(anchorPrefix + s); + } + } + return res; + } + + + + private void link(XhtmlNode x, String id, String anchorPrefix) { + var ah = x.ah("#" + anchorPrefix + id); + ah.attribute("title", "link to here"); + ah.attribute("class", "self-link"); + var svg = ah.svg(); + svg.attribute("viewBox", "0 0 1792 1792"); + svg.attribute("width", "16"); + svg.attribute("height", "16"); + svg.attribute("class", "self-link"); + svg.path("M1520 1216q0-40-28-68l-208-208q-28-28-68-28-42 0-72 32 3 3 19 18.5t21.5 21.5 15 19 13 25.5 3.5 27.5q0 40-28 68t-68 28q-15 0-27.5-3.5t-25.5-13-19-15-21.5-21.5-18.5-19q-33 31-33 73 0 40 28 68l206 207q27 27 68 27 40 0 68-26l147-146q28-28 28-67zm-703-705q0-40-28-68l-206-207q-28-28-68-28-39 0-68 27l-147 146q-28 28-28 67 0 40 28 68l208 208q27 27 68 27 42 0 72-31-3-3-19-18.5t-21.5-21.5-15-19-13-25.5-3.5-27.5q0-40 28-68t68-28q15 0 27.5 3.5t25.5 13 19 15 21.5 21.5 18.5 19q33-31 33-73zm895 705q0 120-85 203l-147 146q-83 83-203 83-121 0-204-85l-206-207q-83-83-83-203 0-123 88-209l-88-88q-86 88-208 88-120 0-204-84l-208-208q-84-84-84-204t85-203l147-146q83-83 203-83 121 0 204 85l206 207q83 83 83 203 0 123-88 209l88 88q86-88 208-88 120 0 204 84l208 208q84 84 84 204z"); + } + + private boolean isProfiledExtension(ElementDefinition ec) { + return ec.getType().size() == 1 && "Extension".equals(ec.getType().get(0).getWorkingCode()) && ec.getType().get(0).hasProfile(); + } + + private ElementDefinition getExtensionValueDefinition(StructureDefinition extDefn) { + for (ElementDefinition ed : extDefn.getSnapshot().getElement()) { + if (ed.getPath().startsWith("Extension.value")) + return ed; + } + return null; + } + + public XhtmlNode compareMarkdown(String location, PrimitiveType md, PrimitiveType compare, int mode) throws FHIRException, IOException { + if (compare == null || mode == GEN_MODE_DIFF) { + if (md.hasValue()) { + String xhtml = hostMd.processMarkdown(location, md); + XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); + VersionComparisonAnnotation.renderDiv(md, x).add(new XhtmlParser().parseFragment(xhtml)); + return x; + } else { + return null; + } + } else if (areEqual(compare, md)) { + if (md.hasValue()) { + String xhtml = "
"+hostMd.processMarkdown(location, md)+"
"; + XhtmlNode div = new XhtmlParser().parseFragment(xhtml); + for (XhtmlNode n : div.getChildNodes()) { + if (n.getNodeType() == NodeType.Element) { + n.style(unchangedStyle()); + } + } + return div; + } else { + return null; + } + } else { + XhtmlNode ndiv = new XhtmlNode(NodeType.Element, "div"); + if (md.hasValue()) { + String xhtml = "
"+hostMd.processMarkdown(location, md)+"
"; + XhtmlNode div = new XhtmlParser().parseFragment(xhtml); + ndiv.copyAllContent(div); + } + if (compare.hasValue()) { + String xhtml = "
"+hostMd.processMarkdown(location, compare)+"
"; + XhtmlNode div = new XhtmlParser().parseFragment(xhtml); + for (XhtmlNode n : div.getChildNodes()) { + if (n.getNodeType() == NodeType.Element) { + n.style(removedStyle()); + } + } + ndiv.br(); + ndiv.copyAllContent(div); + } + return ndiv; + } + } + + private boolean areEqual(PrimitiveType compare, PrimitiveType md) { + if (compare == null && md == null) { + return true; + } else if (compare != null && md != null) { + String one = compare.getValueAsString(); + String two = md.getValueAsString(); + if (one == null && two == null) { + return true; + } else if (one != null && one.equals(two)) { + return true; + } + } + return false; + } + + public XhtmlNode compareString(String newStr, Base source, String nLink, String name, Base parent, String oldStr, String oLink, int mode) { + XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); + if (mode != GEN_MODE_KEY) { + if (newStr != null) { + VersionComparisonAnnotation.render(source, x).ah(nLink).tx(newStr); + } else if (VersionComparisonAnnotation.hasDeleted(parent, name)) { + PrimitiveType p = (PrimitiveType) VersionComparisonAnnotation.getDeletedItem(parent, name); + VersionComparisonAnnotation.render(p, x).tx(p.primitiveValue()); + } else { + return null; + } + } else if (oldStr==null || oldStr.isEmpty()) { + if (newStr==null || newStr.isEmpty()) { + return null; + } else { + VersionComparisonAnnotation.render(source, x).ah(nLink).tx(newStr); + } + } else if (oldStr!=null && !oldStr.isEmpty() && (newStr==null || newStr.isEmpty())) { + if (mode == GEN_MODE_DIFF) { + return null; + } else { + removed(x).ah(oLink).tx(oldStr); + } + } else if (oldStr.equals(newStr)) { + if (mode==GEN_MODE_DIFF) { + return null; + } else { + unchanged(x).ah(nLink).tx(newStr); + } + } else if (newStr.startsWith(oldStr)) { + unchanged(x).ah(oLink).tx(oldStr); + VersionComparisonAnnotation.render(source, x).ah(nLink).tx(newStr.substring(oldStr.length())); + } else { + // TODO: improve comparision in this fall-through case, by looking for matches in sub-paragraphs? + VersionComparisonAnnotation.render(source, x).ah(nLink).tx(newStr); + removed(x).ah(oLink).tx(oldStr); + } + return x; + } + + public boolean compareString(XhtmlNode x, String newStr, Base source, String nLink, String name, Base parent, String oldStr, String oLink, int mode) { + XhtmlNode x1 = compareString(newStr, source, nLink, name, parent, oldStr, oLink, mode); + if (x1 == null) { + return false; + } else { + x.getChildNodes().addAll(x1.getChildNodes()); + return true; + } + } + + public XhtmlNode unchanged(XhtmlNode x) { + return x.span(unchangedStyle()); + } + + private String unchangedStyle() { + return "color:DarkGray"; + } + + public XhtmlNode removed(XhtmlNode x) { + return x.span(removedStyle()); + } + + private String removedStyle() { + return "color:DarkGray;text-decoration:line-through"; + } + + private void generateElementInner(XhtmlNode tbl, StructureDefinition sd, ElementDefinition d, int mode, ElementDefinition value, ElementDefinition compare, ElementDefinition compareValue) throws FHIRException, IOException { + boolean root = !d.getPath().contains("."); + boolean slicedExtension = d.hasSliceName() && (d.getPath().endsWith(".extension") || d.getPath().endsWith(".modifierExtension")); +// int slicedExtensionMode = (mode == GEN_MODE_KEY) && slicedExtension ? GEN_MODE_SNAP : mode; // see ProfileUtilities.checkExtensionDoco / Task 3970 + if (d.hasSliceName()) { + tableRow(tbl, "SliceName", "profiling.html#slicing").tx(d.getSliceName()); + } + tableRow(tbl, "Definition", null, compareMarkdown(sd.getName(), d.getDefinitionElement(), (compare==null) || slicedExtension ? null : compare.getDefinitionElement(), mode)); + tableRow(tbl, "Short", null, compareString(d.hasShort() ? d.getShort() : null, d.getShortElement(), null, "short", d, compare!= null && compare.hasShortElement() ? compare.getShort() : null, null, mode)); + tableRow(tbl, "Note", null, businessIdWarning(sd.getName(), tail(d.getPath()))); + tableRow(tbl, "Control", "conformance-rules.html#conformance", describeCardinality(d, compare, mode)); + tableRow(tbl, "Binding", "terminologies.html", describeBinding(sd, d, d.getPath(), compare, mode)); + if (d.hasContentReference()) { + tableRow(tbl, "Type", null, "See " + d.getContentReference().substring(1)); + } else { + tableRow(tbl, "Type", "datatypes.html", describeTypes(d.getType(), false, compare, mode, value, compareValue, sd)); + } + if (d.hasExtension(ToolingExtensions.EXT_DEF_TYPE)) { + tableRow(tbl, "Default Type", "datatypes.html", ToolingExtensions.readStringExtension(d, ToolingExtensions.EXT_DEF_TYPE)); + } + if (d.hasExtension(ToolingExtensions.EXT_TYPE_SPEC)) { + tableRow(tbl, Utilities.pluralize("Type Specifier", d.getExtensionsByUrl(ToolingExtensions.EXT_TYPE_SPEC).size()), "datatypes.html", formatTypeSpecifiers(d)); + } + if (d.getPath().endsWith("[x]") && !d.prohibited()) { + tableRow(tbl, "[x] Note", null).ahWithText("See ", spec("formats.html#choice"), null, "Choice of Data Types", " for further information about how to use [x]"); + } + tableRow(tbl, "Is Modifier", "conformance-rules.html#ismodifier", displayBoolean(d.getIsModifier(), d.getIsModifierElement(), "isModifier", d, null, mode)); + if (d.getMustHaveValue()) { + tableRow(tbl, "Primitive Value", "elementdefinition.html#primitives", "This primitive type must have a value (the value must be present, and cannot be replaced by an extension)"); + } else if (d.hasValueAlternatives()) { + tableRow(tbl, "Primitive Value", "elementdefinition.html#primitives", renderCanonicalList(d.getValueAlternatives())); + } else if (hasPrimitiveTypes(d)) { + tableRow(tbl, "Primitive Value", "elementdefinition.html#primitives", "This primitive element may be present, or absent, or replaced by an extension"); + } + if (ToolingExtensions.hasAllowedUnits(d)) { + tableRow(tbl, "Allowed Units", "http://hl7.org/fhir/extensions/StructureDefinition-elementdefinition-allowedUnits.html", describeAllowedUnits(d)); + } + tableRow(tbl, "Must Support", "conformance-rules.html#mustSupport", displayBoolean(d.getMustSupport(), d.getMustSupportElement(), "mustSupport", d, compare==null ? null : compare.getMustSupportElement(), mode)); + if (d.getMustSupport()) { + if (hasMustSupportTypes(d.getType())) { + tableRow(tbl, "Must Support Types", "datatypes.html", describeTypes(d.getType(), true, compare, mode, null, null, sd)); + } else if (hasChoices(d.getType())) { + tableRow(tbl, "Must Support Types", "datatypes.html", "No must-support rules about the choice of types/profiles"); + } + } + if (root && sd.getKind() == StructureDefinitionKind.LOGICAL) { + tableRow(tbl, "Logical Model", null, ToolingExtensions.readBoolExtension(sd, ToolingExtensions.EXT_LOGICAL_TARGET) ? "This logical model can be the target of a reference" : "This logical model cannot be the target of a reference"); + } + + if (root && sd.hasExtension(ToolingExtensions.EXT_SD_IMPOSE_PROFILE)) { + tableRow(tbl, "Impose Profile", "http://hl7.org/fhir/extensions/StructureDefinition-structuredefinition-imposeProfile.html", + renderCanonicalListExt(sd.getExtensionsByUrl(ToolingExtensions.EXT_SD_IMPOSE_PROFILE))); + } + if (root && sd.hasExtension(ToolingExtensions.EXT_SD_COMPLIES_WITH_PROFILE)) { + tableRow(tbl, "Complies with Profile", "http://hl7.org/fhir/extensions/StructureDefinition-structuredefinition-compliesWithProfile.html", + renderCanonicalListExt(sd.getExtensionsByUrl(ToolingExtensions.EXT_SD_COMPLIES_WITH_PROFILE))); + } + tableRow(tbl, "Obligations", null, describeObligations(d, root, sd)); + + if (d.hasExtension(ToolingExtensions.EXT_EXTENSION_STYLE)) { + String es = d.getExtensionString(ToolingExtensions.EXT_EXTENSION_STYLE); + if ("named-elements".equals(es)) { + if (context.hasLink(KnownLinkType.JSON_NAMES)) { + tableRow(tbl, "Extension Style", context.getLink(KnownLinkType.JSON_NAMES), "This element can be extended by named JSON elements"); + } else { + tableRow(tbl, "Extension Style", ToolingExtensions.WEB_EXTENSION_STYLE, "This element can be extended by named JSON elements"); + } + } + } + + if (!d.getPath().contains(".") && ToolingExtensions.hasExtension(sd, ToolingExtensions.EXT_BINDING_STYLE)) { + tableRow(tbl, "Binding Style", ToolingExtensions.WEB_BINDING_STYLE, + "This type can be bound to a value set using the " + ToolingExtensions.readStringExtension(sd, ToolingExtensions.EXT_BINDING_STYLE)+" binding style"); + } + + if (d.hasExtension(ToolingExtensions.EXT_DATE_FORMAT)) { + tableRow(tbl, "Date Format", null, ToolingExtensions.readStringExtension(d, ToolingExtensions.EXT_DATE_FORMAT)); + } + String ide = ToolingExtensions.readStringExtension(d, ToolingExtensions.EXT_ID_EXPECTATION); + if (ide != null) { + if (ide.equals("optional")) { + tableRow(tbl, "ID Expectation", null, "Id may or not be present (this is the default for elements but not resources)"); + } else if (ide.equals("required")) { + tableRow(tbl, "ID Expectation", null, "Id is required to be present (this is the default for resources but not elements)"); + } else if (ide.equals("required")) { + tableRow(tbl, "ID Expectation", null, "An ID is not allowed in this context"); + } + } + // tooling extensions for formats + if (ToolingExtensions.hasExtensions(d, ToolingExtensions.EXT_JSON_EMPTY, ToolingExtensions.EXT_JSON_PROP_KEY, ToolingExtensions.EXT_JSON_NULLABLE, + ToolingExtensions.EXT_JSON_NAME, ToolingExtensions.EXT_JSON_PRIMITIVE_CHOICE)) { + tableRow(tbl, "JSON Format", null, describeJson(d)); + } + if (d.hasExtension(ToolingExtensions.EXT_XML_NAMESPACE) || sd.hasExtension(ToolingExtensions.EXT_XML_NAMESPACE) || d.hasExtension(ToolingExtensions.EXT_XML_NAME) || (root && sd.hasExtension(ToolingExtensions.EXT_XML_NO_ORDER)) || + d.hasRepresentation()) { + tableRow(tbl, "XML Format", null, describeXml(sd, d, root)); + } + + if (d.hasExtension(ToolingExtensions.EXT_IMPLIED_PREFIX)) { + tableRow(tbl, "String Format", null).codeWithText("When this element is read ", ToolingExtensions.readStringExtension(d, ToolingExtensions.EXT_IMPLIED_PREFIX), "is prefixed to the value before validation"); + } + + if (d.hasExtension(ToolingExtensions.EXT_STANDARDS_STATUS)) { + StandardsStatus ss = StandardsStatus.fromCode(d.getExtensionString(ToolingExtensions.EXT_STANDARDS_STATUS)); + // gc.addStyledText("Standards Status = "+ss.toDisplay(), ss.getAbbrev(), "black", ss.getColor(), baseSpecUrl()+, true); + StructureDefinition sdb = context.getContext().fetchResource(StructureDefinition.class, sd.getBaseDefinition()); + if (sdb != null) { + StandardsStatus base = determineStandardsStatus(sdb, (ElementDefinition) d.getUserData("derived.pointer")); + if (base != null) { + tableRow(tbl, "Standards Status", "versions.html#std-process", ss.toDisplay()+" (from "+base.toDisplay()+")"); + } else { + tableRow(tbl, "Standards Status", "versions.html#std-process", ss.toDisplay()); + } + } else { + tableRow(tbl, "Standards Status", "versions.html#std-process", ss.toDisplay()); + } + } + if (mode != GEN_MODE_DIFF && d.hasIsSummary()) { + tableRow(tbl, "Summary", "search.html#summary", Boolean.toString(d.getIsSummary())); + } + tableRow(tbl, "Requirements", null, compareMarkdown(sd.getName(), d.getRequirementsElement(), (compare==null) || slicedExtension ? null : compare.getRequirementsElement(), mode)); + tableRow(tbl, "Alternate Names", null, compareSimpleTypeLists(d.getAlias(), ((compare==null) || slicedExtension ? null : compare.getAlias()), mode)); + tableRow(tbl, "Comments", null, compareMarkdown(sd.getName(), d.getCommentElement(), (compare==null) || slicedExtension ? null : compare.getCommentElement(), mode)); + tableRow(tbl, "Max Length", null, compareString(d.hasMaxLength() ? toStr(d.getMaxLength()) : null, d.getMaxLengthElement(), null, "maxLength", d, compare!= null && compare.hasMaxLengthElement() ? toStr(compare.getMaxLength()) : null, null, mode)); + tableRow(tbl, "Default Value", null, encodeValue(d.getDefaultValue(), "defaultValue", d, compare==null ? null : compare.getDefaultValue(), mode)); + tableRow(tbl, "Meaning if Missing", null, d.getMeaningWhenMissing()); + tableRow(tbl, "Fixed Value", null, encodeValue(d.getFixed(), "fixed", d, compare==null ? null : compare.getFixed(), mode)); + tableRow(tbl, "Pattern Value", null, encodeValue(d.getPattern(), "pattern", d, compare==null ? null : compare.getPattern(), mode)); + tableRow(tbl, "Example", null, encodeValues(d.getExample())); + tableRow(tbl, "Invariants", null, invariants(d.getConstraint(), compare==null ? null : compare.getConstraint(), mode)); + tableRow(tbl, "LOINC Code", null, getMapping(sd, d, LOINC_MAPPING, compare, mode)); + tableRow(tbl, "SNOMED-CT Code", null, getMapping(sd, d, SNOMED_MAPPING, compare, mode)); + } + + private String spec(String name) { + return Utilities.pathURL(VersionUtilities.getSpecUrl(context.getWorker().getVersion()) , name); + } + + private XhtmlNode describeXml(StructureDefinition profile, ElementDefinition d, boolean root) { + XhtmlNode ret = new XhtmlNode(NodeType.Element, "div"); + for (PropertyRepresentation pr : PropertyRepresentation.values()) { + if (d.hasRepresentation(pr)) { + switch (pr) { + case CDATEXT: + ret.tx("This property is represented as CDA Text in the XML."); + break; + case TYPEATTR: + ret.codeWithText("The type of this property is determined using the ", "xsi:type", "attribute."); + break; + case XHTML: + ret.tx("This property is represented as XHTML Text in the XML."); + break; + case XMLATTR: + ret.tx("In the XML format, this property is represented as an attribute."); + break; + case XMLTEXT: + ret.tx("In the XML format, this property is represented as unadorned text."); + break; + default: + } + } + } + String name = ToolingExtensions.readStringExtension(d, ToolingExtensions.EXT_XML_NAMESPACE); + if (name == null && root) { + name = ToolingExtensions.readStringExtension(profile, ToolingExtensions.EXT_XML_NAMESPACE); + } + if (name != null) { + ret.codeWithText("In the XML format, this property has the namespace ", name, "."); + } + name = ToolingExtensions.readStringExtension(d, ToolingExtensions.EXT_XML_NAME); + if (name != null) { + ret.codeWithText("In the XML format, this property has the actual name", name, "."); + } + boolean no = root && ToolingExtensions.readBoolExtension(profile, ToolingExtensions.EXT_XML_NO_ORDER); + if (no) { + ret.tx("The children of this property can appear in any order in the XML."); + } + return ret; + } + + private XhtmlNode describeJson(ElementDefinition d) { + XhtmlNode ret = new XhtmlNode(NodeType.Element, "div"); + var ul = ret.ul(); + boolean list = ToolingExtensions.countExtensions(d, ToolingExtensions.EXT_JSON_EMPTY, ToolingExtensions.EXT_JSON_PROP_KEY, ToolingExtensions.EXT_JSON_NULLABLE, ToolingExtensions.EXT_JSON_NAME) > 1; + + String code = ToolingExtensions.readStringExtension(d, ToolingExtensions.EXT_JSON_EMPTY); + if (code != null) { + switch (code) { + case "present": + ul.li().tx("The JSON Array for this property is present even when there are no items in the instance (e.g. as an empty array)"); + break; + case "absent": + ul.li().tx("The JSON Array for this property is not present when there are no items in the instance (e.g. never as an empty array)"); + break; + case "either": + ul.li().tx("The JSON Array for this property may be present even when there are no items in the instance (e.g. may be present as an empty array)"); + break; + } + } + String jn = ToolingExtensions.readStringExtension(d, ToolingExtensions.EXT_JSON_NAME); + if (jn != null) { + if (d.getPath().contains(".")) { + ul.li().codeWithText("This property appears in JSON with the property name ", jn, null); + } else { + ul.li().codeWithText("This type can appear in JSON with the property name ", jn, " (in elements using named extensions)"); + } + } + code = ToolingExtensions.readStringExtension(d, ToolingExtensions.EXT_JSON_PROP_KEY); + if (code != null) { + ul.li().codeWithText("This repeating object is represented as a single JSON object with named properties. The name of the property (key) is the value of the ", code, " child"); + } + if (ToolingExtensions.readBoolExtension(d, ToolingExtensions.EXT_JSON_NULLABLE)) { + ul.li().tx("This object can be represented as null in the JSON structure (which counts as 'present' for cardinality purposes)"); + } + if (ToolingExtensions.readBoolExtension(d, ToolingExtensions.EXT_JSON_PRIMITIVE_CHOICE)) { + ul.li().tx("The type of this element is inferred from the JSON type in the instance"); + } + + switch (ul.getChildNodes().size()) { + case 0: return null; + case 1: return ul.getChildNodes().get(0); + default: return ret; + } + } + + private XhtmlNode describeObligations(ElementDefinition d, boolean root, StructureDefinition sdx) throws IOException { + XhtmlNode ret = new XhtmlNode(NodeType.Element, "div"); + ObligationsRenderer obr = new ObligationsRenderer(corePath, sdx, d.getPath(), context, hostMd, this); + obr.seeObligations(d.getExtensionsByUrl(ToolingExtensions.EXT_OBLIGATION_CORE, ToolingExtensions.EXT_OBLIGATION_TOOLS)); + obr.seeRootObligations(d.getId(), sdx.getExtensionsByUrl(ToolingExtensions.EXT_OBLIGATION_CORE, ToolingExtensions.EXT_OBLIGATION_TOOLS)); + if (obr.hasObligations() || (root && (sdx.hasExtension(ToolingExtensions.EXT_OBLIGATION_PROFILE_FLAG) || sdx.hasExtension(ToolingExtensions.EXT_OBLIGATION_INHERITS)))) { + XhtmlNode ul = ret.ul(); + if (root) { + if (sdx.hasExtension(ToolingExtensions.EXT_OBLIGATION_PROFILE_FLAG)) { + ul.li().tx("This is an obligation profile that only contains obligations and additional bindings"); + } + for (Extension ext : sdx.getExtensionsByUrl(ToolingExtensions.EXT_OBLIGATION_INHERITS)) { + String iu = ext.getValue().primitiveValue(); + XhtmlNode bb = ul.li(); + bb.tx("This profile picks up obligations and additional bindings from "); + StructureDefinition sd = context.getContext().fetchResource(StructureDefinition.class, iu); + if (sd == null) { + bb.code().tx(iu); + } else if (sd.hasWebPath()) { + bb.ah(sd.getWebPath()).tx(sd.present()); + } else { + bb.ah(iu).tx(sd.present()); + } + } + if (ul.isEmpty()) { + ret.remove(ul); + } + } + if (obr.hasObligations()) { + XhtmlNode tbl = ret.table("grid"); + obr.renderTable(tbl.getChildNodes(), true); + if (tbl.isEmpty()) { + ret.remove(tbl); + } + } + return ret.hasChildren() ? ret : null; + } else { + return null; + } + } + + private XhtmlNode describeAllowedUnits(ElementDefinition d) { + XhtmlNode ret = new XhtmlNode(NodeType.Element, "div"); + DataType au = ToolingExtensions.getAllowedUnits(d); + if (au instanceof CanonicalType) { + String url = ((CanonicalType) au).asStringValue(); + ValueSet vs = context.getContext().fetchResource(ValueSet.class, url); + ret.tx("Value set "); + genCT(ret, url, vs); + return ret; + } else if (au instanceof CodeableConcept) { + CodeableConcept cc = (CodeableConcept) au; + if (cc.getCoding().size() != 1) { + ret.tx("One of:"); + } + ret.tx(summarise(cc)); + return ret; + } + return null; + } + + private void genCT(XhtmlNode x, String url, CanonicalResource cr) { + if (cr == null) { + x.code().tx(url); + } else if (!cr.hasWebPath()) { + x.ah(url).tx(cr.present()); + } else { + x.ah(cr.getWebPath()).tx(cr.present()); + } + } + + private boolean hasPrimitiveTypes(ElementDefinition d) { + for (TypeRefComponent tr : d.getType()) { + if (isPrimitive(tr.getCode())) { + return true; + } + } + return false; + } + + + private XhtmlNode renderCanonicalListExt(List list) { + List clist = new ArrayList<>(); + for (Extension ext : list) { + if (ext.hasValueCanonicalType()) { + clist.add(ext.getValueCanonicalType()); + } + } + return renderCanonicalList(clist); + } + + private XhtmlNode renderCanonicalList(List list) { + XhtmlNode ret = new XhtmlNode(NodeType.Element, "div"); + ret.tx("This primitive type may be present, or absent, or replaced by one of the following extensions: "); + var ul = ret.ul(); + for (CanonicalType ct : list) { + CanonicalResource cr = (CanonicalResource) context.getContext().fetchResource(Resource.class, ct.getValue()); + genCT(ul.li(), ct.getValue(), cr); + } + return ret; + } + + private StandardsStatus determineStandardsStatus(StructureDefinition sd, ElementDefinition ed) { + if (ed != null && ed.hasExtension(ToolingExtensions.EXT_STANDARDS_STATUS)) { + return StandardsStatus.fromCode(ed.getExtensionString(ToolingExtensions.EXT_STANDARDS_STATUS)); + } + while (sd != null) { + if (sd.hasExtension(ToolingExtensions.EXT_STANDARDS_STATUS)) { + return ToolingExtensions.getStandardsStatus(sd); + } + sd = context.getContext().fetchResource(StructureDefinition.class, sd.getBaseDefinition()); + } + return null; + } + + private boolean hasChoices(List types) { + for (TypeRefComponent type : types) { + if (type.getProfile().size() > 1 || type.getTargetProfile().size() > 1) { + return true; + } + } + return types.size() > 1; + } + + private String sliceOrderString(ElementDefinitionSlicingComponent slicing) { + if (slicing.getOrdered()) + return "ordered"; + else + return "unordered"; + } + + private void generateSlicing(XhtmlNode tbl, StructureDefinition profile, ElementDefinition ed, ElementDefinitionSlicingComponent slicing, ElementDefinition compare, int mode) throws IOException { + XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); + + x.codeWithText("This element introduces a set of slices on ", ed.getPath(), ". The slices are "); + String newOrdered = sliceOrderString(slicing); + String oldOrdered = (compare==null || !compare.hasSlicing()) ? null : sliceOrderString(compare.getSlicing()); + compareString(x, newOrdered, slicing.getOrderedElement(), null, null, null, oldOrdered, null, mode); + x.tx(" and "); + compareString(x, slicing.hasRules() ? slicing.getRules().getDisplay() : null, slicing.getRulesElement(), null, "rules", slicing, compare!=null && compare.hasSlicing() && compare.getSlicing().hasRules() ? compare.getSlicing().getRules().getDisplay() : null, null, mode); + + if (slicing.hasDiscriminator()) { + x.tx(", and can be differentiated using the following discriminators: "); + StatusList list = new StatusList<>(); + for (ElementDefinitionSlicingDiscriminatorComponent d : slicing.getDiscriminator()) { + list.add(new DiscriminatorWithStatus(d)); + } + if (compare != null) { + for (ElementDefinitionSlicingDiscriminatorComponent d : slicing.getDiscriminator()) { + list.merge(new DiscriminatorWithStatus(d)); + } + } + x.tx(", and can be differentiated using the following discriminators: "); + var ul = x.ul(); + for (DiscriminatorWithStatus rc : list) { + rc.render(x.li()); + } + } else { + x.tx(", and defines no discriminators to differentiate the slices"); + } + tableRow(tbl, "Slicing", "profiling.html#slicing", x); + } + + private XhtmlNode tableRow(XhtmlNode x, String name, String defRef) throws IOException { + var tr = x.tr(); + addFirstCell(name, defRef, tr); + return tr.td(); + } + + + private void tableRow(XhtmlNode x, String name, String defRef, XhtmlNode possibleTd) throws IOException { + if (possibleTd != null && !possibleTd.isEmpty()) { + var tr = x.tr(); + addFirstCell(name, defRef, tr); + tr.td().copyAllContent(possibleTd); + } + } + + private void tableRow(XhtmlNode x, String name, String defRef, String text) throws IOException { + if (!Utilities.noString(text)) { + var tr = x.tr(); + addFirstCell(name, defRef, tr); + tr.td().tx(text); + } + } + + private void addFirstCell(String name, String defRef, XhtmlNode tr) { + var td = tr.td(); + if (name.length() <= 16) { + td.style("white-space: nowrap"); + } + if (defRef == null) { + td.tx(name); + } else if (Utilities.isAbsoluteUrl(defRef)) { + td.ah(defRef).tx(name); + } else { + td.ah(corePath+defRef).tx(name); + } + } + + private String head(String path) { + if (path.contains(".")) + return path.substring(0, path.indexOf(".")); + else + return path; + } + private String nottail(String path) { + if (path.contains(".")) + return path.substring(0, path.lastIndexOf(".")); + else + return path; + } + + private XhtmlNode businessIdWarning(String resource, String name) { + if (name.equals("identifier")) { + XhtmlNode ret = new XhtmlNode(NodeType.Element, "div"); + ret.tx("This is a business identifier, not a resource identifier (see "); + ret.ah(corePath + "resource.html#identifiers").tx("discussion"); + ret.tx(")"); + return ret; + } + if (name.equals("version")) {// && !resource.equals("Device")) + XhtmlNode ret = new XhtmlNode(NodeType.Element, "div"); + ret.tx("This is a business versionId, not a resource version id (see "); + ret.ah(corePath + "resource.html#versions").tx("discussion"); + ret.tx(")"); + return ret; + } + return null; + } + + private XhtmlNode describeCardinality(ElementDefinition d, ElementDefinition compare, int mode) { + XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); + if (compare==null || mode==GEN_MODE_DIFF) { + if (!d.hasMax() && !d.hasMin()) + return null; + else if (d.getMax() == null) { + VersionComparisonAnnotation.render(d.getMinElement(), x).tx(toStr(d.getMin())); + x.tx("..?"); + } else { + VersionComparisonAnnotation.render(d.getMinElement(), x).tx(toStr(d.getMin())); + x.tx( ".."); + VersionComparisonAnnotation.render(d.getMaxElement(), x).tx( d.getMax()); + } + } else { + if (!(mode==GEN_MODE_DIFF && (d.getMin()==compare.getMin() || d.getMin()==0))) { + compareString(x, toStr(d.getMin()), d.getMinElement(), null, "min", d, toStr(compare.getMin()), null, mode); + } + x.tx(".."); + if (!(mode==GEN_MODE_DIFF && (d.getMax().equals(compare.getMax()) || "1".equals(d.getMax())))) { + compareString(x, d.getMax(), d.getMaxElement(), null, "max", d, compare.getMax(), null, mode); + } + } + XhtmlNode t = compareSimpleTypeLists(d.getCondition(), compare == null ? null : compare.getCondition(), mode); + if (t != null) { + x.br(); + x.tx("This element is affected by the following invariants: "); + x.copyAllContent(t); + } + return x; + } + + private boolean hasMustSupportTypes(List types) { + for (TypeRefComponent tr : types) { + if (isMustSupport(tr)) { + return true; + } + } + return false; + } + + private XhtmlNode describeTypes(List types, boolean mustSupportOnly, ElementDefinition compare, int mode, ElementDefinition value, ElementDefinition compareValue, StructureDefinition sd) throws FHIRException { + if (types.isEmpty()) + return null; + + List compareTypes = compare==null ? new ArrayList<>() : compare.getType(); + XhtmlNode ret = new XhtmlNode(NodeType.Element, "div"); + if ((!mustSupportOnly && types.size() == 1 && compareTypes.size() <=1) || (mustSupportOnly && mustSupportCount(types) == 1)) { + if (!mustSupportOnly || isMustSupport(types.get(0))) { + describeType(ret, types.get(0), mustSupportOnly, compareTypes.size()==0 ? null : compareTypes.get(0), mode, sd); + } + } else { + boolean first = true; + ret.tx("Choice of: "); + Map map = new HashMap(); + for (TypeRefComponent t : compareTypes) { + map.put(t.getCode(), t); + } + for (TypeRefComponent t : types) { + TypeRefComponent compareType = map.get(t.getCode()); + if (compareType!=null) + map.remove(t.getCode()); + if (!mustSupportOnly || isMustSupport(t)) { + if (first) { + first = false; + } else { + ret.tx(", "); + } + describeType(ret, t, mustSupportOnly, compareType, mode, sd); + } + } + for (TypeRefComponent t : map.values()) { + ret.tx(", "); + describeType(removed(ret), t, mustSupportOnly, null, mode, sd); + } + } + if (value != null) { + XhtmlNode xt = processSecondary(mode, value, compareValue, mode, sd); + if (xt != null) { + ret.copyAllContent(xt); + } + } + return ret; + } + + private XhtmlNode processSecondary(int mode, ElementDefinition value, ElementDefinition compareValue, int compMode, StructureDefinition sd) throws FHIRException { + switch (mode) { + case 1: + return null; + case 2: + XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); + x.tx(" (Complex Extension)"); + return x; + case 3: + x = new XhtmlNode(NodeType.Element, "div"); + x.tx(" (Extension Type: "); + x.copyAllContent(describeTypes(value.getType(), false, compareValue, compMode, null, null, sd)); + x.tx(")"); + return x; + default: + return null; + } + } + + + private int mustSupportCount(List types) { + int c = 0; + for (TypeRefComponent tr : types) { + if (isMustSupport(tr)) { + c++; + } + } + return c; + } + + + private void describeType(XhtmlNode x, TypeRefComponent t, boolean mustSupportOnly, TypeRefComponent compare, int mode, StructureDefinition sd) throws FHIRException { + if (t.getWorkingCode() == null) { + return; + } + if (t.getWorkingCode().startsWith("=")) { + return; + } + + boolean ts = false; + if (t.getWorkingCode().startsWith("xs:")) { + ts = compareString(x, t.getWorkingCode(), t.getCodeElement(), null, "code", t, compare==null ? null : compare.getWorkingCode(), null, mode); + } else { + ts = compareString(x, t.getWorkingCode(), t.getCodeElement(), getTypeLink(t, sd), "code", t, compare==null ? null : compare.getWorkingCode(), compare==null ? null : getTypeLink(compare, sd), mode); + } + + if ((!mustSupportOnly && (t.hasProfile() || (compare!=null && compare.hasProfile()))) || isMustSupport(t.getProfile())) { + StatusList profiles = analyseProfiles(t.getProfile(), compare == null ? null : compare.getProfile(), mustSupportOnly, mode); + if (profiles.size() > 0) { + if (!ts) { + getTypeLink(unchanged(x), t, sd); + ts = true; + } + x.tx("("); + boolean first = true; + for (ResolvedCanonical rc : profiles) { + if (first) first = false; else x.tx(", "); + rc.render(x); + } + x.tx(")"); + } + } + + if ((!mustSupportOnly && (t.hasTargetProfile() || (compare!=null && compare.hasTargetProfile()))) || isMustSupport(t.getTargetProfile())) { + List profiles = analyseProfiles(t.getTargetProfile(), compare == null ? null : compare.getTargetProfile(), mustSupportOnly, mode); + if (profiles.size() > 0) { + if (!ts) { + getTypeLink(unchanged(x), t, sd); + } + x.tx("("); // todo: double use of "(" is problematic + boolean first = true; + for (ResolvedCanonical rc : profiles) { + if (first) first = false; else x.tx(", "); + rc.render(x); + } + x.tx(")"); + } + + if (!t.getAggregation().isEmpty() || (compare!=null && !compare.getAggregation().isEmpty())) { + + for (Enumeration a :t.getAggregation()) { + a.setUserData("render.link", corePath + "codesystem-resource-aggregation-mode.html#content"); + } + if (compare!=null) { + for (Enumeration a : compare.getAggregation()) { + a.setUserData("render.link", corePath + "codesystem-resource-aggregation-mode.html#content"); + } + } + var xt = compareSimpleTypeLists(t.getAggregation(), compare == null ? null : compare.getAggregation(), mode); + if (xt != null) { + x.copyAllContent(xt); + } + } + } + } + + private StatusList analyseProfiles(List newProfiles, List oldProfiles, boolean mustSupportOnly, int mode) { + StatusList profiles = new StatusList(); + for (CanonicalType pt : newProfiles) { + ResolvedCanonical rc = fetchProfile(pt, mustSupportOnly); + profiles.add(rc); + } + if (oldProfiles!=null && mode != GEN_MODE_DIFF) { + for (CanonicalType pt : oldProfiles) { + profiles.merge(fetchProfile(pt, mustSupportOnly)); + } + } + return profiles; + } + + private ResolvedCanonical fetchProfile(CanonicalType pt, boolean mustSupportOnly) { + if (!pt.hasValue()) { + return null; + } + if (!mustSupportOnly || isMustSupport(pt)) { + StructureDefinition p = context.getContext().fetchResource(StructureDefinition.class, pt.getValue()); + return new ResolvedCanonical(pt.getValue(), p); + } else { + return null; + } + } +// +// private String getTypeProfile(CanonicalType pt, boolean mustSupportOnly) { +// StringBuilder b = new StringBuilder(); +// if (!mustSupportOnly || isMustSupport(pt)) { +// StructureDefinition p = context.getContext().fetchResource(StructureDefinition.class, pt.getValue()); +// if (p == null) +// b.append(pt.getValue()); +// else { +// String pth = p.getWebPath(); +// b.append(""); +// b.append(p.getName()); +// b.append(""); +// } +// } +// return b.toString(); +// } + + private void getTypeLink(XhtmlNode x, TypeRefComponent t, StructureDefinition sd) { + String s = context.getPkp().getLinkFor(sd.getWebPath(), t.getWorkingCode()); + if (s != null) { + x.ah(s).tx(t.getWorkingCode()); + } else { + x.code().tx(t.getWorkingCode()); + } + } + + + private String getTypeLink(TypeRefComponent t, StructureDefinition sd) { + String s = context.getPkp().getLinkFor(sd.getWebPath(), t.getWorkingCode()); + return s; + } + + private XhtmlNode displayBoolean(boolean value, BooleanType source, String name, Base parent, BooleanType compare, int mode) { + String newValue = value ? "true" : source.hasValue() ? "false" : null; + String oldValue = compare==null || compare.getValue()==null ? null : (compare.getValue()!=true ? null : "true"); + return compareString(newValue, source, null, name, parent, oldValue, null, mode); + } + + + private XhtmlNode invariants(List originalList, List compareList, int mode) { + StatusList list = new StatusList<>(); + for (ElementDefinitionConstraintComponent v : originalList) { + if (!v.isEmpty()) { + list.add(new InvariantWithStatus(v)); + } + } + if (compareList != null && mode != GEN_MODE_DIFF) { + for (ElementDefinitionConstraintComponent v : compareList) { + list.merge(new InvariantWithStatus(v)); + } + } + if (list.size() == 0) { + return null; + } + XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); + boolean first = true; + for (InvariantWithStatus t : list) { + if (first) first = false; else x.br(); + t.render(x); + } + return x; + } + + private XhtmlNode describeBinding(StructureDefinition sd, ElementDefinition d, String path, ElementDefinition compare, int mode) throws FHIRException, IOException { + if (!d.hasBinding()) + return null; + else { + ElementDefinitionBindingComponent binding = d.getBinding(); + ElementDefinitionBindingComponent compBinding = compare == null ? null : compare.getBinding(); + XhtmlNode bindingDesc = null; + if (binding.hasDescription()) { + StringType newBinding = PublicationHacker.fixBindingDescriptions(context.getContext(), binding.getDescriptionElement()); + if (mode == GEN_MODE_SNAP || mode == GEN_MODE_MS) { + bindingDesc = new XhtmlNode(NodeType.Element, "div"); + bindingDesc.add(new XhtmlParser().parseFragment(hostMd.processMarkdown("Binding.description", newBinding))); + } else { + StringType oldBinding = compBinding != null && compBinding.hasDescription() ? PublicationHacker.fixBindingDescriptions(context.getContext(), compBinding.getDescriptionElement()) : null; + bindingDesc = compareMarkdown("Binding.description", newBinding, oldBinding, mode); + } + } + if (!binding.hasValueSet()) + return bindingDesc; + + XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); + var nsp = x.span(); + renderBinding(nsp, binding, path, sd); + if (compBinding!=null ) { + var osp = x.span(); + renderBinding(osp, compBinding, path, sd); + if (osp.allText().equals(nsp.allText())) { + nsp.style(unchangedStyle()); + x.remove(osp); + } else { + osp.style(removedStyle()); + } + } + if (bindingDesc != null) { + if (isSimpleContent(bindingDesc)) { + x.tx(": "); + x.copyAllContent(bindingDesc.getChildNodes().get(0)); + } else { + x.br(); + x.copyAllContent(bindingDesc); + } + } + + AdditionalBindingsRenderer abr = new AdditionalBindingsRenderer(context.getPkp(), corePath, sd, d.getPath(), context, hostMd, this); + + if (binding.hasExtension(ToolingExtensions.EXT_MAX_VALUESET)) { + abr.seeMaxBinding(ToolingExtensions.getExtension(binding, ToolingExtensions.EXT_MAX_VALUESET), compBinding==null ? null : ToolingExtensions.getExtension(compBinding, ToolingExtensions.EXT_MAX_VALUESET), mode!=GEN_MODE_SNAP && mode!=GEN_MODE_MS); + } + if (binding.hasExtension(ToolingExtensions.EXT_MIN_VALUESET)) { + abr.seeMinBinding(ToolingExtensions.getExtension(binding, ToolingExtensions.EXT_MIN_VALUESET), compBinding==null ? null : ToolingExtensions.getExtension(compBinding, ToolingExtensions.EXT_MIN_VALUESET), mode!=GEN_MODE_SNAP && mode!=GEN_MODE_MS); + } + if (binding.hasExtension(ToolingExtensions.EXT_BINDING_ADDITIONAL)) { + abr.seeAdditionalBindings(binding.getExtensionsByUrl(ToolingExtensions.EXT_BINDING_ADDITIONAL), compBinding==null ? null : compBinding.getExtensionsByUrl(ToolingExtensions.EXT_BINDING_ADDITIONAL), mode!=GEN_MODE_SNAP && mode!=GEN_MODE_MS); + } + + if (abr.hasBindings()) { + var tbl = x.table("grid"); + abr.render(tbl.getChildNodes(), true); + } + return x; + } + } + + + private boolean isSimpleContent(XhtmlNode bindingDesc) { + return bindingDesc.getChildNodes().size() == 1 && bindingDesc.getChildNodes().get(0).isPara(); + } + + private void renderBinding(XhtmlNode span, ElementDefinitionBindingComponent binding, String path, StructureDefinition sd) { + BindingResolution br = context.getPkp().resolveBinding(sd, binding, path); + span.tx(conf(binding)); + if (br.url == null) { + span.code().tx(br.display); + } else { + span.ah(br.url).tx(br.display); + } + span.tx(confTail(binding)); + + } + + private String stripPara(String s) { + if (s.startsWith("

")) { + s = s.substring(3); + } + if (s.trim().endsWith("

")) { + s = s.substring(0, s.lastIndexOf("

")-1) + s.substring(s.lastIndexOf("

") +4); + } + return s; + } + + private String confTail(ElementDefinitionBindingComponent def) { + if (def.getStrength() == BindingStrength.EXTENSIBLE) + return "; other codes may be used where these codes are not suitable"; + else + return ""; + } + + private String conf(ElementDefinitionBindingComponent def) { + if (def.getStrength() == null) { + return "For codes, see "; + } + switch (def.getStrength()) { + case EXAMPLE: + return "For example codes, see "; + case PREFERRED: + return "The codes SHOULD be taken from "; + case EXTENSIBLE: + return "The codes SHALL be taken from "; + case REQUIRED: + return "The codes SHALL be taken from "; + default: + return "?sd-conf?"; + } + } + + private String encodeValues(List examples) throws FHIRException, IOException { + StringBuilder b = new StringBuilder(); + boolean first = false; + for (ElementDefinitionExampleComponent ex : examples) { + if (first) + first = false; + else + b.append("
"); + b.append("" + Utilities.escapeXml(ex.getLabel()) + ":" + encodeValue(ex.getValue()) + "\r\n"); + } + return b.toString(); + + } + + private XhtmlNode encodeValue(DataType value, String name, Base parent, DataType compare, int mode) throws FHIRException, IOException { + String oldValue = encodeValue(compare); + String newValue = encodeValue(value); + return compareString(newValue, value, null, name, parent, oldValue, null, mode); + } + + private String encodeValue(DataType value) throws FHIRException, IOException { + if (value == null || value.isEmpty()) + return null; + if (value instanceof PrimitiveType) + return Utilities.escapeXml(((PrimitiveType) value).asStringValue()); + + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + XmlParser parser = new XmlParser(); + parser.setOutputStyle(OutputStyle.PRETTY); + parser.compose(bs, null, value); + String[] lines = bs.toString().split("\\r?\\n"); + StringBuilder b = new StringBuilder(); + for (String s : lines) { + if (!Utilities.noString(s) && !s.startsWith(""); + } + } + return b.toString(); + + } + + private XhtmlNode getMapping(StructureDefinition profile, ElementDefinition d, String uri, ElementDefinition compare, int mode) { + String id = null; + for (StructureDefinitionMappingComponent m : profile.getMapping()) { + if (m.hasUri() && m.getUri().equals(uri)) + id = m.getIdentity(); + } + if (id == null) + return null; + String newMap = null; + for (ElementDefinitionMappingComponent m : d.getMapping()) { + if (m.getIdentity().equals(id)) { + newMap = m.getMap(); + break; + } + } + if (compare==null) + return new XhtmlNode(NodeType.Element, "div").tx(newMap); + String oldMap = null; + for (ElementDefinitionMappingComponent m : compare.getMapping()) { + if (m.getIdentity().equals(id)) { + oldMap = m.getMap(); + break; + } + } + + return compareString(Utilities.escapeXml(newMap), null, null, "mapping", d, Utilities.escapeXml(oldMap), null, mode); + } + + private XhtmlNode compareSimpleTypeLists(List original, List compare, int mode) { + return compareSimpleTypeLists(original, compare, mode, ", "); + } + + + private XhtmlNode compareSimpleTypeLists(List originalList, List compareList, int mode, String separator) { + StatusList list = new StatusList<>(); + for (PrimitiveType v : originalList) { + if (!v.isEmpty()) { + list.add(new ValueWithStatus(v)); + } + } + if (compareList != null && mode != GEN_MODE_DIFF) { + for (PrimitiveType v : compareList) { + list.merge(new ValueWithStatus(v)); + } + } + if (list.size() == 0) { + return null; + } + XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); + boolean first = true; + for (ValueWithStatus t : list) { + if (first) first = false; else x.tx(separator); + t.render(x); + } + return x; + } + + + private String summarise(CodeableConcept cc) throws FHIRException { + if (cc.getCoding().size() == 1 && cc.getText() == null) { + return summarise(cc.getCoding().get(0)); + } else if (cc.hasText()) { + return "\"" + cc.getText() + "\""; + } else if (cc.getCoding().size() > 0) { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (Coding c : cc.getCoding()) { + b.append(summarise(c)); + } + return b.toString(); + } else { + throw new FHIRException("Error describing concept - not done yet (no codings, no text)"); + } + } + + private String summarise(Coding coding) throws FHIRException { + if ("http://snomed.info/sct".equals(coding.getSystem())) + return "" + translate("sd.summary", "SNOMED CT code") + " " + coding.getCode() + (!coding.hasDisplay() ? "" : "(\"" + gt(coding.getDisplayElement()) + "\")"); + if ("http://loinc.org".equals(coding.getSystem())) + return "" + translate("sd.summary", "LOINC code") + " " + coding.getCode() + (!coding.hasDisplay() ? "" : "(\"" + gt(coding.getDisplayElement()) + "\")"); + if ("http://unitsofmeasure.org/".equals(coding.getSystem())) + return " (" + translate("sd.summary", "UCUM") + ": " + coding.getCode() + ")"; + CodeSystem cs = context.getContext().fetchCodeSystem(coding.getSystem()); + if (cs == null) + return "" + coding.getCode() + "" + (!coding.hasDisplay() ? "" : "(\"" + gt(coding.getDisplayElement()) + "\")"); + else + return "" + coding.getCode() + "" + (!coding.hasDisplay() ? "" : "(\"" + gt(coding.getDisplayElement()) + "\")"); + } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java index 68a4d8f1c..651817927 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ValueSetRenderer.java @@ -30,6 +30,7 @@ import org.hl7.fhir.r5.model.Extension; import org.hl7.fhir.r5.model.ExtensionHelper; import org.hl7.fhir.r5.model.PrimitiveType; import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.r5.model.StringType; import org.hl7.fhir.r5.model.UriType; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.model.ValueSet.ConceptPropertyComponent; @@ -57,6 +58,7 @@ import org.hl7.fhir.utilities.xhtml.XhtmlNode; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; +import com.ibm.icu.impl.locale.StringTokenIterator; public class ValueSetRenderer extends TerminologyRenderer { @@ -1175,50 +1177,10 @@ public class ValueSetRenderer extends TerminologyRenderer { hasExtensions = true; addMapHeaders(addTableHeaderRowStandard(t, false, true, hasDefinition, hasComments, false, false, null, langs, designations, doDesignations), maps); for (ConceptReferenceComponent c : inc.getConcept()) { - XhtmlNode tr = t.tr(); - XhtmlNode td = tr.td(); - ConceptDefinitionComponent cc = definitions == null ? null : definitions.get(c.getCode()); - addCodeToTable(false, inc.getSystem(), c.getCode(), c.hasDisplay()? c.getDisplay() : cc != null ? cc.getDisplay() : "", td); - - td = tr.td(); - if (!Utilities.noString(c.getDisplay())) - td.addText(c.getDisplay()); - else if (cc != null && !Utilities.noString(cc.getDisplay())) - td.addText(cc.getDisplay()); - - if (hasDefinition) { - td = tr.td(); - if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION)) { - smartAddText(td, ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_DEFINITION)); - } else if (cc != null && !Utilities.noString(cc.getDefinition())) { - smartAddText(td, cc.getDefinition()); - } - } - if (hasComments) { - td = tr.td(); - if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT)) { - smartAddText(td, "Note: "+ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_VS_COMMENT)); - } - } - if (doDesignations) { - addDesignationsToRow(c, designations, tr); - addLangaugesToRow(c, langs, tr); - } - for (UsedConceptMap m : maps) { - td = tr.td(); - List mappings = findMappingsForCode(c.getCode(), m.getMap()); - boolean first = true; - for (TargetElementComponentWrapper mapping : mappings) { - if (!first) - td.br(); - first = false; - XhtmlNode span = td.span(null, mapping.comp.getRelationship().toString()); - span.addText(getCharForRelationship(mapping.comp)); - addRefToCode(td, mapping.group.getTarget(), m.getLink(), mapping.comp.getCode()); - if (!Utilities.noString(mapping.comp.getComment())) - td.i().tx("("+mapping.comp.getComment()+")"); - } - } + renderConcept(inc, langs, doDesignations, maps, designations, definitions, t, hasComments, hasDefinition, c); + } + for (Base b : VersionComparisonAnnotation.getDeleted(inc, "concept" )) { + renderConcept(inc, langs, doDesignations, maps, designations, definitions, t, hasComments, hasDefinition, (ConceptReferenceComponent) b); } } if (inc.getFilter().size() > 0) { @@ -1306,6 +1268,58 @@ public class ValueSetRenderer extends TerminologyRenderer { return hasExtensions; } + private void renderConcept(ConceptSetComponent inc, List langs, boolean doDesignations, + List maps, Map designations, Map definitions, + XhtmlNode t, boolean hasComments, boolean hasDefinition, ConceptReferenceComponent c) { + XhtmlNode tr = t.tr(); + XhtmlNode td = VersionComparisonAnnotation.renderRow(c, t, tr); + ConceptDefinitionComponent cc = definitions == null ? null : definitions.get(c.getCode()); + addCodeToTable(false, inc.getSystem(), c.getCode(), c.hasDisplay()? c.getDisplay() : cc != null ? cc.getDisplay() : "", td); + + td = tr.td(); + if (!Utilities.noString(c.getDisplay())) + VersionComparisonAnnotation.render(c.getDisplayElement(), td).addText(c.getDisplay()); + else if (VersionComparisonAnnotation.hasDeleted(c, "display")) { + StringType d = (StringType) VersionComparisonAnnotation.getDeletedItem(c, "display"); + VersionComparisonAnnotation.render(d, td).addText(d.primitiveValue()); + } else if (cc != null && !Utilities.noString(cc.getDisplay())) + td.style("color: #cccccc").addText(cc.getDisplay()); + + if (hasDefinition) { + td = tr.td(); + if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_DEFINITION)) { + smartAddText(td, ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_DEFINITION)); + } else if (cc != null && !Utilities.noString(cc.getDefinition())) { + smartAddText(td, cc.getDefinition()); + } + } + if (hasComments) { + td = tr.td(); + if (ExtensionHelper.hasExtension(c, ToolingExtensions.EXT_VS_COMMENT)) { + smartAddText(td, "Note: "+ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_VS_COMMENT)); + } + } + if (doDesignations) { + addDesignationsToRow(c, designations, tr); + addLangaugesToRow(c, langs, tr); + } + for (UsedConceptMap m : maps) { + td = tr.td(); + List mappings = findMappingsForCode(c.getCode(), m.getMap()); + boolean first = true; + for (TargetElementComponentWrapper mapping : mappings) { + if (!first) + td.br(); + first = false; + XhtmlNode span = td.span(null, mapping.comp.getRelationship().toString()); + span.addText(getCharForRelationship(mapping.comp)); + addRefToCode(td, mapping.group.getTarget(), m.getLink(), mapping.comp.getCode()); + if (!Utilities.noString(mapping.comp.getComment())) + td.i().tx("("+mapping.comp.getComment()+")"); + } + } + } + public void addDesignationsToRow(ConceptReferenceComponent c, Map designations, XhtmlNode tr) { for (String url : designations.keySet()) { String d = null; diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/RenderingContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/RenderingContext.java index 37cc20ff0..932bc44ea 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/RenderingContext.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/RenderingContext.java @@ -79,6 +79,7 @@ public class RenderingContext { SUMMARY, // 5 cells: tree/name | flags | cardinality | type | details BINDINGS, // tree/name + column for each kind of binding found, cells are lists of bindings OBLIGATIONS, // tree/name + column for each actor that has obligations + DATA_DICT, // detailed element view } public enum ExampleScenarioRendererMode { @@ -132,6 +133,7 @@ public class RenderingContext { LINKS } + public enum KnownLinkType { SELF, // absolute link to where the content is to be found (only used in a few circumstances when making external references to tools) SPEC, // version specific link to core specification diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/DefinitionNavigator.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/DefinitionNavigator.java index 1d549286a..8e0b39ed9 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/DefinitionNavigator.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/DefinitionNavigator.java @@ -138,8 +138,8 @@ public class DefinitionNavigator { if (nameMap.containsKey(path)) { DefinitionNavigator master = nameMap.get(path); ElementDefinition cm = master.current(); - if (!cm.hasSlicing()) - throw new DefinitionException("Found slices with no slicing details at "+dn.current().getPath()); +// if (!cm.hasSlicing()) +// throw new DefinitionException("Found slices with no slicing details at "+dn.current().getPath()); if (master.slices == null) master.slices = new ArrayList(); master.slices.add(dn); From 2b15c545fc01e0e9cc0d4154f4695a9f30366c47 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Tue, 15 Aug 2023 19:33:56 +1000 Subject: [PATCH 2/6] comparison tests to include rendering as well --- .../comparison/tests/ComparisonTests.java | 82 +++++++++++++++---- .../v3-ObservationInterpretation.cache | 57 +++++++++++++ 2 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 org.hl7.fhir.validation/src/test/resources/txCache/org.hl7.fhir.r5/v3-ObservationInterpretation.cache diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/comparison/tests/ComparisonTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/comparison/tests/ComparisonTests.java index 2ce208dfe..a37790920 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/comparison/tests/ComparisonTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/comparison/tests/ComparisonTests.java @@ -45,10 +45,22 @@ import org.hl7.fhir.r5.model.Constants; 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.renderers.CodeSystemRenderer; +import org.hl7.fhir.r5.renderers.StructureDefinitionRenderer; +import org.hl7.fhir.r5.renderers.ValueSetRenderer; +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; +import org.hl7.fhir.r5.renderers.utils.RenderingContext.StructureDefinitionRendererMode; +import org.hl7.fhir.r5.test.utils.CompareUtilities; import org.hl7.fhir.r5.test.utils.TestingUtilities; +import org.hl7.fhir.utilities.MarkDownProcessor; +import org.hl7.fhir.utilities.MarkDownProcessor.Dialect; import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.utilities.json.model.*; +import org.hl7.fhir.utilities.json.parser.*; import org.hl7.fhir.utilities.npm.CommonPackages; import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; import org.hl7.fhir.utilities.npm.NpmPackage; @@ -56,14 +68,13 @@ import org.hl7.fhir.utilities.settings.FhirSettings; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; import org.hl7.fhir.utilities.xhtml.XhtmlComposer; +import org.hl7.fhir.utilities.xhtml.XhtmlNode; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import java.nio.charset.StandardCharsets; -import com.google.common.base.Charsets; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; public class ComparisonTests { @@ -73,9 +84,9 @@ public class ComparisonTests { String contents = TestingUtilities.loadTestResource("comparison", "manifest.json"); Map examples = new HashMap(); - manifest = (JsonObject) new com.google.gson.JsonParser().parse(contents); - for (Entry e : manifest.getAsJsonObject("test-cases").entrySet()) { - examples.put(e.getKey(), e.getValue().getAsJsonObject()); + manifest = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(contents); + for (JsonProperty e : manifest.getJsonObject("test-cases").getProperties()) { + examples.put(e.getName(), e.getValue().asJsonObject()); } List names = new ArrayList(examples.size()); @@ -97,6 +108,8 @@ public class ComparisonTests { private static final String HEADER = ""; private static final String BREAK = "
"; private static final String FOOTER = ""; + private String prefix; + private String suffix; @ParameterizedTest(name = "{index}: id {0}") @MethodSource("data") @@ -104,7 +117,7 @@ public class ComparisonTests { TestingUtilities.injectCorePackageLoader(); this.content = content; - if (content.has("use-test") && !content.get("use-test").getAsBoolean()) + if (content.has("use-test") && !content.asBoolean("use-test")) return; if (context == null) { @@ -132,9 +145,17 @@ public class ComparisonTests { System.out.println("---- " + name + " ----------------------------------------------------------------"); CanonicalResource left = load("left"); CanonicalResource right = load("right"); + prefix = loadResource("html-prefix.html"); + suffix = loadResource("html-suffix.html"); ComparisonSession session = new ComparisonSession(context, context, "Comparison Tests", null, null); - + if (content.has("version")) { + session.setForVersion(content.getJsonObject("version").asString("stated")); + session.setAnnotate(true); + } + RenderingContext lrc = new RenderingContext(context, new MarkDownProcessor(Dialect.COMMON_MARK), null, "http://hl7.org/fhir", "", "en", ResourceRendererMode.TECHNICAL, GenerationRules.IG_PUBLISHER); + lrc.setDestDir(Utilities.path("[tmp]", "comparison")); + if (left instanceof CodeSystem && right instanceof CodeSystem) { CodeSystemComparer cs = new CodeSystemComparer(session); CodeSystemComparison csc = cs.compare((CodeSystem) left, (CodeSystem) right); @@ -147,6 +168,8 @@ public class ComparisonTests { String xml2 = new XhtmlComposer(true).compose(cs.renderConcepts(csc, "", "")); TextFile.stringToFile(HEADER + hd("Messages") + xmle + BREAK + hd("Metadata") + xml1 + BREAK + hd("Concepts") + xml2 + FOOTER, Utilities.path("[tmp]", "comparison", name + ".html")); checkOutcomes(csc.getMessages(), content); + new CodeSystemRenderer(lrc).render(right); + checkOutput(content.getJsonObject("version").asString("filename"), right); } else if (left instanceof ValueSet && right instanceof ValueSet) { ValueSetComparer cs = new ValueSetComparer(session); ValueSetComparison csc = cs.compare((ValueSet) left, (ValueSet) right); @@ -159,6 +182,8 @@ public class ComparisonTests { String xml3 = new XhtmlComposer(true).compose(cs.renderExpansion(csc, "", "")); TextFile.stringToFile(HEADER + hd("Messages") + xmle + BREAK + hd("Metadata") + xml1 + BREAK + hd("Definition") + xml2 + BREAK + hd("Expansion") + xml3 + FOOTER, Utilities.path("[tmp]", "comparison", name + ".html")); checkOutcomes(csc.getMessages(), content); + new ValueSetRenderer(lrc).render(right); + checkOutput(content.getJsonObject("version").asString("filename"), right); } else if (left instanceof StructureDefinition && right instanceof StructureDefinition) { ProfileUtilities utils = new ProfileUtilities(context, null, null); genSnapshot(utils, (StructureDefinition) left); @@ -174,6 +199,14 @@ public class ComparisonTests { // String xml3 = new XhtmlComposer(true).compose(cs.renderExpansion(csc, "", "")); TextFile.stringToFile(HEADER + hd("Messages") + xmle + BREAK + hd("Metadata") + xml1 + BREAK + hd("Structure") + xml2 + FOOTER, Utilities.path("[tmp]", "comparison", name + ".html")); checkOutcomes(csc.getMessages(), content); + + lrc.setStructureMode(StructureDefinitionRendererMode.DATA_DICT); + new StructureDefinitionRenderer(lrc).render(right); + checkOutput(content.getJsonObject("version").asString("filename-dd"), right); + + lrc.setStructureMode(StructureDefinitionRendererMode.SUMMARY); + new StructureDefinitionRenderer(lrc).render(right); + checkOutput(content.getJsonObject("version").asString("filename-tree"), right); } else if (left instanceof CapabilityStatement && right instanceof CapabilityStatement) { CapabilityStatementComparer pc = new CapabilityStatementComparer(session); CapabilityStatementComparison csc = pc.compare((CapabilityStatement) left, (CapabilityStatement) right); @@ -191,6 +224,18 @@ public class ComparisonTests { } } + private void checkOutput(String name, CanonicalResource right) throws Exception { + String output = prefix+ new XhtmlComposer(false, true).compose(right.getText().getDiv()) + suffix; + String an = Utilities.path("[tmp]", "comparison", name); + TextFile.stringToFile(output, an); + String expected = loadResource(name); + String en = Utilities.path("[tmp]", "comparison", Utilities.changeFileExt(name, ".expected.html")); + TextFile.stringToFile(expected, en); + + String msg = CompareUtilities.checkXMLIsSame(en, an); + Assertions.assertTrue(msg == null, "Output does not match expected: "+msg); + + } private void genSnapshot(ProfileUtilities utils, StructureDefinition sd) { StructureDefinition base = context.fetchTypeDefinition(sd.getType()); utils.generateSnapshot(base, sd, sd.getUrl(), "http://hl7.org/fhir/r4", sd.present()); @@ -201,13 +246,18 @@ public class ComparisonTests { } private CanonicalResource load(String name) throws IOException { - JsonObject details = content.getAsJsonObject(name); - String src = TestingUtilities.loadTestResource("comparison", details.get("source").getAsString()); - return (CanonicalResource) loadResource(details.get("source").getAsString(), src, details.get("version").getAsString()); + JsonObject details = content.getJsonObject(name); + String src = TestingUtilities.loadTestResource("comparison", details.asString("source")); + return (CanonicalResource) loadResource(details.asString("source"), src, details.asString("version")); + } + + private String loadResource(String name) throws IOException { + String src = TestingUtilities.loadTestResource("comparison", name); + return src; } public Resource loadResource(String filename, String contents, String ver) throws IOException, FHIRFormatError, FileNotFoundException, FHIRException, DefinitionException { - try (InputStream inputStream = IOUtils.toInputStream(contents, Charsets.UTF_8)) { + try (InputStream inputStream = IOUtils.toInputStream(contents, StandardCharsets.UTF_8)) { if (filename.contains(".json")) { if (Constants.VERSION.equals(ver) || "5.0".equals(ver)) return new JsonParser().parse(inputStream); @@ -239,7 +289,7 @@ public class ComparisonTests { } private void checkOutcomes(List errors, JsonObject focus) { - JsonObject output = focus.getAsJsonObject("output"); + JsonObject output = focus.getJsonObject("output"); int ec = 0; int wc = 0; int hc = 0; @@ -265,11 +315,11 @@ public class ComparisonTests { } } } - Assertions.assertEquals(output.get("errorCount").getAsInt(), ec, "Expected " + Integer.toString(output.get("errorCount").getAsInt()) + " errors, but found " + Integer.toString(ec) + "."); + Assertions.assertEquals(output.asInteger("errorCount"), ec, "Expected " + Integer.toString(output.asInteger("errorCount")) + " errors, but found " + Integer.toString(ec) + "."); if (output.has("warningCount")) - Assertions.assertEquals(output.get("warningCount").getAsInt(), wc, "Expected " + Integer.toString(output.get("warningCount").getAsInt()) + " warnings, but found " + Integer.toString(wc) + "."); + Assertions.assertEquals(output.asInteger("warningCount"), wc, "Expected " + Integer.toString(output.asInteger("warningCount")) + " warnings, but found " + Integer.toString(wc) + "."); if (output.has("infoCount")) - Assertions.assertEquals(output.get("infoCount").getAsInt(), hc, "Expected " + Integer.toString(output.get("infoCount").getAsInt()) + " hints, but found " + Integer.toString(hc) + "."); + Assertions.assertEquals(output.asInteger("infoCount"), hc, "Expected " + Integer.toString(output.asInteger("infoCount")) + " hints, but found " + Integer.toString(hc) + "."); } } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/test/resources/txCache/org.hl7.fhir.r5/v3-ObservationInterpretation.cache b/org.hl7.fhir.validation/src/test/resources/txCache/org.hl7.fhir.r5/v3-ObservationInterpretation.cache new file mode 100644 index 000000000..0e8f15dcd --- /dev/null +++ b/org.hl7.fhir.validation/src/test/resources/txCache/org.hl7.fhir.r5/v3-ObservationInterpretation.cache @@ -0,0 +1,57 @@ +------------------------------------------------------------------------------------- +{"hierarchical" : false, "valueSet" :{ + "resourceType" : "ValueSet", + "compose" : { + "inactive" : true, + "include" : [{ + "system" : "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", + "concept" : [{ + "code" : "LL" + }, + { + "code" : "HH" + }, + { + "code" : "L", + "display" : "Extra Low" + }, + { + "code" : "H" + }] + }] + } +}}#### +e: { + "error" : "Cannot invoke \"org.hl7.fhir.r5.terminologies.client.ITerminologyClient.expandValueset(org.hl7.fhir.r5.model.ValueSet, org.hl7.fhir.r5.model.Parameters, java.util.Map)\" because the return value of \"org.hl7.fhir.r5.terminologies.client.TerminologyClientContext.getClient()\" is null" +} +------------------------------------------------------------------------------------- +{"hierarchical" : false, "valueSet" :{ + "resourceType" : "ValueSet", + "compose" : { + "inactive" : true, + "include" : [{ + "system" : "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", + "concept" : [{ + "code" : "LL" + }, + { + "code" : "HH" + }, + { + "code" : "L", + "display" : "Extra Low" + }, + { + "code" : "H", + "display" : "higher" + }, + { + "code" : "P" + }] + }] + } +}}#### +e: { + "error" : "Cannot invoke \"org.hl7.fhir.r5.terminologies.client.ITerminologyClient.expandValueset(org.hl7.fhir.r5.model.ValueSet, org.hl7.fhir.r5.model.Parameters, java.util.Map)\" because the return value of \"org.hl7.fhir.r5.terminologies.client.TerminologyClientContext.getClient()\" is null" +} +------------------------------------------------------------------------------------- From cc5069ccb3538c87d210de0b7bff28008e34a946 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Tue, 15 Aug 2023 19:34:08 +1000 Subject: [PATCH 3/6] xhtml fluent improvements --- .../hl7/fhir/utilities/xhtml/XhtmlFluent.java | 3 +- .../hl7/fhir/utilities/xhtml/XhtmlNode.java | 46 +++++++++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlFluent.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlFluent.java index 3ae249226..b2e349b1b 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlFluent.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlFluent.java @@ -54,7 +54,8 @@ public abstract class XhtmlFluent { } public XhtmlNode td() { - return addTag("td"); + XhtmlNode x = addTag("td"); + return x; } public XhtmlNode td(String clss) { diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java index 6731a0812..e13cdabec 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/xhtml/XhtmlNode.java @@ -363,6 +363,10 @@ public class XhtmlNode extends XhtmlFluent implements IBaseXhtml { return hasAttributes() && getAttributes().containsKey(name); } + public boolean hasAttribute(String name, String value) { + return hasAttributes() && getAttributes().containsKey(name) && value.equals(getAttributes().get(name)); + } + public String getAttribute(String name) { return hasAttributes() ? getAttributes().get(name) : null; } @@ -569,18 +573,25 @@ public class XhtmlNode extends XhtmlFluent implements IBaseXhtml { throw new UnsupportedOperationException(); } - /** - * NOT SUPPORTED - Throws {@link UnsupportedOperationException} - */ + private Map userData; + public Object getUserData(String theName) { - throw new UnsupportedOperationException(); + if (hasUserData(theName)) { + return userData.get(theName); + } else { + return null; + } + } + + public boolean hasUserData(String theName) { + return userData != null && userData.containsKey(theName); } - /** - * NOT SUPPORTED - Throws {@link UnsupportedOperationException} - */ public void setUserData(String theName, Object theValue) { - throw new UnsupportedOperationException(); + if (userData == null) { + userData = new HashMap<>(); + } + userData.put(theName, theValue); } @@ -892,4 +903,23 @@ public class XhtmlNode extends XhtmlFluent implements IBaseXhtml { } } + public boolean isClass(String name) { + return hasAttribute("class", name); + } + + + public void styleCells(XhtmlNode x) { + setUserData("cells", x); + } + + + public XhtmlNode td() { + XhtmlNode x = addTag("td"); + XhtmlNode t = (XhtmlNode) getUserData("cells"); + if (t != null) { + x.copyAllContent(t); + } + return x; + } + } \ No newline at end of file From ea4e59291087247ad6daecddf2db0ee309940dd9 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Tue, 15 Aug 2023 19:34:24 +1000 Subject: [PATCH 4/6] CodeSystem validation around count and content --- .../fhir/utilities/i18n/I18nConstants.java | 5 ++ .../src/main/resources/Messages.properties | 8 ++- .../instance/type/CodeSystemValidator.java | 49 +++++++++++++++++-- 3 files changed, 56 insertions(+), 6 deletions(-) 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 c3e93c736..3782539c5 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 @@ -966,6 +966,11 @@ public class I18nConstants { public static final String MSG_DEPENDS_ON_EXTENSION = "MSG_DEPENDS_ON_EXTENSION"; public static final String MSG_DEPENDS_ON_PROFILE = "MSG_DEPENDS_ON_PROFILE"; public static final String VALIDATION_VAL_STATUS_INCONSISTENT = "VALIDATION_VAL_STATUS_INCONSISTENT"; + public static final String CODESYSTEM_CS_COUNT_COMPLETE_WRONG = "CODESYSTEM_CS_COUNT_COMPLETE_WRONG"; + public static final String CODESYSTEM_CS_COUNT_FRAGMENT_WRONG = "CODESYSTEM_CS_COUNT_FRAGMENT_WRONG"; + public static final String CODESYSTEM_CS_COUNT_NOTPRESENT_ZERO = "CODESYSTEM_CS_COUNT_NOTPRESENT_ZERO"; + public static final String CODESYSTEM_CS_COUNT_SUPPLEMENT_WRONG = "CODESYSTEM_CS_COUNT_SUPPLEMENT_WRONG"; + public static final String CODESYSTEM_CS_COUNT_NO_CONTENT_ALLOWED = "CODESYSTEM_CS_COUNT_NO_CONTENT_ALLOWED"; } diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index 5674e2b17..618c39368 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -1024,5 +1024,11 @@ MSG_DEPENDS_ON_EXPERIMENTAL = The {0} {1} is an experimental resource MSG_DEPENDS_ON_DRAFT = The {0} {1} is a draft resource MSG_DEPENDS_ON_EXTENSION = extension MSG_DEPENDS_ON_PROFILE = profile -VALIDATION_VAL_STATUS_INCONSISTENT = The resource status ''{0}'' amd the standards status ''{1}'' are not consistent +VALIDATION_VAL_STATUS_INCONSISTENT = The resource status ''{0}'' and the standards status ''{1}'' are not consistent +CODESYSTEM_CS_COUNT_COMPLETE_WRONG = The code system is complete, but the number of concepts ({0}) does not match the stated total number ({1}) +CODESYSTEM_CS_COUNT_FRAGMENT_WRONG = The code system is a fragment/example, but the number of concepts ({0}) exceeds or matches the stated total number ({1}) +CODESYSTEM_CS_COUNT_NOTPRESENT_ZERO = The code system has no content, but the exceeds the stated total number is 0 concepts - check that this isn't a complete code system that has no concepts, or update/remove the stated count +CODESYSTEM_CS_COUNT_SUPPLEMENT_WRONG = The code system supplement states the total number of concepts as {1}, but this is different to the underlying code system that states a value of {0} +CODESYSTEM_CS_COUNT_NO_CONTENT_ALLOWED = The code system says it has no content present, but concepts are found + \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java index 0d60ae90e..aa1b0f706 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/type/CodeSystemValidator.java @@ -6,6 +6,7 @@ import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.elementmodel.Element; import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.utils.XVerExtensionManager; import org.hl7.fhir.utilities.Utilities; @@ -32,8 +33,9 @@ public class CodeSystemValidator extends BaseValidator { String caseSensitive = cs.getNamedChildValue("caseSensitive"); String hierarchyMeaning = cs.getNamedChildValue("hierarchyMeaning"); String supp = cs.getNamedChildValue("supplements"); - - metaChecks(errors, cs, stack, url, content, caseSensitive, hierarchyMeaning, !Utilities.noString(supp)); + int count = countConcepts(cs); + + metaChecks(errors, cs, stack, url, content, caseSensitive, hierarchyMeaning, !Utilities.noString(supp), count, supp); String vsu = cs.getNamedChildValue("valueSet"); if (!Utilities.noString(vsu)) { @@ -51,7 +53,6 @@ public class CodeSystemValidator extends BaseValidator { ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), !vs.getCompose().getInclude().get(0).hasValueSet() && !vs.getCompose().getInclude().get(0).hasConcept() && !vs.getCompose().getInclude().get(0).hasFilter(), I18nConstants.CODESYSTEM_CS_VS_INCLUDEDETAILS, url, vsu) && ok; if (vs.hasExpansion()) { - int count = countConcepts(cs); ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs.getExpansion().getContains().size() == count, I18nConstants.CODESYSTEM_CS_VS_EXP_MISMATCH, url, vsu, count, vs.getExpansion().getContains().size()) && ok; } } else { @@ -71,7 +72,11 @@ public class CodeSystemValidator extends BaseValidator { List concepts = cs.getChildrenByName("concept"); int ce = 0; for (Element concept : concepts) { - ok = validateSupplementConcept(errors, concept, stack.push(concept, ce, null, null), supp, options) && ok; + NodeStack nstack = stack.push(concept, ce, null, null); + if (ce == 0) { + rule(errors, "2023-08-15", IssueType.INVALID, nstack, !"not-present".equals(content), I18nConstants.CODESYSTEM_CS_COUNT_NO_CONTENT_ALLOWED); + } + ok = validateSupplementConcept(errors, concept, nstack, supp, options) && ok; ce++; } } else { @@ -121,7 +126,7 @@ public class CodeSystemValidator extends BaseValidator { return true; } - private void metaChecks(List errors, Element cs, NodeStack stack, String url, String content, String caseSensitive, String hierarchyMeaning, boolean isSupplement) { + private void metaChecks(List errors, Element cs, NodeStack stack, String url, String content, String caseSensitive, String hierarchyMeaning, boolean isSupplement, int count, String supp) { if (isSupplement) { if (!"supplement".equals(content)) { NodeStack s = stack.push(cs.getNamedChild("content"), -1, null, null); @@ -178,6 +183,40 @@ public class CodeSystemValidator extends BaseValidator { } } } + + if (cs.hasChild("count")) { + int statedCount = Utilities.parseInt(cs.getNamedChildValue("count"), -1); + if (statedCount > -1 && content != null) { // error elsewhere + var nstack = stack.push(cs.getNamedChild("count"), -1, null, null); + switch (content) { + case "complete": + rule(errors, "2023-08-15", IssueType.INVALID, nstack, count == statedCount, I18nConstants.CODESYSTEM_CS_COUNT_COMPLETE_WRONG, count, statedCount); + break; + case "example": + case "fragment": + warning(errors, "2023-08-15", IssueType.INVALID, nstack, count < statedCount, I18nConstants.CODESYSTEM_CS_COUNT_FRAGMENT_WRONG, count, statedCount); + break; + case "not-present": + hint(errors, "2023-08-15", IssueType.INVALID, stack.push(cs.getNamedChild("concept"), -1, null, null), statedCount > 0, I18nConstants.CODESYSTEM_CS_COUNT_NOTPRESENT_ZERO, statedCount); + break; + case "supplement": + CodeSystem css = context.fetchCodeSystem(supp); + if (css != null) { + rule(errors, "2023-08-15", IssueType.INVALID, nstack, count == css.getCount(), I18nConstants.CODESYSTEM_CS_COUNT_SUPPLEMENT_WRONG, css.getCount(), statedCount); + } + break; + default: + // do nothing + } + } + } + + if ("not-present".equals(content)) { + List concepts = cs.getChildrenByName("concept"); + if (concepts.size() > 0) { + rule(errors, "2023-08-15", IssueType.INVALID, stack.push(concepts.get(0), 0, null, null), false, I18nConstants.CODESYSTEM_CS_COUNT_NO_CONTENT_ALLOWED); + } + } } From 81eba4b825ffbee98b99b10be399f5df49422314 Mon Sep 17 00:00:00 2001 From: dotasek Date: Tue, 15 Aug 2023 10:34:31 -0400 Subject: [PATCH 5/6] Clean imports --- .../java/org/hl7/fhir/comparison/tests/ComparisonTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/comparison/tests/ComparisonTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/comparison/tests/ComparisonTests.java index a37790920..e7432fabf 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/comparison/tests/ComparisonTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/comparison/tests/ComparisonTests.java @@ -10,7 +10,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; + import java.util.stream.Stream; import org.apache.commons.io.IOUtils; @@ -68,7 +68,7 @@ import org.hl7.fhir.utilities.settings.FhirSettings; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; import org.hl7.fhir.utilities.xhtml.XhtmlComposer; -import org.hl7.fhir.utilities.xhtml.XhtmlNode; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; From 2fa0587c1f008b5a172fc42dc5790c2227df96b0 Mon Sep 17 00:00:00 2001 From: dotasek Date: Tue, 15 Aug 2023 10:34:58 -0400 Subject: [PATCH 6/6] Fix commonmark class not found --- org.hl7.fhir.validation/pom.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/org.hl7.fhir.validation/pom.xml b/org.hl7.fhir.validation/pom.xml index f1d784b1a..bbd8dbb35 100644 --- a/org.hl7.fhir.validation/pom.xml +++ b/org.hl7.fhir.validation/pom.xml @@ -194,7 +194,12 @@ true test - + + com.atlassian.commonmark + commonmark-ext-gfm-tables + true + test + org.slf4j