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