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.CanonicalResource;
012import org.hl7.fhir.r5.model.Resource;
013import org.hl7.fhir.r5.renderers.utils.RenderingContext;
014import org.hl7.fhir.r5.renderers.utils.ResourceWrapper;
015import org.hl7.fhir.r5.utils.EOperationOutcome;
016import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
017import org.hl7.fhir.utilities.Utilities;
018import org.hl7.fhir.utilities.xhtml.XhtmlNode;
019
020@MarkedToMoveToAdjunctPackage
021public class LibraryRenderer extends ResourceRenderer {
022
023  private static final int DATA_IMG_SIZE_CUTOFF = 4000; 
024  
025  public LibraryRenderer(RenderingContext context) { 
026    super(context); 
027  } 
028 
029  @Override
030  public String buildSummary(ResourceWrapper r) throws UnsupportedEncodingException, IOException {
031    return canonicalTitle(r);
032  }
033
034  @Override
035  public void buildNarrative(RenderingStatus status, XhtmlNode x, ResourceWrapper lib) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome {
036    renderResourceTechDetails(lib, x);
037    genSummaryTable(status, x, (CanonicalResource) lib.getResourceNative());
038    List<ResourceWrapper> authors = lib.children("author");
039    List<ResourceWrapper> editors = lib.children("editor");
040    List<ResourceWrapper> reviewers = lib.children("reviewer");
041    List<ResourceWrapper> endorsers = lib.children("endorser");
042    if (!authors.isEmpty() || !editors.isEmpty() || !reviewers.isEmpty() || !endorsers.isEmpty()) {
043      boolean email = hasCT(authors, "email") || hasCT(editors, "email") || hasCT(reviewers, "email") || hasCT(endorsers, "email"); 
044      boolean phone = hasCT(authors, "phone") || hasCT(editors, "phone") || hasCT(reviewers, "phone") || hasCT(endorsers, "phone"); 
045      boolean url = hasCT(authors, "url") || hasCT(editors, "url") || hasCT(reviewers, "url") || hasCT(endorsers, "url"); 
046      x.h2().tx(context.formatPhrase(RenderingContext.LIB_REND_PAR));
047      XhtmlNode t = x.table("grid", false);
048      for (ResourceWrapper cd : authors) {
049        participantRow(status, t, (context.formatPhrase(RenderingContext.LIB_REND_AUT)), cd, email, phone, url);
050      }
051
052      for (ResourceWrapper cd : editors) {
053        participantRow(status, t, (context.formatPhrase(RenderingContext.LIB_REND_ED)), cd, email, phone, url);
054      }
055      for (ResourceWrapper cd : reviewers) {
056        participantRow(status, t, (context.formatPhrase(RenderingContext.LIB_REND_REV)), cd, email, phone, url);
057      }
058      for (ResourceWrapper cd : endorsers) {
059        participantRow(status, t, (context.formatPhrase(RenderingContext.LIB_REND_END)), cd, email, phone, url);
060      }
061    }
062    List<ResourceWrapper> artifacts = lib.children("relatedArtifact");
063    if (!artifacts.isEmpty()) {
064      x.h2().tx(context.formatPhrase(RenderingContext.LIB_REND_ART));
065      XhtmlNode t = x.table("grid", false);
066      boolean label = false;
067      boolean display = false;
068      boolean citation = false;
069      for (ResourceWrapper ra : artifacts) {
070        label = label || ra.has("label");
071        display = display || ra.has("display");
072        citation = citation || ra.has("citation");
073      }
074      for (ResourceWrapper ra : artifacts) {
075        renderArtifact(status, t, ra, lib, label, display, citation);
076      }      
077    }
078    List<ResourceWrapper> parameters = lib.children("parameter");
079    if (!parameters.isEmpty()) {
080      x.h2().tx(context.formatPhrase(RenderingContext.GENERAL_PARS));
081      XhtmlNode t = x.table("grid", false);
082      boolean doco = false;
083      for (ResourceWrapper p : parameters) {
084        doco = doco || p.has("documentation");
085      }
086      for (ResourceWrapper p : parameters) {
087        renderParameter(t, p, doco);
088      }      
089    }
090    List<ResourceWrapper> dataRequirements = lib.children("dataRequirement");
091    if (!dataRequirements.isEmpty()) {
092      x.h2().tx(context.formatPhrase(RenderingContext.LIB_REND_REQ));
093      for (ResourceWrapper p : dataRequirements) {
094        renderDataRequirement(status, x, p);
095      }      
096    }
097    List<ResourceWrapper> contents = lib.children("content");
098    if (!contents.isEmpty()) {
099      x.h2().tx(context.formatPhrase(RenderingContext.LIB_REND_CONT));          
100      boolean isCql = false;
101      int counter = 0;
102      for (ResourceWrapper p : contents) {
103        renderAttachment(x, p, isCql, counter, lib.getId());
104        isCql = isCql || (p.has("contentType") && p.primitiveValue("contentType").startsWith("text/cql"));
105        counter++;
106      }
107    }
108  }
109    
110  private boolean hasCT(List<ResourceWrapper> list, String type) throws UnsupportedEncodingException, FHIRException, IOException {
111    for (ResourceWrapper cd : list) {
112      List<ResourceWrapper> telecoms = cd.children("telecom");
113      if (hasContactPoint(telecoms, type)) {
114        return true;
115      }
116    }
117    return false;
118  }
119
120  private boolean hasContactPoint(List<ResourceWrapper> list, String type) {
121    for (ResourceWrapper cd : list) {
122      for (ResourceWrapper t : cd.children("telecom")) {
123        if (type.equals(t.primitiveValue("system"))) {
124          return true;
125        }
126      }
127    }
128    return false;
129  }
130
131  private ResourceWrapper getContactPoint(List<ResourceWrapper> list, String type) {
132    for (ResourceWrapper cd : list) {
133      for (ResourceWrapper t : cd.children("telecom")) {
134        if (type.equals(t.primitiveValue("system"))) {
135          return t;
136        }
137      }
138    }
139    return null;
140  }
141
142  private void renderParameter(XhtmlNode t, ResourceWrapper p, boolean doco) throws UnsupportedEncodingException, FHIRException, IOException {
143    XhtmlNode tr = t.tr();
144    tr.td().tx(p.has("name") ? p.primitiveValue("name") : null);
145    tr.td().tx(p.has("use") ? p.primitiveValue("use") : null);
146    tr.td().tx(p.has("min") ? p.primitiveValue("min") : null);
147    tr.td().tx(p.has("max") ? p.primitiveValue("max") : null);
148    tr.td().tx(p.has("type") ? p.primitiveValue("type") : null);
149    if (doco) {
150      tr.td().tx(p.has("documentation") ? p.primitiveValue("documentation") : null);
151    }
152  }
153
154
155  private void renderArtifact(RenderingStatus status, XhtmlNode t, ResourceWrapper ra, ResourceWrapper lib, boolean label, boolean display, boolean citation) throws UnsupportedEncodingException, FHIRException, IOException {
156    XhtmlNode tr = t.tr();
157    tr.td().tx(ra.has("type") ? getTranslatedCode(ra.child("type")) : null);
158    if (label) {
159      tr.td().tx(ra.has("label") ? ra.primitiveValue("label") : null);
160    }
161    if (display) {
162      tr.td().tx(ra.has("display") ? ra.primitiveValue("display") : null);
163    }
164    if (citation) {
165      tr.td().markdown(ra.has("citation") ? ra.primitiveValue("citation") : null, "Citation");
166    }
167    if (ra.has("resource")) {
168      renderCanonical(status, tr.td(), Resource.class, ra.child("resource"));
169    } else {
170      tr.td().tx(ra.has("url") ? ra.primitiveValue("url") : null);
171    }
172  }
173
174  private void participantRow(RenderingStatus status, XhtmlNode t, String label, ResourceWrapper cd, boolean email, boolean phone, boolean url) throws UnsupportedEncodingException, FHIRException, IOException {
175    XhtmlNode tr = t.tr();
176    tr.td().tx(label);
177    tr.td().tx(cd.has("name") ? cd.primitiveValue("name") : null);
178    List<ResourceWrapper> telecoms = cd.children("telecom");
179    if (email) {
180      renderContactPoint(status, tr.td(), getContactPoint(telecoms, "email"));
181    }
182    if (phone) {
183      renderContactPoint(status, tr.td(), getContactPoint(telecoms, "phone"));
184    }
185    if (url) {
186      renderContactPoint(status, tr.td(), getContactPoint(telecoms, "url"));
187    }
188  }
189
190
191  private void renderAttachment(XhtmlNode x, ResourceWrapper att, boolean noShowData, int counter, String baseId) {
192    String url = att.primitiveValue("url");
193    String title = att.primitiveValue("title");
194    String ct =  att.primitiveValue("contentType");
195    
196    boolean ref = !att.has("data") && att.has("url");
197    if (ref) {
198      XhtmlNode p = x.para();
199      if (att.has("title")) {
200        p.tx(title);
201        p.tx(": ");
202      }
203      Resource res = context.getContext().fetchResource(Resource.class, url);
204      if (res == null || !res.hasWebPath()) {
205        p.code().ah(context.prefixLocalHref(url)).tx(url);        
206      } else if (res instanceof CanonicalResource) {
207        p.code().ah(context.prefixLocalHref(res.getWebPath())).tx(((CanonicalResource) res).present());        
208      } else {
209        p.code().ah(context.prefixLocalHref(res.getWebPath())).tx(url);        
210      }
211      p.tx(" (");
212      p.code().tx(ct);
213      p.tx(lang(att));
214      p.tx(")");
215    } else if (!att.has("data")) {
216      XhtmlNode p = x.para();
217      if (att.has("title")) {
218        p.tx(title);
219        p.tx(": ");
220      }
221      p.code().tx(context.formatPhrase(RenderingContext.LIB_REND_NOCONT));
222      p.tx(" (");
223      p.code().tx(ct);
224      p.tx(lang(att));
225      p.tx(")");
226    } else {
227      byte[] cnt = Base64.decodeBase64(att.primitiveValue("data"));
228      String txt = getText(cnt);
229      if (isImage(ct)) {
230        XhtmlNode p = x.para();
231        if (att.has("title")) {
232          p.tx(title);
233          p.tx(": (");
234          p.code().tx(ct);
235          p.tx(lang(att));
236          p.tx(")");
237        }
238        else {
239          p.code().tx(ct+lang(att));
240        }
241        if (cnt.length < LibraryRenderer.DATA_IMG_SIZE_CUTOFF) {
242          x.img("data: "+ct+">;base64,"+b64(cnt), "data");
243        } else {
244          String filename = "Library-"+baseId+(counter == 0 ? "" : "-"+Integer.toString(counter))+"."+imgExtension(ct); 
245          x.img(filename, "data");
246        }        
247      } else if (txt != null && !noShowData) {
248        XhtmlNode p = x.para();
249        if (att.has("title")) {
250          p.tx(title);
251          p.tx(": (");
252          p.code().tx(ct);
253          p.tx(lang(att));
254          p.tx(")");
255        }
256        else {
257          p.code().tx(ct+lang(att));
258        }
259        String prismCode = determinePrismCode(ct);
260        if (prismCode != null && !tooBig(txt)) {
261          x.pre().code().setAttribute("class", "language-"+prismCode).tx(txt);
262        } else {
263          x.pre().code().tx(txt);
264        }
265      } else {
266        XhtmlNode p = x.para();
267        if (att.has("title")) {
268          p.tx(title);
269          p.tx(": ");
270        }
271        p.code().tx(context.formatPhrase(RenderingContext.LIB_REND_SHOW));
272        p.code().tx(ct);
273        p.tx(lang(att));
274        p.tx((context.formatPhrase(RenderingContext.LIB_REND_SIZE, Utilities.describeSize(cnt.length))+" ")+")");
275      }
276    }    
277  }
278
279  private boolean tooBig(String txt) {
280    return txt.length() > 16384;
281  }
282
283  private String imgExtension(String contentType) {
284    if (contentType != null && contentType.startsWith("image/")) {
285      if (contentType.startsWith("image/png")) {
286        return "png";
287      }
288      if (contentType.startsWith("image/jpeg")) {
289        return "jpg";
290      }
291    }
292    return null;
293  }
294
295  private String b64(byte[] data) {
296    byte[] encodeBase64 = Base64.encodeBase64(data);
297    return new String(encodeBase64);
298  }
299
300  private boolean isImage(String contentType) {
301    return imgExtension(contentType) != null;
302  }
303
304  private String lang(ResourceWrapper att) {
305    if (att.has("language")) {
306      return ", language = "+describeLang(att.primitiveValue("language"));
307    }
308    return "";
309  }
310
311  private String getText( byte[] cnt) {
312    try {
313      try {
314        String src = new String(cnt, "UTF-8");
315        if (checkString(src)) {
316          return src;
317        }
318      } catch (Exception e) {
319        // ignore
320      }
321      try {
322        String src = new String(cnt, "UTF-16");
323        if (checkString(src)) {
324          return src;
325        }
326      } catch (Exception e) {
327        // ignore
328      }
329      try {
330        String src = new String(cnt, "ASCII");
331        if (checkString(src)) {
332          return src;
333        }
334      } catch (Exception e) {
335        // ignore
336      }
337      return null;      
338    } catch (Exception e) {
339      return null;
340    }
341  }
342
343  public boolean checkString(String src) {
344    for (char ch : src.toCharArray()) {
345      if (ch < ' ' && ch != '\r' && ch != '\n' && ch != '\t') {
346        return false;
347      }
348    }
349    return true;
350  }
351
352  private String determinePrismCode(String ct) {
353    if (!Utilities.noString(ct)) {
354      if (ct.contains(";")) {
355        ct = ct.substring(0, ct.indexOf(";"));
356      }
357      switch (ct) {
358      case "text/html" : return "html";
359      case "text/xml" : return "xml";
360      case "application/xml" : return "xml";
361      case "text/markdown" : return "markdown";
362      case "application/js" : return "JavaScript";
363      case "application/css" : return "css";
364      case "text/x-csrc" : return "c";
365      case "text/x-csharp" : return "csharp";
366      case "text/x-c++src" : return "cpp";
367      case "application/graphql" : return "graphql";
368      case "application/x-java" : return "java";
369      case "application/json" : return "json";
370      case "text/json" : return "json";
371      case "application/liquid" : return "liquid";
372      case "text/x-pascal" : return "pascal";
373      case "text/x-python" : return "python";
374      case "text/x-rsrc" : return "r";
375      case "text/x-ruby" : return "ruby";
376      case "text/x-sas" : return "sas";
377      case "text/x-sql" : return "sql";
378      case "application/typescript" : return "typescript";
379      case "text/cql" : return "sql"; // not that bad...
380      }
381      if (ct.contains("json+") || ct.contains("+json")) {
382        return "json";
383      }
384      if (ct.contains("xml+") || ct.contains("+xml")) {
385        return "xml";
386      }
387    }
388    return null;
389  }
390  
391  
392}