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}