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