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