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  protected AdditionalBindingDetail additionalBinding(ElementDefinitionBindingAdditionalComponent ab) {
190    AdditionalBindingDetail abr = new AdditionalBindingDetail();
191    abr.purpose =  ab.getPurpose().toCode();
192    abr.valueSet =  ab.getValueSet();
193    abr.doco =  ab.getDocumentation();
194    abr.docoShort =  ab.getShortDoco();
195    abr.usage = ab.hasUsage() ? ab.getUsageFirstRep() : null;
196    abr.any = ab.getAny();
197    abr.isUnchanged = ab.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS);
198    return abr;
199  }
200
201  public String render() throws IOException {
202    if (bindings.isEmpty()) {
203      return "";
204    } else {
205      XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table");
206      tbl.attribute("class", "grid");
207      render(tbl.getChildNodes(), true);
208      return new XhtmlComposer(false).compose(tbl);
209    }
210  }
211
212  public void render(HierarchicalTableGenerator gen, Cell c) throws FHIRFormatError, DefinitionException, IOException {
213    if (bindings.isEmpty()) {
214      return;
215    } else {
216      Piece piece = gen.new Piece("table").attr("class", "grid");
217      c.getPieces().add(piece);
218      render(piece.getChildren(), false);
219    }
220  }
221  
222  public void render(List<XhtmlNode> children, boolean fullDoco) throws FHIRFormatError, DefinitionException, IOException {
223    boolean doco = false;
224    boolean usage = false;
225    boolean any = false;
226    for (AdditionalBindingDetail binding : bindings) {
227      doco = doco || binding.getDoco(fullDoco)!=null  || (binding.compare!=null && binding.compare.getDoco(fullDoco)!=null);
228      usage = usage || binding.usage != null || (binding.compare!=null && binding.compare.usage!=null);
229      any = any || binding.any || (binding.compare!=null && binding.compare.any);
230    }
231
232    XhtmlNode tr = new XhtmlNode(NodeType.Element, "tr");
233    children.add(tr);
234    tr.td().style("font-size: 11px").b().tx(context.formatPhrase(RenderingContext.ADD_BIND_ADD_BIND));
235    tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_PURPOSE));
236    if (usage) {
237      tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_USAGE));
238    }
239    if (any) {
240      tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.ADD_BIND_ANY));
241    }
242    if (doco) {
243      tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_DOCUMENTATION));
244    }
245    for (AdditionalBindingDetail binding : bindings) {
246      tr =  new XhtmlNode(NodeType.Element, "tr");
247      if (binding.unchanged()) {
248        tr.style(STYLE_REMOVED);
249      } else if (binding.removed) {
250        tr.style(STYLE_REMOVED);
251      }
252      children.add(tr);
253      BindingResolution br = pkp == null ? makeNullBr(binding) : pkp.resolveBinding(profile, binding.valueSet, path);
254      BindingResolution compBr = null;
255      if (binding.compare!=null  && binding.compare.valueSet!=null)
256        compBr = pkp == null ? makeNullBr(binding.compare) : pkp.resolveBinding(profile, binding.compare.valueSet, path);
257
258      XhtmlNode valueset = tr.td().style("font-size: 11px");
259      if (binding.compare!=null && binding.valueSet.equals(binding.compare.valueSet))
260        valueset.style(STYLE_UNCHANGED);
261      if (br.url != null) {
262        XhtmlNode a = valueset.ah(determineUrl(br.url), br.uri);
263        a.tx(br.display);
264        if (br.external) {
265          a.tx(" ");
266          a.img("external.png", null);
267        }
268      } else {
269        valueset.span(null, binding.valueSet).tx(br.display);
270      }
271      if (binding.compare!=null && binding.compare.valueSet!=null && !binding.valueSet.equals(binding.compare.valueSet)) {
272        valueset.br();
273        valueset = valueset.span(STYLE_REMOVED, null);
274        if (compBr.url != null) {
275          valueset.ah(determineUrl(compBr.url), binding.compare.valueSet).tx(compBr.display);
276        } else {
277          valueset.span(null, binding.compare.valueSet).tx(compBr.display);
278        }
279      }
280
281      XhtmlNode purpose = tr.td().style("font-size: 11px");
282      if (binding.compare!=null && binding.purpose.equals(binding.compare.purpose))
283        purpose.style("font-color: darkgray");
284      renderPurpose(purpose, binding.purpose);
285      if (binding.compare!=null && binding.compare.purpose!=null && !binding.purpose.equals(binding.compare.purpose)) {
286        purpose.br();
287        purpose = purpose.span(STYLE_UNCHANGED, null);
288        renderPurpose(purpose, binding.compare.purpose);
289      }
290      if (usage) {
291        if (binding.usage != null) {
292          // TODO: This isn't rendered at all yet.  Ideally, we want it to render with comparison...
293          new DataRenderer(context).render(tr.td(), binding.usage);
294        } else {
295          tr.td();          
296        }
297      }
298      if (any) {
299        String newRepeat = binding.any ? context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP) : context.formatPhrase(RenderingContext.ADD_BIND_ALL_REP);
300        String oldRepeat = binding.compare!=null && binding.compare.any ? context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP) : context.formatPhrase(RenderingContext.ADD_BIND_ALL_REP);
301        compareString(tr.td().style("font-size: 11px"), newRepeat, oldRepeat);
302      }
303      if (doco) {
304        if (binding.doco != null) {
305          String d = fullDoco ? md.processMarkdown("Binding.description", binding.doco) : binding.docoShort;
306          String oldD = binding.compare==null ? null : fullDoco ? md.processMarkdown("Binding.description.compare", binding.compare.doco) : binding.compare.docoShort;
307          tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD));
308        } else {
309          tr.td().style("font-size: 11px");
310        }
311      }
312    }
313  }
314
315  private XhtmlNode compareString(XhtmlNode node, String newS, String oldS) {
316    if (oldS==null)
317      return node.tx(newS);
318    if (newS.equals(oldS))
319      return node.style(STYLE_UNCHANGED).tx(newS);
320    node.tx(newS);
321    node.br();
322    return node.span(STYLE_REMOVED,null).tx(oldS);
323  }
324
325  private String compareHtml(String newS, String oldS) {
326    if (oldS==null)
327      return newS;
328    if (newS.equals(oldS))
329      return "<span style=\"" + STYLE_UNCHANGED + "\">" + newS + "</span>";
330    return newS + "<br/><span style=\"" + STYLE_REMOVED + "\">" + oldS + "</span>";
331  }
332
333  private String determineUrl(String url) {
334    return Utilities.isAbsoluteUrl(url) || !pkp.prependLinks() ? url : corePath + url;
335  }
336
337  private void renderPurpose(XhtmlNode td, String purpose) {
338    boolean r5 = context == null || context.getWorker() == null ? false : VersionUtilities.isR5Plus(context.getWorker().getVersion());
339    switch (purpose) {
340    case "maximum": 
341      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));
342      break;
343    case "minimum": 
344      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));
345      break;
346    case "required" :
347      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));
348      break;
349    case "extensible" :
350      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));
351      break;
352    case "current" :
353      if (r5) {
354        td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-current" : corePath+"terminologies.html#strength", context.formatPhrase(RenderingContext.ADD_BIND_NEW_REC)).tx(context.formatPhrase(RenderingContext.ADD_BIND_CURR_BIND));
355      } else {
356        td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_NEW_REC)).tx(context.formatPhrase(RenderingContext.GENERAL_REQUIRED));
357      }
358      break;
359    case "preferred" :
360      if (r5) {
361        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));
362      } else {
363        td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_RECOM_VALUE_SET)).tx(context.formatPhrase(RenderingContext.GENERAL_PREFERRED));
364      }
365      break;
366    case "ui" :
367      if (r5) {
368        td.ah(r5 ? corePath+"valueset-additional-binding-purpose.html#additional-binding-purpose-ui" : corePath+"terminologies.html#strength", 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(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");
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(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");
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(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    abr.vs = valueSet;
464    bindings.add(abr);
465  }
466
467  public void seeAdditionalBinding(String purpose, String doco, String ref) {
468    AdditionalBindingDetail abr = new AdditionalBindingDetail();
469    abr.purpose =  purpose;
470    abr.valueSet =  ref;
471    bindings.add(abr);
472    
473  }
474
475  public void seeAdditionalBindings(ElementDefinition definition, ElementDefinition compDef, boolean compare) {
476    HashMap<String, AdditionalBindingDetail> compBindings = new HashMap<String, AdditionalBindingDetail>();
477    if (compare && compDef.getBinding().getAdditional() != null) {
478      for (ElementDefinitionBindingAdditionalComponent ab : compDef.getBinding().getAdditional()) {
479        AdditionalBindingDetail abr = additionalBinding(ab);
480        if (compBindings.containsKey(abr.getKey())) {
481          abr.incrementCount();
482        }
483        compBindings.put(abr.getKey(), abr);
484      }
485    }
486
487    for (ElementDefinitionBindingAdditionalComponent ab : definition.getBinding().getAdditional()) {
488      AdditionalBindingDetail abr = additionalBinding(ab);
489      if (compare && compDef != null) {
490        AdditionalBindingDetail match = null;
491        do {
492          match = compBindings.get(abr.getKey());
493          if (abr.alreadyMatched())
494            abr.incrementCount();
495        } while (match!=null && abr.alreadyMatched());
496        if (match!=null)
497          abr.setCompare(match);
498        bindings.add(abr);
499        if (abr.compare!=null)
500          compBindings.remove(abr.compare.getKey());
501      } else
502        bindings.add(abr);
503    }
504    for (AdditionalBindingDetail b: compBindings.values()) {
505      b.removed = true;
506      bindings.add(b);
507    }
508    
509  }
510
511}