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.ActorDefinition;
017import org.hl7.fhir.r5.model.CodeSystem;
018import org.hl7.fhir.r5.model.Coding;
019import org.hl7.fhir.r5.model.ElementDefinition;
020import org.hl7.fhir.r5.model.Extension;
021import org.hl7.fhir.r5.model.PrimitiveType;
022import org.hl7.fhir.r5.model.StructureDefinition;
023import org.hl7.fhir.r5.model.UsageContext;
024import org.hl7.fhir.r5.model.ValueSet;
025import org.hl7.fhir.r5.renderers.CodeResolver.CodeResolution;
026import org.hl7.fhir.r5.renderers.ObligationsRenderer.ObligationDetail;
027import org.hl7.fhir.r5.renderers.utils.RenderingContext;
028import org.hl7.fhir.r5.utils.PublicationHacker;
029import org.hl7.fhir.r5.utils.ToolingExtensions;
030import org.hl7.fhir.utilities.MarkDownProcessor;
031import org.hl7.fhir.utilities.Utilities;
032import org.hl7.fhir.utilities.VersionUtilities;
033import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
034import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell;
035import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece;
036import org.hl7.fhir.utilities.xhtml.NodeType;
037import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
038import org.hl7.fhir.utilities.xhtml.XhtmlNode;
039import org.hl7.fhir.utilities.xhtml.XhtmlNodeList;
040
041public class ObligationsRenderer {
042  public static class ObligationDetail {
043    private String code;
044    private List<String> elementIds = new ArrayList<>();
045    private String actor;
046    private String doco;
047    private String docoShort;
048    private String filter;
049    private String filterDoco;
050    private List<UsageContext> usage = new ArrayList<>();
051    private boolean isUnchanged = false;
052    private boolean matched = false;
053    private boolean removed = false;
054    private ValueSet vs;
055    
056    private ObligationDetail compare;
057    private int count = 1;
058    
059    public ObligationDetail(Extension ext) {
060      this.code =  ext.getExtensionString("code");
061      this.actor =  ext.getExtensionString("actor");
062      if (this.actor == null) {
063        this.actor =  ext.getExtensionString("actorId");
064      }
065      this.doco =  ext.getExtensionString("documentation");
066      this.docoShort =  ext.getExtensionString("shortDoco");
067      this.filter =  ext.getExtensionString("filter");
068      this.filterDoco =  ext.getExtensionString("filterDocumentation");
069      if (this.filterDoco == null) {
070        this.filterDoco =  ext.getExtensionString("filter-desc");
071      }
072      for (Extension usage : ext.getExtensionsByUrl("usage")) {
073        this.usage.add(usage.getValueUsageContext());
074      }
075      for (Extension eid : ext.getExtensionsByUrl("elementId")) {
076        this.elementIds.add(eid.getValue().primitiveValue());
077      }
078      this.isUnchanged = ext.hasUserData(ProfileUtilities.UD_DERIVATION_EQUALS);
079    }
080    
081    private String getKey() {
082      // Todo: Consider extending this with content from usageContext if purpose isn't sufficiently differentiating
083      return code + Integer.toString(count);
084    }
085    
086    private void incrementCount() {
087      count++;
088    }
089    private void setCompare(ObligationDetail match) {
090      compare = match;
091      match.matched = true;
092    }
093    private boolean alreadyMatched() {
094      return matched;
095    }
096    public String getDoco(boolean full) {
097      return full ? doco : docoShort;
098    }
099    public String getCode() {
100      return code;
101    }
102    public boolean unchanged() {
103      if (!isUnchanged)
104        return false;
105      if (compare==null)
106        return true;
107      isUnchanged = true;
108      isUnchanged = isUnchanged && ((code==null && compare.code==null) || code.equals(compare.code));
109      isUnchanged = elementIds.equals(compare.elementIds);
110      isUnchanged = isUnchanged && ((actor==null && compare.actor==null) || actor.equals(compare.actor));
111      isUnchanged = isUnchanged && ((doco==null && compare.doco==null) || doco.equals(compare.doco));
112      isUnchanged = isUnchanged && ((docoShort==null && compare.docoShort==null) || docoShort.equals(compare.docoShort));
113      isUnchanged = isUnchanged && ((filter==null && compare.filter==null) || filter.equals(compare.filter));
114      isUnchanged = isUnchanged && ((filterDoco==null && compare.filterDoco==null) || filterDoco.equals(compare.filterDoco));
115      isUnchanged = isUnchanged && ((usage==null && compare.usage==null) || usage.equals(compare.usage));
116      return isUnchanged;
117    }
118    
119    public boolean hasFilter() {
120      return filter != null;
121    }
122
123    public boolean hasUsage() {
124      return !usage.isEmpty();
125    }
126
127    public String getFilterDesc() {
128      return filterDoco;
129    }
130
131    public String getFilter() {
132      return filter;
133    }
134
135    public List<UsageContext> getUsage() {
136      return usage;
137    }
138
139    public boolean hasActor() {
140      return actor != null;
141    }
142
143    public boolean hasActor(String id) {
144      return id.equals(actor);
145    }
146  }
147
148  private static String STYLE_UNCHANGED = "opacity: 0.5;";
149  private static String STYLE_REMOVED = STYLE_UNCHANGED + "text-decoration: line-through;";
150
151  private List<ObligationDetail> obligations = new ArrayList<>();
152  private String corePath;
153  private StructureDefinition profile;
154  private String path;
155  private RenderingContext context;
156  private IMarkdownProcessor md;
157  private CodeResolver cr;
158
159  public ObligationsRenderer(String corePath, StructureDefinition profile, String path, RenderingContext context, IMarkdownProcessor md, CodeResolver cr) {
160    this.corePath = corePath;
161    this.profile = profile;
162    this.path = path;
163    this.context = context;
164    this.md = md;
165    this.cr = cr;
166  }
167
168
169  public void seeObligations(ElementDefinition element, String id) {
170    seeObligations(element.getExtension(), null, false, id);
171  }
172
173  public void seeObligations(List<Extension> list) {
174    seeObligations(list, null, false, "$all");
175  }
176
177  public void seeRootObligations(String eid, List<Extension> list) {
178    seeRootObligations(eid, list, null, false, "$all");
179  }
180
181  public void seeObligations(List<Extension> list, List<Extension> compList, boolean compare, String id) {
182    HashMap<String, ObligationDetail> compBindings = new HashMap<String, ObligationDetail>();
183    if (compare && compList!=null) {
184      for (Extension ext : compList) {
185        ObligationDetail abr = obligationDetail(ext);
186        if (compBindings.containsKey(abr.getKey())) {
187          abr.incrementCount();
188        }
189        compBindings.put(abr.getKey(), abr);
190      }
191    }
192
193    for (Extension ext : list) {
194      ObligationDetail obd = obligationDetail(ext);
195      if ("$all".equals(id) || (obd.hasActor(id))) {
196        if (compare && compList!=null) {
197          ObligationDetail match = null;
198          do {
199            match = compBindings.get(obd.getKey());
200            if (obd.alreadyMatched())
201              obd.incrementCount();
202          } while (match!=null && obd.alreadyMatched());
203          if (match!=null)
204            obd.setCompare(match);
205          obligations.add(obd);
206          if (obd.compare!=null)
207            compBindings.remove(obd.compare.getKey());
208        } else {
209          obligations.add(obd);
210        }
211      }
212    }
213    for (ObligationDetail b: compBindings.values()) {
214      b.removed = true;
215      obligations.add(b);
216    }
217  }
218
219  public void seeRootObligations(String eid, List<Extension> list, List<Extension> compList, boolean compare, String id) {
220    HashMap<String, ObligationDetail> compBindings = new HashMap<String, ObligationDetail>();
221    if (compare && compList!=null) {
222      for (Extension ext : compList) {
223        if (forElement(eid, ext)) {
224          ObligationDetail abr = obligationDetail(ext);
225          if (compBindings.containsKey(abr.getKey())) {
226            abr.incrementCount();
227          }
228          compBindings.put(abr.getKey(), abr);
229        }
230      }
231    }
232
233    for (Extension ext : list) {
234      if (forElement(eid, ext)) {
235        ObligationDetail obd = obligationDetail(ext);
236        obd.elementIds.clear();
237        if ("$all".equals(id) || (obd.hasActor(id))) {
238          if (compare && compList!=null) {
239            ObligationDetail match = null;
240            do {
241              match = compBindings.get(obd.getKey());
242              if (obd.alreadyMatched())
243                obd.incrementCount();
244            } while (match!=null && obd.alreadyMatched());
245            if (match!=null)
246              obd.setCompare(match);
247            obligations.add(obd);
248            if (obd.compare!=null)
249              compBindings.remove(obd.compare.getKey());
250          } else {
251            obligations.add(obd);
252          }
253        }
254      }
255    }
256    for (ObligationDetail b: compBindings.values()) {
257      b.removed = true;
258      obligations.add(b);
259    }
260  }
261
262
263  private boolean forElement(String eid, Extension ext) {
264
265    for (Extension exid : ext.getExtensionsByUrl("elementId")) {
266      if (eid.equals(exid.getValue().primitiveValue())) {
267        return true;
268      }
269    } 
270    return false;
271  }
272
273
274  protected ObligationDetail obligationDetail(Extension ext) {
275    ObligationDetail abr = new ObligationDetail(ext);
276    return abr;
277  }
278
279  public String render() throws IOException {
280    if (obligations.isEmpty()) {
281      return "";
282    } else {
283      XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table");
284      tbl.attribute("class", "grid");
285      renderTable(tbl.getChildNodes(), true);
286      return new XhtmlComposer(false).compose(tbl);
287    }
288  }
289
290  public void renderTable(HierarchicalTableGenerator gen, Cell c) throws FHIRFormatError, DefinitionException, IOException {
291    if (obligations.isEmpty()) {
292      return;
293    } else {
294      Piece piece = gen.new Piece("table").attr("class", "grid");
295      c.getPieces().add(piece);
296      renderTable(piece.getChildren(), false);
297    }
298  }
299
300  public void renderList(HierarchicalTableGenerator gen, Cell c) throws FHIRFormatError, DefinitionException, IOException {
301    if (obligations.size() > 0) {
302      Piece p = gen.new Piece(null);
303      c.addPiece(p);
304      if (obligations.size() == 1) {
305        renderObligationLI(p.getChildren(), obligations.get(0));
306      } else {
307        XhtmlNode ul = p.getChildren().ul();
308        for (ObligationDetail ob : obligations) {
309          renderObligationLI(ul.li().getChildNodes(), ob);
310        }
311      }
312    }
313  }
314
315  private void renderObligationLI(XhtmlNodeList children, ObligationDetail ob) throws IOException {
316    renderCode(children, ob.getCode());
317    if (ob.hasFilter() || ob.hasUsage()) {
318      children.tx(" (");
319      boolean ffirst = !ob.hasFilter();
320      if (ob.hasFilter()) {
321        children.span(null, ob.getFilterDesc()).code().tx(ob.getFilter());
322      }
323      for (UsageContext uc : ob.getUsage()) {
324        if (ffirst) ffirst = false; else children.tx(",");
325        if (!uc.getCode().is("http://terminology.hl7.org/CodeSystem/usage-context-type", "jurisdiction")) {
326          children.tx(displayForUsage(uc.getCode()));
327          children.tx("=");
328        }
329        CodeResolution ccr = this.cr.resolveCode(uc.getValueCodeableConcept());
330        children.ah(ccr.getLink(), ccr.getHint()).tx(ccr.getDisplay());
331      }
332      children.tx(")");
333    }
334    // usage
335    // filter
336    // process 
337  }
338
339
340  public void renderTable(List<XhtmlNode> children, boolean fullDoco) throws FHIRFormatError, DefinitionException, IOException {
341    boolean doco = false;
342    boolean usage = false;
343    boolean actor = false;
344    boolean filter = false;
345    boolean elementId = false;
346    for (ObligationDetail binding : obligations) {
347      actor = actor || binding.actor!=null  || (binding.compare!=null && binding.compare.actor !=null);
348      doco = doco || binding.getDoco(fullDoco)!=null  || (binding.compare!=null && binding.compare.getDoco(fullDoco)!=null);
349      usage = usage || !binding.usage.isEmpty() || (binding.compare!=null && !binding.compare.usage.isEmpty());
350      filter = filter || binding.filter != null || (binding.compare!=null && binding.compare.filter!=null);
351      elementId = elementId || !binding.elementIds.isEmpty()  || (binding.compare!=null && !binding.compare.elementIds.isEmpty());
352    }
353
354    XhtmlNode tr = new XhtmlNode(NodeType.Element, "tr");
355    children.add(tr);
356    tr.td().style("font-size: 11px").b().tx("Obligations");
357    if (actor) {
358      tr.td().style("font-size: 11px").tx("Actor");
359    }
360    if (elementId) {
361      tr.td().style("font-size: 11px").tx("Elements");
362    }
363    if (usage) {
364      tr.td().style("font-size: 11px").tx("Usage");
365    }
366    if (doco) {
367      tr.td().style("font-size: 11px").tx("Documentation");
368    }
369    if (filter) {
370      tr.td().style("font-size: 11px").tx("Filter");
371    }
372    for (ObligationDetail ob : obligations) {
373      tr =  new XhtmlNode(NodeType.Element, "tr");
374      if (ob.unchanged()) {
375        tr.style(STYLE_REMOVED);
376      } else if (ob.removed) {
377        tr.style(STYLE_REMOVED);
378      }
379      children.add(tr);
380
381      XhtmlNode code = tr.td().style("font-size: 11px");
382      if (ob.compare!=null && ob.code.equals(ob.compare.code))
383        code.style("font-color: darkgray");
384      renderCode(code.getChildNodes(), ob.code);
385      if (ob.compare!=null && ob.compare.code != null && !ob.code.equals(ob.compare.code)) {
386        code.br();
387        code = code.span(STYLE_UNCHANGED, null);
388        renderCode(code.getChildNodes(), ob.compare.code);
389      }
390      if (actor) {
391
392        ActorDefinition ad = context.getContext().fetchResource(ActorDefinition.class, ob.actor);
393        ActorDefinition compAd = null;
394        if (ob.compare!=null  && ob.compare.actor!=null) {
395          compAd = context.getContext().fetchResource(ActorDefinition.class, ob.compare.actor);
396        }
397
398        XhtmlNode actorId = tr.td().style("font-size: 11px");
399        if (ob.compare!=null && ob.actor.equals(ob.compare.actor))
400          actorId.style(STYLE_UNCHANGED);
401        if (ad != null && ad.hasWebPath()) {
402          actorId.ah(ad.getWebPath(), ob.actor).tx(ad.present());
403        } else if (ad != null) {
404          actorId.span(null, ob.actor).tx(ad.present());
405        }
406
407        if (ob.compare!=null && ob.compare.actor!=null && !ob.actor.equals(ob.compare.actor)) {
408          actorId.br();
409          actorId = actorId.span(STYLE_REMOVED, null);
410          if (compAd != null) {
411            if (compAd.hasWebPath()) {
412              actorId.ah(compAd.getWebPath(), ob.compare.actor).tx(compAd.present());
413            } else {
414              actorId.span(null, ob.compare.actor).tx(compAd.present());
415            }
416          }
417        }
418      }
419      if (elementId) {
420        XhtmlNode elementIds = tr.td().style("font-size: 11px");
421        if (ob.compare!=null && ob.elementIds.equals(ob.compare.elementIds))
422          elementIds.style(STYLE_UNCHANGED);
423        for (String eid : ob.elementIds) {
424          elementIds.sep(", ");
425          ElementDefinition ed = profile.getSnapshot().getElementById(eid);
426          if (ed != null) {
427            elementIds.ah("#"+eid).tx(ed.getName());
428          } else {
429            elementIds.code().tx(eid);
430          }
431        }
432
433        if (ob.compare!=null && !ob.compare.elementIds.isEmpty()) {
434          for (String eid : ob.compare.elementIds) {
435            if (!ob.elementIds.contains(eid)) {
436              elementIds.sep(", ");
437              elementIds.span(STYLE_REMOVED, null).code().tx(eid);
438            }
439          }
440        }
441      }
442      if (usage) {
443        if (ob.usage != null) {
444          boolean first = true;
445          XhtmlNode td = tr.td();
446          for (UsageContext u : ob.usage) {
447            if (first) first = false; else td.tx(", ");
448            new DataRenderer(context).render(td, u);
449          }
450        } else {
451          tr.td();          
452        }
453      }
454      if (doco) {
455        if (ob.doco != null) {
456          String d = fullDoco ? md.processMarkdown("Obligation.documentation", ob.doco) : ob.docoShort;
457          String oldD = ob.compare==null ? null : fullDoco ? md.processMarkdown("Binding.description.compare", ob.compare.doco) : ob.compare.docoShort;
458          tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD));
459        } else {
460          tr.td().style("font-size: 11px");
461        }
462      }
463
464      if (filter) {
465        if (ob.filter != null) {
466          String d = "<code>"+ob.filter+"</code>" + (fullDoco ? md.processMarkdown("Binding.description", ob.filterDoco) : "");
467          String oldD = ob.compare==null ? null : "<code>"+ob.compare.filter+"</code>" + (fullDoco ? md.processMarkdown("Binding.description", ob.compare.filterDoco) : "");
468          tr.td().style("font-size: 11px").innerHTML(compareHtml(d, oldD));
469        } else {
470          tr.td().style("font-size: 11px");
471        }
472      }
473    }
474  }
475
476  private XhtmlNode compareString(XhtmlNode node, String newS, String oldS) {
477    if (oldS==null)
478      return node.tx(newS);
479    if (newS.equals(oldS))
480      return node.style(STYLE_UNCHANGED).tx(newS);
481    node.tx(newS);
482    node.br();
483    return node.span(STYLE_REMOVED,null).tx(oldS);
484  }
485
486  private String compareHtml(String newS, String oldS) {
487    if (oldS==null)
488      return newS;
489    if (newS.equals(oldS))
490      return "<span style=\"" + STYLE_UNCHANGED + "\">" + newS + "</span>";
491    return newS + "<br/><span style=\"" + STYLE_REMOVED + "\">" + oldS + "</span>";
492  }
493
494  private void renderCode(XhtmlNodeList children, String codeExpr) {
495    if (codeExpr != null) {
496      boolean first = true;
497      String[] codes = codeExpr.split("\\+");
498      for (String code : codes) {
499        if (first) first = false; else children.tx(" & ");
500        int i = code.indexOf(":");
501        if (i > -1) {
502          String c = code.substring(0, i);
503          code = code.substring(i+1);
504          children.b().tx(c.toUpperCase());
505          children.tx(":");
506        }
507        CodeResolution cr = this.cr.resolveCode("http://hl7.org/fhir/tools/CodeSystem/obligation", code);
508        code = code.replace("will-", "").replace("can-", "");
509        if (cr.getLink() != null) {
510          children.ah(cr.getLink(), cr.getHint()).tx(code);          
511        } else {
512          children.span(null, cr.getHint()).tx(code);
513        }
514      }
515    } else {
516      children.span(null, "No Obligation Code?").tx("??");
517    }
518  }
519
520  public boolean hasObligations() {
521    return !obligations.isEmpty();
522  }
523
524  private String displayForUsage(Coding c) {
525    if (c.hasDisplay()) {
526      return c.getDisplay();
527    }
528    if ("http://terminology.hl7.org/CodeSystem/usage-context-type".equals(c.getSystem())) {
529      return c.getCode();
530    }
531    return c.getCode();
532  }
533
534}