001package org.hl7.fhir.r5.renderers;
002
003import java.util.List;
004import java.util.Map;
005import java.util.HashMap;
006import java.util.ArrayList;
007import java.io.IOException;
008import java.io.UnsupportedEncodingException;
009
010import org.hl7.fhir.exceptions.DefinitionException;
011import org.hl7.fhir.exceptions.FHIRException;
012import org.hl7.fhir.exceptions.FHIRFormatError;
013import org.hl7.fhir.r5.elementmodel.Element;
014import org.hl7.fhir.r5.model.Base;
015import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent;
016import org.hl7.fhir.r5.model.CanonicalResource;
017import org.hl7.fhir.r5.model.CodeSystem;
018import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
019import org.hl7.fhir.r5.model.CodeableReference;
020import org.hl7.fhir.r5.model.Coding;
021import org.hl7.fhir.r5.model.DataType;
022import org.hl7.fhir.r5.model.DomainResource;
023import org.hl7.fhir.r5.model.Enumerations.PublicationStatus;
024import org.hl7.fhir.r5.model.Extension;
025import org.hl7.fhir.r5.model.Narrative;
026import org.hl7.fhir.r5.model.Narrative.NarrativeStatus;
027import org.hl7.fhir.r5.model.Reference;
028import org.hl7.fhir.r5.model.Resource;
029import org.hl7.fhir.r5.renderers.utils.BaseWrappers.BaseWrapper;
030import org.hl7.fhir.r5.renderers.utils.BaseWrappers.PropertyWrapper;
031import org.hl7.fhir.r5.renderers.utils.BaseWrappers.ResourceWrapper;
032import org.hl7.fhir.r5.renderers.utils.DirectWrappers.ResourceWrapperDirect;
033import org.hl7.fhir.r5.renderers.utils.ElementWrappers.ResourceWrapperMetaElement;
034import org.hl7.fhir.r5.renderers.utils.RenderingContext;
035import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext;
036import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceWithReference;
037import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
038import org.hl7.fhir.r5.utils.EOperationOutcome;
039import org.hl7.fhir.r5.utils.ToolingExtensions;
040import org.hl7.fhir.r5.utils.XVerExtensionManager;
041import org.hl7.fhir.utilities.Utilities;
042import org.hl7.fhir.utilities.xhtml.NodeType;
043import org.hl7.fhir.utilities.xhtml.XhtmlNode;
044
045public abstract class ResourceRenderer extends DataRenderer {
046
047  public enum RendererType {
048    NATIVE, PROFILE, LIQUID
049
050  }
051
052  protected ResourceContext rcontext;
053  protected XVerExtensionManager xverManager;
054  
055  
056  public ResourceRenderer(RenderingContext context) {
057    super(context);
058  }
059
060  public ResourceRenderer(RenderingContext context, ResourceContext rcontext) {
061    super(context);
062    this.rcontext = rcontext;
063  }
064
065  public ResourceContext getRcontext() {
066    return rcontext;
067  }
068
069  public ResourceRenderer setRcontext(ResourceContext rcontext) {
070    this.rcontext = rcontext;
071    return this;
072  }
073
074  public XhtmlNode build(Resource dr) throws FHIRFormatError, DefinitionException, FHIRException, IOException, EOperationOutcome {
075    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
076    render(x, dr);
077    return x;
078  }
079  /**
080   * given a resource, update it's narrative with the best rendering available
081   * 
082   * @param r - the domain resource in question
083   * 
084   * @throws IOException
085   * @throws EOperationOutcome 
086   * @throws FHIRException 
087   */
088  
089  public void render(DomainResource r) throws IOException, FHIRException, EOperationOutcome {  
090    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
091    boolean hasExtensions;
092    hasExtensions = render(x, r);
093    inject(r, x, hasExtensions ? NarrativeStatus.EXTENSIONS :  NarrativeStatus.GENERATED);
094  }
095
096  public XhtmlNode render(ResourceWrapper r) throws IOException, FHIRException, EOperationOutcome { 
097    assert r.getContext() == context;
098    XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
099    boolean hasExtensions = render(x, r);
100    if (r.hasNarrative()) {
101      r.injectNarrative(x, hasExtensions ? NarrativeStatus.EXTENSIONS :  NarrativeStatus.GENERATED);
102    }
103    return x;
104  }
105
106  public abstract boolean render(XhtmlNode x, Resource r) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome;
107  
108  public boolean render(XhtmlNode x, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome {
109    ProfileDrivenRenderer pr = new ProfileDrivenRenderer(context);
110    return pr.render(x, r);
111  }
112  
113  public void describe(XhtmlNode x, Resource r) throws UnsupportedEncodingException, IOException {
114    x.tx(display(r));
115  }
116
117  public void describe(XhtmlNode x, ResourceWrapper r) throws UnsupportedEncodingException, IOException {
118    x.tx(display(r));
119  }
120
121  public abstract String display(Resource r) throws UnsupportedEncodingException, IOException;
122  public abstract String display(ResourceWrapper r) throws UnsupportedEncodingException, IOException;
123  
124  public static void inject(DomainResource r, XhtmlNode x, NarrativeStatus status) {
125    if (!x.hasAttribute("xmlns"))
126      x.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
127    if (r.hasLanguage()) {
128      // use both - see https://www.w3.org/TR/i18n-html-tech-lang/#langvalues
129      x.setAttribute("lang", r.getLanguage());
130      x.setAttribute("xml:lang", r.getLanguage());
131    }
132    r.getText().setUserData("renderer.generated", true);
133    if (!r.hasText() || !r.getText().hasDiv() || r.getText().getDiv().getChildNodes().isEmpty()) {
134      r.setText(new Narrative());
135      r.getText().setDiv(x);
136      r.getText().setStatus(status);
137    } else {
138      XhtmlNode n = r.getText().getDiv();
139      n.clear();
140      n.getChildNodes().addAll(x.getChildNodes());
141    }
142  }
143
144  public void renderCanonical(Resource res, XhtmlNode x, String url) throws UnsupportedEncodingException, IOException {
145    ResourceWrapper rw = new ResourceWrapperDirect(this.context, res);
146    renderCanonical(rw, x, url);
147  }
148
149  public void renderCanonical(ResourceWrapper rw, XhtmlNode x, String url) throws UnsupportedEncodingException, IOException {
150    renderCanonical(rw, x, url, true, rw.getResource()); 
151  }
152  
153  public void renderCanonical(ResourceWrapper rw, XhtmlNode x, String url, boolean allowLinks, Resource src) throws UnsupportedEncodingException, IOException {
154    if (url == null) {
155      return;
156    }
157    Resource target = context.getWorker().fetchResource(Resource.class, url, src);
158    if (target == null || !(target instanceof CanonicalResource)) {
159      x.code().tx(url);
160    } else {
161      CanonicalResource cr = (CanonicalResource) target;
162      if (url.contains("|")) {
163        if (target.hasWebPath()) {
164          x.ah(target.getWebPath()).tx(cr.present()+" (version "+cr.getVersion()+")");
165        } else {
166          url = url.substring(0, url.indexOf("|"));
167          x.code().tx(url);
168          x.tx(": "+cr.present()+" (version "+cr.getVersion()+")");          
169        }
170      } else {
171        if (target.hasWebPath()) {
172          x.ah(target.getWebPath()).tx(cr.present());
173        } else {
174          x.code().tx(url);
175          x.tx(" ("+cr.present()+")");          
176        }
177      }
178    }
179  }
180
181  public void render(Resource res, XhtmlNode x, DataType type) throws FHIRFormatError, DefinitionException, IOException {
182    if (type instanceof Reference) {
183      renderReference(res, x, (Reference) type);
184    } else if (type instanceof CodeableReference) {
185      CodeableReference cr = (CodeableReference) type;
186      if (cr.hasReference()) {
187        renderReference(res, x, cr.getReference());
188      } else {
189        render(x, type);
190      } 
191    } else { 
192      render(x, type);
193    }
194  }
195
196  public void render(ResourceWrapper res, XhtmlNode x, DataType type) throws FHIRFormatError, DefinitionException, IOException {
197    if (type instanceof Reference) {
198      renderReference(res, x, (Reference) type);
199    } else if (type instanceof CodeableReference) {
200      CodeableReference cr = (CodeableReference) type;
201      if (cr.hasReference()) {
202        renderReference(res, x, cr.getReference());
203      } else {
204        render(x, type);
205      } 
206    } else { 
207      render(x, type);
208    }
209  }
210
211  public void renderReference(Resource res, XhtmlNode x, Reference r) throws UnsupportedEncodingException, IOException {
212    ResourceWrapper rw = new ResourceWrapperDirect(this.context, res);
213    renderReference(rw, x, r);
214  }
215
216  public void renderReference(ResourceWrapper rw, XhtmlNode x, Reference r) throws UnsupportedEncodingException, IOException {
217    renderReference(rw, x, r, true); 
218  }
219  
220  public void renderReference(ResourceWrapper rw, XhtmlNode x, Reference r, boolean allowLinks) throws UnsupportedEncodingException, IOException {
221    if (r == null) {
222      x.tx("null!");
223      return;
224    }
225    XhtmlNode c = null;
226    ResourceWithReference tr = null;
227    if (r.hasReferenceElement() && allowLinks) {
228      tr = resolveReference(rw, r.getReference());
229
230      if (!r.getReference().startsWith("#")) {
231        if (tr != null && tr.getReference() != null) {
232          c = x.ah(tr.getReference());
233        } else if (r.getReference().contains("?")) {
234          x.tx("Conditional Reference: ");
235          c = x.code("");
236        } else {
237          c = x.ah(r.getReference());
238        }
239      } else {
240        c = x.ah(r.getReference());
241      }
242    } else {
243      c = x.span(null, null);
244    }
245    if (tr != null && tr.getReference() != null && tr.getReference().startsWith("#")) {
246      c.tx("See above (");
247    }
248    // what to display: if text is provided, then that. if the reference was resolved, then show the name, or the generated narrative
249    String display = r.hasDisplayElement() ? r.getDisplay() : null;
250    String name = tr != null && tr.getResource() != null ? tr.getResource().getNameFromResource() : null; 
251    
252    if (display == null && (tr == null || tr.getResource() == null)) {
253      if (!Utilities.noString(r.getReference())) {
254        c.addText(r.getReference());
255      } else if (r.hasIdentifier()) {
256        renderIdentifier(c, r.getIdentifier());
257      } else {
258        c.addText("??");        
259      }
260    } else if (context.isTechnicalMode()) {
261      c.addText(r.getReference());
262      if (display != null) {
263        c.addText(": "+display);
264      }
265      if ((tr == null || (tr.getReference() != null && !tr.getReference().startsWith("#"))) && name != null) {
266        x.addText(" \""+name+"\"");
267      }
268      if (r.hasExtension(ToolingExtensions.EXT_TARGET_ID) || r.hasExtension(ToolingExtensions.EXT_TARGET_PATH)) {
269        x.addText("(");
270        for (Extension ex : r.getExtensionsByUrl(ToolingExtensions.EXT_TARGET_ID)) {
271          if (ex.hasValue()) {
272            x.sep(", ");
273            x.addText("#"+ex.getValue().primitiveValue());
274          }
275        }
276        for (Extension ex : r.getExtensionsByUrl(ToolingExtensions.EXT_TARGET_PATH)) {
277          if (ex.hasValue()) {
278            x.sep(", ");
279            x.addText("/#"+ex.getValue().primitiveValue());
280          }
281        }
282        x.addText(")");
283      }  
284    } else {
285      if (display != null) {
286        c.addText(display);
287      } else if (name != null) {
288        c.addText(name);
289      } else {
290        c.tx(". Generated Summary: ");
291        if (tr != null) {
292          new ProfileDrivenRenderer(context).generateResourceSummary(c, tr.getResource(), true, r.getReference().startsWith("#"), true);
293        }
294      }
295    }
296    if (tr != null && tr.getReference() != null && tr.getReference().startsWith("#")) {
297      c.tx(")");
298    }
299  }
300
301  public void renderReference(ResourceWrapper rw, XhtmlNode x, BaseWrapper r) throws UnsupportedEncodingException, IOException {
302    XhtmlNode c = x;
303    ResourceWithReference tr = null;
304    String v;
305    if (r.has("reference")) {
306      v = r.get("reference").primitiveValue();
307      tr = resolveReference(rw, v);
308
309      if (!v.startsWith("#")) {
310        if (tr != null && tr.getReference() != null)
311          c = x.ah(tr.getReference());
312        else
313          c = x.ah(v);
314      }
315    } else {
316      v = "";
317    }
318    // what to display: if text is provided, then that. if the reference was resolved, then show the generated narrative
319    if (r.has("display")) {
320      c.addText(r.get("display").primitiveValue());
321      if (tr != null && tr.getResource() != null) {
322        c.tx(". Generated Summary: ");
323        new ProfileDrivenRenderer(context).generateResourceSummary(c, tr.getResource(), true, v.startsWith("#"), false);
324      }
325    } else if (tr != null && tr.getResource() != null) {
326      new ProfileDrivenRenderer(context).generateResourceSummary(c, tr.getResource(), v.startsWith("#"), v.startsWith("#"), false);
327    } else {
328      c.addText(v);
329    }
330  }
331  
332  protected ResourceWithReference resolveReference(ResourceWrapper res, String url) {
333    if (url == null)
334      return null;
335    if (url.startsWith("#") && res != null) {
336      for (ResourceWrapper r : res.getContained()) {
337        if (r.getId().equals(url.substring(1)))
338          return new ResourceWithReference(null, r);
339      }
340      return null;
341    }
342    String version = null;
343    if (url.contains("/_history/")) {
344      version = url.substring(url.indexOf("/_history/")+10);
345      url = url.substring(0, url.indexOf("/_history/"));
346    }
347
348    if (rcontext != null) {
349      BundleEntryComponent bundleResource = rcontext.resolve(url);
350      if (bundleResource != null) {
351        String id = bundleResource.getResource().getId();
352        if (id == null) {
353          id = makeIdFromBundleEntry(bundleResource.getFullUrl());
354        }
355        String bundleUrl = "#" + bundleResource.getResource().getResourceType().name() + "_" + id; 
356        return new ResourceWithReference(bundleUrl, new ResourceWrapperDirect(this.context, bundleResource.getResource()));
357      }
358      org.hl7.fhir.r5.elementmodel.Element bundleElement = rcontext.resolveElement(url, version);
359      if (bundleElement != null) {
360        String bundleUrl = null;
361        Element br = bundleElement.getNamedChild("resource");
362        if (br.getChildValue("id") != null) {
363          bundleUrl = "#" + br.fhirType() + "_" + br.getChildValue("id");
364        } else {
365          bundleUrl = "#" +fullUrlToAnchor(bundleElement.getChildValue("fullUrl"));          
366        }
367        return new ResourceWithReference(bundleUrl, new ResourceWrapperMetaElement(this.context, br));
368      }
369    }
370
371    Resource ae = getContext().getWorker().fetchResource(null, url, version);
372    if (ae != null)
373      return new ResourceWithReference(url, new ResourceWrapperDirect(this.context, ae));
374    else if (context.getResolver() != null) {
375      return context.getResolver().resolve(context, url);
376    } else
377      return null;
378  }
379  
380  
381  protected String makeIdFromBundleEntry(String url) {
382    if (url == null) {
383      return null;
384    }
385    if (url.startsWith("urn:uuid:")) {
386      return url.substring(9).toLowerCase();
387    }
388    return fullUrlToAnchor(url);    
389  }
390
391  private String fullUrlToAnchor(String url) {
392    return url.replace(":", "").replace("/", "_");
393  }
394
395  protected void generateCopyright(XhtmlNode x, CanonicalResource cs) {
396    XhtmlNode p = x.para();
397    p.b().tx(getContext().getWorker().translator().translate("xhtml-gen-cs", "Copyright Statement:", context.getLang()));
398    smartAddText(p, " " + cs.getCopyright());
399  }
400
401  public String displayReference(Resource res, Reference r) throws UnsupportedEncodingException, IOException {
402    return "todo"; 
403   }
404   
405
406   public Base parseType(String string, String type) {
407     return null;
408   }
409
410   protected PropertyWrapper getProperty(ResourceWrapper res, String name) {
411     for (PropertyWrapper t : res.children()) {
412       if (t.getName().equals(name))
413         return t;
414     }
415     return null;
416   }
417
418   protected PropertyWrapper getProperty(BaseWrapper res, String name) {
419     for (PropertyWrapper t : res.children()) {
420       if (t.getName().equals(name))
421         return t;
422     }
423     return null;
424   }
425
426   protected boolean valued(PropertyWrapper pw) {
427     return pw != null && pw.hasValues();
428   }
429
430
431   protected ResourceWrapper fetchResource(BaseWrapper subject) throws UnsupportedEncodingException, FHIRException, IOException {
432     if (context.getResolver() == null)
433       return null;
434
435     PropertyWrapper ref = subject.getChildByName("reference");
436     if (ref == null || !ref.hasValues()) {
437       return null;
438     }
439     String url = ref.value().getBase().primitiveValue();
440     ResourceWithReference rr = context.getResolver().resolve(context, url);
441     return rr == null ? null : rr.getResource();
442   }
443
444
445   protected String describeStatus(PublicationStatus status, boolean experimental) {
446     switch (status) {
447     case ACTIVE: return experimental ? "Experimental" : "Active"; 
448     case DRAFT: return "draft";
449     case RETIRED: return "retired";
450     default: return "Unknown";
451     }
452   }
453
454   protected void renderCommitteeLink(XhtmlNode x, CanonicalResource cr) {
455     String code = ToolingExtensions.readStringExtension(cr, ToolingExtensions.EXT_WORKGROUP);
456     CodeSystem cs = context.getWorker().fetchCodeSystem("http://terminology.hl7.org/CodeSystem/hl7-work-group");
457     if (cs == null || !cs.hasWebPath())
458       x.tx(code);
459     else {
460       ConceptDefinitionComponent cd = CodeSystemUtilities.findCode(cs.getConcept(), code);
461       if (cd == null) {
462         x.tx(code);
463       } else {
464         x.ah(cs.getWebPath()+"#"+cs.getId()+"-"+cd.getCode()).tx(cd.getDisplay());
465       }
466     }
467   }
468
469   public static String makeInternalBundleLink(String fullUrl) {
470     return fullUrl.replace(":", "-");
471   }
472
473  public boolean canRender(Resource resource) {
474    return true;
475  }
476
477  protected void renderResourceHeader(ResourceWrapper r, XhtmlNode x, boolean doId) throws UnsupportedEncodingException, FHIRException, IOException {
478    XhtmlNode div = x.div().style("display: inline-block").style("background-color: #d9e0e7").style("padding: 6px")
479         .style("margin: 4px").style("border: 1px solid #8da1b4")
480         .style("border-radius: 5px").style("line-height: 60%");
481
482    String id = getPrimitiveValue(r, "id"); 
483    if (doId) {
484      div.an(id);
485    }
486
487    String lang = getPrimitiveValue(r, "language"); 
488    String ir = getPrimitiveValue(r, "implicitRules"); 
489    BaseWrapper meta = r.getChildByName("meta").hasValues() ? r.getChildByName("meta").getValues().get(0) : null;
490    String versionId = getPrimitiveValue(meta, "versionId");
491    String lastUpdated = getPrimitiveValue(meta, "lastUpdated");
492    String source = getPrimitiveValue(meta, "source");
493    
494    if (id != null || lang != null || versionId != null || lastUpdated != null) {
495      XhtmlNode p = plateStyle(div.para());
496      p.tx("Resource ");
497      p.tx(r.fhirType());
498      p.tx(" ");
499      if (id != null) {
500        p.tx("\""+id+"\" ");
501      }
502      if (versionId != null) {
503        p.tx("Version \""+versionId+"\" ");
504      }
505      if (lastUpdated != null) {
506        p.tx("Updated \"");
507        renderDateTime(p, lastUpdated);
508        p.tx("\" ");
509      }
510      if (lang != null) {
511        p.tx(" (Language \""+lang+"\") ");
512      }
513    }
514    if (ir != null) {
515      plateStyle(div.para()).b().tx("Special rules apply: "+ir+"!");     
516    }
517    if (source != null) {
518      plateStyle(div.para()).tx("Information Source: "+source+"!");           
519    }
520    if (meta != null) {
521      PropertyWrapper pl = meta.getChildByName("profile");
522      if (pl.hasValues()) {
523        XhtmlNode p = plateStyle(div.para());
524        p.tx(Utilities.pluralize("Profile", pl.getValues().size())+": ");
525        boolean first = true;
526        for (BaseWrapper bw : pl.getValues()) {
527          if (first) first = false; else p.tx(", ");
528          renderCanonical(r, p, bw.getBase().primitiveValue());
529        }
530      }
531      PropertyWrapper tl = meta.getChildByName("tag");
532      if (tl.hasValues()) {
533        XhtmlNode p = plateStyle(div.para());
534        p.tx(Utilities.pluralize("Tag", tl.getValues().size())+": ");
535        boolean first = true;
536        for (BaseWrapper bw : tl.getValues()) {
537          if (first) first = false; else p.tx(", ");
538          String system = getPrimitiveValue(bw, "system");
539          String version = getPrimitiveValue(bw, "version");
540          String code = getPrimitiveValue(bw, "system");
541          String display = getPrimitiveValue(bw, "system");
542          renderCoding(p, new Coding(system, version, code, display));
543        }        
544      }
545      PropertyWrapper sl = meta.getChildByName("security");
546      if (sl.hasValues()) {
547        XhtmlNode p = plateStyle(div.para());
548        p.tx(Utilities.pluralize("Security Label", tl.getValues().size())+": ");
549        boolean first = true;
550        for (BaseWrapper bw : sl.getValues()) {
551          if (first) first = false; else p.tx(", ");
552          String system = getPrimitiveValue(bw, "system");
553          String version = getPrimitiveValue(bw, "version");
554          String code = getPrimitiveValue(bw, "system");
555          String display = getPrimitiveValue(bw, "system");
556          renderCoding(p, new Coding(system, version, code, display));
557        }        
558      }
559    }
560      
561  }
562
563  private XhtmlNode plateStyle(XhtmlNode para) {
564    return para.style("margin-bottom: 0px");
565  }
566
567  private String getPrimitiveValue(BaseWrapper b, String name) throws UnsupportedEncodingException, FHIRException, IOException {
568    return b != null && b.has(name) && b.getChildByName(name).hasValues() ? b.getChildByName(name).getValues().get(0).getBase().primitiveValue() : null;
569  }
570
571  private String getPrimitiveValue(ResourceWrapper r, String name) throws UnsupportedEncodingException, FHIRException, IOException {
572    return r.has(name) && r.getChildByName(name).hasValues() ? r.getChildByName(name).getValues().get(0).getBase().primitiveValue() : null;
573  }
574
575  public void renderOrError(DomainResource dr) {
576    try {
577      render(dr);
578    } catch (Exception e) {
579      XhtmlNode x = new XhtmlNode(NodeType.Element, "div");
580      x.para().tx("Error rendering: "+e.getMessage());
581      dr.setText(null);
582      inject(dr, x, NarrativeStatus.GENERATED);   
583    }
584    
585  }
586  
587  public RendererType getRendererType() {
588    return RendererType.NATIVE;
589  }
590  
591  public class TableRowData {
592    private Map<String, List<DataType>> cols = new HashMap<>();
593    private TableData data;
594    
595    public void value(String name, DataType value) {
596      if (!cols.containsKey(name)) {
597        cols.put(name, new ArrayList<>());
598      }
599      if (!data.columns.contains(name)) {
600        data.columns.add(name);
601      }
602      cols.get(name).add(value);
603    }
604
605    public boolean hasCol(String name) {
606      return cols.containsKey(name);
607    }
608
609    public List<DataType> get(String name) {
610      return cols.get(name);
611    }
612    
613  }
614  public class TableData {
615    private String title;
616    private List<String> columns = new ArrayList<>();
617    private List<TableRowData> rows = new ArrayList<>();
618    public TableData(String title) {
619      this.title = title;
620    }
621    public String getTitle() {
622      return title;
623    }
624    public List<String> getColumns() {
625      return columns;
626    }
627    public List<TableRowData> getRows() {
628      return rows;
629    }
630    public void addColumn(String name) {
631      columns.add(name);
632    }
633    public TableRowData addRow() {
634      TableRowData res = new TableRowData();
635      rows.add(res);
636      res.data = this;
637      return res;
638    }
639  }
640
641
642  public void renderTable(TableData provider, XhtmlNode x) throws FHIRFormatError, DefinitionException, IOException {
643    List<String> columns = new ArrayList<>();
644    for (String name : provider.getColumns()) {
645      boolean hasData = false;
646      for (TableRowData row : provider.getRows()) {
647        if (row.hasCol(name)) {
648          hasData = true;
649        }
650      }
651      if (hasData) {
652        columns.add(name);
653      }
654    }
655    if (columns.size() > 0) {
656      XhtmlNode table = x.table("grid");
657      
658      if (provider.getTitle() != null) {
659        table.tr().td().colspan(columns.size()).b().tx(provider.getTitle());
660      }
661      XhtmlNode tr = table.tr();
662      for (String col : columns) {
663        tr.th().b().tx(col);
664      }
665      for (TableRowData row : provider.getRows()) {
666        tr = table.tr();
667        for (String col : columns) {
668          XhtmlNode td = tr.td();
669          boolean first = true;
670          List<DataType> list = row.get(col);
671          if (list != null) {
672            for (DataType value : list) {
673              if (first) first = false; else td.tx(", ");
674              render(td, value);
675            }
676          }
677        }
678      }      
679    }
680  }
681}