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  private boolean makeTargets;
388  
389  /**
390   * There are circumstances where the table has to present in the absence of a stable supporting infrastructure.
391   * and the file paths cannot be guaranteed. For these reasons, you can tell the builder to inline all the graphics
392   * (all the styles are inlined anyway, since the table fbuiler has even less control over the styling
393   *  
394   */
395  private boolean inLineGraphics;
396  
397  
398  public HierarchicalTableGenerator() {
399    super();
400  }
401
402  public HierarchicalTableGenerator(String dest, boolean inlineGraphics) {
403    super();
404    this.dest = dest;
405    this.inLineGraphics = inlineGraphics;
406    this.makeTargets = true;
407  }
408
409  public HierarchicalTableGenerator(String dest, boolean inlineGraphics, boolean makeTargets) {
410    super();
411    this.dest = dest;
412    this.inLineGraphics = inlineGraphics;
413    this.makeTargets = makeTargets;
414  }
415
416  public TableModel initNormalTable(String prefix, boolean isLogical) {
417    TableModel model = new TableModel();
418    
419    model.setDocoImg(prefix+"help16.png");
420    model.setDocoRef(prefix+"formats.html#table");
421    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Name"), translate("sd.hint", "The logical name of the element"), null, 0));
422    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Flags"), translate("sd.hint", "Information about the use of the element"), null, 0));
423    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));
424    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Type"), translate("sd.hint", "Reference to the type of the element"), null, 100));
425    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Description & Constraints"), translate("sd.hint", "Additional information about the element"), null, 0));
426    if (isLogical) {
427      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));
428    }
429    return model;
430  }
431
432
433  public TableModel initGridTable(String prefix) {
434    TableModel model = new TableModel();
435    
436    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));
437    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));
438    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Type"), translate("sd.hint", "Reference to the type of the element"), null, 100));
439    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));
440    return model;
441  }
442
443  public XhtmlNode generate(TableModel model, String imagePath, int border, Set<String> outputTracker) throws IOException, FHIRException  {
444    checkModel(model);
445    XhtmlNode table = new XhtmlNode(NodeType.Element, "table").setAttribute("border", Integer.toString(border)).setAttribute("cellspacing", "0").setAttribute("cellpadding", "0");
446    table.setAttribute("style", "border: " + border + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top;");
447    XhtmlNode tr = table.addTag("tr");
448    tr.setAttribute("style", "border: " + Integer.toString(1 + border) + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top;");
449    XhtmlNode tc = null;
450    for (Title t : model.getTitles()) {
451      tc = renderCell(tr, t, "th", null, null, null, false, null, "white", 0, imagePath, border, outputTracker);
452      if (t.width != 0)
453        tc.setAttribute("style", "width: "+Integer.toString(t.width)+"px");
454    }
455    if (tc != null && model.getDocoRef() != null)
456      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());
457      
458    for (Row r : model.getRows()) {
459      renderRow(table, r, 0, new ArrayList<Integer>(), imagePath, border, outputTracker);
460    }
461    if (model.getDocoRef() != null) {
462      tr = table.addTag("tr");
463      tc = tr.addTag("td");
464      tc.setAttribute("class", "hierarchy");
465      tc.setAttribute("colspan", Integer.toString(model.getTitles().size()));
466      tc.addTag("br");
467      XhtmlNode a = tc.addTag("a").setAttribute("title", translate("sd.doco", "Legend for this format")).setAttribute("href", model.getDocoRef());
468      if (model.getDocoImg() != null)
469        a.addTag("img").setAttribute("alt", "doco").setAttribute("style", "background-color: inherit").setAttribute("src", model.getDocoImg());
470      a.addText(" "+translate("sd.doco", "Documentation for this format"));
471    }
472    return table;
473  }
474
475
476  private void renderRow(XhtmlNode table, Row r, int indent, List<Integer> indents, String imagePath, int border, Set<String> outputTracker) throws IOException  {
477    XhtmlNode tr = table.addTag("tr");
478    String color = "white";
479    if (r.getColor() != null)
480      color = r.getColor();
481    tr.setAttribute("style", "border: " + border + "px #F0F0F0 solid; padding:0px; vertical-align: top; background-color: "+color+";");
482    boolean first = true;
483    for (Cell t : r.getCells()) {
484      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);
485      first = false;
486    }
487    table.addText("\r\n");
488    
489    for (int i = 0; i < r.getSubRows().size(); i++) {
490      Row c = r.getSubRows().get(i);
491      List<Integer> ind = new ArrayList<Integer>();
492      ind.addAll(indents);
493      if (i == r.getSubRows().size() - 1) {
494        ind.add(r.getLineColor()*2);
495      } else {
496        ind.add(r.getLineColor()*2+1);
497      }
498      renderRow(table, c, indent+1, ind, imagePath, border, outputTracker);
499    }
500  }
501
502
503  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  {
504    XhtmlNode tc = tr.addTag(name);
505    tc.setAttribute("class", "hierarchy");
506    if (indents != null) {
507      tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_spacer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
508      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)+")");
509      for (int i = 0; i < indents.size()-1; i++) {
510        switch (indents.get(i)) {
511          case NEW_REGULAR:
512          case NEW_SLICER:
513          case NEW_SLICE:
514            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_blank.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
515            break;
516          case CONTINUE_REGULAR:
517            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
518            break;
519          case CONTINUE_SLICER:
520            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline_slicer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
521            break;
522          case CONTINUE_SLICE:
523            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline_slice.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
524            break;
525          default:
526            throw new Error("Unrecognized indent level: " + indents.get(i));
527        }
528      }
529      if (!indents.isEmpty())
530        switch (indents.get(indents.size()-1)) {
531        case NEW_REGULAR:
532          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_end.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
533          break;
534        case NEW_SLICER:
535          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_end_slicer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
536          break;
537        case NEW_SLICE:
538          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_end_slice.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
539          break;
540        case CONTINUE_REGULAR:
541          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
542          break;
543        case CONTINUE_SLICER:
544          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_slicer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
545          break;
546        case CONTINUE_SLICE:
547          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_slice.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
548          break;
549        default:
550          throw new Error("Unrecognized indent level: " + indents.get(indents.size()-1));
551        }
552    }
553    else
554      tc.setAttribute("style", "vertical-align: top; text-align : left; background-color: "+color+"; border: "+ border +"px #F0F0F0 solid; padding:0px 4px 0px 4px");
555    if (!Utilities.noString(icon)) {
556      XhtmlNode img = tc.addTag("img").setAttribute("src", srcFor(imagePath, icon)).setAttribute("class", "hierarchy").setAttribute("style", "background-color: "+color+"; background-color: inherit").setAttribute("alt", ".");
557      if (hint != null)
558        img.setAttribute("title", hint);
559      tc.addText(" ");
560    }
561    for (Piece p : c.pieces) {
562      if (!Utilities.noString(p.getTag())) {
563        XhtmlNode tag = tc.addTag(p.getTag());
564        if (p.attributes != null)
565          for (String n : p.attributes.keySet())
566            tag.setAttribute(n, p.attributes.get(n));
567        if (p.getHint() != null)
568          tag.setAttribute("title", p.getHint());
569        addStyle(tag, p);
570        if (p.hasChildren())
571          tag.getChildNodes().addAll(p.getChildren());
572      } else if (!Utilities.noString(p.getReference())) {
573        XhtmlNode a = addStyle(tc.addTag("a"), p);
574        a.setAttribute("href", p.getReference());
575        if (!Utilities.noString(p.getHint()))
576          a.setAttribute("title", p.getHint());
577        a.addText(p.getText());
578        addStyle(a, p);
579      } else { 
580        if (!Utilities.noString(p.getHint())) {
581          XhtmlNode s = addStyle(tc.addTag("span"), p);
582          s.setAttribute("title", p.getHint());
583          s.addText(p.getText());
584        } else if (p.getStyle() != null) {
585          XhtmlNode s = addStyle(tc.addTag("span"), p);
586          s.addText(p.getText());
587        } else
588          tc.addText(p.getText());
589      }
590    }
591    if (makeTargets && !Utilities.noString(anchor))
592      tc.addTag("a").setAttribute("name", nmTokenize(anchor)).addText(" ");
593    return tc;
594  }
595
596
597  private XhtmlNode addStyle(XhtmlNode node, Piece p) {
598    if (p.getStyle() != null)
599      node.setAttribute("style", p.getStyle());
600    return node;
601  }
602
603  private String nmTokenize(String anchor) {
604    return anchor.replace("[", "_").replace("]", "_");
605  }
606  
607  private String srcFor(String corePrefix, String filename) throws IOException {
608    if (inLineGraphics) {
609      if (files.containsKey(filename))
610        return files.get(filename);
611      StringBuilder b = new StringBuilder();
612      b.append("data: image/png;base64,");
613      byte[] bytes;
614      File file = new File(Utilities.path(dest, filename));
615      if (!file.exists()) // because sometime this is called real early before the files exist. it will be built again later because of this
616        bytes = new byte[0]; 
617      else
618        bytes = FileUtils.readFileToByteArray(file);
619      b.append(new String(Base64.encodeBase64(bytes)));
620//      files.put(filename, b.toString());
621      return b.toString();
622    } else
623      return corePrefix+filename;
624  }
625
626
627  private void checkModel(TableModel model) throws FHIRException  {
628    check(!model.getRows().isEmpty(), "Must have rows");
629    check(!model.getTitles().isEmpty(), "Must have titles");
630    for (Cell c : model.getTitles())
631      check(c);
632    int i = 0;
633    for (Row r : model.getRows()) { 
634      check(r, "rows", model.getTitles().size(), Integer.toString(i));
635      i++;
636    }
637  }
638
639
640  private void check(Cell c) throws FHIRException  {  
641    boolean hasText = false;
642    for (Piece p : c.pieces)
643      if (!Utilities.noString(p.getText()))
644        hasText = true;
645    check(hasText, "Title cells must have text");    
646  }
647
648
649  private void check(Row r, String string, int size, String path) throws FHIRException  {    
650    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());
651    int i = 0;
652    for (Row c : r.getSubRows()) {
653      check(c, "rows", size, path+"."+Integer.toString(i));
654      i++;
655    }
656  }
657
658
659  private String checkExists(List<Integer> indents, boolean hasChildren, int lineColor, Set<String> outputTracker) throws IOException  {
660    String filename = makeName(indents);
661    
662    StringBuilder b = new StringBuilder();
663    if (inLineGraphics) {
664      if (files.containsKey(filename))
665        return files.get(filename);
666      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
667      genImage(indents, hasChildren, lineColor, bytes);
668      b.append("data: image/png;base64,");
669      byte[] encodeBase64 = Base64.encodeBase64(bytes.toByteArray());
670      b.append(new String(encodeBase64));
671      files.put(filename, b.toString());
672      return b.toString();
673    } else {
674      b.append("tbl_bck");
675      for (Integer i : indents)
676        b.append(Integer.toString(i));
677      int indent = lineColor*2 + (hasChildren?1:0);
678      b.append(Integer.toString(indent));
679      b.append(".png");
680      String file = Utilities.path(dest, b.toString());
681      if (!new File(file).exists()) {
682        FileOutputStream stream = new FileOutputStream(file);
683        genImage(indents, hasChildren, lineColor, stream);
684        if (outputTracker!=null)
685          outputTracker.add(file);
686      }
687      return b.toString();
688    }
689  }
690
691
692  private void genImage(List<Integer> indents, boolean hasChildren, int lineColor, OutputStream stream) throws IOException {
693    BufferedImage bi = new BufferedImage(800, 2, BufferedImage.TYPE_INT_ARGB);
694    // i have no idea why this works to make these pixels transparent. It defies logic. 
695    // But this combination of INT_ARGB and filling with grey magically worked when nothing else did. So it stays as is.
696    Color grey = new Color(99,99,99,0); 
697    for (int i = 0; i < 800; i++) {
698      bi.setRGB(i, 0, grey.getRGB());
699      bi.setRGB(i, 1, grey.getRGB());
700    }
701    Color black = new Color(0, 0, 0);
702    Color green = new Color(14,209,69);
703    Color gold = new Color(212,168,21);
704    for (int i = 0; i < indents.size(); i++) {
705      int indent = indents.get(i).intValue();
706      if (indent == CONTINUE_REGULAR)
707        bi.setRGB(12+(i*16), 0, black.getRGB());
708      else if (indent == CONTINUE_SLICER)
709        bi.setRGB(12+(i*16), 0, green.getRGB());
710      else if (indent == CONTINUE_SLICE)
711        bi.setRGB(12+(i*16), 0, gold.getRGB());
712    }
713    if (hasChildren) {
714      if (lineColor==0)
715        bi.setRGB(12+(indents.size()*16), 0, black.getRGB());
716      else if (lineColor==1)
717        bi.setRGB(12+(indents.size()*16), 0, green.getRGB());
718      else if (lineColor==2)
719        bi.setRGB(12+(indents.size()*16), 0, gold.getRGB());
720    }
721    ImageIO.write(bi, "PNG", stream);
722  }
723
724  private String makeName(List<Integer> indents) {
725    StringBuilder b = new StringBuilder();
726    b.append("indents:");
727    for (Integer i : indents)
728      b.append(Integer.toString(i));
729    return b.toString();
730  }
731
732  private void check(boolean check, String message) throws FHIRException  {
733    if (!check)
734      throw new FHIRException(message);
735  }
736}