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