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 UsageContext usage;
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 && ((usage==null && compare.usage==null) || usage.equals(compare.usage));
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    abr.usage =  (ext.hasExtension("usage")) && ext.getExtensionByUrl("usage").hasValueUsageContext() ? ext.getExtensionByUrl("usage").getValueUsageContext() : null;
179    abr.any = "any".equals(ext.getExtensionString("scope"));
180    abr.isUnchanged = ext.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS);
181    return abr;
182  }
183
184  protected AdditionalBindingDetail additionalBinding(ElementDefinitionBindingAdditionalComponent ab) {
185    AdditionalBindingDetail abr = new AdditionalBindingDetail();
186    abr.purpose =  ab.getPurpose().toCode();
187    abr.valueSet =  ab.getValueSet();
188    abr.doco =  ab.getDocumentation();
189    abr.docoShort =  ab.getShortDoco();
190    abr.usage = ab.hasUsage() ? ab.getUsageFirstRep() : null;
191    abr.any = ab.getAny();
192    abr.isUnchanged = ab.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS);
193    return abr;
194  }
195
196  public String render() throws IOException {
197    if (bindings.isEmpty()) {
198      return "";
199    } else {
200      XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table");
201      tbl.attribute("class", "grid");
202      render(tbl.getChildNodes(), true);
203      return new XhtmlComposer(false).compose(tbl);
204    }
205  }
206
207  public void render(HierarchicalTableGenerator gen, Cell c) throws FHIRFormatError, DefinitionException, IOException {
208    if (bindings.isEmpty()) {
209      return;
210    } else {
211      Piece piece = gen.new Piece("table").attr("class", "grid");
212      c.getPieces().add(piece);
213      render(piece.getChildren(), false);
214    }
215  }
216  
217  public void render(List<XhtmlNode> children, boolean fullDoco) throws FHIRFormatError, DefinitionException, IOException {
218    boolean doco = false;
219    boolean usage = false;
220    boolean any = false;
221    for (AdditionalBindingDetail binding : bindings) {
222      doco = doco || binding.getDoco(fullDoco)!=null  || (binding.compare!=null && binding.compare.getDoco(fullDoco)!=null);
223      usage = usage || binding.usage != null || (binding.compare!=null && binding.compare.usage!=null);
224      any = any || binding.any || (binding.compare!=null && binding.compare.any);
225    }
226
227    XhtmlNode tr = new XhtmlNode(NodeType.Element, "tr");
228    children.add(tr);
229    tr.td().style("font-size: 11px").b().tx(context.formatPhrase(RenderingContext.ADD_BIND_ADD_BIND));
230    tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_PURPOSE));
231    if (usage) {
232      tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_USAGE));
233    }
234    if (any) {
235      tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.ADD_BIND_ANY));
236    }
237    if (doco) {
238      tr.td().style("font-size: 11px").tx(context.formatPhrase(RenderingContext.GENERAL_DOCUMENTATION));
239    }
240    for (AdditionalBindingDetail binding : bindings) {
241      tr =  new XhtmlNode(NodeType.Element, "tr");
242      if (binding.unchanged()) {
243        tr.style(STYLE_REMOVED);
244      } else if (binding.removed) {
245        tr.style(STYLE_REMOVED);
246      }
247      children.add(tr);
248      BindingResolution br = pkp == null ? makeNullBr(binding) : pkp.resolveBinding(profile, binding.valueSet, path);
249      BindingResolution compBr = null;
250      if (binding.compare!=null  && binding.compare.valueSet!=null)
251        compBr = pkp == null ? makeNullBr(binding.compare) : pkp.resolveBinding(profile, binding.compare.valueSet, path);
252
253      XhtmlNode valueset = tr.td().style("font-size: 11px");
254      if (binding.compare!=null && binding.valueSet.equals(binding.compare.valueSet))
255        valueset.style(STYLE_UNCHANGED);
256      if (br.url != null) {
257        XhtmlNode a = valueset.ah(context.prefixLocalHref(determineUrl(br.url)), br.uri);
258        a.tx(br.display);
259        if (br.external) {
260          a.tx(" ");
261          a.img("external.png", null);
262        }
263      } else {
264        valueset.span(null, binding.valueSet).tx(br.display);
265      }
266      if (binding.compare!=null && binding.compare.valueSet!=null && !binding.valueSet.equals(binding.compare.valueSet)) {
267        valueset.br();
268        valueset = valueset.span(STYLE_REMOVED, null);
269        if (compBr.url != null) {
270          valueset.ah(context.prefixLocalHref(determineUrl(compBr.url)), binding.compare.valueSet).tx(compBr.display);
271        } else {
272          valueset.span(null, binding.compare.valueSet).tx(compBr.display);
273        }
274      }
275
276      XhtmlNode purpose = tr.td().style("font-size: 11px");
277      if (binding.compare!=null && binding.purpose.equals(binding.compare.purpose))
278        purpose.style("font-color: darkgray");
279      renderPurpose(purpose, binding.purpose);
280      if (binding.compare!=null && binding.compare.purpose!=null && !binding.purpose.equals(binding.compare.purpose)) {
281        purpose.br();
282        purpose = purpose.span(STYLE_UNCHANGED, null);
283        renderPurpose(purpose, binding.compare.purpose);
284      }
285      if (usage) {
286        if (binding.usage != null) {
287          // TODO: This isn't rendered at all yet.  Ideally, we want it to render with comparison...
288          new DataRenderer(context).renderBase(new RenderingStatus(), tr.td(), binding.usage);
289        } else {
290          tr.td();          
291        }
292      }
293      if (any) {
294        String newRepeat = binding.any ? context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP) : context.formatPhrase(RenderingContext.ADD_BIND_ALL_REP);
295        String oldRepeat = binding.compare!=null && binding.compare.any ? context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP) : context.formatPhrase(RenderingContext.ADD_BIND_ALL_REP);
296        compareString(tr.td().style("font-size: 11px"), newRepeat, oldRepeat);
297      }
298      if (doco) {
299        if (binding.doco != null) {
300          String d = fullDoco ? md.processMarkdown("Binding.description", binding.doco) : binding.docoShort;
301          String oldD = binding.compare==null ? null : fullDoco ? md.processMarkdown("Binding.description.compare", binding.compare.doco) : binding.compare.docoShort;
302          tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD));
303        } else {
304          tr.td().style("font-size: 11px");
305        }
306      }
307    }
308  }
309
310  private XhtmlNode compareString(XhtmlNode node, String newS, String oldS) {
311    if (oldS==null)
312      return node.tx(newS);
313    if (newS.equals(oldS))
314      return node.style(STYLE_UNCHANGED).tx(newS);
315    node.tx(newS);
316    node.br();
317    return node.span(STYLE_REMOVED,null).tx(oldS);
318  }
319
320  private String compareHtml(String newS, String oldS) {
321    if (oldS==null)
322      return newS;
323    if (newS.equals(oldS))
324      return "<span style=\"" + STYLE_UNCHANGED + "\">" + newS + "</span>";
325    return newS + "<br/><span style=\"" + STYLE_REMOVED + "\">" + oldS + "</span>";
326  }
327
328  private String determineUrl(String url) {
329    return Utilities.isAbsoluteUrl(url) || !pkp.prependLinks() ? url : corePath + url;
330  }
331
332  private void renderPurpose(XhtmlNode td, String purpose) {
333    boolean r5 = context == null || context.getWorker() == null ? false : VersionUtilities.isR5Plus(context.getWorker().getVersion());
334    switch (purpose) {
335    case "maximum": 
336      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));
337      break;
338    case "minimum": 
339      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));
340      break;
341    case "required" :
342      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));
343      break;
344    case "extensible" :
345      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));
346      break;
347    case "preferred" :
348      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));
349      break;
350    case "current" :
351      if (r5) {
352        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));
353      } else {
354        td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_NEW_REC)).tx(context.formatPhrase(RenderingContext.GENERAL_REQUIRED));
355      }
356      break;
357    case "ui" :
358      if (r5) {
359        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));
360      } else {
361        td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_GIVEN_CONT)).tx(context.formatPhrase(RenderingContext.ADD_BIND_UI));        
362      }
363      break;
364    case "starter" :
365      if (r5) {
366        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");
367      } else {
368        td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_DESIG_SYS)).tx(context.formatPhrase(RenderingContext.GENERAL_STARTER));        
369      }
370      break;
371    case "component" :
372      if (r5) {
373        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");
374      } else {
375        td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_VALUE_COMP)).tx(context.formatPhrase(RenderingContext.GENERAL_COMPONENT));        
376      }
377      break;
378    default:  
379      td.span(null, context.formatPhrase(RenderingContext.ADD_BIND_UNKNOWN_PUR)).tx(purpose);
380    }
381  }
382
383  private BindingResolution makeNullBr(AdditionalBindingDetail binding) {
384    BindingResolution br = new BindingResolution();
385    br.url = "http://none.none/none";
386    br.display = "todo";
387    return br;
388  }
389
390  public boolean hasBindings() {
391    return !bindings.isEmpty();
392  }
393
394  public void render(XhtmlNodeList children, List<ElementDefinitionBindingAdditionalComponent> list) {
395    if (list.size() == 1) {
396      render(children, list.get(0));
397    } else {
398      XhtmlNode ul = children.ul();
399      for (ElementDefinitionBindingAdditionalComponent b : list) {
400        render(ul.li().getChildNodes(), b);
401      }
402    }
403  }
404
405  private void render(XhtmlNodeList children, ElementDefinitionBindingAdditionalComponent b) {
406    if (b.getValueSet() == null) {
407      return; // what should happen?
408    }
409    BindingResolution br = pkp.resolveBinding(profile, b.getValueSet(), corePath);
410    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);
411    if (b.hasDocumentation()) {
412      a.attribute("title", b.getDocumentation());
413    } 
414    a.tx(br.display);
415
416    if (b.hasShortDoco()) {
417      children.tx(": ");
418      children.tx(b.getShortDoco());
419    } 
420    if (b.getAny() || b.hasUsage()) {
421      children.tx(" (");
422      boolean ffirst = !b.getAny();
423      if (b.getAny()) {
424        children.tx(context.formatPhrase(RenderingContext.ADD_BIND_ANY_REP));
425      }
426      for (UsageContext uc : b.getUsage()) {
427        if (ffirst) ffirst = false; else children.tx(",");
428        if (!uc.getCode().is("http://terminology.hl7.org/CodeSystem/usage-context-type", "jurisdiction")) {
429          children.tx(displayForUsage(uc.getCode()));
430          children.tx("=");
431        }
432        CodeResolution ccr = cr.resolveCode(uc.getValueCodeableConcept());
433        children.ah(context.prefixLocalHref(ccr.getLink()), ccr.getHint()).tx(ccr.getDisplay());
434      }
435      children.tx(")");
436    }
437  }
438
439  
440  private String displayForUsage(Coding c) {
441    if (c.hasDisplay()) {
442      return c.getDisplay();
443    }
444    if ("http://terminology.hl7.org/CodeSystem/usage-context-type".equals(c.getSystem())) {
445      return c.getCode();
446    }
447    return c.getCode();
448  }
449
450  public void seeAdditionalBinding(String purpose, String doco, ValueSet valueSet) {
451    AdditionalBindingDetail abr = new AdditionalBindingDetail();
452    abr.purpose =  purpose;
453    abr.valueSet =  valueSet.getUrl();
454    abr.vs = valueSet;
455    bindings.add(abr);
456  }
457
458  public void seeAdditionalBinding(String purpose, String doco, String ref) {
459    AdditionalBindingDetail abr = new AdditionalBindingDetail();
460    abr.purpose =  purpose;
461    abr.valueSet =  ref;
462    bindings.add(abr);
463    
464  }
465
466  public void seeAdditionalBindings(ElementDefinition definition, ElementDefinition compDef, boolean compare) {
467    HashMap<String, AdditionalBindingDetail> compBindings = new HashMap<String, AdditionalBindingDetail>();
468    if (compare && compDef.getBinding().getAdditional() != null) {
469      for (ElementDefinitionBindingAdditionalComponent ab : compDef.getBinding().getAdditional()) {
470        AdditionalBindingDetail abr = additionalBinding(ab);
471        if (compBindings.containsKey(abr.getKey())) {
472          abr.incrementCount();
473        }
474        compBindings.put(abr.getKey(), abr);
475      }
476    }
477
478    for (ElementDefinitionBindingAdditionalComponent ab : definition.getBinding().getAdditional()) {
479      AdditionalBindingDetail abr = additionalBinding(ab);
480      if (compare && compDef != null) {
481        AdditionalBindingDetail match = null;
482        do {
483          match = compBindings.get(abr.getKey());
484          if (abr.alreadyMatched())
485            abr.incrementCount();
486        } while (match!=null && abr.alreadyMatched());
487        if (match!=null)
488          abr.setCompare(match);
489        bindings.add(abr);
490        if (abr.compare!=null)
491          compBindings.remove(abr.compare.getKey());
492      } else
493        bindings.add(abr);
494    }
495    for (AdditionalBindingDetail b: compBindings.values()) {
496      b.removed = true;
497      bindings.add(b);
498    }
499    
500  }
501
502}