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