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