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