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