001package org.hl7.fhir.r5.conformance;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.HashMap;
006import java.util.List;
007import java.util.Set;
008
009import org.hl7.fhir.exceptions.DefinitionException;
010import org.hl7.fhir.exceptions.FHIRFormatError;
011import org.hl7.fhir.r5.conformance.profile.BindingResolution;
012import org.hl7.fhir.r5.conformance.profile.ProfileKnowledgeProvider;
013import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
014import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingAdditionalComponent;
015import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingComponent;
016import org.hl7.fhir.r5.model.Coding;
017import org.hl7.fhir.r5.model.ElementDefinition;
018import org.hl7.fhir.r5.model.Extension;
019import org.hl7.fhir.r5.model.PrimitiveType;
020import org.hl7.fhir.r5.model.StructureDefinition;
021import org.hl7.fhir.r5.model.UsageContext;
022import org.hl7.fhir.r5.renderers.CodeResolver;
023import org.hl7.fhir.r5.renderers.CodeResolver.CodeResolution;
024import org.hl7.fhir.r5.renderers.DataRenderer;
025import org.hl7.fhir.r5.renderers.IMarkdownProcessor;
026import org.hl7.fhir.r5.renderers.utils.RenderingContext;
027import org.hl7.fhir.r5.utils.PublicationHacker;
028import org.hl7.fhir.r5.utils.ToolingExtensions;
029import org.hl7.fhir.utilities.MarkDownProcessor;
030import org.hl7.fhir.utilities.Utilities;
031import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
032import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell;
033import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece;
034import org.hl7.fhir.utilities.xhtml.NodeType;
035import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
036import org.hl7.fhir.utilities.xhtml.XhtmlNode;
037import org.hl7.fhir.utilities.xhtml.XhtmlNodeList;
038
039public class AdditionalBindingsRenderer {
040  public class AdditionalBindingDetail {
041    private String purpose;
042    private String valueSet;
043    private String doco;
044    private String docoShort;
045    private UsageContext usage;
046    private boolean any = false;
047    private boolean isUnchanged = false;
048    private boolean matched = false;
049    private boolean removed = false;
050    private AdditionalBindingDetail compare;
051    private int count = 1;
052    private String getKey() {
053      // Todo: Consider extending this with content from usageContext if purpose isn't sufficiently differentiating
054      return purpose + Integer.toString(count);
055    }
056    private void incrementCount() {
057      count++;
058    }
059    private void setCompare(AdditionalBindingDetail match) {
060      compare = match;
061      match.matched = true;
062    }
063    private boolean alreadyMatched() {
064      return matched;
065    }
066    public String getDoco(boolean full) {
067      return full ? doco : docoShort;
068    }
069    public boolean unchanged() {
070      if (!isUnchanged)
071        return false;
072      if (compare==null)
073        return true;
074      isUnchanged = true;
075      isUnchanged = isUnchanged && ((purpose==null && compare.purpose==null) || purpose.equals(compare.purpose));
076      isUnchanged = isUnchanged && ((valueSet==null && compare.valueSet==null) || valueSet.equals(compare.valueSet));
077      isUnchanged = isUnchanged && ((doco==null && compare.doco==null) || doco.equals(compare.doco));
078      isUnchanged = isUnchanged && ((usage==null && compare.usage==null) || usage.equals(compare.usage));
079      return isUnchanged;
080    }
081  }
082
083  private static String STYLE_UNCHANGED = "opacity: 0.5;";
084  private static String STYLE_REMOVED = STYLE_UNCHANGED + "text-decoration: line-through;";
085
086  private List<AdditionalBindingDetail> bindings = new ArrayList<>();
087  private ProfileKnowledgeProvider pkp;
088  private String corePath;
089  private StructureDefinition profile;
090  private String path;
091  private RenderingContext context;
092  private IMarkdownProcessor md;
093  private CodeResolver cr;
094
095  public AdditionalBindingsRenderer(ProfileKnowledgeProvider pkp, String corePath, StructureDefinition profile, String path, RenderingContext context, IMarkdownProcessor md, CodeResolver cr) {
096    this.pkp = pkp;
097    this.corePath = corePath;
098    this.profile = profile;
099    this.path = path;
100    this.context = context;
101    this.md = md;
102    this.cr = cr;
103  }
104
105  public void seeMaxBinding(Extension ext) {
106    seeMaxBinding(ext, null, false);
107  }
108
109  public void seeMaxBinding(Extension ext, Extension compExt, boolean compare) {
110    seeBinding(ext, compExt, compare, "maximum");
111  }
112
113  protected void seeBinding(Extension ext, Extension compExt, boolean compare, String label) {
114    AdditionalBindingDetail abr = new AdditionalBindingDetail();
115    abr.purpose =  label;
116    abr.valueSet =  ext.getValue().primitiveValue();
117    if (compare) {
118      abr.isUnchanged = compExt!=null && ext.getValue().primitiveValue().equals(compExt.getValue().primitiveValue());
119
120      abr.compare = new AdditionalBindingDetail();
121      abr.compare.valueSet = compExt==null ? null : compExt.getValue().primitiveValue();
122    } else {
123      abr.isUnchanged = ext.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS);
124    }
125    bindings.add(abr);
126  }
127
128  public void seeMinBinding(Extension ext) {
129    seeMinBinding(ext, null, false);
130  }
131
132  public void seeMinBinding(Extension ext, Extension compExt, boolean compare) {
133    seeBinding(ext, compExt, compare, "minimum");
134  }
135
136  public void seeAdditionalBindings(List<Extension> list) {
137    seeAdditionalBindings(list, null, false);
138  }
139
140  public void seeAdditionalBindings(List<Extension> list, List<Extension> compList, boolean compare) {
141    HashMap<String, AdditionalBindingDetail> compBindings = new HashMap<String, AdditionalBindingDetail>();
142    if (compare && compList!=null) {
143      for (Extension ext : compList) {
144        AdditionalBindingDetail abr = additionalBinding(ext);
145        if (compBindings.containsKey(abr.getKey())) {
146          abr.incrementCount();
147        }
148        compBindings.put(abr.getKey(), abr);
149      }
150    }
151
152    for (Extension ext : list) {
153      AdditionalBindingDetail abr = additionalBinding(ext);
154      if (compare && compList!=null) {
155        AdditionalBindingDetail match = null;
156        do {
157          match = compBindings.get(abr.getKey());
158          if (abr.alreadyMatched())
159            abr.incrementCount();
160        } while (match!=null && abr.alreadyMatched());
161        if (match!=null)
162          abr.setCompare(match);
163        bindings.add(abr);
164        if (abr.compare!=null)
165          compBindings.remove(abr.compare.getKey());
166      } else
167        bindings.add(abr);
168    }
169    for (AdditionalBindingDetail b: compBindings.values()) {
170      b.removed = true;
171      bindings.add(b);
172    }
173  }
174
175  protected AdditionalBindingDetail additionalBinding(Extension ext) {
176    AdditionalBindingDetail abr = new AdditionalBindingDetail();
177    abr.purpose =  ext.getExtensionString("purpose");
178    abr.valueSet =  ext.getExtensionString("valueSet");
179    abr.doco =  ext.getExtensionString("documentation");
180      abr.docoShort =  ext.getExtensionString("shortDoco");
181    abr.usage =  (ext.hasExtension("usage")) && ext.getExtensionByUrl("usage").hasValueUsageContext() ? ext.getExtensionByUrl("usage").getValueUsageContext() : null;
182    abr.any = "any".equals(ext.getExtensionString("scope"));
183    abr.isUnchanged = ext.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS);
184    return abr;
185  }
186
187  public String render() throws IOException {
188    if (bindings.isEmpty()) {
189      return "";
190    } else {
191      XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table");
192      tbl.attribute("class", "grid");
193      render(tbl.getChildNodes(), true);
194      return new XhtmlComposer(false).compose(tbl);
195    }
196  }
197
198  public void render(HierarchicalTableGenerator gen, Cell c) throws FHIRFormatError, DefinitionException, IOException {
199    if (bindings.isEmpty()) {
200      return;
201    } else {
202      Piece piece = gen.new Piece("table").attr("class", "grid");
203      c.getPieces().add(piece);
204      render(piece.getChildren(), false);
205    }
206  }
207  
208  public void render(List<XhtmlNode> children, boolean fullDoco) throws FHIRFormatError, DefinitionException, IOException {
209    boolean doco = false;
210    boolean usage = false;
211    boolean any = false;
212    for (AdditionalBindingDetail binding : bindings) {
213      doco = doco || binding.getDoco(fullDoco)!=null  || (binding.compare!=null && binding.compare.getDoco(fullDoco)!=null);
214      usage = usage || binding.usage != null || (binding.compare!=null && binding.compare.usage!=null);
215      any = any || binding.any || (binding.compare!=null && binding.compare.any);
216    }
217
218    XhtmlNode tr = new XhtmlNode(NodeType.Element, "tr");
219    children.add(tr);
220    tr.td().style("font-size: 11px").b().tx("Additional Bindings");
221    tr.td().style("font-size: 11px").tx("Purpose");
222    if (usage) {
223      tr.td().style("font-size: 11px").tx("Usage");
224    }
225    if (any) {
226      tr.td().style("font-size: 11px").tx("Any");
227    }
228    if (doco) {
229      tr.td().style("font-size: 11px").tx("Documentation");
230    }
231    for (AdditionalBindingDetail binding : bindings) {
232      tr =  new XhtmlNode(NodeType.Element, "tr");
233      if (binding.unchanged()) {
234        tr.style(STYLE_REMOVED);
235      } else if (binding.removed) {
236        tr.style(STYLE_REMOVED);
237      }
238      children.add(tr);
239      BindingResolution br = pkp == null ? makeNullBr(binding) : pkp.resolveBinding(profile, binding.valueSet, path);
240      BindingResolution compBr = null;
241      if (binding.compare!=null  && binding.compare.valueSet!=null)
242        compBr = pkp == null ? makeNullBr(binding.compare) : pkp.resolveBinding(profile, binding.compare.valueSet, path);
243
244      XhtmlNode valueset = tr.td().style("font-size: 11px");
245      if (binding.compare!=null && binding.valueSet.equals(binding.compare.valueSet))
246        valueset.style(STYLE_UNCHANGED);
247      if (br.url != null) {
248        valueset.ah(determineUrl(br.url), binding.valueSet).tx(br.display);
249      } else {
250        valueset.span(null, binding.valueSet).tx(br.display);
251      }
252      if (binding.compare!=null && binding.compare.valueSet!=null && !binding.valueSet.equals(binding.compare.valueSet)) {
253        valueset.br();
254        valueset = valueset.span(STYLE_REMOVED, null);
255        if (compBr.url != null) {
256          valueset.ah(determineUrl(compBr.url), binding.compare.valueSet).tx(compBr.display);
257        } else {
258          valueset.span(null, binding.compare.valueSet).tx(compBr.display);
259        }
260      }
261
262      XhtmlNode purpose = tr.td().style("font-size: 11px");
263      if (binding.compare!=null && binding.purpose.equals(binding.compare.purpose))
264        purpose.style("font-color: darkgray");
265      renderPurpose(purpose, binding.purpose);
266      if (binding.compare!=null && binding.compare.purpose!=null && !binding.purpose.equals(binding.compare.purpose)) {
267        purpose.br();
268        purpose = purpose.span(STYLE_UNCHANGED, null);
269        renderPurpose(purpose, binding.compare.purpose);
270      }
271      if (usage) {
272        if (binding.usage != null) {
273          // TODO: This isn't rendered at all yet.  Ideally, we want it to render with comparison...
274          new DataRenderer(context).render(tr.td(), binding.usage);
275        } else {
276          tr.td();          
277        }
278      }
279      if (any) {
280        String newRepeat = binding.any ? "Any repeats" : "All repeats";
281        String oldRepeat = binding.compare!=null && binding.compare.any ? "Any repeats" : "All repeats";
282        compareString(tr.td().style("font-size: 11px"), newRepeat, oldRepeat);
283      }
284      if (doco) {
285        if (binding.doco != null) {
286          String d = fullDoco ? md.processMarkdown("Binding.description", binding.doco) : binding.docoShort;
287          String oldD = binding.compare==null ? null : fullDoco ? md.processMarkdown("Binding.description.compare", binding.compare.doco) : binding.compare.docoShort;
288          tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD));
289        } else {
290          tr.td().style("font-size: 11px");
291        }
292      }
293    }
294  }
295
296  private XhtmlNode compareString(XhtmlNode node, String newS, String oldS) {
297    if (oldS==null)
298      return node.tx(newS);
299    if (newS.equals(oldS))
300      return node.style(STYLE_UNCHANGED).tx(newS);
301    node.tx(newS);
302    node.br();
303    return node.span(STYLE_REMOVED,null).tx(oldS);
304  }
305
306  private String compareHtml(String newS, String oldS) {
307    if (oldS==null)
308      return newS;
309    if (newS.equals(oldS))
310      return "<span style=\"" + STYLE_UNCHANGED + "\">" + newS + "</span>";
311    return newS + "<br/><span style=\"" + STYLE_REMOVED + "\">" + oldS + "</span>";
312  }
313
314  private String determineUrl(String url) {
315    return Utilities.isAbsoluteUrl(url) || !pkp.prependLinks() ? url : corePath + url;
316  }
317
318  private void renderPurpose(XhtmlNode td, String purpose) {
319    switch (purpose) {
320    case "maximum": 
321      td.ah(corePath+"extension-elementdefinition-maxvalueset.html", "A required binding, for use when the binding strength is 'extensible' or 'preferred'").tx("Max Binding");
322      break;
323    case "minimum": 
324      td.ah(corePath+"extension-elementdefinition-minvalueset.html", "The minimum allowable value set - any conformant system SHALL support all these codes").tx("Min Binding");
325      break;
326    case "required" :
327      td.ah(corePath+"terminologies.html#strength", "Validators will check this binding (strength = required)").tx("Validation Binding");
328      break;
329    case "extensible" :
330      td.ah(corePath+"terminologies.html#strength", "Validators will check this binding (strength = extensible)").tx("Validation Binding");
331      break;
332    case "candidate" :
333      td.ah(corePath+"terminologies.html#strength", "This is a candidate binding that constraints on this profile may consider (see doco)").tx("Candidate Validation Binding");
334      break;
335    case "current" :
336      td.span(null, "New records are required to use this value set, but legacy records may use other codes").tx("Required");
337      break;
338    case "preferred" :
339      td.span(null, "This is the value set that is recommended (documentation should explain why)").tx("Recommended");
340      break;
341    case "ui" :
342      td.span(null, "This value set is provided to user look up in a given context").tx("UI");
343      break;
344    case "starter" :
345      td.span(null, "This value set is a good set of codes to start with when designing your system").tx("Starter");
346      break;
347    case "component" :
348      td.span(null, "This value set is a component of the base value set").tx("Component");
349      break;
350    default:  
351      td.span(null, "Unknown code for purpose").tx(purpose);
352    }
353  }
354
355  private BindingResolution makeNullBr(AdditionalBindingDetail binding) {
356    BindingResolution br = new BindingResolution();
357    br.url = "http://none.none/none";
358    br.display = "todo";
359    return br;
360  }
361
362  public boolean hasBindings() {
363    return !bindings.isEmpty();
364  }
365
366  public void render(XhtmlNodeList children, List<ElementDefinitionBindingAdditionalComponent> list) {
367    if (list.size() == 1) {
368      render(children, list.get(0));
369    } else {
370      XhtmlNode ul = children.ul();
371      for (ElementDefinitionBindingAdditionalComponent b : list) {
372        render(ul.li().getChildNodes(), b);
373      }
374    }
375  }
376
377  private void render(XhtmlNodeList children, ElementDefinitionBindingAdditionalComponent b) {
378    if (b.getValueSet() == null) {
379      return; // what should happen?
380    }
381    BindingResolution br = pkp.resolveBinding(profile, b.getValueSet(), corePath);
382    XhtmlNode a = children.ah(br.url == null ? null : Utilities.isAbsoluteUrl(br.url) || !context.getPkp().prependLinks() ? br.url : corePath+br.url, b.hasDocumentation() ? b.getDocumentation() : null);
383    if (b.hasDocumentation()) {
384      a.attribute("title", b.getDocumentation());
385    } 
386    a.tx(br.display);
387
388    if (b.hasShortDoco()) {
389      children.tx(": ");
390      children.tx(b.getShortDoco());
391    } 
392    if (b.getAny() || b.hasUsage()) {
393      children.tx(" (");
394      boolean ffirst = !b.getAny();
395      if (b.getAny()) {
396        children.tx("any repeat");
397      }
398      for (UsageContext uc : b.getUsage()) {
399        if (ffirst) ffirst = false; else children.tx(",");
400        if (!uc.getCode().is("http://terminology.hl7.org/CodeSystem/usage-context-type", "jurisdiction")) {
401          children.tx(displayForUsage(uc.getCode()));
402          children.tx("=");
403        }
404        CodeResolution ccr = cr.resolveCode(uc.getValueCodeableConcept());
405        children.ah(ccr.getLink(), ccr.getHint()).tx(ccr.getDisplay());
406      }
407      children.tx(")");
408    }
409  }
410
411  
412  private String displayForUsage(Coding c) {
413    if (c.hasDisplay()) {
414      return c.getDisplay();
415    }
416    if ("http://terminology.hl7.org/CodeSystem/usage-context-type".equals(c.getSystem())) {
417      return c.getCode();
418    }
419    return c.getCode();
420  }
421
422}