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