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