001package org.hl7.fhir.r5.renderers;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.HashMap;
006import java.util.List;
007
008import org.hl7.fhir.exceptions.DefinitionException;
009import org.hl7.fhir.exceptions.FHIRFormatError;
010import org.hl7.fhir.r5.conformance.profile.BindingResolution;
011import org.hl7.fhir.r5.conformance.profile.ProfileKnowledgeProvider;
012import org.hl7.fhir.r5.model.Coding;
013import org.hl7.fhir.r5.model.ElementDefinition;
014import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingAdditionalComponent;
015import org.hl7.fhir.r5.model.Extension;
016import org.hl7.fhir.r5.model.StructureDefinition;
017import org.hl7.fhir.r5.model.UsageContext;
018import org.hl7.fhir.r5.model.ValueSet;
019import org.hl7.fhir.r5.renderers.CodeResolver.CodeResolution;
020import org.hl7.fhir.r5.renderers.Renderer.RenderingStatus;
021import org.hl7.fhir.r5.renderers.utils.RenderingContext;
022import org.hl7.fhir.r5.utils.UserDataNames;
023import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
024import org.hl7.fhir.utilities.Utilities;
025import org.hl7.fhir.utilities.VersionUtilities;
026import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
027import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell;
028import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece;
029import org.hl7.fhir.utilities.xhtml.NodeType;
030import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
031import org.hl7.fhir.utilities.xhtml.XhtmlNode;
032import org.hl7.fhir.utilities.xhtml.XhtmlNodeList;
033
034@MarkedToMoveToAdjunctPackage
035public class AdditionalBindingsRenderer {
036  public class AdditionalBindingDetail {
037    private String purpose;
038    private String valueSet;
039    private String doco;
040    private String docoShort;
041    private List<UsageContext> usages = new ArrayList<UsageContext>();
042    private boolean any = false;
043    private boolean isUnchanged = false;
044    private boolean matched = false;
045    private boolean removed = false;
046//    private ValueSet vs;
047    
048    private AdditionalBindingDetail compare;
049    private int count = 1;
050    private String getKey() {
051      // Todo: Consider extending this with content from usageContext if purpose isn't sufficiently differentiating
052      return purpose + Integer.toString(count);
053    }
054    private void incrementCount() {
055      count++;
056    }
057    private void setCompare(AdditionalBindingDetail match) {
058      compare = match;
059      match.matched = true;
060    }
061    private boolean alreadyMatched() {
062      return matched;
063    }
064    public String getDoco(boolean full) {
065      return full ? doco : docoShort;
066    }
067    public boolean unchanged() {
068      if (!isUnchanged)
069        return false;
070      if (compare==null)
071        return true;
072      isUnchanged = true;
073      isUnchanged = isUnchanged && ((purpose==null && compare.purpose==null) || purpose.equals(compare.purpose));
074      isUnchanged = isUnchanged && ((valueSet==null && compare.valueSet==null) || valueSet.equals(compare.valueSet));
075      isUnchanged = isUnchanged && ((doco==null && compare.doco==null) || doco.equals(compare.doco));
076      isUnchanged = isUnchanged && ((docoShort==null && compare.docoShort==null) || docoShort.equals(compare.docoShort));
077      isUnchanged = isUnchanged && ((usages==null && compare.usages==null) || usages.equals(compare.usages));
078      return isUnchanged;
079    }
080  }
081
082  private static String STYLE_UNCHANGED = "opacity: 0.5;";
083  private static String STYLE_REMOVED = STYLE_UNCHANGED + "text-decoration: line-through;";
084
085  private List<AdditionalBindingDetail> bindings = new ArrayList<>();
086  private ProfileKnowledgeProvider pkp;
087  private String corePath;
088  private StructureDefinition profile;
089  private String path;
090  private RenderingContext context;
091  private IMarkdownProcessor md;
092  private CodeResolver cr;
093
094  public AdditionalBindingsRenderer(ProfileKnowledgeProvider pkp, String corePath, StructureDefinition profile, String path, RenderingContext context, IMarkdownProcessor md, CodeResolver cr) {
095    this.pkp = pkp;
096    this.corePath = corePath;
097    this.profile = profile;
098    this.path = path;
099    this.context = context;
100    this.md = md;
101    this.cr = cr;
102  }
103
104  public void seeMaxBinding(Extension ext) {
105    seeMaxBinding(ext, null, false);
106  }
107
108  public void seeMaxBinding(Extension ext, Extension compExt, boolean compare) {
109    seeBinding(ext, compExt, compare, "maximum");
110  }
111
112  protected void seeBinding(Extension ext, Extension compExt, boolean compare, String label) {
113    AdditionalBindingDetail abr = new AdditionalBindingDetail();
114    abr.purpose =  label;
115    abr.valueSet =  ext.getValue().primitiveValue();
116    if (compare) {
117      abr.isUnchanged = compExt!=null && ext.getValue().primitiveValue().equals(compExt.getValue().primitiveValue());
118
119      abr.compare = new AdditionalBindingDetail();
120      abr.compare.valueSet = compExt==null ? null : compExt.getValue().primitiveValue();
121    } else {
122      abr.isUnchanged = ext.hasUserData(UserDataNames.SNAPSHOT_DERIVATION_EQUALS);
123    }
124    bindings.add(abr);
125  }
126
127  public void seeMinBinding(Extension ext) {
128    seeMinBinding(ext, null, false);
129  }
130
131  public void seeMinBinding(Extension ext, Extension compExt, boolean compare) {
132    seeBinding(ext, compExt, compare, "minimum");
133  }
134
135  public void seeAdditionalBindings(List<Extension> list) {
136    seeAdditionalBindings(list, null, false);
137  }
138
139  public void seeAdditionalBindings(List<Extension> list, List<Extension> compList, boolean compare) {
140    HashMap<String, AdditionalBindingDetail> compBindings = new HashMap<String, AdditionalBindingDetail>();
141    if (compare && compList!=null) {
142      for (Extension ext : compList) {
143        AdditionalBindingDetail abr = additionalBinding(ext);
144        if (compBindings.containsKey(abr.getKey())) {
145          abr.incrementCount();
146        }
147        compBindings.put(abr.getKey(), abr);
148      }
149    }
150
151    for (Extension ext : list) {
152      AdditionalBindingDetail abr = additionalBinding(ext);
153      if (compare && compList!=null) {
154        AdditionalBindingDetail match = null;
155        do {
156          match = compBindings.get(abr.getKey());
157          if (abr.alreadyMatched())
158            abr.incrementCount();
159        } while (match!=null && abr.alreadyMatched());
160        if (match!=null)
161          abr.setCompare(match);
162        bindings.add(abr);
163        if (abr.compare!=null)
164          compBindings.remove(abr.compare.getKey());
165      } else
166        bindings.add(abr);
167    }
168    for (AdditionalBindingDetail b: compBindings.values()) {
169      b.removed = true;
170      bindings.add(b);
171    }
172  }
173
174  protected AdditionalBindingDetail additionalBinding(Extension ext) {
175    AdditionalBindingDetail abr = new AdditionalBindingDetail();
176    abr.purpose =  ext.getExtensionString("purpose");
177    abr.valueSet =  ext.getExtensionString("valueSet");
178    abr.doco =  ext.getExtensionString("documentation");
179    abr.docoShort =  ext.getExtensionString("shortDoco");
180    for (Extension x : ext.getExtensionsByUrl("usage")) {
181      if (x.hasValueUsageContext()) {
182        abr.usages.add(x.getValueUsageContext());
183      }
184    }
185    abr.any = "any".equals(ext.getExtensionString("scope"));
186    abr.isUnchanged = ext.hasUserData(UserDataNames.SNAPSHOT_DERIVATION_EQUALS);
187    return abr;
188  }
189
190  protected AdditionalBindingDetail additionalBinding(ElementDefinitionBindingAdditionalComponent ab) {
191    AdditionalBindingDetail abr = new AdditionalBindingDetail();
192    abr.purpose =  ab.getPurpose().toCode();
193    abr.valueSet =  ab.getValueSet();
194    abr.doco =  ab.getDocumentation();
195    abr.docoShort =  ab.getShortDoco();
196    abr.usages.addAll(ab.getUsage());
197    abr.any = ab.getAny();
198    abr.isUnchanged = ab.hasUserData(UserDataNames.SNAPSHOT_DERIVATION_EQUALS);
199    return abr;
200  }
201
202  public String render() throws IOException {
203    if (bindings.isEmpty()) {
204      return "";
205    } else {
206      XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table");
207      tbl.attribute("class", "grid");
208      render(tbl.getChildNodes(), true);
209      return new XhtmlComposer(false).compose(tbl);
210    }
211  }
212
213  public void render(HierarchicalTableGenerator gen, Cell c) throws FHIRFormatError, DefinitionException, IOException {
214    if (bindings.isEmpty()) {
215      return;
216    } else {
217      Piece piece = gen.new Piece("table").attr("class", "grid");
218      c.getPieces().add(piece);
219      render(piece.getChildren(), false);
220    }
221  }
222  
223  public void render(List<XhtmlNode> children, boolean fullDoco) throws FHIRFormatError, DefinitionException, IOException {
224    boolean doco = false;
225    boolean usage = false;
226    boolean any = false;
227    for (AdditionalBindingDetail binding : bindings) {
228      doco = doco || binding.getDoco(fullDoco)!=null  || (binding.compare!=null && binding.compare.getDoco(fullDoco)!=null);
229      usage = usage || !binding.usages.isEmpty() || (binding.compare!=null && !binding.compare.usages.isEmpty());
230      any = any || binding.any || (binding.compare!=null && binding.compare.any);
231    }
232
233    XhtmlNode tr = new XhtmlNode(NodeType.Element, "tr");
234    children.add(tr);
235    tr.td().style("font-size: 11px").b().tx(context.formatPhrase(RenderingContext.ADD_BIND_ADD_BIND));
236    tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_PURPOSE));
237    if (usage) {
238      tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_USAGE));
239    }
240    if (any) {
241      tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.ADD_BIND_ANY));
242    }
243    if (doco) {
244      tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_DOCUMENTATION));
245    }
246    for (AdditionalBindingDetail binding : bindings) {
247      tr =  new XhtmlNode(NodeType.Element, "tr");
248      if (binding.unchanged()) {
249        tr.style(STYLE_REMOVED);
250      } else if (binding.removed) {
251        tr.style(STYLE_REMOVED);
252      }
253      children.add(tr);
254      BindingResolution br = pkp == null ? makeNullBr(binding) : pkp.resolveBinding(profile, binding.valueSet, path);
255      BindingResolution compBr = null;
256      if (binding.compare!=null  && binding.compare.valueSet!=null)
257        compBr = pkp == null ? makeNullBr(binding.compare) : pkp.resolveBinding(profile, binding.compare.valueSet, path);
258
259      XhtmlNode valueset = tr.td().style("font-size: 11px");
260      if (binding.compare!=null && binding.valueSet.equals(binding.compare.valueSet))
261        valueset.style(STYLE_UNCHANGED);
262      if (br.url != null) {
263        XhtmlNode a = valueset.ah(context.prefixLocalHref(determineUrl(br.url)), br.uri);
264        a.tx(br.display);
265        if (br.external) {
266          a.tx(" ");
267          a.img("external.png", null);
268        }
269      } else {
270        valueset.span(null, binding.valueSet).tx(br.display);
271      }
272      if (binding.compare!=null && binding.compare.valueSet!=null && !binding.valueSet.equals(binding.compare.valueSet)) {
273        valueset.br();
274        valueset = valueset.span(STYLE_REMOVED, null);
275        if (compBr.url != null) {
276          valueset.ah(context.prefixLocalHref(determineUrl(compBr.url)), binding.compare.valueSet).tx(compBr.display);
277        } else {
278          valueset.span(null, binding.compare.valueSet).tx(compBr.display);
279        }
280      }
281
282      XhtmlNode purpose = tr.td().style("font-size: 11px");
283      if (binding.compare!=null && binding.purpose.equals(binding.compare.purpose))
284        purpose.style("font-color: darkgray");
285      renderPurpose(purpose, binding.purpose);
286      if (binding.compare!=null && binding.compare.purpose!=null && !binding.purpose.equals(binding.compare.purpose)) {
287        purpose.br();
288        purpose = purpose.span(STYLE_UNCHANGED, null);
289        renderPurpose(purpose, binding.compare.purpose);
290      }
291      if (usage) {
292        if (!binding.usages.isEmpty()) {
293          XhtmlNode td = tr.td();
294          for (UsageContext uc : binding.usages) {
295            td.sep(", ");
296            new DataRenderer(context).renderBase(new RenderingStatus(), td, uc);
297          }
298        } else {
299          tr.td();          
300        }
301      }
302      if (any) {
303        String newRepeat = binding.any ? context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP) : context.formatPhrase(RenderingContext.ADD_BIND_ALL_REP);
304        String oldRepeat = binding.compare!=null && binding.compare.any ? context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP) : context.formatPhrase(RenderingContext.ADD_BIND_ALL_REP);
305        compareString(tr.td().style("font-size: 11px"), newRepeat, oldRepeat);
306      }
307      if (doco) {
308        if (binding.doco != null) {
309          String d = fullDoco ? md.processMarkdown("Binding.description", binding.doco) : binding.docoShort;
310          String oldD = binding.compare==null ? null : fullDoco ? md.processMarkdown("Binding.description.compare", binding.compare.doco) : binding.compare.docoShort;
311          tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD));
312        } else {
313          tr.td().style("font-size: 11px");
314        }
315      }
316    }
317  }
318
319  private XhtmlNode compareString(XhtmlNode node, String newS, String oldS) {
320    if (oldS==null)
321      return node.tx(newS);
322    if (newS.equals(oldS))
323      return node.style(STYLE_UNCHANGED).tx(newS);
324    node.tx(newS);
325    node.br();
326    return node.span(STYLE_REMOVED,null).tx(oldS);
327  }
328
329  private String compareHtml(String newS, String oldS) {
330    if (oldS==null)
331      return newS;
332    if (newS.equals(oldS))
333      return "<span style=\"" + STYLE_UNCHANGED + "\">" + newS + "</span>";
334    return newS + "<br/><span style=\"" + STYLE_REMOVED + "\">" + oldS + "</span>";
335  }
336
337  private String determineUrl(String url) {
338    return Utilities.isAbsoluteUrl(url) || !pkp.prependLinks() ? url : corePath + url;
339  }
340
341  private void renderPurpose(XhtmlNode td, String purpose) {
342    boolean r5 = context == null || context.getWorker() == null ? false : VersionUtilities.isR5Plus(context.getWorker().getVersion());
343    switch (purpose) {
344    case "maximum": 
345      td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-maximum" : corePath+"extension-elementdefinition-maxvalueset.html", context.formatPhrase(RenderingContext.ADD_BIND_EXT_PREF)).tx(context.formatPhrase(RenderingContext.ADD_BIND_MAX));
346      break;
347    case "minimum": 
348      td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-minimum" : corePath+"extension-elementdefinition-minvalueset.html", context.formatPhrase(RenderingContext.GENERAL_BIND_MIN_ALLOW)).tx(context.formatPhrase(RenderingContext.ADD_BIND_MIN));
349      break;
350    case "required" :
351      td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-required" : corePath+"terminologies.html#strength", context.formatPhrase(RenderingContext.ADD_BIND_VALID_REQ)).tx(context.formatPhrase(RenderingContext.ADD_BIND_REQ_BIND));
352      break;
353    case "extensible" :
354      td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-extensible" : corePath+"terminologies.html#strength", context.formatPhrase(RenderingContext.ADD_BIND_VALID_EXT)).tx(context.formatPhrase(RenderingContext.ADD_BIND_EX_BIND));
355      break;
356    case "preferred" :
357      td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-preferred" : corePath+"terminologies.html#strength", context.formatPhrase(RenderingContext.ADD_BIND_RECOM_VALUE_SET)).tx(context.formatPhrase(RenderingContext.ADD_BIND_PREF_BIND));
358      break;
359    case "current" :
360      if (r5) {
361        td.ah(corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-current", context.formatPhrase(RenderingContext.ADD_BIND_NEW_REC)).tx(context.formatPhrase(RenderingContext.ADD_BIND_CURR_BIND));
362      } else {
363        td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_NEW_REC)).tx(context.formatPhrase(RenderingContext.ADD_BIND_CURR_BIND));
364      }
365      break;
366    case "ui" :
367      if (r5) {
368        td.ah(corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-ui", context.formatPhrase(RenderingContext.ADD_BIND_GIVEN_CONT)).tx(context.formatPhrase(RenderingContext.ADD_BIND_UI_BIND));
369      } else {
370        td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_GIVEN_CONT)).tx(context.formatPhrase(RenderingContext.ADD_BIND_UI));        
371      }
372      break;
373    case "starter" :
374      if (r5) {
375        td.ah(corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-starter",  context.formatPhrase(RenderingContext.ADD_BIND_DESIG_SYS)).tx(context.formatPhrase(RenderingContext.GENERAL_STARTER));
376      } else {
377        td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_DESIG_SYS)).tx(context.formatPhrase(RenderingContext.GENERAL_STARTER));        
378      }
379      break;
380    case "component" :
381      if (r5) {
382        td.ah(corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-component", context.formatPhrase(RenderingContext.ADD_BIND_VALUE_COMP)).tx(context.formatPhrase(RenderingContext.GENERAL_COMPONENT));
383      } else {
384        td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_VALUE_COMP)).tx(context.formatPhrase(RenderingContext.GENERAL_COMPONENT));        
385      }
386      break;
387    default:  
388      td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_UNKNOWN_PUR)).tx(purpose);
389    }
390  }
391
392  private BindingResolution makeNullBr(AdditionalBindingDetail binding) {
393    BindingResolution br = new BindingResolution();
394    br.url = "http://none.none/none";
395    br.display = "todo";
396    return br;
397  }
398
399  public boolean hasBindings() {
400    return !bindings.isEmpty();
401  }
402
403  public void render(XhtmlNodeList children, List<ElementDefinitionBindingAdditionalComponent> list) {
404    if (list.size() == 1) {
405      render(children, list.get(0));
406    } else {
407      XhtmlNode ul = children.ul();
408      for (ElementDefinitionBindingAdditionalComponent b : list) {
409        render(ul.li().getChildNodes(), b);
410      }
411    }
412  }
413
414  private void render(XhtmlNodeList children, ElementDefinitionBindingAdditionalComponent b) {
415    if (b.getValueSet() == null) {
416      return; // what should happen?
417    }
418    BindingResolution br = pkp.resolveBinding(profile, b.getValueSet(), corePath);
419    XhtmlNode a = children.ahOrCode(br.url == null ? null : Utilities.isAbsoluteUrl(br.url) || !context.getPkp().prependLinks() ? br.url : corePath+br.url, b.hasDocumentation() ? b.getDocumentation() : br.uri);
420    if (b.hasDocumentation()) {
421      a.attribute("title", b.getDocumentation());
422    } 
423    a.tx(br.display);
424
425    if (b.hasShortDoco()) {
426      children.tx(": ");
427      children.tx(b.getShortDoco());
428    } 
429    if (b.getAny() || b.hasUsage()) {
430      children.tx(" (");
431      boolean ffirst = !b.getAny();
432      if (b.getAny()) {
433        children.tx(context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP));
434      }
435      for (UsageContext uc : b.getUsage()) {
436        if (ffirst) ffirst = false; else children.tx(",");
437        if (!uc.getCode().is("http://terminology.hl7.org/CodeSystem/usage-context-type", "jurisdiction")) {
438          children.tx(displayForUsage(uc.getCode()));
439          children.tx("=");
440        }
441        CodeResolution ccr = cr.resolveCode(uc.getValueCodeableConcept());
442        children.ah(context.prefixLocalHref(ccr.getLink()), ccr.getHint()).tx(ccr.getDisplay());
443      }
444      children.tx(")");
445    }
446  }
447
448  
449  private String displayForUsage(Coding c) {
450    if (c.hasDisplay()) {
451      return c.getDisplay();
452    }
453    if ("http://terminology.hl7.org/CodeSystem/usage-context-type".equals(c.getSystem())) {
454      return c.getCode();
455    }
456    return c.getCode();
457  }
458
459  public void seeAdditionalBinding(String purpose, String doco, ValueSet valueSet) {
460    AdditionalBindingDetail abr = new AdditionalBindingDetail();
461    abr.purpose =  purpose;
462    abr.valueSet =  valueSet.getUrl();
463    bindings.add(abr);
464  }
465
466  public void seeAdditionalBinding(String purpose, String doco, String ref) {
467    AdditionalBindingDetail abr = new AdditionalBindingDetail();
468    abr.purpose =  purpose;
469    abr.valueSet =  ref;
470    bindings.add(abr);
471    
472  }
473
474  public void seeAdditionalBindings(ElementDefinition definition, ElementDefinition compDef, boolean compare) {
475    HashMap<String, AdditionalBindingDetail> compBindings = new HashMap<String, AdditionalBindingDetail>();
476    if (compare && compDef.getBinding().getAdditional() != null) {
477      for (ElementDefinitionBindingAdditionalComponent ab : compDef.getBinding().getAdditional()) {
478        AdditionalBindingDetail abr = additionalBinding(ab);
479        if (compBindings.containsKey(abr.getKey())) {
480          abr.incrementCount();
481        }
482        compBindings.put(abr.getKey(), abr);
483      }
484    }
485
486    for (ElementDefinitionBindingAdditionalComponent ab : definition.getBinding().getAdditional()) {
487      AdditionalBindingDetail abr = additionalBinding(ab);
488      if (compare && compDef != null) {
489        AdditionalBindingDetail match = null;
490        do {
491          match = compBindings.get(abr.getKey());
492          if (abr.alreadyMatched())
493            abr.incrementCount();
494        } while (match!=null && abr.alreadyMatched());
495        if (match!=null)
496          abr.setCompare(match);
497        bindings.add(abr);
498        if (abr.compare!=null)
499          compBindings.remove(abr.compare.getKey());
500      } else
501        bindings.add(abr);
502    }
503    for (AdditionalBindingDetail b: compBindings.values()) {
504      b.removed = true;
505      bindings.add(b);
506    }
507    
508  }
509
510}