001package org.hl7.fhir.utilities.xhtml;
002
003/*
004Copyright (c) 2011+, HL7, Inc
005All rights reserved.
006
007Redistribution and use in source and binary forms, with or without modification, 
008are permitted provided that the following conditions are met:
009
010 * Redistributions of source code must retain the above copyright notice, this 
011   list of conditions and the following disclaimer.
012 * Redistributions in binary form must reproduce the above copyright notice, 
013   this list of conditions and the following disclaimer in the documentation 
014   and/or other materials provided with the distribution.
015 * Neither the name of HL7 nor the names of its contributors may be used to 
016   endorse or promote products derived from this software without specific 
017   prior written permission.
018
019THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028POSSIBILITY OF SUCH DAMAGE.
029
030*/
031
032import java.awt.Color;
033import java.awt.image.BufferedImage;
034import java.io.ByteArrayOutputStream;
035import java.io.File;
036import java.io.FileOutputStream;
037import java.io.IOException;
038import java.io.OutputStream;
039import java.util.ArrayList;
040import java.util.HashMap;
041import java.util.List;
042import java.util.Map;
043import java.util.Set;
044
045import javax.imageio.ImageIO;
046
047import org.apache.commons.codec.binary.Base64;
048import org.apache.commons.io.FileUtils;
049import org.commonmark.node.Node;
050import org.commonmark.parser.Parser;
051import org.commonmark.renderer.html.HtmlRenderer;
052import org.hl7.fhir.exceptions.FHIRException;
053import org.hl7.fhir.utilities.TranslatingUtilities;
054import org.hl7.fhir.utilities.Utilities;
055
056
057public class HierarchicalTableGenerator extends TranslatingUtilities {
058  public static final String TEXT_ICON_REFERENCE = "Reference to another Resource";
059  public static final String TEXT_ICON_PRIMITIVE = "Primitive Data Type";
060  public static final String TEXT_ICON_DATATYPE = "Data Type";
061  public static final String TEXT_ICON_RESOURCE = "Resource";
062  public static final String TEXT_ICON_ELEMENT = "Element";
063  public static final String TEXT_ICON_REUSE = "Reference to another Element";
064  public static final String TEXT_ICON_EXTENSION = "Extension";
065  public static final String TEXT_ICON_CHOICE = "Choice of Types";
066  public static final String TEXT_ICON_SLICE = "Slice Definition";
067  public static final String TEXT_ICON_EXTENSION_SIMPLE = "Simple Extension";
068  public static final String TEXT_ICON_PROFILE = "Profile";
069  public static final String TEXT_ICON_EXTENSION_COMPLEX = "Complex Extension";
070
071  public static final int NEW_REGULAR = 0;
072  public static final int CONTINUE_REGULAR = 1;
073  public static final int NEW_SLICER = 2;
074  public static final int CONTINUE_SLICER = 3;
075  public static final int NEW_SLICE = 4;
076  public static final int CONTINUE_SLICE = 5;  
077  
078  private static Map<String, String> files = new HashMap<String, String>();
079
080  public class Piece {
081    private String tag;
082    private String reference;
083    private String text;
084    private String hint;
085    private String style;
086    private Map<String, String> attributes;
087    private List<XhtmlNode> children;
088    
089    public Piece(String tag) {
090      super();
091      this.tag = tag;
092    }
093    
094    public Piece(String reference, String text, String hint) {
095      super();
096      this.reference = reference;
097      this.text = text;
098      this.hint = hint;
099    }
100    public String getReference() {
101      return reference;
102    }
103    public void setReference(String value) {
104      reference = value;
105    }
106    public String getText() {
107      return text;
108    }
109    public String getHint() {
110      return hint;
111    }
112
113    public String getTag() {
114      return tag;
115    }
116
117    public String getStyle() {
118      return style;
119    }
120
121    public void setTag(String tag) {
122      this.tag = tag;
123    }
124
125    public Piece setText(String text) {
126      this.text = text;
127      return this;
128    }
129
130    public void setHint(String hint) {
131      this.hint = hint;
132    }
133
134    public Piece setStyle(String style) {
135      this.style = style;
136      return this;
137    }
138
139    public Piece addStyle(String style) {
140      if (this.style != null)
141        this.style = this.style+"; "+style;
142      else
143        this.style = style;
144      return this;
145    }
146
147    public void addToHint(String text) {
148      if (this.hint == null)
149        this.hint = text;
150      else
151        this.hint += (this.hint.endsWith(".") || this.hint.endsWith("?") ? " " : ". ")+text;
152    }
153    
154    public boolean hasChildren() {
155      return children != null && !children.isEmpty();
156    }
157
158    public List<XhtmlNode> getChildren() {
159      if (children == null)
160        children = new ArrayList<XhtmlNode>();
161      return children;
162    }
163    
164  }
165  
166  public class Cell {
167    private List<Piece> pieces = new ArrayList<HierarchicalTableGenerator.Piece>();
168
169    public Cell() {
170      
171    }
172    public Cell(String prefix, String reference, String text, String hint, String suffix) {
173      super();
174      if (!Utilities.noString(prefix))
175        pieces.add(new Piece(null, prefix, null));
176      pieces.add(new Piece(reference, text, hint));
177      if (!Utilities.noString(suffix))
178        pieces.add(new Piece(null, suffix, null));
179    }
180    public List<Piece> getPieces() {
181      return pieces;
182    }
183    public Cell addPiece(Piece piece) {
184      pieces.add(piece);
185      return this;
186    }
187    public Cell addMarkdown(String md) {
188      try {
189        Parser parser = Parser.builder().build();
190        Node document = parser.parse(md);
191        HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build();
192        String html = renderer.render(document);  
193        pieces.addAll(htmlToParagraphPieces(html));
194      } catch (Exception e) {
195        e.printStackTrace();
196      }
197      return this;
198    }
199    private List<Piece> htmlToParagraphPieces(String html) throws IOException, FHIRException {
200      List<Piece> myPieces = new ArrayList<Piece>();
201      String[] paragraphs = html.replace("<p>", "").split("<\\/p>|<br  \\/>");
202      for (int i=0;i<paragraphs.length;i++) {
203        if (!paragraphs[i].isEmpty()) {
204          if (i!=0) {
205            myPieces.add(new Piece("br"));
206            myPieces.add(new Piece("br"));
207          }
208          myPieces.addAll(htmlFormattingToPieces(paragraphs[i]));
209        }
210      }
211      
212      return myPieces;
213    }
214    private List<Piece> htmlFormattingToPieces(String html) throws IOException, FHIRException {
215      List<Piece> myPieces = new ArrayList<Piece>();
216      if (html.contains(("<"))) {
217        XhtmlNode node = new XhtmlParser().parseFragment("<p>"+html+"</p>");
218        for (XhtmlNode c : node.getChildNodes()) {
219          addNode(myPieces, c);
220        }
221      } else
222        myPieces.add(new Piece(null, html, null));        
223      return myPieces;
224    }
225    private void addNode(List<Piece> list, XhtmlNode c) {
226      if (c.getNodeType() == NodeType.Text)
227        list.add(new Piece(null, c.getContent(), null));
228      else if (c.getNodeType() == NodeType.Element) {
229        if (c.getName().equals("a")) {
230          list.add(new Piece(c.getAttribute("href"), c.allText(), c.getAttribute("title")));                    
231        } else if (c.getName().equals("b") || c.getName().equals("em") || c.getName().equals("strong")) {
232          list.add(new Piece(null, c.allText(), null).setStyle("font-face: bold"));                    
233        } else if (c.getName().equals("code")) {
234          list.add(new Piece(null, c.allText(), null).setStyle("padding: 2px 4px; color: #005c00; background-color: #f9f2f4; white-space: nowrap; border-radius: 4px"));                    
235        } else if (c.getName().equals("i")) {
236          list.add(new Piece(null, c.allText(), null).setStyle("font-style: italic"));
237        } else if (c.getName().equals("pre")) {
238          Piece p = new Piece(c.getName()).setStyle("white-space: pre; font-family: courier");
239          list.add(p);
240          p.getChildren().addAll(c.getChildNodes());
241        } else if (c.getName().equals("ul") || c.getName().equals("ol")) {
242          Piece p = new Piece(c.getName());
243          list.add(p);
244          p.getChildren().addAll(c.getChildNodes());
245        } else if (c.getName().equals("i")) {
246          list.add(new Piece(null, c.allText(), null).setStyle("font-style: italic"));                    
247        } else if (c.getName().equals("h1")||c.getName().equals("h2")||c.getName().equals("h3")||c.getName().equals("h4")) {
248          Piece p = new Piece(c.getName());
249          list.add(p);
250          p.getChildren().addAll(c.getChildNodes());
251        } else if (c.getName().equals("br")) {
252          list.add(new Piece(c.getName()));
253        } else {
254          
255          throw new Error("Not handled yet: "+c.getName());
256        }
257      } else
258        throw new Error("Unhandled type "+c.getNodeType().toString());
259
260    }
261    public void addStyle(String style) {
262      for (Piece p : pieces)
263        p.addStyle(style);      
264    }
265    public void addToHint(String text) {
266      for (Piece p : pieces)
267        p.addToHint(text);            
268    }
269    public Piece addStyledText(String hint, String alt, String fgColor, String bgColor, String link, boolean border) {
270      Piece p = new Piece(link, alt, hint);
271      p.addStyle("padding-left: 3px");
272      p.addStyle("padding-right: 3px");
273      if (border) {
274        p.addStyle("border: 1px grey solid");
275        p.addStyle("font-weight: bold");
276      }
277      if (fgColor != null) {
278        p.addStyle("color: "+fgColor);
279        p.addStyle("background-color: "+bgColor);
280      } else {
281        p.addStyle("color: black");
282        p.addStyle("background-color: white");       
283      }
284      pieces.add(p);
285      return p;
286    }
287    public String text() {
288      StringBuilder b = new StringBuilder();
289      for (Piece p : pieces)
290        b.append(p.text);
291      return b.toString();
292    }
293    @Override
294    public String toString() {
295      return text();
296    }
297    
298    
299  }
300
301  public class Title extends Cell {
302    private int width;
303
304    public Title(String prefix, String reference, String text, String hint, String suffix, int width) {
305      super(prefix, reference, text, hint, suffix);
306      this.width = width;
307    }
308  }
309  
310  public class Row {
311    private List<Row> subRows = new ArrayList<HierarchicalTableGenerator.Row>();
312    private List<Cell> cells = new ArrayList<HierarchicalTableGenerator.Cell>();
313    private String icon;
314    private String anchor;
315    private String hint;
316    private String color;
317    private int lineColor;
318    
319    public List<Row> getSubRows() {
320      return subRows;
321    }
322    public List<Cell> getCells() {
323      return cells;
324    }
325    public String getIcon() {
326      return icon;
327    }
328    public void setIcon(String icon, String hint) {
329      this.icon = icon;
330      this.hint = hint;
331    }
332    public String getAnchor() {
333      return anchor;
334    }
335    public void setAnchor(String anchor) {
336      this.anchor = anchor;
337    }
338    public String getHint() {
339      return hint;
340    }
341    public String getColor() {
342      return color;
343    }
344    public void setColor(String color) {
345      this.color = color;
346    }
347    public int getLineColor() {
348      return lineColor;
349    }
350    public void setLineColor(int lineColor) {
351      assert lineColor >= 0;
352      assert lineColor <= 2;
353      this.lineColor = lineColor;
354    }
355    
356    
357  }
358
359  public class TableModel {
360    private List<Title> titles = new ArrayList<HierarchicalTableGenerator.Title>();
361    private List<Row> rows = new ArrayList<HierarchicalTableGenerator.Row>();
362    private String docoRef;
363    private String docoImg;
364    public List<Title> getTitles() {
365      return titles;
366    }
367    public List<Row> getRows() {
368      return rows;
369    }
370    public String getDocoRef() {
371      return docoRef;
372    }
373    public String getDocoImg() {
374      return docoImg;
375    }
376    public void setDocoRef(String docoRef) {
377      this.docoRef = docoRef;
378    }
379    public void setDocoImg(String docoImg) {
380      this.docoImg = docoImg;
381    }
382    
383  }
384
385
386  private String dest;
387  
388  /**
389   * There are circumstances where the table has to present in the absence of a stable supporting infrastructure.
390   * and the file paths cannot be guaranteed. For these reasons, you can tell the builder to inline all the graphics
391   * (all the styles are inlined anyway, since the table fbuiler has even less control over the styling
392   *  
393   */
394  private boolean inLineGraphics;
395  
396  
397  public HierarchicalTableGenerator() {
398    super();
399  }
400
401  public HierarchicalTableGenerator(String dest, boolean inlineGraphics) {
402    super();
403    this.dest = dest;
404    this.inLineGraphics = inlineGraphics;
405  }
406
407  public TableModel initNormalTable(String prefix, boolean isLogical) {
408    TableModel model = new TableModel();
409    
410    model.setDocoImg(prefix+"help16.png");
411    model.setDocoRef(prefix+"formats.html#table");
412    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Name"), translate("sd.hint", "The logical name of the element"), null, 0));
413    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Flags"), translate("sd.hint", "Information about the use of the element"), null, 0));
414    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Card."), translate("sd.hint", "Minimum and Maximum # of times the the element can appear in the instance"), null, 0));
415    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Type"), translate("sd.hint", "Reference to the type of the element"), null, 100));
416    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Description & Constraints"), translate("sd.hint", "Additional information about the element"), null, 0));
417    if (isLogical) {
418      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));
419    }
420    return model;
421  }
422
423
424  public TableModel initGridTable(String prefix) {
425    TableModel model = new TableModel();
426    
427    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Name"), translate("sd.hint", "The name of the element (Slice name in brackets).  Mouse-over provides definition"), null, 0));
428    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Card."), translate("sd.hint", "Minimum and Maximum # of times the the element can appear in the instance. Super-scripts indicate additional constraints on appearance"), null, 0));
429    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Type"), translate("sd.hint", "Reference to the type of the element"), null, 100));
430    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Constraints and Usage"), translate("sd.hint", "Fixed values, length limits, vocabulary bindings and other usage notes"), null, 0));
431    return model;
432  }
433
434  public XhtmlNode generate(TableModel model, String imagePath, int border, Set<String> outputTracker) throws IOException, FHIRException  {
435    checkModel(model);
436    XhtmlNode table = new XhtmlNode(NodeType.Element, "table").setAttribute("border", Integer.toString(border)).setAttribute("cellspacing", "0").setAttribute("cellpadding", "0");
437    table.setAttribute("style", "border: " + border + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top;");
438    XhtmlNode tr = table.addTag("tr");
439    tr.setAttribute("style", "border: " + Integer.toString(1 + border) + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top;");
440    XhtmlNode tc = null;
441    for (Title t : model.getTitles()) {
442      tc = renderCell(tr, t, "th", null, null, null, false, null, "white", 0, imagePath, border, outputTracker);
443      if (t.width != 0)
444        tc.setAttribute("style", "width: "+Integer.toString(t.width)+"px");
445    }
446    if (tc != null && model.getDocoRef() != null)
447      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());
448      
449    for (Row r : model.getRows()) {
450      renderRow(table, r, 0, new ArrayList<Integer>(), imagePath, border, outputTracker);
451    }
452    if (model.getDocoRef() != null) {
453      tr = table.addTag("tr");
454      tc = tr.addTag("td");
455      tc.setAttribute("class", "hierarchy");
456      tc.setAttribute("colspan", Integer.toString(model.getTitles().size()));
457      tc.addTag("br");
458      XhtmlNode a = tc.addTag("a").setAttribute("title", translate("sd.doco", "Legend for this format")).setAttribute("href", model.getDocoRef());
459      if (model.getDocoImg() != null)
460        a.addTag("img").setAttribute("alt", "doco").setAttribute("style", "background-color: inherit").setAttribute("src", model.getDocoImg());
461      a.addText(" "+translate("sd.doco", "Documentation for this format"));
462    }
463    return table;
464  }
465
466
467  private void renderRow(XhtmlNode table, Row r, int indent, List<Integer> indents, String imagePath, int border, Set<String> outputTracker) throws IOException  {
468    XhtmlNode tr = table.addTag("tr");
469    String color = "white";
470    if (r.getColor() != null)
471      color = r.getColor();
472    tr.setAttribute("style", "border: " + border + "px #F0F0F0 solid; padding:0px; vertical-align: top; background-color: "+color+";");
473    boolean first = true;
474    for (Cell t : r.getCells()) {
475      renderCell(tr, t, "td", first ? r.getIcon() : null, first ? r.getHint() : null, first ? indents : null, !r.getSubRows().isEmpty(), first ? r.getAnchor() : null, color, r.getLineColor(), imagePath, border, outputTracker);
476      first = false;
477    }
478    table.addText("\r\n");
479    
480    for (int i = 0; i < r.getSubRows().size(); i++) {
481      Row c = r.getSubRows().get(i);
482      List<Integer> ind = new ArrayList<Integer>();
483      ind.addAll(indents);
484      if (i == r.getSubRows().size() - 1) {
485        ind.add(r.getLineColor()*2);
486      } else {
487        ind.add(r.getLineColor()*2+1);
488      }
489      renderRow(table, c, indent+1, ind, imagePath, border, outputTracker);
490    }
491  }
492
493
494  private XhtmlNode renderCell(XhtmlNode tr, Cell c, String name, String icon, String hint, List<Integer> indents, boolean hasChildren, String anchor, String color, int lineColor, String imagePath, int border, Set<String> outputTracker) throws IOException  {
495    XhtmlNode tc = tr.addTag(name);
496    tc.setAttribute("class", "hierarchy");
497    if (indents != null) {
498      tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_spacer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
499      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, lineColor, outputTracker)+")");
500      for (int i = 0; i < indents.size()-1; i++) {
501        switch (indents.get(i)) {
502          case NEW_REGULAR:
503          case NEW_SLICER:
504          case NEW_SLICE:
505            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_blank.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
506            break;
507          case CONTINUE_REGULAR:
508            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
509            break;
510          case CONTINUE_SLICER:
511            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline_slicer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
512            break;
513          case CONTINUE_SLICE:
514            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline_slice.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
515            break;
516          default:
517            throw new Error("Unrecognized indent level: " + indents.get(i));
518        }
519      }
520      if (!indents.isEmpty())
521        switch (indents.get(indents.size()-1)) {
522        case NEW_REGULAR:
523          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_end.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
524          break;
525        case NEW_SLICER:
526          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_end_slicer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
527          break;
528        case NEW_SLICE:
529          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_end_slice.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
530          break;
531        case CONTINUE_REGULAR:
532          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
533          break;
534        case CONTINUE_SLICER:
535          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_slicer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
536          break;
537        case CONTINUE_SLICE:
538          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_slice.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
539          break;
540        default:
541          throw new Error("Unrecognized indent level: " + indents.get(indents.size()-1));
542        }
543    }
544    else
545      tc.setAttribute("style", "vertical-align: top; text-align : left; background-color: "+color+"; border: "+ border +"px #F0F0F0 solid; padding:0px 4px 0px 4px");
546    if (!Utilities.noString(icon)) {
547      XhtmlNode img = tc.addTag("img").setAttribute("src", srcFor(imagePath, icon)).setAttribute("class", "hierarchy").setAttribute("style", "background-color: "+color+"; background-color: inherit").setAttribute("alt", ".");
548      if (hint != null)
549        img.setAttribute("title", hint);
550      tc.addText(" ");
551    }
552    for (Piece p : c.pieces) {
553      if (!Utilities.noString(p.getTag())) {
554        XhtmlNode tag = tc.addTag(p.getTag());
555        if (p.attributes != null)
556          for (String n : p.attributes.keySet())
557            tag.setAttribute(n, p.attributes.get(n));
558        if (p.getHint() != null)
559          tag.setAttribute("title", p.getHint());
560        addStyle(tag, p);
561        if (p.hasChildren())
562          tag.getChildNodes().addAll(p.getChildren());
563      } else if (!Utilities.noString(p.getReference())) {
564        XhtmlNode a = addStyle(tc.addTag("a"), p);
565        a.setAttribute("href", p.getReference());
566        if (!Utilities.noString(p.getHint()))
567          a.setAttribute("title", p.getHint());
568        a.addText(p.getText());
569        addStyle(a, p);
570      } else { 
571        if (!Utilities.noString(p.getHint())) {
572          XhtmlNode s = addStyle(tc.addTag("span"), p);
573          s.setAttribute("title", p.getHint());
574          s.addText(p.getText());
575        } else if (p.getStyle() != null) {
576          XhtmlNode s = addStyle(tc.addTag("span"), p);
577          s.addText(p.getText());
578        } else
579          tc.addText(p.getText());
580      }
581    }
582    if (!Utilities.noString(anchor))
583      tc.addTag("a").setAttribute("name", nmTokenize(anchor)).addText(" ");
584    return tc;
585  }
586
587
588  private XhtmlNode addStyle(XhtmlNode node, Piece p) {
589    if (p.getStyle() != null)
590      node.setAttribute("style", p.getStyle());
591    return node;
592  }
593
594  private String nmTokenize(String anchor) {
595    return anchor.replace("[", "_").replace("]", "_");
596  }
597  
598  private String srcFor(String corePrefix, String filename) throws IOException {
599    if (inLineGraphics) {
600      if (files.containsKey(filename))
601        return files.get(filename);
602      StringBuilder b = new StringBuilder();
603      b.append("data: image/png;base64,");
604      byte[] bytes;
605      File file = new File(Utilities.path(dest, filename));
606      if (!file.exists()) // because sometime this is called real early before the files exist. it will be built again later because of this
607        bytes = new byte[0]; 
608      else
609        bytes = FileUtils.readFileToByteArray(file);
610      b.append(new String(Base64.encodeBase64(bytes)));
611//      files.put(filename, b.toString());
612      return b.toString();
613    } else
614      return corePrefix+filename;
615  }
616
617
618  private void checkModel(TableModel model) throws FHIRException  {
619    check(!model.getRows().isEmpty(), "Must have rows");
620    check(!model.getTitles().isEmpty(), "Must have titles");
621    for (Cell c : model.getTitles())
622      check(c);
623    int i = 0;
624    for (Row r : model.getRows()) { 
625      check(r, "rows", model.getTitles().size(), Integer.toString(i));
626      i++;
627    }
628  }
629
630
631  private void check(Cell c) throws FHIRException  {  
632    boolean hasText = false;
633    for (Piece p : c.pieces)
634      if (!Utilities.noString(p.getText()))
635        hasText = true;
636    check(hasText, "Title cells must have text");    
637  }
638
639
640  private void check(Row r, String string, int size, String path) throws FHIRException  {    
641    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());
642    int i = 0;
643    for (Row c : r.getSubRows()) {
644      check(c, "rows", size, path+"."+Integer.toString(i));
645      i++;
646    }
647  }
648
649
650  private String checkExists(List<Integer> indents, boolean hasChildren, int lineColor, Set<String> outputTracker) throws IOException  {
651    String filename = makeName(indents);
652    
653    StringBuilder b = new StringBuilder();
654    if (inLineGraphics) {
655      if (files.containsKey(filename))
656        return files.get(filename);
657      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
658      genImage(indents, hasChildren, lineColor, bytes);
659      b.append("data: image/png;base64,");
660      byte[] encodeBase64 = Base64.encodeBase64(bytes.toByteArray());
661      b.append(new String(encodeBase64));
662      files.put(filename, b.toString());
663      return b.toString();
664    } else {
665      b.append("tbl_bck");
666      for (Integer i : indents)
667        b.append(Integer.toString(i));
668      int indent = lineColor*2 + (hasChildren?1:0);
669      b.append(Integer.toString(indent));
670      b.append(".png");
671      String file = Utilities.path(dest, b.toString());
672      if (!new File(file).exists()) {
673        FileOutputStream stream = new FileOutputStream(file);
674        genImage(indents, hasChildren, lineColor, stream);
675        if (outputTracker!=null)
676          outputTracker.add(file);
677      }
678      return b.toString();
679    }
680  }
681
682
683  private void genImage(List<Integer> indents, boolean hasChildren, int lineColor, OutputStream stream) throws IOException {
684    BufferedImage bi = new BufferedImage(800, 2, BufferedImage.TYPE_INT_ARGB);
685    // i have no idea why this works to make these pixels transparent. It defies logic. 
686    // But this combination of INT_ARGB and filling with grey magically worked when nothing else did. So it stays as is.
687    Color grey = new Color(99,99,99,0); 
688    for (int i = 0; i < 800; i++) {
689      bi.setRGB(i, 0, grey.getRGB());
690      bi.setRGB(i, 1, grey.getRGB());
691    }
692    Color black = new Color(0, 0, 0);
693    Color green = new Color(14,209,69);
694    Color gold = new Color(212,168,21);
695    for (int i = 0; i < indents.size(); i++) {
696      int indent = indents.get(i).intValue();
697      if (indent == CONTINUE_REGULAR)
698        bi.setRGB(12+(i*16), 0, black.getRGB());
699      else if (indent == CONTINUE_SLICER)
700        bi.setRGB(12+(i*16), 0, green.getRGB());
701      else if (indent == CONTINUE_SLICE)
702        bi.setRGB(12+(i*16), 0, gold.getRGB());
703    }
704    if (hasChildren) {
705      if (lineColor==0)
706        bi.setRGB(12+(indents.size()*16), 0, black.getRGB());
707      else if (lineColor==1)
708        bi.setRGB(12+(indents.size()*16), 0, green.getRGB());
709      else if (lineColor==2)
710        bi.setRGB(12+(indents.size()*16), 0, gold.getRGB());
711    }
712    ImageIO.write(bi, "PNG", stream);
713  }
714
715  private String makeName(List<Integer> indents) {
716    StringBuilder b = new StringBuilder();
717    b.append("indents:");
718    for (Integer i : indents)
719      b.append(Integer.toString(i));
720    return b.toString();
721  }
722
723  private void check(boolean check, String message) throws FHIRException  {
724    if (!check)
725      throw new FHIRException(message);
726  }
727}