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