001package org.hl7.fhir.utilities.xhtml;
002
003/*-
004 * #%L
005 * org.hl7.fhir.utilities
006 * %%
007 * Copyright (C) 2014 - 2019 Health Level 7
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;
064import java.util.Set;
065
066import javax.imageio.ImageIO;
067
068import org.apache.commons.codec.binary.Base64;
069import org.apache.commons.io.FileUtils;
070import org.commonmark.node.Node;
071import org.commonmark.parser.Parser;
072import org.commonmark.renderer.html.HtmlRenderer;
073import org.hl7.fhir.exceptions.FHIRException;
074import org.hl7.fhir.utilities.TranslatingUtilities;
075import org.hl7.fhir.utilities.Utilities;
076
077
078public class HierarchicalTableGenerator extends TranslatingUtilities {
079  public static final String TEXT_ICON_REFERENCE = "Reference to another Resource";
080  public static final String TEXT_ICON_PRIMITIVE = "Primitive Data Type";
081  public static final String TEXT_ICON_DATATYPE = "Data Type";
082  public static final String TEXT_ICON_RESOURCE = "Resource";
083  public static final String TEXT_ICON_ELEMENT = "Element";
084  public static final String TEXT_ICON_REUSE = "Reference to another Element";
085  public static final String TEXT_ICON_EXTENSION = "Extension";
086  public static final String TEXT_ICON_CHOICE = "Choice of Types";
087  public static final String TEXT_ICON_SLICE = "Slice Definition";
088  public static final String TEXT_ICON_SLICE_ITEM = "Slice Item";
089  public static final String TEXT_ICON_FIXED = "Fixed Value";
090  public static final String TEXT_ICON_EXTENSION_SIMPLE = "Simple Extension";
091  public static final String TEXT_ICON_PROFILE = "Profile";
092  public static final String TEXT_ICON_EXTENSION_COMPLEX = "Complex Extension";
093
094  public static final int NEW_REGULAR = 0;
095  public static final int CONTINUE_REGULAR = 1;
096  public static final int NEW_SLICER = 2;
097  public static final int CONTINUE_SLICER = 3;
098  public static final int NEW_SLICE = 4;
099  public static final int CONTINUE_SLICE = 5;
100  private static final String BACKGROUND_ALT_COLOR = "#F7F7F7";
101  public static boolean ACTIVE_TABLES = false;
102    
103  private static Map<String, String> files = new HashMap<String, String>();
104
105  private class Counter {
106    private int count = -1;
107    private void row() {
108      count++;
109    }
110    private boolean isOdd() {
111      return count % 2 == 1;
112    }
113  }
114  public class Piece {
115    private String tag;
116    private String reference;
117    private String text;
118    private String hint;
119    private String style;
120    private Map<String, String> attributes;
121    private List<XhtmlNode> children;
122    
123    public Piece(String tag) {
124      super();
125      this.tag = tag;
126    }
127    
128    public Piece(String reference, String text, String hint) {
129      super();
130      this.reference = reference;
131      this.text = text;
132      this.hint = hint;
133    }
134    public String getReference() {
135      return reference;
136    }
137    public void setReference(String value) {
138      reference = value;
139    }
140    public String getText() {
141      return text;
142    }
143    public String getHint() {
144      return hint;
145    }
146
147    public String getTag() {
148      return tag;
149    }
150
151    public String getStyle() {
152      return style;
153    }
154
155    public void setTag(String tag) {
156      this.tag = tag;
157    }
158
159    public Piece setText(String text) {
160      this.text = text;
161      return this;
162    }
163
164    public void setHint(String hint) {
165      this.hint = hint;
166    }
167
168    public Piece setStyle(String style) {
169      this.style = style;
170      return this;
171    }
172
173    public Piece addStyle(String style) {
174      if (this.style != null)
175        this.style = this.style+"; "+style;
176      else
177        this.style = style;
178      return this;
179    }
180
181    public void addToHint(String text) {
182      if (this.hint == null)
183        this.hint = text;
184      else
185        this.hint += (this.hint.endsWith(".") || this.hint.endsWith("?") ? " " : ". ")+text;
186    }
187    
188    public boolean hasChildren() {
189      return children != null && !children.isEmpty();
190    }
191
192    public List<XhtmlNode> getChildren() {
193      if (children == null)
194        children = new ArrayList<XhtmlNode>();
195      return children;
196    }
197    
198  }
199  
200  public class Cell {
201    private List<Piece> pieces = new ArrayList<HierarchicalTableGenerator.Piece>();
202
203    public Cell() {
204      
205    }
206    public Cell(String prefix, String reference, String text, String hint, String suffix) {
207      super();
208      if (!Utilities.noString(prefix))
209        pieces.add(new Piece(null, prefix, null));
210      pieces.add(new Piece(reference, text, hint));
211      if (!Utilities.noString(suffix))
212        pieces.add(new Piece(null, suffix, null));
213    }
214    public List<Piece> getPieces() {
215      return pieces;
216    }
217    public Cell addPiece(Piece piece) {
218      pieces.add(piece);
219      return this;
220    }
221    public Cell addMarkdown(String md) {
222      try {
223        Parser parser = Parser.builder().build();
224        Node document = parser.parse(md);
225        HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build();
226        String html = renderer.render(document);  
227        pieces.addAll(htmlToParagraphPieces(html));
228      } catch (Exception e) {
229        e.printStackTrace();
230      }
231      return this;
232    }
233    
234    private List<Piece> htmlToParagraphPieces(String html)  {
235      List<Piece> myPieces = new ArrayList<Piece>();
236      try {
237        XhtmlNode node = new XhtmlParser().parseFragment("<html>"+html+"</html>");
238        boolean first = true;
239        for (XhtmlNode c : node.getChildNodes()) {
240          if (first) {
241            first = false;
242          } else {
243            myPieces.add(new Piece("br"));
244            myPieces.add(new Piece("br"));            
245          }
246          if (c.getNodeType() == NodeType.Text) {
247            if (!Utilities.isWhitespace(c.getContent()))
248              addNode(myPieces, c);
249          } else if ("p".equals(c.getName())) {
250            for (XhtmlNode g : c.getChildNodes()) {
251              addNode(myPieces, g);
252            }
253          } else {
254           Piece x = new Piece(c.getName());
255           x.getChildren().addAll(c.getChildNodes());
256           myPieces.add(x);            
257          }
258        }
259//        String[] paragraphs = html.replace("<p>", "").split("<\\/p>|<br  \\/>");
260//        for (int i=0;i<paragraphs.length;i++) {
261//          if (!paragraphs[i].isEmpty()) {
262//            if (i!=0) {
263//              myPieces.add(new Piece("br"));
264//              myPieces.add(new Piece("br"));
265//            }
266//            myPieces.addAll(htmlFormattingToPieces(paragraphs[i]));
267//          }
268//        }
269      } catch (Exception e) {
270        throw new FHIRException("Exception parsing html: "+e.getMessage()+" for "+html, e);
271      }
272
273      return myPieces;
274    }
275    
276    private List<Piece> htmlFormattingToPieces(String html) throws IOException, FHIRException {
277      List<Piece> myPieces = new ArrayList<Piece>();
278      if (html.contains(("<"))) {
279        XhtmlNode node = new XhtmlParser().parseFragment("<p>"+html+"</p>");
280        for (XhtmlNode c : node.getChildNodes()) {
281          addNode(myPieces, c);
282        }
283      } else
284        myPieces.add(new Piece(null, html, null));        
285      return myPieces;
286    }
287    
288    private void addNode(List<Piece> list, XhtmlNode c) {
289      if (c.getNodeType() == NodeType.Text)
290        list.add(new Piece(null, c.getContent(), null));
291      else if (c.getNodeType() == NodeType.Element) {
292        if (c.getName().equals("a")) {
293          list.add(new Piece(c.getAttribute("href"), c.allText(), c.getAttribute("title")));                    
294        } else if (c.getName().equals("b") || c.getName().equals("em") || c.getName().equals("strong")) {
295          list.add(new Piece(null, c.allText(), null).setStyle("font-face: bold"));                    
296        } else if (c.getName().equals("code")) {
297          list.add(new Piece(null, c.allText(), null).setStyle("padding: 2px 4px; color: #005c00; background-color: #f9f2f4; white-space: nowrap; border-radius: 4px"));                    
298        } else if (c.getName().equals("i")) {
299          list.add(new Piece(null, c.allText(), null).setStyle("font-style: italic"));
300        } else if (c.getName().equals("pre")) {
301          Piece p = new Piece(c.getName()).setStyle("white-space: pre; font-family: courier");
302          list.add(p);
303          p.getChildren().addAll(c.getChildNodes());
304        } else if (c.getName().equals("ul") || c.getName().equals("ol")) {
305          Piece p = new Piece(c.getName());
306          list.add(p);
307          p.getChildren().addAll(c.getChildNodes());
308        } else if (c.getName().equals("i")) {
309          list.add(new Piece(null, c.allText(), null).setStyle("font-style: italic"));                    
310        } else if (c.getName().equals("h1")||c.getName().equals("h2")||c.getName().equals("h3")||c.getName().equals("h4")) {
311          Piece p = new Piece(c.getName());
312          list.add(p);
313          p.getChildren().addAll(c.getChildNodes());
314        } else if (c.getName().equals("br")) {
315          list.add(new Piece(c.getName()));
316        } else {
317          
318          throw new Error("Not handled yet: "+c.getName());
319        }
320      } else
321        throw new Error("Unhandled type "+c.getNodeType().toString());
322
323    }
324    public void addStyle(String style) {
325      for (Piece p : pieces)
326        p.addStyle(style);      
327    }
328    public void addToHint(String text) {
329      for (Piece p : pieces)
330        p.addToHint(text);            
331    }
332    public Piece addStyledText(String hint, String alt, String fgColor, String bgColor, String link, boolean border) {
333      Piece p = new Piece(link, alt, hint);
334      p.addStyle("padding-left: 3px");
335      p.addStyle("padding-right: 3px");
336      if (border) {
337        p.addStyle("border: 1px grey solid");
338        p.addStyle("font-weight: bold");
339      }
340      if (fgColor != null) {
341        p.addStyle("color: "+fgColor);
342        p.addStyle("background-color: "+bgColor);
343      } else {
344        p.addStyle("color: black");
345        p.addStyle("background-color: "+bgColor != null ? bgColor : "white");       
346      }
347      pieces.add(p);
348      return p;
349    }
350    public String text() {
351      StringBuilder b = new StringBuilder();
352      for (Piece p : pieces)
353        b.append(p.text);
354      return b.toString();
355    }
356    @Override
357    public String toString() {
358      return text();
359    }
360    
361    
362  }
363
364  public class Title extends Cell {
365    private int width;
366
367    public Title(String prefix, String reference, String text, String hint, String suffix, int width) {
368      super(prefix, reference, text, hint, suffix);
369      this.width = width;
370    }
371  }
372  
373  public class Row {
374    private List<Row> subRows = new ArrayList<HierarchicalTableGenerator.Row>();
375    private List<Cell> cells = new ArrayList<HierarchicalTableGenerator.Cell>();
376    private String icon;
377    private String anchor;
378    private String hint;
379    private String color;
380    private int lineColor;
381    private String id;
382    
383    public List<Row> getSubRows() {
384      return subRows;
385    }
386    public List<Cell> getCells() {
387      return cells;
388    }
389    public String getIcon() {
390      return icon;
391    }
392    public void setIcon(String icon, String hint) {
393      this.icon = icon;
394      this.hint = hint;
395    }
396    public String getAnchor() {
397      return anchor;
398    }
399    public void setAnchor(String anchor) {
400      this.anchor = anchor;
401    }
402    public String getHint() {
403      return hint;
404    }
405    public String getColor() {
406      return color;
407    }
408    public void setColor(String color) {
409      this.color = color;
410    }
411    public int getLineColor() {
412      return lineColor;
413    }
414    public void setLineColor(int lineColor) {
415      assert lineColor >= 0;
416      assert lineColor <= 2;
417      this.lineColor = lineColor;
418    }
419    public String getId() {
420      return id;
421    }
422    public void setId(String id) {
423      this.id = id;
424    }
425  }
426
427  public class TableModel {
428    private String id;
429    private boolean active;
430    private List<Title> titles = new ArrayList<HierarchicalTableGenerator.Title>();
431    private List<Row> rows = new ArrayList<HierarchicalTableGenerator.Row>();
432    private String docoRef;
433    private String docoImg;
434    private boolean alternating;
435        
436    public TableModel(String id, boolean active) {
437      super();
438      this.id = id;
439      this.active = active;
440    }
441    public List<Title> getTitles() {
442      return titles;
443    }
444    public List<Row> getRows() {
445      return rows;
446    }
447    public String getDocoRef() {
448      return docoRef;
449    }
450    public String getDocoImg() {
451      return docoImg;
452    }
453    public void setDocoRef(String docoRef) {
454      this.docoRef = docoRef;
455    }
456    public void setDocoImg(String docoImg) {
457      this.docoImg = docoImg;
458    }
459    public String getId() {
460      return id;
461    }
462    
463    public void setId(String id) {
464      this.id = id;
465    }
466    public boolean isActive() {
467      return active && ACTIVE_TABLES;
468    }
469    public boolean isAlternating() {
470      return alternating;
471    }
472    
473  }
474
475
476  private String dest;
477  private boolean makeTargets;
478  
479  /**
480   * There are circumstances where the table has to present in the absence of a stable supporting infrastructure.
481   * and the file paths cannot be guaranteed. For these reasons, you can tell the builder to inline all the graphics
482   * (all the styles are inlined anyway, since the table fbuiler has even less control over the styling
483   *  
484   */
485  private boolean inLineGraphics;  
486  
487  public HierarchicalTableGenerator() {
488    super();
489  }
490
491  public HierarchicalTableGenerator(String dest, boolean inlineGraphics) {
492    super();
493    this.dest = dest;
494    this.inLineGraphics = inlineGraphics;
495    this.makeTargets = true;
496  }
497
498  public HierarchicalTableGenerator(String dest, boolean inlineGraphics, boolean makeTargets) {
499    super();
500    this.dest = dest;
501    this.inLineGraphics = inlineGraphics;
502    this.makeTargets = makeTargets;
503  }
504
505  public TableModel initNormalTable(String prefix, boolean isLogical, boolean alternating, String id, boolean isActive) {
506    TableModel model = new TableModel(id, isActive);
507    
508    model.alternating = alternating;
509    model.setDocoImg(prefix+"help16.png");
510    model.setDocoRef(prefix+"formats.html#table");
511    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Name"), translate("sd.hint", "The logical name of the element"), null, 0));
512    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Flags"), translate("sd.hint", "Information about the use of the element"), null, 0));
513    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));
514    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Type"), translate("sd.hint", "Reference to the type of the element"), null, 100));
515    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Description & Constraints"), translate("sd.hint", "Additional information about the element"), null, 0));
516    if (isLogical) {
517      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));
518    }
519    return model;
520  }
521
522
523  public TableModel initGridTable(String prefix, String id) {
524    TableModel model = new TableModel(id, false);
525    
526    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));
527    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));
528    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Type"), translate("sd.hint", "Reference to the type of the element"), null, 100));
529    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));
530    return model;
531  }
532
533  public XhtmlNode generate(TableModel model, String imagePath, int border, Set<String> outputTracker) throws IOException, FHIRException  {
534    checkModel(model);
535    XhtmlNode table = new XhtmlNode(NodeType.Element, "table").setAttribute("border", Integer.toString(border)).setAttribute("cellspacing", "0").setAttribute("cellpadding", "0");
536    
537    if (model.isActive()) {      
538      table.setAttribute("id", model.getId());
539    }
540    table.setAttribute("style", "border: " + border + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top;");
541    XhtmlNode tr = table.addTag("tr");
542    tr.setAttribute("style", "border: " + Integer.toString(1 + border) + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top;");
543    XhtmlNode tc = null;
544    for (Title t : model.getTitles()) {
545      tc = renderCell(tr, t, "th", null, null, null, false, null, "white", 0, imagePath, border, outputTracker, model, null);
546      if (t.width != 0)
547        tc.setAttribute("style", "width: "+Integer.toString(t.width)+"px");
548    }
549    if (tc != null && model.getDocoRef() != null) {
550      XhtmlNode img = tc.addTag("span").setAttribute("style", "float: right").addTag("a").setAttribute("title", "Legend for this format").setAttribute("href", model.getDocoRef()).addTag("img");
551      img.setAttribute("alt", "doco").setAttribute("style", "background-color: inherit").setAttribute("src", model.getDocoImg());
552      if (model.isActive()) {
553        img.setAttribute("onload", "fhirTableInit(this)");
554      }
555    }
556      
557    Counter counter = new Counter();
558    for (Row r : model.getRows()) {
559      renderRow(table, r, 0, new ArrayList<Integer>(), imagePath, border, outputTracker, counter, model);
560    }
561    if (model.getDocoRef() != null) {
562      tr = table.addTag("tr");
563      tc = tr.addTag("td");
564      tc.setAttribute("class", "hierarchy");
565      tc.setAttribute("colspan", Integer.toString(model.getTitles().size()));
566      tc.addTag("br");
567      XhtmlNode a = tc.addTag("a").setAttribute("title", translate("sd.doco", "Legend for this format")).setAttribute("href", model.getDocoRef());
568      if (model.getDocoImg() != null)
569        a.addTag("img").setAttribute("alt", "doco").setAttribute("style", "background-color: inherit").setAttribute("src", model.getDocoImg());
570      a.addText(" "+translate("sd.doco", "Documentation for this format"));
571    }
572    return table;
573  }
574
575
576  private void renderRow(XhtmlNode table, Row r, int indent, List<Integer> indents, String imagePath, int border, Set<String> outputTracker, Counter counter, TableModel model) throws IOException  {
577    counter.row();
578    XhtmlNode tr = table.addTag("tr");
579    String color = "white";
580    if (r.getColor() != null)
581      color = r.getColor();
582    else if (model.alternating  && counter.isOdd())
583      color = BACKGROUND_ALT_COLOR;
584    
585    tr.setAttribute("style", "border: " + border + "px #F0F0F0 solid; padding:0px; vertical-align: top; background-color: "+color+";");
586    if (model.isActive()) {
587      tr.setAttribute("id", r.getId());
588    }
589    boolean first = true;
590    for (Cell t : r.getCells()) {
591      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, model, r);
592      first = false;
593    }
594    table.addText("\r\n");
595    
596    for (int i = 0; i < r.getSubRows().size(); i++) {
597      Row c = r.getSubRows().get(i);
598      List<Integer> ind = new ArrayList<Integer>();
599      ind.addAll(indents);
600      if (i == r.getSubRows().size() - 1) {
601        ind.add(r.getLineColor()*2);
602      } else {
603        ind.add(r.getLineColor()*2+1);
604      }
605      renderRow(table, c, indent+1, ind, imagePath, border, outputTracker, counter, model);
606    }
607  }
608
609
610  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, TableModel table, Row row) throws IOException  {
611    XhtmlNode tc = tr.addTag(name);
612    tc.setAttribute("class", "hierarchy");
613    if (indents != null) {
614      tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_spacer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
615      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)+")");
616      for (int i = 0; i < indents.size()-1; i++) {
617        switch (indents.get(i)) {
618          case NEW_REGULAR:
619          case NEW_SLICER:
620          case NEW_SLICE:
621            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_blank.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
622            break;
623          case CONTINUE_REGULAR:
624            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
625            break;
626          case CONTINUE_SLICER:
627            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline_slicer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
628            break;
629          case CONTINUE_SLICE:
630            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline_slice.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
631            break;
632          default:
633            throw new Error("Unrecognized indent level: " + indents.get(i));
634        }
635      }
636      if (!indents.isEmpty()) {
637        String sfx = table.isActive() && hasChildren ? "-open" : "";
638        XhtmlNode img = tc.addTag("img");
639        switch (indents.get(indents.size()-1)) {
640        case NEW_REGULAR:
641          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin_end"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
642          break;
643        case NEW_SLICER:
644          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin_end_slicer"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
645          break;
646        case NEW_SLICE:
647          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin_end_slice"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
648          break;
649        case CONTINUE_REGULAR:
650          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
651          break;
652        case CONTINUE_SLICER:
653          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin_slicer"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
654          break;
655        case CONTINUE_SLICE:
656          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin_slice"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
657          break;
658        default:
659          throw new Error("Unrecognized indent level: " + indents.get(indents.size()-1));
660        }
661        if (table.isActive() && hasChildren) {
662          img.setAttribute("onClick", "tableRowAction(this)");
663        }
664      }
665    }
666    else
667      tc.setAttribute("style", "vertical-align: top; text-align : left; background-color: "+color+"; border: "+ border +"px #F0F0F0 solid; padding:0px 4px 0px 4px");
668    if (!Utilities.noString(icon)) {
669      XhtmlNode img = tc.addTag("img").setAttribute("src", srcFor(imagePath, icon)).setAttribute("class", "hierarchy").setAttribute("style", "background-color: "+color+"; background-color: inherit").setAttribute("alt", ".");
670      if (hint != null)
671        img.setAttribute("title", hint);
672      tc.addText(" ");
673    }
674    for (Piece p : c.pieces) {
675      if (!Utilities.noString(p.getTag())) {
676        XhtmlNode tag = tc.addTag(p.getTag());
677        if (p.attributes != null)
678          for (String n : p.attributes.keySet())
679            tag.setAttribute(n, p.attributes.get(n));
680        if (p.getHint() != null)
681          tag.setAttribute("title", p.getHint());
682        addStyle(tag, p);
683        if (p.hasChildren())
684          tag.getChildNodes().addAll(p.getChildren());
685      } else if (!Utilities.noString(p.getReference())) {
686        XhtmlNode a = addStyle(tc.addTag("a"), p);
687        a.setAttribute("href", p.getReference());
688        if (!Utilities.noString(p.getHint()))
689          a.setAttribute("title", p.getHint());
690        a.addText(p.getText());
691        addStyle(a, p);
692      } else { 
693        if (!Utilities.noString(p.getHint())) {
694          XhtmlNode s = addStyle(tc.addTag("span"), p);
695          s.setAttribute("title", p.getHint());
696          s.addText(p.getText());
697        } else if (p.getStyle() != null) {
698          XhtmlNode s = addStyle(tc.addTag("span"), p);
699          s.addText(p.getText());
700        } else
701          tc.addText(p.getText());
702      }
703    }
704    if (makeTargets && !Utilities.noString(anchor))
705      tc.addTag("a").setAttribute("name", nmTokenize(anchor)).addText(" ");
706    return tc;
707  }
708
709
710  private XhtmlNode addStyle(XhtmlNode node, Piece p) {
711    if (p.getStyle() != null)
712      node.setAttribute("style", p.getStyle());
713    return node;
714  }
715
716  private String nmTokenize(String anchor) {
717    return anchor.replace("[", "_").replace("]", "_");
718  }
719  
720  private String srcFor(String corePrefix, String filename) throws IOException {
721    if (inLineGraphics) {
722      if (files.containsKey(filename))
723        return files.get(filename);
724      StringBuilder b = new StringBuilder();
725      b.append("data: image/png;base64,");
726      byte[] bytes;
727      File file = new File(Utilities.path(dest, filename));
728      if (!file.exists()) // because sometime this is called real early before the files exist. it will be built again later because of this
729        bytes = new byte[0]; 
730      else
731        bytes = FileUtils.readFileToByteArray(file);
732      b.append(new String(Base64.encodeBase64(bytes)));
733//      files.put(filename, b.toString());
734      return b.toString();
735    } else
736      return corePrefix+filename;
737  }
738
739
740  private void checkModel(TableModel model) throws FHIRException  {
741    check(!model.getRows().isEmpty(), "Must have rows");
742    check(!model.getTitles().isEmpty(), "Must have titles");
743    for (Cell c : model.getTitles())
744      check(c);
745    int i = 0;
746    for (Row r : model.getRows()) { 
747      check(r, "rows", model.getTitles().size(), "", i, model.getRows().size());
748      i++;
749    }
750  }
751
752
753  private void check(Cell c) throws FHIRException  {  
754    boolean hasText = false;
755    for (Piece p : c.pieces)
756      if (!Utilities.noString(p.getText()))
757        hasText = true;
758    check(hasText, "Title cells must have text");    
759  }
760
761
762  private void check(Row r, String string, int size, String path, int index, int total) throws FHIRException  {
763    String id = Integer.toString(index)+".";
764    if (total <= 26) {
765      char c = (char) ('a'+index);
766      id = Character.toString(c);
767    }
768    path = path + id;
769    r.setId(path);
770    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());
771    int i = 0;
772    for (Row c : r.getSubRows()) {
773      check(c, "rows", size, path, i, r.getSubRows().size());
774      i++;
775    }
776  }
777
778
779  private String checkExists(List<Integer> indents, boolean hasChildren, int lineColor, Set<String> outputTracker) throws IOException  {
780    String filename = makeName(indents);
781    
782    StringBuilder b = new StringBuilder();
783    if (inLineGraphics) {
784      if (files.containsKey(filename))
785        return files.get(filename);
786      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
787      genImage(indents, hasChildren, lineColor, bytes);
788      b.append("data: image/png;base64,");
789      byte[] encodeBase64 = Base64.encodeBase64(bytes.toByteArray());
790      b.append(new String(encodeBase64));
791      files.put(filename, b.toString());
792      return b.toString();
793    } else {
794      b.append("tbl_bck");
795      for (Integer i : indents)
796        b.append(Integer.toString(i));
797      int indent = lineColor*2 + (hasChildren?1:0);
798      b.append(Integer.toString(indent));
799      b.append(".png");
800      String file = Utilities.path(dest, b.toString());
801      if (!new File(file).exists()) {
802        FileOutputStream stream = new FileOutputStream(file);
803        genImage(indents, hasChildren, lineColor, stream);
804        if (outputTracker!=null)
805          outputTracker.add(file);
806      }
807      return b.toString();
808    }
809  }
810
811
812  private void genImage(List<Integer> indents, boolean hasChildren, int lineColor, OutputStream stream) throws IOException {
813    BufferedImage bi = new BufferedImage(800, 2, BufferedImage.TYPE_INT_ARGB);
814    // i have no idea why this works to make these pixels transparent. It defies logic. 
815    // But this combination of INT_ARGB and filling with grey magically worked when nothing else did. So it stays as is.
816    Color grey = new Color(99,99,99,0); 
817    for (int i = 0; i < 800; i++) {
818      bi.setRGB(i, 0, grey.getRGB());
819      bi.setRGB(i, 1, grey.getRGB());
820    }
821    Color black = new Color(0, 0, 0);
822    Color green = new Color(14,209,69);
823    Color gold = new Color(212,168,21);
824    for (int i = 0; i < indents.size(); i++) {
825      int indent = indents.get(i).intValue();
826      if (indent == CONTINUE_REGULAR)
827        bi.setRGB(12+(i*16), 0, black.getRGB());
828      else if (indent == CONTINUE_SLICER)
829        bi.setRGB(12+(i*16), 0, green.getRGB());
830      else if (indent == CONTINUE_SLICE)
831        bi.setRGB(12+(i*16), 0, gold.getRGB());
832    }
833    if (hasChildren) {
834      if (lineColor==0)
835        bi.setRGB(12+(indents.size()*16), 0, black.getRGB());
836      else if (lineColor==1)
837        bi.setRGB(12+(indents.size()*16), 0, green.getRGB());
838      else if (lineColor==2)
839        bi.setRGB(12+(indents.size()*16), 0, gold.getRGB());
840    }
841    ImageIO.write(bi, "PNG", stream);
842  }
843
844  private String makeName(List<Integer> indents) {
845    StringBuilder b = new StringBuilder();
846    b.append("indents:");
847    for (Integer i : indents)
848      b.append(Integer.toString(i));
849    return b.toString();
850  }
851
852  private void check(boolean check, String message) throws FHIRException  {
853    if (!check)
854      throw new FHIRException(message);
855  }
856}