
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}