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