From 95813d9004900c39ff20907bcac13f4eb900d6e2 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 9 Mar 2023 19:44:57 +1100 Subject: [PATCH] Fix FML Comments parsing, and add StructureMap rendering to pretty FML --- .../fhir/r5/elementmodel/ResourceParser.java | 1 + .../main/java/org/hl7/fhir/r5/model/Base.java | 58 +- .../fhir/r5/renderers/RendererFactory.java | 3 + .../r5/renderers/SearchParameterRenderer.java | 2 +- .../r5/renderers/StructureMapRenderer.java | 644 ++++++++++++++++++ .../java/org/hl7/fhir/r5/utils/FHIRLexer.java | 23 +- .../structuremap/StructureMapUtilities.java | 43 +- .../r5/test/NarrativeGenerationTests.java | 7 +- .../hl7/fhir/utilities/SourceLocation.java | 3 + .../hl7/fhir/utilities/xhtml/XhtmlNode.java | 4 + 10 files changed, 770 insertions(+), 18 deletions(-) create mode 100644 org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/StructureMapRenderer.java diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ResourceParser.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ResourceParser.java index 482849fe9..c214c5d52 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ResourceParser.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/elementmodel/ResourceParser.java @@ -63,6 +63,7 @@ public class ResourceParser extends ParserBase { private void parseChildren(String path, Base src, Element dst) { dst.setSource(src); + dst.copyFormatComments(src); List properties = dst.getProperty().getChildProperties(dst.getName(), null); for (org.hl7.fhir.r5.model.Property c : src.children()) { if (c.hasValues()) { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java index 0aeba8345..22a320ace 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java @@ -9,6 +9,7 @@ import java.util.Map; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.xhtml.XhtmlNode; import ca.uhn.fhir.model.api.IElement; @@ -105,7 +106,9 @@ public abstract class Base implements Serializable, IBase, IElement { /** * Round tracking xml comments for testing convenience */ - private List formatCommentsPost; + private List formatCommentsPost; + + private List validationMessages; public Object getUserData(String name) { @@ -167,7 +170,15 @@ public abstract class Base implements Serializable, IBase, IElement { } public boolean hasFormatComment() { - return (formatCommentsPre != null && !formatCommentsPre.isEmpty()) || (formatCommentsPost != null && !formatCommentsPost.isEmpty()); + return hasFormatCommentPre() || hasFormatCommentPost(); + } + + public boolean hasFormatCommentPre() { + return formatCommentsPre != null && !formatCommentsPre.isEmpty(); + } + + public boolean hasFormatCommentPost() { + return formatCommentsPost != null && !formatCommentsPost.isEmpty(); } public List getFormatCommentsPre() { @@ -182,6 +193,29 @@ public abstract class Base implements Serializable, IBase, IElement { return formatCommentsPost; } + + public void copyFormatComments(Base other) { + if (other.hasFormatComment()) { + formatCommentsPre = new ArrayList<>(); + formatCommentsPre.addAll(other.formatCommentsPre); + } else { + formatCommentsPre = null; + } + } + + + public void addFormatCommentsPre(List comments) { + if (comments != null && !comments.isEmpty()) { + getFormatCommentsPre().addAll(comments); + } + } + + public void addFormatCommentsPost(List comments) { + if (comments != null && !comments.isEmpty()) { + getFormatCommentsPost().addAll(comments); + } + } + // these 3 allow evaluation engines to get access to primitive values public boolean isPrimitive() { return false; @@ -463,4 +497,24 @@ public abstract class Base implements Serializable, IBase, IElement { this.validationInfo.add(vi); return vi; } + + + + // validation messages: the validator does not populate these (yet) + public Base addValidationMessage(ValidationMessage msg) { + if (validationMessages == null) { + validationMessages = new ArrayList<>(); + } + validationMessages.add(msg); + return this; + } + + public boolean hasValidationMessages() { + return validationMessages != null && !validationMessages.isEmpty(); + } + + public List getValidationMessages() { + return validationMessages != null ? validationMessages : new ArrayList<>(); + } + } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/RendererFactory.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/RendererFactory.java index a37eea90d..0f2dd01d4 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/RendererFactory.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/RendererFactory.java @@ -91,6 +91,9 @@ public class RendererFactory { if ("Requirements".equals(resourceName)) { return new RequirementsRenderer(context); } + if ("StructureMap".equals(resourceName)) { + return new StructureMapRenderer(context); + } return new ProfileDrivenRenderer(context); } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/SearchParameterRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/SearchParameterRenderer.java index d998934cc..5fa052782 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/SearchParameterRenderer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/SearchParameterRenderer.java @@ -183,7 +183,7 @@ public class SearchParameterRenderer extends TerminologyRenderer { @Override public String display(Resource r) throws UnsupportedEncodingException, IOException { - return ((OperationDefinition) r).present(); + return ((SearchParameter) r).present(); } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/StructureMapRenderer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/StructureMapRenderer.java new file mode 100644 index 000000000..5ef569996 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/StructureMapRenderer.java @@ -0,0 +1,644 @@ +package org.hl7.fhir.r5.renderers; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.model.CodeType; +import org.hl7.fhir.r5.model.ConceptMap; +import org.hl7.fhir.r5.model.Enumeration; +import org.hl7.fhir.r5.model.IdType; +import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship; +import org.hl7.fhir.r5.model.Enumerations.SearchComparator; +import org.hl7.fhir.r5.model.Enumerations.SearchModifierCode; +import org.hl7.fhir.r5.model.Enumerations.VersionIndependentResourceTypesAll; +import org.hl7.fhir.r5.model.OperationDefinition; +import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.r5.model.SearchParameter; +import org.hl7.fhir.r5.model.SearchParameter.SearchParameterComponentComponent; +import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupComponent; +import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupInputComponent; +import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupRuleComponent; +import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupRuleDependentComponent; +import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupRuleSourceComponent; +import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupRuleTargetComponent; +import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupRuleTargetParameterComponent; +import org.hl7.fhir.r5.model.StructureMap.StructureMapStructureComponent; +import org.hl7.fhir.r5.model.StructureMap.StructureMapTargetListMode; +import org.hl7.fhir.r5.model.StructureMap.StructureMapTransform; +import org.hl7.fhir.r5.model.StringType; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.model.StructureMap; +import org.hl7.fhir.r5.model.UriType; +import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupComponent; +import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent; +import org.hl7.fhir.r5.model.DataType; +import org.hl7.fhir.r5.renderers.utils.RenderingContext; +import org.hl7.fhir.r5.renderers.utils.RenderingContext.KnownLinkType; +import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext; +import org.hl7.fhir.r5.utils.EOperationOutcome; +import org.hl7.fhir.r5.utils.ToolingExtensions; +import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities; +import org.hl7.fhir.utilities.StandardsStatus; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.utilities.xhtml.XhtmlFluent; +import org.hl7.fhir.utilities.xhtml.XhtmlNode; + +public class StructureMapRenderer extends TerminologyRenderer { + + private static final String COLOR_COMMENT = "green"; + private static final String COLOR_METADATA = "#cc00cc"; + private static final String COLOR_CONST = "blue"; + private static final String COLOR_VARIABLE = "maroon"; + private static final String COLOR_SYNTAX = "navy"; + private static final boolean RENDER_MULTIPLE_TARGETS_ONELINE = true; + private static final String COLOR_SPECIAL = "#b36b00"; + private static final String DEFAULT_COMMENT = "This element was not defined prior to R5"; + + private String clauseComment = DEFAULT_COMMENT; + + public StructureMapRenderer(RenderingContext context) { + super(context); + } + + public StructureMapRenderer(RenderingContext context, ResourceContext rcontext) { + super(context, rcontext); + } + + public boolean render(XhtmlNode x, Resource dr) throws IOException, FHIRException, EOperationOutcome { + return render(x, (StructureMap) dr); + } + + public boolean render(XhtmlNode x, StructureMap map) throws IOException, FHIRException, EOperationOutcome { + renderMap(x.pre("fml"), map); + return false; + } + + public void renderMap(XhtmlNode x, StructureMap map) { + x.tx("\r\n"); + if (VersionUtilities.isR5Plus(context.getContext().getVersion())) { + renderMetadata(x, "url", map.getUrlElement()); + renderMetadata(x, "name", map.getNameElement()); + renderMetadata(x, "title", map.getTitleElement()); + renderMetadata(x, "url", map.getStatusElement(), "draft"); + x.tx("\r\n"); + } else { + x.b().tx("map"); + x.color(COLOR_SYNTAX).tx(" \""); + x.tx(map.getUrl()); + x.color(COLOR_SYNTAX).tx("\" = \""); + x.tx(Utilities.escapeJson(map.getName())); + x.color(COLOR_SYNTAX).tx("\"\r\n\r\n"); + if (map.getDescription() != null) { + renderMultilineDoco(x, map.getDescription(), 0, null); + x.tx("\r\n"); + } + } + renderConceptMaps(x, map); + renderUses(x, map); + renderImports(x, map); + for (StructureMapGroupComponent g : map.getGroup()) + renderGroup(x, g); + } + + private void renderMetadata(XhtmlNode x, String name, DataType value) { + renderMetadata(x, name, value, null); + } + + private void renderMetadata(XhtmlNode x, String name, DataType value, String def) { + String v = value.primitiveValue(); + if (def == null || !def.equals(v)) { + XhtmlNode c = x.color(COLOR_METADATA); + c.tx("/// "); + c.b().tx(name); + c.tx(" = "); + if (Utilities.existsInList(v, "true", "false") || Utilities.isDecimal(v, true)) { + x.color(COLOR_CONST).tx(v); + } else { + x.color(COLOR_CONST).tx("'"+v+"'"); + } + x.tx("\r\n"); + } + } + + private void renderConceptMaps(XhtmlNode x,StructureMap map) { + for (Resource r : map.getContained()) { + if (r instanceof ConceptMap) { + produceConceptMap(x, (ConceptMap) r); + } + } + } + + private void produceConceptMap(XhtmlNode x,ConceptMap cm) { + if (cm.hasFormatCommentPre()) { + renderMultilineDoco(x, cm.getFormatCommentsPre(), 0, null); + } + x.b().tx("conceptmap"); + x.color(COLOR_SYNTAX).tx(" \""); + x.tx(cm.getId()); + x.color(COLOR_SYNTAX).tx("\" {\r\n"); + Map prefixesSrc = new HashMap(); + Map prefixesTgt = new HashMap(); + char prefix = 's'; + for (ConceptMapGroupComponent cg : cm.getGroup()) { + if (!prefixesSrc.containsKey(cg.getSource())) { + prefixesSrc.put(cg.getSource(), String.valueOf(prefix)); + x.b().tx(" prefix "); + x.tx(prefix); + x.color(COLOR_SYNTAX).tx(" = \""); + x.tx(""+cg.getSource()); + x.color(COLOR_SYNTAX).tx("\"\r\n"); + prefix++; + } + if (!prefixesTgt.containsKey(cg.getTarget())) { + prefixesTgt.put(cg.getTarget(), String.valueOf(prefix)); + x.b().tx(" prefix "); + x.tx(prefix); + x.color(COLOR_SYNTAX).tx(" = \""); + x.tx(""+cg.getTarget()); + x.color(COLOR_SYNTAX).tx("\"\r\n"); + prefix++; + } + } + x.tx("\r\n"); + for (ConceptMapGroupComponent cg : cm.getGroup()) { + if (cg.hasUnmapped()) { + x.b().tx(" unmapped for "); + x.tx(prefixesSrc.get(cg.getSource())); + x.color(COLOR_SYNTAX).tx(" = "); + x.tx(cg.getUnmapped().getMode().toCode()); + x.tx("\r\n"); + } + } + + for (ConceptMapGroupComponent cg : cm.getGroup()) { + if (cg.hasFormatCommentPre()) { + renderMultilineDoco(x, cg.getFormatCommentsPre(), 2, prefixesSrc.values()); + } + for (SourceElementComponent ce : cg.getElement()) { + if (ce.hasFormatCommentPre()) { + renderMultilineDoco(x, ce.getFormatCommentsPre(), 2, prefixesSrc.values()); + } + + x.tx(" "); + x.tx(prefixesSrc.get(cg.getSource())); + x.color(COLOR_SYNTAX).tx(":"); + if (Utilities.isToken(ce.getCode())) { + x.tx(ce.getCode()); + } else { + x.tx("\""); + x.tx(ce.getCode()); + x.tx("\""); + } + x.tx(" "); + x.b().tx(getChar(ce.getTargetFirstRep().getRelationship())); + x.tx(" "); + x.tx(prefixesTgt.get(cg.getTarget())); + x.color(COLOR_SYNTAX).tx(":"); + if (Utilities.isToken(ce.getTargetFirstRep().getCode())) { + x.tx(ce.getTargetFirstRep().getCode()); + } else { + x.color(COLOR_SYNTAX).tx("\""); + x.tx(ce.getTargetFirstRep().getCode()); + x.color(COLOR_SYNTAX).tx("\""); + } + x.tx("\r\n"); + if (ce.hasFormatCommentPost()) { + renderMultilineDoco(x, ce.getFormatCommentsPost(), 2, prefixesSrc.values()); + } + } + if (cg.hasFormatCommentPost()) { + renderMultilineDoco(x, cg.getFormatCommentsPost(), 2, prefixesSrc.values()); + } + } + if (cm.hasFormatCommentPost()) { + renderMultilineDoco(x, cm.getFormatCommentsPost(), 2, prefixesSrc.values()); + } + x.color(COLOR_SYNTAX).tx("}\r\n\r\n"); + } + + private String getChar(ConceptMapRelationship relationship) { + switch (relationship) { + case RELATEDTO: + return "-"; + case EQUIVALENT: + return "=="; + case NOTRELATEDTO: + return "!="; + case SOURCEISNARROWERTHANTARGET: + return "<="; + case SOURCEISBROADERTHANTARGET: + return ">="; + default: + return "??"; + } + } + + private void renderUses(XhtmlNode x,StructureMap map) { + for (StructureMapStructureComponent s : map.getStructure()) { + x.b().tx("uses"); + x.color(COLOR_SYNTAX).tx(" \""); + x.tx(s.getUrl()); + x.color(COLOR_SYNTAX).tx("\" "); + if (s.hasAlias()) { + x.b().tx("alias "); + x.tx(s.getAlias()); + x.tx(" "); + } + x.b().tx("as "); + x.b().tx(s.getMode().toCode()); + renderDoco(x, s.getDocumentation(), false, null); + x.tx("\r\n"); + } + if (map.hasStructure()) + x.tx("\r\n"); + } + + private void renderImports(XhtmlNode x,StructureMap map) { + for (UriType s : map.getImport()) { + x.b().tx("imports"); + x.color(COLOR_SYNTAX).tx(" \""); + x.tx(s.getValue()); + x.color(COLOR_SYNTAX).tx("\"\r\n"); + } + if (map.hasImport()) + x.tx("\r\n"); + } + + private void renderGroup(XhtmlNode x,StructureMapGroupComponent g) { + Collection tokens = scanVariables(g, null); + if (g.hasFormatCommentPre()) { + renderMultilineDoco(x, g.getFormatCommentsPre(), 0, tokens); + } + if (g.hasDocumentation()) { + renderMultilineDoco(x, g.getDocumentation(), 0, tokens); + } + x.b().tx("group "); + x.tx(g.getName()); + x.color(COLOR_SYNTAX).tx("("); + boolean first = true; + for (StructureMapGroupInputComponent gi : g.getInput()) { + if (first) + first = false; + else + x.tx(", "); + x.b().tx(gi.getMode().toCode()); + x.tx(" "); + x.color(COLOR_VARIABLE).tx(gi.getName()); + if (gi.hasType()) { + x.color(COLOR_SYNTAX).tx(" : "); + x.tx(gi.getType()); + } + } + x.color(COLOR_SYNTAX).tx(")"); + if (g.hasExtends()) { + x.b().tx(" extends "); + x.tx(g.getExtends()); + } + + if (g.hasTypeMode()) { + switch (g.getTypeMode()) { + case TYPES: + x.b().tx(" <>"); + break; + case TYPEANDTYPES: + x.b().tx(" <>"); + break; + default: // NONE, NULL + } + } + x.color(COLOR_SYNTAX).tx(" {\r\n"); + for (StructureMapGroupRuleComponent r : g.getRule()) { + renderRule(x, g, r, 2); + } + if (g.hasFormatCommentPost()) { + renderMultilineDoco(x, g.getFormatCommentsPost(), 0, scanVariables(g, null)); + } + x.color(COLOR_SYNTAX).tx("}\r\n\r\n"); + } + + private void renderRule(XhtmlNode x, StructureMapGroupComponent g, StructureMapGroupRuleComponent r, int indent) { + Collection tokens = scanVariables(g, r); + if (r.hasFormatCommentPre()) { + renderMultilineDoco(x, r.getFormatCommentsPre(), indent, tokens); + } + for (int i = 0; i < indent; i++) + x.tx(" "); + boolean canBeAbbreviated = checkisSimple(r); + { + boolean first = true; + for (StructureMapGroupRuleSourceComponent rs : r.getSource()) { + if (first) + first = false; + else + x.color(COLOR_SYNTAX).tx(", "); + renderSource(x, rs, canBeAbbreviated); + } + } + if (r.getTarget().size() > 1) { + x.b().tx(" -> "); + boolean first = true; + for (StructureMapGroupRuleTargetComponent rt : r.getTarget()) { + if (first) + first = false; + else + x.color(COLOR_SYNTAX).tx(", "); + if (RENDER_MULTIPLE_TARGETS_ONELINE) + x.tx(" "); + else { + x.tx("\r\n"); + for (int i = 0; i < indent + 4; i++) + x.tx(" "); + } + renderTarget(x, rt, false); + } + } else if (r.hasTarget()) { + x.b().tx(" -> "); + renderTarget(x, r.getTarget().get(0), canBeAbbreviated); + } + if (r.hasRule()) { + x.b().tx(" then"); + x.color(COLOR_SYNTAX).tx(" {\r\n"); + for (StructureMapGroupRuleComponent ir : r.getRule()) { + renderRule(x, g, ir, indent + 2); + } + for (int i = 0; i < indent; i++) + x.tx(" "); + x.color(COLOR_SYNTAX).tx("}"); + } else { + if (r.hasDependent()) { + x.b().tx(" then "); + boolean first = true; + for (StructureMapGroupRuleDependentComponent rd : r.getDependent()) { + if (first) + first = false; + else + x.color(COLOR_SYNTAX).tx(", "); + x.tx(rd.getName()); + x.color(COLOR_SYNTAX).tx("("); + boolean ifirst = true; + for (StructureMapGroupRuleTargetParameterComponent rdp : rd.getParameter()) { + if (ifirst) + ifirst = false; + else + x.color(COLOR_SYNTAX).tx(", "); + renderTransformParam(x, rdp); + } + x.color(COLOR_SYNTAX).tx(")"); + } + } + } + if (r.hasName()) { + String n = ntail(r.getName()); + if (!n.startsWith("\"")) + n = "\"" + n + "\""; + if (!matchesName(n, r.getSource())) { + x.tx(" "); + x.i().tx(n); + } + } + x.color(COLOR_SYNTAX).tx(";"); + if (r.hasDocumentation()) { + renderDoco(x, r.getDocumentation(), false, null); + } + x.tx("\r\n"); + if (r.hasFormatCommentPost()) { + renderMultilineDoco(x, r.getFormatCommentsPost(), indent, tokens); + } + } + + private Collection scanVariables(StructureMapGroupComponent g, StructureMapGroupRuleComponent r) { + Set res = new HashSet<>(); + for (StructureMapGroupInputComponent input : g.getInput()) { + res.add(input.getName()); + } + if (r != null) { + for (StructureMapGroupRuleSourceComponent src : r.getSource()) { + if (src.hasVariable()) { + res.add(src.getVariable()); + } + } + } + return res; + } + + private boolean matchesName(String n, List source) { + if (source.size() != 1) + return false; + if (!source.get(0).hasElement()) + return false; + String s = source.get(0).getElement(); + if (n.equals(s) || n.equals("\"" + s + "\"")) + return true; + if (source.get(0).hasType()) { + s = source.get(0).getElement() + "-" + source.get(0).getType(); + return n.equals(s) || n.equals("\"" + s + "\""); + } + return false; + } + + private String ntail(String name) { + if (name == null) + return null; + if (name.startsWith("\"")) { + name = name.substring(1); + name = name.substring(0, name.length() - 1); + } + return "\"" + (name.contains(".") ? name.substring(name.lastIndexOf(".") + 1) : name) + "\""; + } + + private boolean checkisSimple(StructureMapGroupRuleComponent r) { + return + (r.getSource().size() == 1 && r.getSourceFirstRep().hasElement() && r.getSourceFirstRep().hasVariable()) && + (r.getTarget().size() == 1 && r.getTargetFirstRep().hasVariable() && (r.getTargetFirstRep().getTransform() == null || r.getTargetFirstRep().getTransform() == StructureMapTransform.CREATE) && r.getTargetFirstRep().getParameter().size() == 0) && + (r.getDependent().size() == 0) && (r.getRule().size() == 0); + } + + private void renderSource(XhtmlNode x,StructureMapGroupRuleSourceComponent rs, boolean abbreviate) { + x.tx(rs.getContext()); + if (rs.getContext().equals("@search")) { + x.color(COLOR_SYNTAX).tx("("); + x.tx(rs.getElement()); + x.color(COLOR_SYNTAX).tx(")"); + } else if (rs.hasElement()) { + x.tx("."); + x.tx(rs.getElement()); + } + if (rs.hasType()) { + x.color(COLOR_SYNTAX).tx(" : "); + x.tx(rs.getType()); + if (rs.hasMin()) { + x.tx(" "); + x.tx(rs.getMin()); + x.color(COLOR_SYNTAX).tx(".."); + x.tx(rs.getMax()); + } + } + + if (rs.hasListMode()) { + x.tx(" "); + x.tx(rs.getListMode().toCode()); + } + if (rs.hasDefaultValue()) { + x.b().tx(" default "); + x.tx("\"" + Utilities.escapeJson(rs.getDefaultValue()) + "\""); + } + if (!abbreviate && rs.hasVariable()) { + x.b().tx(" as "); + x.color(COLOR_VARIABLE).tx(rs.getVariable()); + } + if (rs.hasCondition()) { + x.b().tx(" where "); + x.tx(rs.getCondition()); + } + if (rs.hasCheck()) { + x.b().tx(" check "); + x.tx(rs.getCheck()); + } + if (rs.hasLogMessage()) { + x.b().tx(" log "); + x.tx(rs.getLogMessage()); + } + } + + private void renderTarget(XhtmlNode x,StructureMapGroupRuleTargetComponent rt, boolean abbreviate) { + if (rt.hasContext()) { + x.tx(rt.getContext()); + if (rt.hasElement()) { + x.tx("."); + x.tx(rt.getElement()); + } + } + if (!abbreviate && rt.hasTransform()) { + if (rt.hasContext()) + x.tx(" = "); + if (rt.getTransform() == StructureMapTransform.COPY && rt.getParameter().size() == 1) { + renderTransformParam(x, rt.getParameter().get(0)); + } else if (rt.getTransform() == StructureMapTransform.EVALUATE && rt.getParameter().size() == 1) { + x.color(COLOR_SYNTAX).tx("("); + x.tx(((StringType) rt.getParameter().get(0).getValue()).asStringValue()); + x.color(COLOR_SYNTAX).tx(")"); + } else if (rt.getTransform() == StructureMapTransform.EVALUATE && rt.getParameter().size() == 2) { + x.tx(rt.getTransform().toCode()); + x.color(COLOR_SYNTAX).tx("("); + x.tx(((IdType) rt.getParameter().get(0).getValue()).asStringValue()); + x.color(COLOR_SYNTAX).tx(", "); + x.tx(((StringType) rt.getParameter().get(1).getValue()).asStringValue()); + x.color(COLOR_SYNTAX).tx(")"); + } else { + x.b().tx(rt.getTransform().toCode()); + x.color(COLOR_SYNTAX).tx("("); + boolean first = true; + for (StructureMapGroupRuleTargetParameterComponent rtp : rt.getParameter()) { + if (first) + first = false; + else + x.color(COLOR_SYNTAX).tx(", "); + renderTransformParam(x, rtp); + } + x.color(COLOR_SYNTAX).tx(")"); + } + } + if (!abbreviate && rt.hasVariable()) { + x.b().tx(" as "); + x.color(COLOR_VARIABLE).tx(rt.getVariable()); + } + for (Enumeration lm : rt.getListMode()) { + x.tx(" "); + x.b().tx(lm.getValue().toCode()); + if (lm.getValue() == StructureMapTargetListMode.SHARE) { + x.tx(" "); + x.b().tx(rt.getListRuleId()); + } + } + } + + + private void renderTransformParam(XhtmlNode x,StructureMapGroupRuleTargetParameterComponent rtp) { + try { + if (rtp.hasValueBooleanType()) + x.color(COLOR_CONST).tx(rtp.getValueBooleanType().asStringValue()); + else if (rtp.hasValueDecimalType()) + x.color(COLOR_CONST).tx(rtp.getValueDecimalType().asStringValue()); + else if (rtp.hasValueIdType()) + x.color(COLOR_VARIABLE).tx(rtp.getValueIdType().asStringValue()); + else if (rtp.hasValueIntegerType()) + x.color(COLOR_CONST).tx(rtp.getValueIntegerType().asStringValue()); + else + x.color(COLOR_CONST).tx("'" + Utilities.escapeJava(rtp.getValueStringType().asStringValue()) + "'"); + } catch (FHIRException e) { + e.printStackTrace(); + x.tx("error!"); + } + } + + private void renderDoco(XhtmlNode x,String doco, boolean startLine, Collection tokens) { + if (Utilities.noString(doco)) + return; + if (!startLine) { + x.tx(" "); + } + boolean isClause = false; + String t = doco.trim().replace(" ", ""); + if (tokens != null) { + for (String s : tokens) { + if (t.startsWith(s+":") || t.startsWith(s+".") || t.startsWith(s+"->")) { + isClause = true; + break; + } + } + } + if (isClause) { + XhtmlNode s= x.color(COLOR_SPECIAL); + s.setAttribute("title", clauseComment ); + s.tx("// "); + s.tx(doco.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")); + } else { + x.color(COLOR_SYNTAX).tx("// "); + x.color(COLOR_COMMENT).tx(doco.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")); + } + } + + private void renderMultilineDoco(XhtmlNode x,String doco, int indent, Collection tokens) { + if (Utilities.noString(doco)) + return; + String[] lines = doco.split("\\r?\\n"); + for (String line : lines) { + for (int i = 0; i < indent; i++) + x.tx(" "); + renderDoco(x, line, true, tokens); + x.tx("\r\n"); + } + } + + private void renderMultilineDoco(XhtmlNode x, List doco, int indent, Collection tokens) { + for (String line : doco) { + for (int i = 0; i < indent; i++) + x.tx(" "); + renderDoco(x, line, true, tokens); + x.tx("\r\n"); + } + } + + + public void describe(XhtmlNode x, OperationDefinition opd) { + x.tx(display(opd)); + } + + public String display(OperationDefinition opd) { + return opd.present(); + } + + @Override + public String display(Resource r) throws UnsupportedEncodingException, IOException { + return ((StructureMap) r).present(); + } + +} diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRLexer.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRLexer.java index f7df5b03e..1b521b471 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRLexer.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/FHIRLexer.java @@ -315,18 +315,23 @@ public class FHIRLexer { private void skipWhitespaceAndComments() { comments.clear(); + commentLocation = null; boolean last13 = false; boolean done = false; while (cursor < source.length() && !done) { if (cursor < source.length() -1 && "//".equals(source.substring(cursor, cursor+2)) && !isMetadataStart()) { - commentLocation = currentLocation; + if (commentLocation == null) { + commentLocation = currentLocation.copy(); + } int start = cursor+2; while (cursor < source.length() && !((source.charAt(cursor) == '\r') || source.charAt(cursor) == '\n')) { cursor++; } comments.add(source.substring(start, cursor).trim()); } else if (cursor < source.length() - 1 && "/*".equals(source.substring(cursor, cursor+2))) { - commentLocation = currentLocation; + if (commentLocation == null) { + commentLocation = currentLocation.copy(); + } int start = cursor+2; while (cursor < source.length() - 1 && !"*/".equals(source.substring(cursor, cursor+2))) { last13 = currentLocation.checkChar(source.charAt(cursor), last13); @@ -569,5 +574,19 @@ public class FHIRLexer { public void setMetadataFormat(boolean metadataFormat) { this.metadataFormat = metadataFormat; } + public List cloneComments() { + List res = new ArrayList<>(); + res.addAll(getComments()); + return res; + } + public String tokenWithTrailingComment(String token) { + int line = getCurrentLocation().getLine(); + token(token); + if (getComments().size() > 0 && getCommentLocation().getLine() == line) { + return getFirstComment(); + } else { + return null; + } + } } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java index 7936e305a..c84a3dd2b 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/utils/structuremap/StructureMapUtilities.java @@ -71,6 +71,7 @@ import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.SourceLocation; import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.VersionUtilities; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; @@ -113,6 +114,7 @@ public class StructureMapUtilities { private final Map ids = new HashMap(); private ValidationOptions terminologyServiceOptions = new ValidationOptions(); private final ProfileUtilities profileUtilities; + private boolean exceptionsForChecks = true; public StructureMapUtilities(IWorkerContext worker, ITransformerServices services, ProfileKnowledgeProvider pkp) { super(); @@ -672,8 +674,9 @@ public class StructureMapUtilities { private void parseConceptMap(StructureMap result, FHIRLexer lexer) throws FHIRLexerException { - lexer.token("conceptmap"); ConceptMap map = new ConceptMap(); + map.addFormatCommentsPre(lexer.getComments()); + lexer.token("conceptmap"); String id = lexer.readConstant("map id"); if (id.startsWith("#")) throw lexer.error("Concept Map identifier must start with #"); @@ -694,10 +697,12 @@ public class StructureMapUtilities { prefixes.put(n, v); } while (lexer.hasToken("unmapped")) { + List comments = lexer.cloneComments(); lexer.token("unmapped"); lexer.token("for"); String n = readPrefix(prefixes, lexer); ConceptMapGroupComponent g = getGroup(map, n, null); + g.addFormatCommentsPre(comments); lexer.token("="); String v = lexer.take(); if (v.equals("provided")) { @@ -706,6 +711,7 @@ public class StructureMapUtilities { throw lexer.error("Only unmapped mode PROVIDED is supported at this time"); } while (!lexer.hasToken("}")) { + List comments = lexer.cloneComments(); String srcs = readPrefix(prefixes, lexer); lexer.token(":"); String sc = lexer.getCurrent().startsWith("\"") ? lexer.readConstant("code") : lexer.take(); @@ -713,17 +719,21 @@ public class StructureMapUtilities { String tgts = readPrefix(prefixes, lexer); ConceptMapGroupComponent g = getGroup(map, srcs, tgts); SourceElementComponent e = g.addElement(); + e.addFormatCommentsPre(comments); e.setCode(sc); - if (e.getCode().startsWith("\"")) + if (e.getCode().startsWith("\"")) { e.setCode(lexer.processConstant(e.getCode())); + } TargetElementComponent tgt = e.addTarget(); tgt.setRelationship(rel); lexer.token(":"); tgt.setCode(lexer.take()); - if (tgt.getCode().startsWith("\"")) + if (tgt.getCode().startsWith("\"")) { tgt.setCode(lexer.processConstant(tgt.getCode())); - tgt.setComment(lexer.getFirstComment()); + } + // tgt.setComment(lexer.getAllComments()); } + map.addFormatCommentsPost(lexer.getComments()); lexer.token("}"); } @@ -781,7 +791,7 @@ public class StructureMapUtilities { lexer.token("as"); st.setMode(StructureMapModelMode.fromCode(lexer.take())); lexer.skipToken(";"); - st.setDocumentation(lexer.getFirstComment()); + st.setDocumentation(lexer.getAllComments()); } @@ -859,6 +869,7 @@ public class StructureMapUtilities { parseRule(result, group.getRule(), lexer, false); } } + group.addFormatCommentsPost(lexer.getComments()); lexer.next(); if (newFmt && lexer.hasToken(";")) lexer.next(); @@ -880,7 +891,7 @@ public class StructureMapUtilities { if (!newFmt) { lexer.token("as"); input.setMode(StructureMapInputMode.fromCode(lexer.take())); - input.setDocumentation(lexer.getFirstComment()); + input.setDocumentation(lexer.getAllComments()); lexer.skipToken(";"); } } @@ -894,7 +905,7 @@ public class StructureMapUtilities { lexer.token(":"); lexer.token("for"); } else { - rule.setDocumentation(lexer.getFirstComment()); + rule.addFormatCommentsPre(lexer.getComments()); } list.add(rule); boolean done = false; @@ -934,9 +945,6 @@ public class StructureMapUtilities { } } } - if (!rule.hasDocumentation() && lexer.hasComments()) { - rule.setDocumentation(lexer.getFirstComment()); - } if (isSimpleSyntax(rule)) { rule.getSourceFirstRep().setVariable(AUTO_VAR_NAME); rule.getTargetFirstRep().setVariable(AUTO_VAR_NAME); @@ -951,14 +959,17 @@ public class StructureMapUtilities { rule.setName(lexer.take()); } } else { - if (rule.getSource().size() != 1 || !rule.getSourceFirstRep().hasElement()) + if (rule.getSource().size() != 1 || !rule.getSourceFirstRep().hasElement() && exceptionsForChecks ) throw lexer.error("Complex rules must have an explicit name"); if (rule.getSourceFirstRep().hasType()) rule.setName(rule.getSourceFirstRep().getElement() + "-" + rule.getSourceFirstRep().getType()); else rule.setName(rule.getSourceFirstRep().getElement()); } - lexer.token(";"); + String doco = lexer.tokenWithTrailingComment(";"); + if (doco != null) { + rule.setDocumentation(doco); + } } } @@ -2640,4 +2651,12 @@ public class StructureMapUtilities { this.terminologyServiceOptions = terminologyServiceOptions; } + public boolean isExceptionsForChecks() { + return exceptionsForChecks; + } + + public void setExceptionsForChecks(boolean exceptionsForChecks) { + this.exceptionsForChecks = exceptionsForChecks; + } + } \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/NarrativeGenerationTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/NarrativeGenerationTests.java index 7947db473..f0e4f46ce 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/NarrativeGenerationTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/NarrativeGenerationTests.java @@ -34,6 +34,7 @@ import org.hl7.fhir.r5.renderers.utils.RenderingContext.StructureDefinitionRende import org.hl7.fhir.r5.test.utils.CompareUtilities; import org.hl7.fhir.r5.test.utils.TestPackageLoader; import org.hl7.fhir.r5.test.utils.TestingUtilities; +import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities; import org.hl7.fhir.utilities.TerminologyServiceOptions; import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; @@ -153,6 +154,7 @@ public class NarrativeGenerationTests { private String id; private String sdmode; private boolean header; + private boolean pretty; private boolean meta; private boolean technical; private String register; @@ -169,6 +171,7 @@ public class NarrativeGenerationTests { register = null; } header = "true".equals(test.getAttribute("header")); + pretty = !"false".equals(test.getAttribute("pretty")); meta = "true".equals(test.getAttribute("meta")); technical = "technical".equals(test.getAttribute("mode")); } @@ -247,13 +250,15 @@ public class NarrativeGenerationTests { Resource source; if (TestingUtilities.findTestResource("r5", "narrative", test.getId() + ".json")) { source = (Resource) new JsonParser().parse(TestingUtilities.loadTestResourceStream("r5", "narrative", test.getId() + ".json")); + } else if (TestingUtilities.findTestResource("r5", "narrative", test.getId() + ".fml")) { + source = (Resource) new StructureMapUtilities(context).parse(TextFile.streamToString(TestingUtilities.loadTestResourceStream("r5", "narrative", test.getId() + ".fml")), "source"); } else { source = (Resource) new XmlParser().parse(TestingUtilities.loadTestResourceStream("r5", "narrative", test.getId() + ".xml")); } XhtmlNode x = RendererFactory.factory(source, rc).build(source); String expected = TextFile.streamToString(TestingUtilities.loadTestResourceStream("r5", "narrative", test.getId() + ".html")); - String actual = HEADER+new XhtmlComposer(true, true).compose(x)+FOOTER; + String actual = HEADER+new XhtmlComposer(true, test.pretty).compose(x)+FOOTER; String expectedFileName = CompareUtilities.tempFile("narrative", test.getId() + ".expected.html"); String actualFileName = CompareUtilities.tempFile("narrative", test.getId() + ".actual.html"); TextFile.stringToFile(expected, expectedFileName); diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/SourceLocation.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/SourceLocation.java index 93c15dacb..6287442fb 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/SourceLocation.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/SourceLocation.java @@ -43,4 +43,7 @@ public class SourceLocation { return false; } } + public SourceLocation copy() { + return new SourceLocation(line, column); + } } 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 1d398f576..9537c7daa 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 @@ -765,6 +765,10 @@ public class XhtmlNode extends XhtmlFluent implements IBaseXhtml { this.getChildNodes().addAll(childNodes); } + + public XhtmlNode color(String color) { + return span("color: "+color, null); + } } \ No newline at end of file