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