001package org.hl7.fhir.utilities.xhtml; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2017 University Health Network 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023 024/* 025Copyright (c) 2011+, HL7, Inc 026All rights reserved. 027 028Redistribution and use in source and binary forms, with or without modification, 029are permitted provided that the following conditions are met: 030 031 * Redistributions of source code must retain the above copyright notice, this 032 list of conditions and the following disclaimer. 033 * Redistributions in binary form must reproduce the above copyright notice, 034 this list of conditions and the following disclaimer in the documentation 035 and/or other materials provided with the distribution. 036 * Neither the name of HL7 nor the names of its contributors may be used to 037 endorse or promote products derived from this software without specific 038 prior written permission. 039 040THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 041ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 042WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 043IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 044INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 045NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 046PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 047WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 048ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 049POSSIBILITY OF SUCH DAMAGE. 050 051*/ 052 053import java.awt.Color; 054import java.awt.image.BufferedImage; 055import java.io.ByteArrayOutputStream; 056import java.io.File; 057import java.io.FileOutputStream; 058import java.io.IOException; 059import java.io.OutputStream; 060import java.util.ArrayList; 061import java.util.HashMap; 062import java.util.List; 063import java.util.Map; 064 065import javax.imageio.ImageIO; 066 067import org.apache.commons.codec.binary.Base64; 068import org.apache.commons.io.FileUtils; 069import org.hl7.fhir.exceptions.FHIRException; 070import org.hl7.fhir.utilities.Utilities; 071 072 073public class HierarchicalTableGenerator { 074 public static final String TEXT_ICON_REFERENCE = "Reference to another Resource"; 075 public static final String TEXT_ICON_PRIMITIVE = "Primitive Data Type"; 076 public static final String TEXT_ICON_DATATYPE = "Data Type"; 077 public static final String TEXT_ICON_RESOURCE = "Resource"; 078 public static final String TEXT_ICON_ELEMENT = "Element"; 079 public static final String TEXT_ICON_REUSE = "Reference to another Element"; 080 public static final String TEXT_ICON_EXTENSION = "Extension"; 081 public static final String TEXT_ICON_CHOICE = "Choice of Types"; 082 public static final String TEXT_ICON_SLICE = "Slice Definition"; 083 public static final String TEXT_ICON_EXTENSION_SIMPLE = "Simple Extension"; 084 public static final String TEXT_ICON_PROFILE = "Profile"; 085 public static final String TEXT_ICON_EXTENSION_COMPLEX = "Complex Extension"; 086 087 private static Map<String, String> files = new HashMap<String, String>(); 088 089 public class Piece { 090 private String tag; 091 private String reference; 092 private String text; 093 private String hint; 094 private String style; 095 private Map<String, String> attributes; 096 097 public Piece(String tag) { 098 super(); 099 this.tag = tag; 100 } 101 102 public Piece(String reference, String text, String hint) { 103 super(); 104 this.reference = reference; 105 this.text = text; 106 this.hint = hint; 107 } 108 public String getReference() { 109 return reference; 110 } 111 public void setReference(String value) { 112 reference = value; 113 } 114 public String getText() { 115 return text; 116 } 117 public String getHint() { 118 return hint; 119 } 120 121 public String getTag() { 122 return tag; 123 } 124 125 public String getStyle() { 126 return style; 127 } 128 129 public void setTag(String tag) { 130 this.tag = tag; 131 } 132 133 public void setText(String text) { 134 this.text = text; 135 } 136 137 public void setHint(String hint) { 138 this.hint = hint; 139 } 140 141 public Piece setStyle(String style) { 142 this.style = style; 143 return this; 144 } 145 146 public Piece addStyle(String style) { 147 if (this.style != null) 148 this.style = this.style+"; "+style; 149 else 150 this.style = style; 151 return this; 152 } 153 154 public void addToHint(String text) { 155 if (this.hint == null) 156 this.hint = text; 157 else 158 this.hint += (this.hint.endsWith(".") || this.hint.endsWith("?") ? " " : ". ")+text; 159 } 160 } 161 162 public class Cell { 163 private List<Piece> pieces = new ArrayList<HierarchicalTableGenerator.Piece>(); 164 165 public Cell() { 166 167 } 168 public Cell(String prefix, String reference, String text, String hint, String suffix) { 169 super(); 170 if (!Utilities.noString(prefix)) 171 pieces.add(new Piece(null, prefix, null)); 172 pieces.add(new Piece(reference, text, hint)); 173 if (!Utilities.noString(suffix)) 174 pieces.add(new Piece(null, suffix, null)); 175 } 176 public List<Piece> getPieces() { 177 return pieces; 178 } 179 public Cell addPiece(Piece piece) { 180 pieces.add(piece); 181 return this; 182 } 183// private List<Piece> htmlToParagraphPieces(String html) { 184// List<Piece> myPieces = new ArrayList<Piece>(); 185// String[] paragraphs = html.replace("<p>", "").split("<\\/p>|<br \\/>"); 186// for (int i=0;i<paragraphs.length;i++) { 187// if (!paragraphs[i].isEmpty()) { 188// if (i!=0) { 189// myPieces.add(new Piece("br")); 190// myPieces.add(new Piece("br")); 191// } 192// myPieces.addAll(htmlFormattingToPieces(paragraphs[i])); 193// } 194// } 195// 196// return myPieces; 197// } 198// private List<Piece> htmlFormattingToPieces(String html) { 199// List<Piece> myPieces = new ArrayList<Piece>(); 200// //Todo: At least handle bold and italics and turn them into formatted spans. (Will need to handle nesting though) 201// myPieces.add(new Piece(null, html, null)); 202// 203// return myPieces; 204// } 205 public void addStyle(String style) { 206 for (Piece p : pieces) 207 p.addStyle(style); 208 } 209 public void addToHint(String text) { 210 for (Piece p : pieces) 211 p.addToHint(text); 212 } 213 public Piece addImage(String src, String hint, String alt, String fgColor, String bgColor) { 214// Piece img = new Piece("img"); 215 Piece img = new Piece(null, alt, hint); 216 img.addStyle("padding: 3px"); 217 if (fgColor != null) { 218 img.addStyle("color: "+fgColor); 219 img.addStyle("background-color: "+bgColor); 220 } 221 222// img.attributes = new HashMap<String, String>(); 223// img.attributes.put("src", src); 224// img.attributes.put("alt", alt); 225// img.hint = hint; 226 pieces.add(img); 227 return img; 228 } 229 public String text() { 230 StringBuilder b = new StringBuilder(); 231 for (Piece p : pieces) 232 b.append(p.text); 233 return b.toString(); 234 } 235 @Override 236 public String toString() { 237 return text(); 238 } 239 240 241 } 242 243 public class Title extends Cell { 244 private int width; 245 246 public Title(String prefix, String reference, String text, String hint, String suffix, int width) { 247 super(prefix, reference, text, hint, suffix); 248 this.width = width; 249 } 250 } 251 252 public class Row { 253 private List<Row> subRows = new ArrayList<HierarchicalTableGenerator.Row>(); 254 private List<Cell> cells = new ArrayList<HierarchicalTableGenerator.Cell>(); 255 private String icon; 256 private String anchor; 257 private String hint; 258 private String color; 259 260 public List<Row> getSubRows() { 261 return subRows; 262 } 263 public List<Cell> getCells() { 264 return cells; 265 } 266 public String getIcon() { 267 return icon; 268 } 269 public void setIcon(String icon, String hint) { 270 this.icon = icon; 271 this.hint = hint; 272 } 273 public String getAnchor() { 274 return anchor; 275 } 276 public void setAnchor(String anchor) { 277 this.anchor = anchor; 278 } 279 public String getHint() { 280 return hint; 281 } 282 public String getColor() { 283 return color; 284 } 285 public void setColor(String color) { 286 this.color = color; 287 } 288 289 290 } 291 292 public class TableModel { 293 private List<Title> titles = new ArrayList<HierarchicalTableGenerator.Title>(); 294 private List<Row> rows = new ArrayList<HierarchicalTableGenerator.Row>(); 295 private String docoRef; 296 private String docoImg; 297 public List<Title> getTitles() { 298 return titles; 299 } 300 public List<Row> getRows() { 301 return rows; 302 } 303 public String getDocoRef() { 304 return docoRef; 305 } 306 public String getDocoImg() { 307 return docoImg; 308 } 309 public void setDocoRef(String docoRef) { 310 this.docoRef = docoRef; 311 } 312 public void setDocoImg(String docoImg) { 313 this.docoImg = docoImg; 314 } 315 316 } 317 318 319 private String dest; 320 321 /** 322 * There are circumstances where the table has to present in the absence of a stable supporting infrastructure. 323 * and the file paths cannot be guaranteed. For these reasons, you can tell the builder to inline all the graphics 324 * (all the styles are inlined anyway, since the table fbuiler has even less control over the styling 325 * 326 */ 327 private boolean inLineGraphics; 328 329 330 public HierarchicalTableGenerator() { 331 super(); 332 } 333 334 public HierarchicalTableGenerator(String dest, boolean inlineGraphics) { 335 super(); 336 this.dest = dest; 337 this.inLineGraphics = inlineGraphics; 338 } 339 340 public TableModel initNormalTable(String prefix, boolean isLogical) { 341 TableModel model = new TableModel(); 342 343 model.setDocoImg(prefix+"help16.png"); 344 model.setDocoRef(prefix+"formats.html#table"); 345 model.getTitles().add(new Title(null, model.getDocoRef(), "Name", "The logical name of the element", null, 0)); 346 model.getTitles().add(new Title(null, model.getDocoRef(), "Flags", "Information about the use of the element", null, 0)); 347 model.getTitles().add(new Title(null, model.getDocoRef(), "Card.", "Minimum and Maximum # of times the the element can appear in the instance", null, 0)); 348 model.getTitles().add(new Title(null, model.getDocoRef(), "Type", "Reference to the type of the element", null, 100)); 349 model.getTitles().add(new Title(null, model.getDocoRef(), "Description & Constraints", "Additional information about the element", null, 0)); 350 if (isLogical) { 351 model.getTitles().add(new Title(null, prefix+"structuredefinition.html#logical", "Implemented As", "How this logical data item is implemented in a concrete resource", null, 0)); 352 } 353 return model; 354 } 355 356 public TableModel initGridTable(String prefix) { 357 TableModel model = new TableModel(); 358 359 model.getTitles().add(new Title(null, model.getDocoRef(), "Name", "The name of the element (Slice name in brackets). Mouse-over provides definition", null, 0)); 360 model.getTitles().add(new Title(null, model.getDocoRef(), "Card.", "Minimum and Maximum # of times the the element can appear in the instance. Super-scripts indicate additional constraints on appearance", null, 0)); 361 model.getTitles().add(new Title(null, model.getDocoRef(), "Type", "Reference to the type of the element", null, 100)); 362 model.getTitles().add(new Title(null, model.getDocoRef(), "Constraints and Usage", "Fixed values, length limits, vocabulary bindings and other usage notes", null, 0)); 363 return model; 364 } 365 366 public XhtmlNode generate(TableModel model, String imagePath, int border) throws IOException, FHIRException { 367 checkModel(model); 368 XhtmlNode table = new XhtmlNode(NodeType.Element, "table").setAttribute("border", Integer.toString(border)).setAttribute("cellspacing", "0").setAttribute("cellpadding", "0"); 369 table.setAttribute("style", "border: " + border + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top;"); 370 XhtmlNode tr = table.addTag("tr"); 371 tr.setAttribute("style", "border: " + Integer.toString(1 + border) + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top;"); 372 XhtmlNode tc = null; 373 for (Title t : model.getTitles()) { 374 tc = renderCell(tr, t, "th", null, null, null, false, null, "white", imagePath, border); 375 if (t.width != 0) 376 tc.setAttribute("style", "width: "+Integer.toString(t.width)+"px"); 377 } 378 if (tc != null && model.getDocoRef() != null) 379 tc.addTag("span").setAttribute("style", "float: right").addTag("a").setAttribute("title", "Legend for this format").setAttribute("href", model.getDocoRef()).addTag("img").setAttribute("alt", "doco").setAttribute("style", "background-color: inherit").setAttribute("src", model.getDocoImg()); 380 381 for (Row r : model.getRows()) { 382 renderRow(table, r, 0, new ArrayList<Boolean>(), imagePath, border); 383 } 384 if (model.getDocoRef() != null) { 385 tr = table.addTag("tr"); 386 tc = tr.addTag("td"); 387 tc.setAttribute("class", "hierarchy"); 388 tc.setAttribute("colspan", Integer.toString(model.getTitles().size())); 389 tc.addTag("br"); 390 XhtmlNode a = tc.addTag("a").setAttribute("title", "Legend for this format").setAttribute("href", model.getDocoRef()); 391 if (model.getDocoImg() != null) 392 a.addTag("img").setAttribute("alt", "doco").setAttribute("style", "background-color: inherit").setAttribute("src", model.getDocoImg()); 393 a.addText(" Documentation for this format"); 394 } 395 return table; 396 } 397 398 399 private void renderRow(XhtmlNode table, Row r, int indent, List<Boolean> indents, String imagePath, int border) throws IOException { 400 XhtmlNode tr = table.addTag("tr"); 401 String color = "white"; 402 if (r.getColor() != null) 403 color = r.getColor(); 404 tr.setAttribute("style", "border: " + border + "px #F0F0F0 solid; padding:0px; vertical-align: top; background-color: "+color+";"); 405 boolean first = true; 406 for (Cell t : r.getCells()) { 407 renderCell(tr, t, "td", first ? r.getIcon() : null, first ? r.getHint() : null, first ? indents : null, !r.getSubRows().isEmpty(), first ? r.getAnchor() : null, color, imagePath, border); 408 first = false; 409 } 410 table.addText("\r\n"); 411 412 for (int i = 0; i < r.getSubRows().size(); i++) { 413 Row c = r.getSubRows().get(i); 414 List<Boolean> ind = new ArrayList<Boolean>(); 415 ind.addAll(indents); 416 if (i == r.getSubRows().size() - 1) 417 ind.add(true); 418 else 419 ind.add(false); 420 renderRow(table, c, indent+1, ind, imagePath, border); 421 } 422 } 423 424 425 private XhtmlNode renderCell(XhtmlNode tr, Cell c, String name, String icon, String hint, List<Boolean> indents, boolean hasChildren, String anchor, String color, String imagePath, int border) throws IOException { 426 XhtmlNode tc = tr.addTag(name); 427 tc.setAttribute("class", "hierarchy"); 428 if (indents != null) { 429 tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_spacer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", "."); 430 tc.setAttribute("style", "vertical-align: top; text-align : left; background-color: "+color+"; border: "+ border +"px #F0F0F0 solid; padding:0px 4px 0px 4px; white-space: nowrap; background-image: url("+imagePath+checkExists(indents, hasChildren)+")"); 431 for (int i = 0; i < indents.size()-1; i++) { 432 if (indents.get(i)) 433 tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_blank.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", "."); 434 else 435 tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", "."); 436 } 437 if (!indents.isEmpty()) 438 if (indents.get(indents.size()-1)) 439 tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_end.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", "."); 440 else 441 tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", "."); 442 } 443 else 444 tc.setAttribute("style", "vertical-align: top; text-align : left; background-color: "+color+"; border: "+ border +"px #F0F0F0 solid; padding:0px 4px 0px 4px"); 445 if (!Utilities.noString(icon)) { 446 XhtmlNode img = tc.addTag("img").setAttribute("src", srcFor(imagePath, icon)).setAttribute("class", "hierarchy").setAttribute("style", "background-color: "+color+"; background-color: inherit").setAttribute("alt", "."); 447 if (hint != null) 448 img.setAttribute("title", hint); 449 tc.addText(" "); 450 } 451 for (Piece p : c.pieces) { 452 if (!Utilities.noString(p.getTag())) { 453 XhtmlNode tag = tc.addTag(p.getTag()); 454 if (p.attributes != null) 455 for (String n : p.attributes.keySet()) 456 tag.setAttribute(n, p.attributes.get(n)); 457 if (p.getHint() != null) 458 tag.setAttribute("title", p.getHint()); 459 addStyle(tag, p); 460 } else if (!Utilities.noString(p.getReference())) { 461 XhtmlNode a = addStyle(tc.addTag("a"), p); 462 a.setAttribute("href", p.getReference()); 463 if (!Utilities.noString(p.getHint())) 464 a.setAttribute("title", p.getHint()); 465 a.addText(p.getText()); 466 addStyle(a, p); 467 } else { 468 if (!Utilities.noString(p.getHint())) { 469 XhtmlNode s = addStyle(tc.addTag("span"), p); 470 s.setAttribute("title", p.getHint()); 471 s.addText(p.getText()); 472 } else if (p.getStyle() != null) { 473 XhtmlNode s = addStyle(tc.addTag("span"), p); 474 s.addText(p.getText()); 475 } else 476 tc.addText(p.getText()); 477 } 478 } 479 if (!Utilities.noString(anchor)) 480 tc.addTag("a").setAttribute("name", nmTokenize(anchor)).addText(" "); 481 return tc; 482 } 483 484 485 private XhtmlNode addStyle(XhtmlNode node, Piece p) { 486 if (p.getStyle() != null) 487 node.setAttribute("style", p.getStyle()); 488 return node; 489 } 490 491 private String nmTokenize(String anchor) { 492 return anchor.replace("[", "_").replace("]", "_"); 493 } 494 495 private String srcFor(String corePrefix, String filename) throws IOException { 496 if (inLineGraphics) { 497 if (files.containsKey(filename)) 498 return files.get(filename); 499 StringBuilder b = new StringBuilder(); 500 b.append("data: image/png;base64,"); 501 byte[] bytes; 502 File file = new File(Utilities.path(dest, filename)); 503 if (!file.exists()) // because sometime this is called real early before the files exist. it will be built again later because of this 504 bytes = new byte[0]; 505 else 506 bytes = FileUtils.readFileToByteArray(file); 507 b.append(new String(Base64.encodeBase64(bytes))); 508// files.put(filename, b.toString()); 509 return b.toString(); 510 } 511 return corePrefix+filename; 512 } 513 514 515 private void checkModel(TableModel model) throws FHIRException { 516 check(!model.getRows().isEmpty(), "Must have rows"); 517 check(!model.getTitles().isEmpty(), "Must have titles"); 518 for (Cell c : model.getTitles()) 519 check(c); 520 int i = 0; 521 for (Row r : model.getRows()) { 522 check(r, "rows", model.getTitles().size(), Integer.toString(i)); 523 i++; 524 } 525 } 526 527 528 private void check(Cell c) throws FHIRException { 529 boolean hasText = false; 530 for (Piece p : c.pieces) 531 if (!Utilities.noString(p.getText())) 532 hasText = true; 533 check(hasText, "Title cells must have text"); 534 } 535 536 537 private void check(Row r, String string, int size, String path) throws FHIRException { 538 check(r.getCells().size() == size, "All rows must have the same number of columns ("+Integer.toString(size)+") as the titles but row "+path+" doesn't ("+r.getCells().get(0).text()+"): "+r.getCells()); 539 int i = 0; 540 for (Row c : r.getSubRows()) { 541 check(c, "rows", size, path+"."+Integer.toString(i)); 542 i++; 543 } 544 } 545 546 547 private String checkExists(List<Boolean> indents, boolean hasChildren) throws IOException { 548 String filename = makeName(indents); 549 550 StringBuilder b = new StringBuilder(); 551 if (inLineGraphics) { 552 if (files.containsKey(filename)) 553 return files.get(filename); 554 ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 555 genImage(indents, hasChildren, bytes); 556 b.append("data: image/png;base64,"); 557 byte[] encodeBase64 = Base64.encodeBase64(bytes.toByteArray()); 558 b.append(new String(encodeBase64)); 559 files.put(filename, b.toString()); 560 return b.toString(); 561 } 562 b.append("tbl_bck"); 563 for (Boolean i : indents) 564 b.append(i ? "0" : "1"); 565 if (hasChildren) 566 b.append("1"); 567 else 568 b.append("0"); 569 b.append(".png"); 570 String file = Utilities.path(dest, b.toString()); 571 if (!new File(file).exists()) { 572 //FIXME resource leak 573 FileOutputStream stream = new FileOutputStream(file); 574 genImage(indents, hasChildren, stream); 575 } 576 return b.toString(); 577 } 578 579 580 private void genImage(List<Boolean> indents, boolean hasChildren, OutputStream stream) throws IOException { 581 BufferedImage bi = new BufferedImage(800, 2, BufferedImage.TYPE_INT_ARGB); 582 // i have no idea why this works to make these pixels transparent. It defies logic. 583 // But this combination of INT_ARGB and filling with grey magically worked when nothing else did. So it stays as is. 584 Color grey = new Color(99,99,99,0); 585 for (int i = 0; i < 800; i++) { 586 bi.setRGB(i, 0, grey.getRGB()); 587 bi.setRGB(i, 1, grey.getRGB()); 588 } 589 Color black = new Color(0, 0, 0); 590 for (int i = 0; i < indents.size(); i++) { 591 if (!indents.get(i)) 592 bi.setRGB(12+(i*16), 0, black.getRGB()); 593 } 594 if (hasChildren) 595 bi.setRGB(12+(indents.size()*16), 0, black.getRGB()); 596 ImageIO.write(bi, "PNG", stream); 597 } 598 599 private String makeName(List<Boolean> indents) { 600 StringBuilder b = new StringBuilder(); 601 b.append("indents:"); 602 for (Boolean i : indents) 603 b.append(i ? "1" : "0"); 604 return b.toString(); 605 } 606 607 private void check(boolean check, String message) throws FHIRException { 608 if (!check) 609 throw new FHIRException(message); 610 } 611}