001package org.hl7.fhir.r5.renderers;
002
003import java.io.IOException;
004import java.io.UnsupportedEncodingException;
005import java.util.List;
006
007import org.apache.commons.codec.binary.Base64;
008import org.hl7.fhir.exceptions.DefinitionException;
009import org.hl7.fhir.exceptions.FHIRException;
010import org.hl7.fhir.exceptions.FHIRFormatError;
011import org.hl7.fhir.r5.model.Annotation;
012import org.hl7.fhir.r5.model.Attachment;
013import org.hl7.fhir.r5.model.Base;
014import org.hl7.fhir.r5.model.ContactDetail;
015import org.hl7.fhir.r5.model.ContactPoint;
016import org.hl7.fhir.r5.model.DataRequirement;
017import org.hl7.fhir.r5.model.DomainResource;
018import org.hl7.fhir.r5.model.Library;
019import org.hl7.fhir.r5.model.ListResource;
020import org.hl7.fhir.r5.model.ListResource.ListResourceEntryComponent;
021import org.hl7.fhir.r5.model.ParameterDefinition;
022import org.hl7.fhir.r5.model.Reference;
023import org.hl7.fhir.r5.model.RelatedArtifact;
024import org.hl7.fhir.r5.model.Resource;
025import org.hl7.fhir.r5.renderers.utils.BaseWrappers.BaseWrapper;
026import org.hl7.fhir.r5.renderers.utils.BaseWrappers.PropertyWrapper;
027import org.hl7.fhir.r5.renderers.utils.BaseWrappers.ResourceWrapper;
028import org.hl7.fhir.r5.renderers.utils.RenderingContext;
029import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext;
030import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceWithReference;
031import org.hl7.fhir.utilities.Utilities;
032import org.hl7.fhir.utilities.xhtml.XhtmlNode;
033
034public class LibraryRenderer extends ResourceRenderer {
035
036  private static final int DATA_IMG_SIZE_CUTOFF = 4000;
037
038  public LibraryRenderer(RenderingContext context) {
039    super(context);
040  }
041
042  public LibraryRenderer(RenderingContext context, ResourceContext rcontext) {
043    super(context, rcontext);
044  }
045  
046  public boolean render(XhtmlNode x, Resource dr) throws FHIRFormatError, DefinitionException, IOException {
047    return render(x, (Library) dr);
048  }
049
050  public boolean render(XhtmlNode x, ResourceWrapper lib) throws FHIRFormatError, DefinitionException, IOException {
051    PropertyWrapper authors = lib.getChildByName("author");
052    PropertyWrapper editors = lib.getChildByName("editor");
053    PropertyWrapper reviewers = lib.getChildByName("reviewer");
054    PropertyWrapper endorsers = lib.getChildByName("endorser");
055    if ((authors != null && authors.hasValues()) || (editors != null && editors.hasValues()) || (reviewers != null && reviewers.hasValues()) || (endorsers != null && endorsers.hasValues())) {
056      boolean email = hasCT(authors, "email") || hasCT(editors, "email") || hasCT(reviewers, "email") || hasCT(endorsers, "email"); 
057      boolean phone = hasCT(authors, "phone") || hasCT(editors, "phone") || hasCT(reviewers, "phone") || hasCT(endorsers, "phone"); 
058      boolean url = hasCT(authors, "url") || hasCT(editors, "url") || hasCT(reviewers, "url") || hasCT(endorsers, "url"); 
059      x.h2().tx("Participants");
060      XhtmlNode t = x.table("grid");
061      if (authors != null) {
062        for (BaseWrapper cd : authors.getValues()) {
063          participantRow(t, "Author", cd, email, phone, url);
064        }
065      }
066      if (authors != null) {
067        for (BaseWrapper cd : editors.getValues()) {
068          participantRow(t, "Editor", cd, email, phone, url);
069        }
070      }
071      if (authors != null) {
072        for (BaseWrapper cd : reviewers.getValues()) {
073          participantRow(t, "Reviewer", cd, email, phone, url);
074        }
075      }
076      if (authors != null) {
077        for (BaseWrapper cd : endorsers.getValues()) {
078          participantRow(t, "Endorser", cd, email, phone, url);
079        }
080      }
081    }
082    PropertyWrapper artifacts = lib.getChildByName("relatedArtifact");
083    if (artifacts != null && artifacts.hasValues()) {
084      x.h2().tx("Related Artifacts");
085      XhtmlNode t = x.table("grid");
086      boolean label = false;
087      boolean display = false;
088      boolean citation = false;
089      for (BaseWrapper ra : artifacts.getValues()) {
090        label = label || ra.has("label");
091        display = display || ra.has("display");
092        citation = citation || ra.has("citation");
093      }
094      for (BaseWrapper ra : artifacts.getValues()) {
095        renderArtifact(t, ra, lib, label, display, citation);
096      }      
097    }
098    PropertyWrapper parameters = lib.getChildByName("parameter");
099    if (parameters != null && parameters.hasValues()) {
100      x.h2().tx("Parameters");
101      XhtmlNode t = x.table("grid");
102      boolean doco = false;
103      for (BaseWrapper p : parameters.getValues()) {
104        doco = doco || p.has("documentation");
105      }
106      for (BaseWrapper p : parameters.getValues()) {
107        renderParameter(t, p, doco);
108      }      
109    }
110    PropertyWrapper dataRequirements = lib.getChildByName("dataRequirement");
111    if (dataRequirements != null && dataRequirements.hasValues()) {
112      x.h2().tx("Data Requirements");
113      for (BaseWrapper p : dataRequirements.getValues()) {
114        renderDataRequirement(x, (DataRequirement) p.getBase());
115      }      
116    }
117    PropertyWrapper contents = lib.getChildByName("content");
118    if (contents != null) {
119      x.h2().tx("Contents");          
120      boolean isCql = false;
121      int counter = 0;
122      for (BaseWrapper p : contents.getValues()) {
123        Attachment att = (Attachment) p.getBase();
124        renderAttachment(x, att, isCql, counter, lib.getId());
125        isCql = isCql || (att.hasContentType() && att.getContentType().startsWith("text/cql"));
126        counter++;
127      }
128    }
129    return false;
130  }
131    
132  private boolean hasCT(PropertyWrapper prop, String type) throws UnsupportedEncodingException, FHIRException, IOException {
133    if (prop != null) {
134      for (BaseWrapper cd : prop.getValues()) {
135        PropertyWrapper telecoms = cd.getChildByName("telecom");
136        if (getContactPoint(telecoms, type) != null) {
137          return true;
138        }
139      }
140    }
141    return false;
142  }
143
144  private boolean hasCT(List<ContactDetail> list, String type) {
145    for (ContactDetail cd : list) {
146      for (ContactPoint t : cd.getTelecom()) {
147        if (type.equals(t.getSystem().toCode())) {
148          return true;
149        }
150      }
151    }
152    return false;
153  }
154
155  
156  public boolean render(XhtmlNode x, Library lib) throws FHIRFormatError, DefinitionException, IOException {
157    if (lib.hasAuthor() || lib.hasEditor() || lib.hasReviewer() || lib.hasEndorser()) {
158      boolean email = hasCT(lib.getAuthor(), "email") || hasCT(lib.getEditor(), "email") || hasCT(lib.getReviewer(), "email") || hasCT(lib.getEndorser(), "email"); 
159      boolean phone = hasCT(lib.getAuthor(), "phone") || hasCT(lib.getEditor(), "phone") || hasCT(lib.getReviewer(), "phone") || hasCT(lib.getEndorser(), "phone"); 
160      boolean url = hasCT(lib.getAuthor(), "url") || hasCT(lib.getEditor(), "url") || hasCT(lib.getReviewer(), "url") || hasCT(lib.getEndorser(), "url"); 
161      x.h2().tx("Participants");
162      XhtmlNode t = x.table("grid");
163      for (ContactDetail cd : lib.getAuthor()) {
164        participantRow(t, "Author", cd, email, phone, url);
165      }
166      for (ContactDetail cd : lib.getEditor()) {
167        participantRow(t, "Editor", cd, email, phone, url);
168      }
169      for (ContactDetail cd : lib.getReviewer()) {
170        participantRow(t, "Reviewer", cd, email, phone, url);
171      }
172      for (ContactDetail cd : lib.getEndorser()) {
173        participantRow(t, "Endorser", cd, email, phone, url);
174      }
175    }
176    if (lib.hasRelatedArtifact()) {
177      x.h2().tx("Related Artifacts");
178      XhtmlNode t = x.table("grid");
179      boolean label = false;
180      boolean display = false;
181      boolean citation = false;
182      for (RelatedArtifact ra : lib.getRelatedArtifact()) {
183        label = label || ra.hasLabel();
184        display = display || ra.hasDisplay();
185        citation = citation || ra.hasCitation();
186      }
187      for (RelatedArtifact ra : lib.getRelatedArtifact()) {
188        renderArtifact(t, ra, lib, label, display, citation);
189      }      
190    }
191    if (lib.hasParameter()) {
192      x.h2().tx("Parameters");
193      XhtmlNode t = x.table("grid");
194      boolean doco = false;
195      for (ParameterDefinition p : lib.getParameter()) {
196        doco = doco || p.hasDocumentation();
197      }
198      for (ParameterDefinition p : lib.getParameter()) {
199        renderParameter(t, p, doco);
200      }      
201    }
202    if (lib.hasDataRequirement()) {
203      x.h2().tx("Data Requirements");
204      for (DataRequirement p : lib.getDataRequirement()) {
205        renderDataRequirement(x, p);
206      }      
207    }
208    if (lib.hasContent()) {
209      x.h2().tx("Contents");          
210      boolean isCql = false;
211      int counter = 0;
212      for (Attachment att : lib.getContent()) {
213        renderAttachment(x, att, isCql, counter, lib.getId());
214        isCql = isCql || (att.hasContentType() && att.getContentType().startsWith("text/cql"));
215        counter++;
216      }
217    }
218    return false;
219  }
220
221  private void renderParameter(XhtmlNode t, BaseWrapper p, boolean doco) throws UnsupportedEncodingException, FHIRException, IOException {
222    XhtmlNode tr = t.tr();
223    tr.td().tx(p.has("name") ? p.get("name").primitiveValue() : null);
224    tr.td().tx(p.has("use") ? p.get("use").primitiveValue() : null);
225    tr.td().tx(p.has("min") ? p.get("min").primitiveValue() : null);
226    tr.td().tx(p.has("max") ? p.get("max").primitiveValue() : null);
227    tr.td().tx(p.has("type") ? p.get("type").primitiveValue() : null);
228    if (doco) {
229      tr.td().tx(p.has("documentation") ? p.get("documentation").primitiveValue() : null);
230    }
231  }
232
233  private void renderParameter(XhtmlNode t, ParameterDefinition p, boolean doco) {
234    XhtmlNode tr = t.tr();
235    tr.td().tx(p.getName());
236    tr.td().tx(p.getUse().getDisplay());
237    tr.td().tx(p.getMin());
238    tr.td().tx(p.getMax());
239    tr.td().tx(p.getType().getDisplay());
240    if (doco) {
241      tr.td().tx(p.getDocumentation());
242    }
243  }
244
245  private void renderArtifact(XhtmlNode t, BaseWrapper ra, ResourceWrapper lib, boolean label, boolean display, boolean citation) throws UnsupportedEncodingException, FHIRException, IOException {
246    XhtmlNode tr = t.tr();
247    tr.td().tx(ra.has("type") ? ra.get("type").primitiveValue() : null);
248    if (label) {
249      tr.td().tx(ra.has("label") ? ra.get("label").primitiveValue() : null);
250    }
251    if (display) {
252      tr.td().tx(ra.has("display") ? ra.get("display").primitiveValue() : null);
253    }
254    if (citation) {
255      tr.td().markdown(ra.has("citation") ? ra.get("citation").primitiveValue() : null, "Citation");
256    }
257    if (ra.has("resource")) {
258      renderCanonical(lib, tr.td(), ra.get("resource").primitiveValue());
259    } else {
260      tr.td().tx(ra.has("url") ? ra.get("url").primitiveValue() : null);
261    }
262  }
263
264  private void renderArtifact(XhtmlNode t, RelatedArtifact ra, Resource lib, boolean label, boolean display, boolean citation) throws IOException {
265    XhtmlNode tr = t.tr();
266    tr.td().tx(ra.getType().getDisplay());
267    if (label) {
268      tr.td().tx(ra.getLabel());
269    }
270    if (display) {
271      tr.td().tx(ra.getDisplay());
272    }
273    if (citation) {
274      tr.td().markdown(ra.getCitation(), "Citation");
275    }
276    if (ra.hasResource()) {
277      renderCanonical(lib, tr.td(), ra.getResource());
278    } else {
279      renderAttachment(tr.td(), ra.getDocument(), false, 0, lib.getId());
280    }
281  }
282
283  private void participantRow(XhtmlNode t, String label, BaseWrapper cd, boolean email, boolean phone, boolean url) throws UnsupportedEncodingException, FHIRException, IOException {
284    XhtmlNode tr = t.tr();
285    tr.td().tx(label);
286    tr.td().tx(cd.get("name") != null ? cd.get("name").primitiveValue() : null);
287    PropertyWrapper telecoms = cd.getChildByName("telecom");
288    if (email) {
289      renderContactPoint(tr.td(), getContactPoint(telecoms, "email"));
290    }
291    if (phone) {
292      renderContactPoint(tr.td(), getContactPoint(telecoms, "phone"));
293    }
294    if (url) {
295      renderContactPoint(tr.td(), getContactPoint(telecoms, "url"));
296    }
297  }
298
299  private ContactPoint getContactPoint(PropertyWrapper telecoms, String value) throws UnsupportedEncodingException, FHIRException, IOException {
300    for (BaseWrapper t : telecoms.getValues()) {
301      if (t.has("system")) {
302        String system = t.get("system").primitiveValue();
303        if (value.equals(system)) {
304          return (ContactPoint) t.getBase();
305        }
306      }
307    } 
308    return null;
309  }
310
311  private void participantRow(XhtmlNode t, String label, ContactDetail cd, boolean email, boolean phone, boolean url) {
312    XhtmlNode tr = t.tr();
313    tr.td().tx(label);
314    tr.td().tx(cd.getName());
315    if (email) {
316      renderContactPoint(tr.td(), cd.getEmail());
317    }
318    if (phone) {
319      renderContactPoint(tr.td(), cd.getPhone());
320    }
321    if (url) {
322      renderContactPoint(tr.td(), cd.getUrl());
323    }
324  }
325
326  public void describe(XhtmlNode x, Library lib) {
327    x.tx(display(lib));
328  }
329
330  public String display(Library lib) {
331    return lib.present();
332  }
333
334  @Override
335  public String display(Resource r) throws UnsupportedEncodingException, IOException {
336    return ((Library) r).present();
337  }
338
339  @Override
340  public String display(ResourceWrapper r) throws UnsupportedEncodingException, IOException {
341    if (r.has("title")) {
342      return r.children("title").get(0).getBase().primitiveValue();
343    }
344    return "??";
345  }
346
347  private void renderAttachment(XhtmlNode x, Attachment att, boolean noShowData, int counter, String baseId) {
348    boolean ref = !att.hasData() && att.hasUrl();
349    if (ref) {
350      XhtmlNode p = x.para();
351      if (att.hasTitle()) {
352        p.tx(att.getTitle());
353        p.tx(": ");
354      }
355      p.code().ah(att.getUrl()).tx(att.getUrl());
356      p.tx(" (");
357      p.code().tx(att.getContentType());
358      p.tx(lang(att));
359      p.tx(")");
360    } else if (!att.hasData()) {
361      XhtmlNode p = x.para();
362      if (att.hasTitle()) {
363        p.tx(att.getTitle());
364        p.tx(": ");
365      }
366      p.code().tx("No Content");
367      p.tx(" (");
368      p.code().tx(att.getContentType());
369      p.tx(lang(att));
370      p.tx(")");
371    } else {
372      String txt = getText(att);
373      if (isImage(att.getContentType())) {
374        XhtmlNode p = x.para();
375        if (att.hasTitle()) {
376          p.tx(att.getTitle());
377          p.tx(": (");
378          p.code().tx(att.getContentType());
379          p.tx(lang(att));
380          p.tx(")");
381        }
382        else {
383          p.code().tx(att.getContentType()+lang(att));
384        }
385        if (att.getData().length < LibraryRenderer.DATA_IMG_SIZE_CUTOFF) {
386          x.img("data: "+att.getContentType()+">;base64,"+b64(att.getData()));
387        } else {
388          String filename = "Library-"+baseId+(counter == 0 ? "" : "-"+Integer.toString(counter))+"."+imgExtension(att.getContentType()); 
389          x.img(filename);
390        }        
391      } else if (txt != null && !noShowData) {
392        XhtmlNode p = x.para();
393        if (att.hasTitle()) {
394          p.tx(att.getTitle());
395          p.tx(": (");
396          p.code().tx(att.getContentType());
397          p.tx(lang(att));
398          p.tx(")");
399        }
400        else {
401          p.code().tx(att.getContentType()+lang(att));
402        }
403        String prismCode = determinePrismCode(att);
404        if (prismCode != null && !tooBig(txt)) {
405          x.pre().code().setAttribute("class", "language-"+prismCode).tx(txt);
406        } else {
407          x.pre().code().tx(txt);
408        }
409      } else {
410        XhtmlNode p = x.para();
411        if (att.hasTitle()) {
412          p.tx(att.getTitle());
413          p.tx(": ");
414        }
415        p.code().tx("Content not shown - (");
416        p.code().tx(att.getContentType());
417        p.tx(lang(att));
418        p.tx(", size = "+Utilities.describeSize(att.getData().length)+")");
419      }
420    }    
421  }
422
423  private boolean tooBig(String txt) {
424    return txt.length() > 16384;
425  }
426
427  private String imgExtension(String contentType) {
428    if (contentType != null && contentType.startsWith("image/")) {
429      if (contentType.startsWith("image/png")) {
430        return "png";
431      }
432      if (contentType.startsWith("image/jpeg")) {
433        return "jpg";
434      }
435    }
436    return null;
437  }
438
439  private String b64(byte[] data) {
440    byte[] encodeBase64 = Base64.encodeBase64(data);
441    return new String(encodeBase64);
442  }
443
444  private boolean isImage(String contentType) {
445    return imgExtension(contentType) != null;
446  }
447
448  private String lang(Attachment att) {
449    if (att.hasLanguage()) {
450      return ", language = "+describeLang(att.getLanguage());
451    }
452    return "";
453  }
454
455  private String getText(Attachment att) {
456    try {
457      try {
458        String src = new String(att.getData(), "UTF-8");
459        if (checkString(src)) {
460          return src;
461        }
462      } catch (Exception e) {
463        // ignore
464      }
465      try {
466        String src = new String(att.getData(), "UTF-16");
467        if (checkString(src)) {
468          return src;
469        }
470      } catch (Exception e) {
471        // ignore
472      }
473      try {
474        String src = new String(att.getData(), "ASCII");
475        if (checkString(src)) {
476          return src;
477        }
478      } catch (Exception e) {
479        // ignore
480      }
481      return null;      
482    } catch (Exception e) {
483      return null;
484    }
485  }
486
487  public boolean checkString(String src) {
488    for (char ch : src.toCharArray()) {
489      if (ch < ' ' && ch != '\r' && ch != '\n' && ch != '\t') {
490        return false;
491      }
492    }
493    return true;
494  }
495
496  private String determinePrismCode(Attachment att) {
497    if (att.hasContentType()) {
498      String ct = att.getContentType();
499      if (ct.contains(";")) {
500        ct = ct.substring(0, ct.indexOf(";"));
501      }
502      switch (ct) {
503      case "text/html" : return "html";
504      case "text/xml" : return "xml";
505      case "application/xml" : return "xml";
506      case "text/markdown" : return "markdown";
507      case "application/js" : return "JavaScript";
508      case "application/css" : return "css";
509      case "text/x-csrc" : return "c";
510      case "text/x-csharp" : return "csharp";
511      case "text/x-c++src" : return "cpp";
512      case "application/graphql" : return "graphql";
513      case "application/x-java" : return "java";
514      case "application/json" : return "json";
515      case "text/json" : return "json";
516      case "application/liquid" : return "liquid";
517      case "text/x-pascal" : return "pascal";
518      case "text/x-python" : return "python";
519      case "text/x-rsrc" : return "r";
520      case "text/x-ruby" : return "ruby";
521      case "text/x-sas" : return "sas";
522      case "text/x-sql" : return "sql";
523      case "application/typescript" : return "typescript";
524      case "text/cql" : return "sql"; // not that bad...
525      }
526      if (att.getContentType().contains("json+") || att.getContentType().contains("+json")) {
527        return "json";
528      }
529      if (att.getContentType().contains("xml+") || att.getContentType().contains("+xml")) {
530        return "xml";
531      }
532    }
533    return null;
534  }
535  
536  
537}