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