From c13c2919490937ee6efe04fc13e1237a0c55e8cf Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 17 Jan 2022 10:51:48 +1100 Subject: [PATCH] more work on value set renderer (designations, expand-groups) --- .../fhir/r5/renderers/CodeSystemRenderer.java | 2 +- .../fhir/r5/renderers/ResourceRenderer.java | 13 ++ .../r5/renderers/TerminologyRenderer.java | 8 +- .../fhir/r5/renderers/ValueSetRenderer.java | 205 ++++++++++++++++-- .../hl7/fhir/r5/utils/ToolingExtensions.java | 2 + 5 files changed, 207 insertions(+), 23 deletions(-) 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 464423299..cf647b1a2 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 @@ -175,7 +175,7 @@ public class CodeSystemRenderer extends TerminologyRenderer { hierarchy = hierarchy || csNav.isRestructure(); List langs = new ArrayList<>(); - addMapHeaders(addTableHeaderRowStandard(t, hierarchy, display, definitions, commentS, version, deprecated, properties, null, false), maps); + addMapHeaders(addTableHeaderRowStandard(t, hierarchy, display, definitions, commentS, version, deprecated, properties, null, null, false), maps); for (ConceptDefinitionComponent c : csNav.getConcepts(null)) { hasExtensions = addDefineRowToTable(t, c, 0, hierarchy, display, definitions, commentS, version, deprecated, maps, cs.getUrl(), cs, properties, csNav, langs, isSupplement) || hasExtensions; } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java index 76da6f160..c9b7f1281 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/ResourceRenderer.java @@ -20,6 +20,7 @@ import org.hl7.fhir.r5.model.Narrative; import org.hl7.fhir.r5.model.Narrative.NarrativeStatus; import org.hl7.fhir.r5.model.Reference; import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.renderers.utils.BaseWrappers.BaseWrapper; import org.hl7.fhir.r5.renderers.utils.BaseWrappers.PropertyWrapper; import org.hl7.fhir.r5.renderers.utils.BaseWrappers.ResourceWrapper; @@ -488,4 +489,16 @@ public abstract class ResourceRenderer extends DataRenderer { private String getPrimitiveValue(ResourceWrapper r, String name) throws UnsupportedEncodingException, FHIRException, IOException { return r.has(name) && r.getChildByName(name).hasValues() ? r.getChildByName(name).getValues().get(0).getBase().primitiveValue() : null; } + + public void renderOrError(DomainResource dr) { + try { + render(dr); + } catch (Exception e) { + XhtmlNode x = new XhtmlNode(NodeType.Element, "div"); + x.para().tx("Error rendering: "+e.getMessage()); + dr.setText(null); + inject(dr, x, NarrativeStatus.GENERATED); + } + + } } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/TerminologyRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/TerminologyRenderer.java index 5794b3c4c..d7fe9c06e 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/TerminologyRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/TerminologyRenderer.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRFormatError; @@ -201,7 +202,7 @@ public abstract class TerminologyRenderer extends ResourceRenderer { return null; } - protected XhtmlNode addTableHeaderRowStandard(XhtmlNode t, boolean hasHierarchy, boolean hasDisplay, boolean definitions, boolean comments, boolean version, boolean deprecated, List properties, List langs, boolean doLangs) { + protected XhtmlNode addTableHeaderRowStandard(XhtmlNode t, boolean hasHierarchy, boolean hasDisplay, boolean definitions, boolean comments, boolean version, boolean deprecated, List properties, List langs, Map designations, boolean doDesignations) { XhtmlNode tr = t.tr(); if (hasHierarchy) { tr.td().b().tx("Lvl"); @@ -234,7 +235,10 @@ public abstract class TerminologyRenderer extends ResourceRenderer { tr.td().b().tx(getContext().getWorker().translator().translate("xhtml-gen-cs", display, getContext().getLang())); } } - if (doLangs) { + if (doDesignations) { + for (String url : designations.keySet()) { + tr.td().b().addText(designations.get(url)); + } for (String lang : langs) { tr.td().b().addText(describeLang(lang)); } 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 e88e85122..869a19907 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 @@ -29,6 +29,7 @@ import org.hl7.fhir.r5.model.ConceptMap; import org.hl7.fhir.r5.model.DataType; import org.hl7.fhir.r5.model.DomainResource; import org.hl7.fhir.r5.model.Enumerations.FilterOperator; +import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.r5.model.Extension; import org.hl7.fhir.r5.model.ExtensionHelper; import org.hl7.fhir.r5.model.PrimitiveType; @@ -39,6 +40,7 @@ import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent; import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent; import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.r5.model.ValueSet.ConceptSetFilterComponent; +import org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent; import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionComponent; import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionParameterComponent; @@ -48,7 +50,11 @@ import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome; import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator; import org.hl7.fhir.utilities.xhtml.XhtmlNode; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel; +import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Title; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; @@ -65,7 +71,7 @@ public class ValueSetRenderer extends TerminologyRenderer { private static final String ABSTRACT_CODE_HINT = "This code is not selectable ('Abstract')"; - private static final int MAX_LANGS_IN_LINE = 5; + private static final int MAX_DESIGNATIONS_IN_LINE = 5; private List renderingMaps = new ArrayList(); @@ -222,7 +228,7 @@ public class ValueSetRenderer extends TerminologyRenderer { doLangs = false; } else { // if we're not doing definitions and we don't have too many languages, we'll do them in line - if (langs.size() < MAX_LANGS_IN_LINE) { + if (langs.size() < MAX_DESIGNATIONS_IN_LINE) { doLangs = true; if (vs.hasLanguage()) { tdDisp.tx(" - "+describeLang(vs.getLanguage())); @@ -586,8 +592,12 @@ public class ValueSetRenderer extends TerminologyRenderer { private boolean checkDoDefinition(List contains) { for (ValueSetExpansionContainsComponent c : contains) { CodeSystem cs = getContext().getWorker().fetchCodeSystem(c.getSystem()); - if (cs != null) - return true; + if (cs != null) { + ConceptDefinitionComponent cd = CodeSystemUtilities.getCode(cs, c.getCode()); + if (cd != null && cd.hasDefinition()) { + return true; + } + } if (checkDoDefinition(c.getContains())) return true; } @@ -754,13 +764,14 @@ public class ValueSetRenderer extends TerminologyRenderer { private boolean generateComposition(XhtmlNode x, ValueSet vs, boolean header, List maps) throws FHIRException, IOException { boolean hasExtensions = false; List langs = new ArrayList(); + Map designations = new HashMap<>(); // map of url = description, where url is the designation code. Designations that are for languages won't make it into this list for (ConceptSetComponent inc : vs.getCompose().getInclude()) { - scanForLangs(inc, langs); + scanDesignations(inc, langs, designations); } for (ConceptSetComponent inc : vs.getCompose().getExclude()) { - scanForLangs(inc, langs); + scanDesignations(inc, langs, designations); } - boolean doLangs = langs.size() < MAX_LANGS_IN_LINE; + boolean doDesignations = langs.size() + designations.size() < MAX_DESIGNATIONS_IN_LINE; if (header) { XhtmlNode h = x.h2(); @@ -769,35 +780,48 @@ public class ValueSetRenderer extends TerminologyRenderer { if (vs.hasCopyrightElement()) generateCopyright(x, vs); } + int index = 0; if (vs.getCompose().getInclude().size() == 1 && vs.getCompose().getExclude().size() == 0) { - hasExtensions = genInclude(x.ul(), vs.getCompose().getInclude().get(0), "Include", langs, doLangs, maps) || hasExtensions; + hasExtensions = genInclude(x.ul(), vs.getCompose().getInclude().get(0), "Include", langs, doDesignations, maps, designations, index) || hasExtensions; } else { XhtmlNode p = x.para(); p.tx("This value set includes codes based on the following rules:"); XhtmlNode ul = x.ul(); for (ConceptSetComponent inc : vs.getCompose().getInclude()) { - hasExtensions = genInclude(ul, inc, "Include", langs, doLangs, maps) || hasExtensions; + hasExtensions = genInclude(ul, inc, "Include", langs, doDesignations, maps, designations, index) || hasExtensions; + index++; } if (vs.getCompose().hasExclude()) { p = x.para(); p.tx("This value set excludes codes based on the following rules:"); ul = x.ul(); for (ConceptSetComponent exc : vs.getCompose().getExclude()) { - hasExtensions = genInclude(ul, exc, "Exclude", langs, doLangs, maps) || hasExtensions; + hasExtensions = genInclude(ul, exc, "Exclude", langs, doDesignations, maps, designations, index) || hasExtensions; + index++; } } } // now, build observed languages - if (!doLangs && langs.size() > 0) { + if (!doDesignations && langs.size() + designations.size() > 0) { Collections.sort(langs); - x.para().b().tx("Additional Language Displays"); - XhtmlNode t = x.table( "codes"); + if (designations.size() == 0) { + x.para().b().tx("Additional Language Displays"); + } else if (langs.size() == 0) { + x.para().b().tx("Additional Designations"); + } else { + x.para().b().tx("Additional Designations and Language Displays"); + } + XhtmlNode t = x.table("codes"); XhtmlNode tr = t.tr(); tr.td().b().tx("Code"); - for (String lang : langs) + for (String url : designations.keySet()) { + tr.td().b().addText(designations.get(url)); + } + for (String lang : langs) { tr.td().b().addText(describeLang(lang)); + } for (ConceptSetComponent c : vs.getCompose().getInclude()) { for (ConceptReferenceComponent cc : c.getConcept()) { addLanguageRow(cc, t, langs); @@ -805,10 +829,102 @@ public class ValueSetRenderer extends TerminologyRenderer { } } + return hasExtensions; } - private void scanForLangs(ConceptSetComponent inc, List langs) { + private void renderExpansionRules(XhtmlNode x, ConceptSetComponent inc, int index, Map definitions) throws FHIRException, IOException { + String s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface, but the rules are not properly defined"; + if (inc.hasExtension(ToolingExtensions.EXT_EXPAND_RULES)) { + String rule = inc.getExtensionString(ToolingExtensions.EXT_EXPAND_RULES); + if (rule != null) { + switch (rule) { + case "all-codes": s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface. The expansion contains all the codes, and also this structure:"; + case "ungrouped": s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface. The expansion contains this structure, and any codes not found in the structure:"; + case "groups-only": s = "This include specifies a heirarchy for when value sets are generated for use in a User Interface. The expansion contains this structure:"; + } + } + } + x.br(); + x.tx(s); + HierarchicalTableGenerator gen = new HierarchicalTableGenerator(context.getDestDir(), context.isInlineGraphics(), true); + TableModel model = gen.new TableModel("exp.h="+index, !forResource); + model.setAlternating(true); + model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("vs.exp.header", "Code"), translate("vs.exp.hint", "The code for the item"), null, 0)); + model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("vs.exp.header", "Display"), translate("vs.exp.hint", "The display for the item"), null, 0)); + + for (Extension ext : inc.getExtensionsByUrl(ToolingExtensions.EXT_EXPAND_GROUP)) { + renderExpandGroup(gen, model, ext, inc, definitions); + } + x.br(); + x.tx("table"); + XhtmlNode xn = gen.generate(model, context.getLocalPrefix(), 1, null); + x.getChildNodes().add(xn); + } + + private void renderExpandGroup(HierarchicalTableGenerator gen, TableModel model, Extension ext, ConceptSetComponent inc, Map definitions) { + Row row = gen.new Row(); + model.getRows().add(row); + row.setIcon("icon_entry_blue.png", "entry"); + String code = ext.getExtensionString("code"); + if (code != null) { + row.getCells().add(gen.new Cell(null, null, code, null, null)); + row.getCells().add(gen.new Cell(null, null, getDisplayForCode(inc, code, definitions), null, null)); + } else if (ext.hasId()) { + row.getCells().add(gen.new Cell(null, null, "(#"+ext.getId()+")", null, null)); + row.getCells().add(gen.new Cell(null, null, ext.getExtensionString("display"), null, null)); + } else { + row.getCells().add(gen.new Cell(null, null, null, null, null)); + row.getCells().add(gen.new Cell(null, null, ext.getExtensionString("display"), null, null)); + } + for (Extension member : ext.getExtensionsByUrl("member")) { + Row subRow = gen.new Row(); + row.getSubRows().add(subRow); + subRow.setIcon("icon_entry_blue.png", "entry"); + String mc = member.getValue().primitiveValue(); + // mc might be a reference to another expansion group - we check that first, or to a code in the compose + if (mc.startsWith("#")) { + // it's a reference by id + subRow.getCells().add(gen.new Cell(null, null, "("+mc+")", null, null)); + subRow.getCells().add(gen.new Cell(null, null, "group reference by id", null, null)); + } else { + Extension tgt = findTargetByCode(inc, mc); + if (tgt != null) { + subRow.getCells().add(gen.new Cell(null, null, mc, null, null)); + subRow.getCells().add(gen.new Cell(null, null, "group reference by code", null, null)); + } else { + subRow.getCells().add(gen.new Cell(null, null, mc, null, null)); + subRow.getCells().add(gen.new Cell(null, null, getDisplayForCode(inc, mc, definitions), null, null)); + } + } + } + } + + private Extension findTargetByCode(ConceptSetComponent inc, String mc) { + for (Extension ext : inc.getExtensionsByUrl(ToolingExtensions.EXT_EXPAND_GROUP)) { + String code = ext.getExtensionString("code"); + if (mc.equals(code)) { + return ext; + } + } + return null; + } + + private String getDisplayForCode(ConceptSetComponent inc, String code, Map definitions) { + for (ConceptReferenceComponent cc : inc.getConcept()) { + if (code.equals(cc.getCode())) { + if (cc.hasDisplay()) { + return cc.getDisplay(); + } + } + } + if (definitions.containsKey(code)) { + return definitions.get(code).getDisplay(); + } + return null; + } + + private void scanDesignations(ConceptSetComponent inc, List langs, Map designations) { for (ConceptReferenceComponent cc : inc.getConcept()) { for (Extension ext : cc.getExtension()) { if (ToolingExtensions.EXT_TRANSLATION.equals(ext.getUrl())) { @@ -822,17 +938,47 @@ public class ValueSetRenderer extends TerminologyRenderer { String lang = d.getLanguage(); if (!Utilities.noString(lang) && !langs.contains(lang)) { langs.add(lang); + } else { + // can we present this as a designation that we know? + String url = getUrlForDesignation(d); + String disp = getDisplayForUrl(url); + if (disp != null && !designations.containsKey(url)) { + designations.put(url, disp); + } } } } } - private boolean genInclude(XhtmlNode ul, ConceptSetComponent inc, String type, List langs, boolean doLangs, List maps) throws FHIRException, IOException { + private String getDisplayForUrl(String url) { + if (url == null) { + return null; + } + switch (url) { + case "http://snomed.info/sct#900000000000003001": + return "Fully specified name"; + case "http://snomed.info/sct#900000000000013009": + return "Synonym"; + default: + return null; + } + } + + private String getUrlForDesignation(ConceptReferenceDesignationComponent d) { + if (d.hasUse() && d.getUse().hasSystem() && d.getUse().hasCode()) { + return d.getUse().getSystem()+"#"+d.getUse().getCode(); + } else { + return null; + } + } + + private boolean genInclude(XhtmlNode ul, ConceptSetComponent inc, String type, List langs, boolean doDesignations, List maps, Map designations, int index) throws FHIRException, IOException { boolean hasExtensions = false; XhtmlNode li; li = ul.li(); CodeSystem e = getContext().getWorker().fetchCodeSystem(inc.getSystem()); - + Map definitions = new HashMap<>(); + if (inc.hasSystem()) { if (inc.getConcept().size() == 0 && inc.getFilter().size() == 0) { li.addText(type+" all codes defined in "); @@ -847,7 +993,7 @@ public class ValueSetRenderer extends TerminologyRenderer { } // for performance reasons, we do all the fetching in one batch - Map definitions = getConceptsForCodes(e, inc); + definitions = getConceptsForCodes(e, inc); XhtmlNode t = li.table("none"); boolean hasComments = false; @@ -859,7 +1005,7 @@ public class ValueSetRenderer extends TerminologyRenderer { } if (hasComments || hasDefinition) hasExtensions = true; - addMapHeaders(addTableHeaderRowStandard(t, false, true, hasDefinition, hasComments, false, false, null, langs, doLangs), maps); + 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(); @@ -886,7 +1032,8 @@ public class ValueSetRenderer extends TerminologyRenderer { smartAddText(td, "Note: "+ToolingExtensions.readStringExtension(c, ToolingExtensions.EXT_VS_COMMENT)); } } - if (doLangs) { + if (doDesignations) { + addDesignationsToRow(c, designations, tr); addLangaugesToRow(c, langs, tr); } } @@ -946,6 +1093,10 @@ public class ValueSetRenderer extends TerminologyRenderer { AddVsRef(vs.asStringValue(), li); } } + if (inc.hasExtension(ToolingExtensions.EXT_EXPAND_RULES) || inc.hasExtension(ToolingExtensions.EXT_EXPAND_GROUP)) { + hasExtensions = true; + renderExpansionRules(li, inc, index, definitions); + } } else { li.tx("Import all the codes that are contained in "); if (inc.getValueSet().size() < 4) { @@ -968,6 +1119,20 @@ public class ValueSetRenderer extends TerminologyRenderer { return hasExtensions; } + public void addDesignationsToRow(ConceptReferenceComponent c, Map designations, XhtmlNode tr) { + for (String url : designations.keySet()) { + String d = null; + if (d == null) { + for (ConceptReferenceDesignationComponent dd : c.getDesignation()) { + if (url.equals(getUrlForDesignation(dd))) { + d = dd.getValue(); + } + } + } + tr.td().addText(d == null ? "" : d); + } + } + public void addLangaugesToRow(ConceptReferenceComponent c, List langs, XhtmlNode tr) { for (String lang : langs) { String d = null; diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java index 284737538..1815f56cf 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/ToolingExtensions.java @@ -198,6 +198,8 @@ public class ToolingExtensions { public static final String EXT_TARGET_ID = "http://hl7.org/fhir/StructureDefinition/targetElement"; public static final String EXT_TARGET_PATH = "http://hl7.org/fhir/StructureDefinition/targetPath"; public static final String EXT_VALUESET_SYSTEM = "http://hl7.org/fhir/StructureDefinition/valueset-system"; + public static final String EXT_EXPAND_RULES = "http://hl7.org/fhir/StructureDefinition/valueset-expand-rules"; + public static final String EXT_EXPAND_GROUP = "http://hl7.org/fhir/StructureDefinition/valueset-expand-group"; // specific extension helpers