001package org.hl7.fhir.utilities.xhtml;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are 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  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033
034/*
035Copyright (c) 2011+, HL7, Inc
036All rights reserved.
037
038Redistribution and use in source and binary forms, with or without modification, 
039are permitted provided that the following conditions are met:
040
041 * Redistributions of source code must retain the above copyright notice, this 
042   list of conditions and the following disclaimer.
043 * Redistributions in binary form must reproduce the above copyright notice, 
044   this list of conditions and the following disclaimer in the documentation 
045   and/or other materials provided with the distribution.
046 * Neither the name of HL7 nor the names of its contributors may be used to 
047   endorse or promote products derived from this software without specific 
048   prior written permission.
049
050THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
051ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
052WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
053IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
054INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
055NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
056PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
057WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
058ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
059POSSIBILITY OF SUCH DAMAGE.
060
061*/
062
063import java.awt.Color;
064import java.awt.image.BufferedImage;
065import java.io.ByteArrayOutputStream;
066import java.io.File;
067import java.io.FileOutputStream;
068import java.io.IOException;
069import java.io.OutputStream;
070import java.util.ArrayList;
071import java.util.HashMap;
072import java.util.List;
073import java.util.Map;
074import java.util.Set;
075
076import javax.imageio.ImageIO;
077
078import org.apache.commons.codec.binary.Base64;
079import org.apache.commons.io.FileUtils;
080import org.commonmark.node.Node;
081import org.commonmark.parser.Parser;
082import org.commonmark.renderer.html.HtmlRenderer;
083import org.hl7.fhir.exceptions.FHIRException;
084import org.hl7.fhir.utilities.TranslatingUtilities;
085import org.hl7.fhir.utilities.Utilities;
086
087
088public class HierarchicalTableGenerator extends TranslatingUtilities {
089  public static final String TEXT_ICON_REFERENCE = "Reference to another Resource";
090  public static final String TEXT_ICON_PRIMITIVE = "Primitive Data Type";
091  public static final String TEXT_ICON_DATATYPE = "Data Type";
092  public static final String TEXT_ICON_RESOURCE = "Resource";
093  public static final String TEXT_ICON_ELEMENT = "Element";
094  public static final String TEXT_ICON_REUSE = "Reference to another Element";
095  public static final String TEXT_ICON_EXTENSION = "Extension";
096  public static final String TEXT_ICON_CHOICE = "Choice of Types";
097  public static final String TEXT_ICON_SLICE = "Slice Definition";
098  public static final String TEXT_ICON_SLICE_ITEM = "Slice Item";
099  public static final String TEXT_ICON_FIXED = "Fixed Value";
100  public static final String TEXT_ICON_EXTENSION_SIMPLE = "Simple Extension";
101  public static final String TEXT_ICON_PROFILE = "Profile";
102  public static final String TEXT_ICON_EXTENSION_COMPLEX = "Complex Extension";
103
104  public static final int NEW_REGULAR = 0;
105  public static final int CONTINUE_REGULAR = 1;
106  public static final int NEW_SLICER = 2;
107  public static final int CONTINUE_SLICER = 3;
108  public static final int NEW_SLICE = 4;
109  public static final int CONTINUE_SLICE = 5;
110  private static final String BACKGROUND_ALT_COLOR = "#F7F7F7";
111  public static boolean ACTIVE_TABLES = false;
112    
113  public enum TextAlignment {
114    LEFT, CENTER, RIGHT;  
115  }
116  
117  private static Map<String, String> files = new HashMap<String, String>();
118
119  private class Counter {
120    private int count = -1;
121    private void row() {
122      count++;
123    }
124    private boolean isOdd() {
125      return count % 2 == 1;
126    }
127  }
128  public class Piece {
129    private String tag;
130    private String reference;
131    private String text;
132    private String hint;
133    private String style;
134    private Map<String, String> attributes;
135    private List<XhtmlNode> children;
136    
137    public Piece(String tag) {
138      super();
139      this.tag = tag;
140    }
141    
142    public Piece(String reference, String text, String hint) {
143      super();
144      this.reference = reference;
145      this.text = text;
146      this.hint = hint;
147    }
148    public String getReference() {
149      return reference;
150    }
151    public void setReference(String value) {
152      reference = value;
153    }
154    public String getText() {
155      return text;
156    }
157    public String getHint() {
158      return hint;
159    }
160
161    public String getTag() {
162      return tag;
163    }
164
165    public String getStyle() {
166      return style;
167    }
168
169    public void setTag(String tag) {
170      this.tag = tag;
171    }
172
173    public Piece setText(String text) {
174      this.text = text;
175      return this;
176    }
177
178    public void setHint(String hint) {
179      this.hint = hint;
180    }
181
182    public Piece setStyle(String style) {
183      this.style = style;
184      return this;
185    }
186
187    public Piece addStyle(String style) {
188      if (this.style != null)
189        this.style = this.style+"; "+style;
190      else
191        this.style = style;
192      return this;
193    }
194
195    public void addToHint(String text) {
196      if (this.hint == null)
197        this.hint = text;
198      else
199        this.hint += (this.hint.endsWith(".") || this.hint.endsWith("?") ? " " : ". ")+text;
200    }
201    
202    public boolean hasChildren() {
203      return children != null && !children.isEmpty();
204    }
205
206    public List<XhtmlNode> getChildren() {
207      if (children == null)
208        children = new ArrayList<XhtmlNode>();
209      return children;
210    }
211
212    public Piece addHtml(XhtmlNode x) {
213      getChildren().add(x);
214      return this;
215    }
216    
217  }
218  
219  public class Cell {
220    private List<Piece> pieces = new ArrayList<HierarchicalTableGenerator.Piece>();
221    private String cellStyle;
222    protected int span = 1;
223    private TextAlignment alignment = TextAlignment.LEFT;
224
225    public Cell() {
226      
227    }
228    public Cell(String prefix, String reference, String text, String hint, String suffix) {
229      super();
230      if (!Utilities.noString(prefix))
231        pieces.add(new Piece(null, prefix, null));
232      pieces.add(new Piece(reference, text, hint));
233      if (!Utilities.noString(suffix))
234        pieces.add(new Piece(null, suffix, null));
235    }
236    public List<Piece> getPieces() {
237      return pieces;
238    }
239    public Cell addPiece(Piece piece) {
240      pieces.add(piece);
241      return this;
242    }
243
244    public Cell addMarkdown(String md) {
245      if (!Utilities.noString(md)) {
246        try {
247          Parser parser = Parser.builder().build();
248          Node document = parser.parse(md);
249          HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build();
250          String html = renderer.render(document);  
251          pieces.addAll(htmlToParagraphPieces(html, null));
252        } catch (Exception e) {
253          e.printStackTrace();
254        }
255      }
256      return this;
257    }
258    
259    public Cell addMarkdownNoPara(String md) {
260      return addMarkdownNoPara(md, null);
261    }
262    
263    public Cell addMarkdownNoPara(String md, String style) {
264      try {
265        Parser parser = Parser.builder().build();
266        Node document = parser.parse(md);
267        HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build();
268        String html = renderer.render(document);  
269        pieces.addAll(htmlToParagraphPieces(html, style));
270      } catch (Exception e) {
271        e.printStackTrace();
272      }
273      return this;
274    }
275    
276    private List<Piece> htmlToParagraphPieces(String html, String style)  {
277      List<Piece> myPieces = new ArrayList<Piece>();
278      try {
279        XhtmlNode node = new XhtmlParser().parseFragment("<html>"+html+"</html>");
280        boolean first = true;
281        for (XhtmlNode c : node.getChildNodes()) {
282          if (first) {
283            first = false;
284          } else {
285            myPieces.add(new Piece("br"));
286            myPieces.add(new Piece("br"));            
287          }
288          if (c.getNodeType() == NodeType.Text) {
289            if (!Utilities.isWhitespace(c.getContent()))
290              addNode(myPieces, c, style);
291          } else if ("p".equals(c.getName())) {
292            for (XhtmlNode g : c.getChildNodes()) {
293              addNode(myPieces, g, style);
294            }
295          } else {
296           Piece x = new Piece(c.getName());
297           x.getChildren().addAll(c.getChildNodes());
298           if (style != null) {
299             x.addStyle(style);
300           }
301           myPieces.add(x);            
302          }
303        }
304//        String[] paragraphs = html.replace("<p>", "").split("<\\/p>|<br  \\/>");
305//        for (int i=0;i<paragraphs.length;i++) {
306//          if (!paragraphs[i].isEmpty()) {
307//            if (i!=0) {
308//              myPieces.add(new Piece("br"));
309//              myPieces.add(new Piece("br"));
310//            }
311//            myPieces.addAll(htmlFormattingToPieces(paragraphs[i]));
312//          }
313//        }
314      } catch (Exception e) {
315        throw new FHIRException("Exception parsing html: "+e.getMessage()+" for "+html, e);
316      }
317
318      return myPieces;
319    }
320    
321    private List<Piece> htmlFormattingToPieces(String html) throws IOException, FHIRException {
322      List<Piece> myPieces = new ArrayList<Piece>();
323      if (html.contains(("<"))) {
324        XhtmlNode node = new XhtmlParser().parseFragment("<p>"+html+"</p>");
325        for (XhtmlNode c : node.getChildNodes()) {
326          addNode(myPieces, c, null);
327        }
328      } else
329        myPieces.add(new Piece(null, html, null));        
330      return myPieces;
331    }
332    
333    private void addNode(List<Piece> list, XhtmlNode c, String style) {
334      if (c.getNodeType() == NodeType.Text)
335        list.add(styleIt(new Piece(null, c.getContent(), null), style));
336      else if (c.getNodeType() == NodeType.Element) {
337        if (c.getName().equals("a")) {
338          list.add(styleIt(new Piece(c.getAttribute("href"), c.allText(), c.getAttribute("title")), style));                    
339        } else if (c.getName().equals("b") || c.getName().equals("em") || c.getName().equals("strong")) {
340          list.add(styleIt(new Piece(null, c.allText(), null).setStyle("font-face: bold"), style));                    
341        } else if (c.getName().equals("code")) {
342          list.add(styleIt(new Piece(null, c.allText(), null).setStyle("padding: 2px 4px; color: #005c00; background-color: #f9f2f4; white-space: nowrap; border-radius: 4px"), style));                    
343        } else if (c.getName().equals("i")) {
344          list.add(styleIt(new Piece(null, c.allText(), null).setStyle("font-style: italic"), style));
345        } else if (c.getName().equals("pre")) {
346          Piece p = styleIt(new Piece(c.getName()).setStyle("white-space: pre; font-family: courier"), style);
347          list.add(p);
348          p.getChildren().addAll(c.getChildNodes());
349        } else if (c.getName().equals("ul") || c.getName().equals("ol")) {
350          Piece p = styleIt(new Piece(c.getName()), style);
351          list.add(p);
352          p.getChildren().addAll(c.getChildNodes());
353        } else if (c.getName().equals("i")) {
354          list.add(styleIt(new Piece(null, c.allText(), null).setStyle("font-style: italic"), style));                    
355        } else if (c.getName().equals("h1")||c.getName().equals("h2")||c.getName().equals("h3")||c.getName().equals("h4")) {
356          Piece p = styleIt(new Piece(c.getName()), style);
357          list.add(p);
358          p.getChildren().addAll(c.getChildNodes());
359        } else if (c.getName().equals("br")) {
360          list.add(styleIt(new Piece(c.getName()), style));
361        } else {
362          throw new Error("Not handled yet: "+c.getName());
363        }
364      } else
365        throw new Error("Unhandled type "+c.getNodeType().toString());
366    }
367    
368    
369    private Piece styleIt(Piece piece, String style) {
370      if (style != null) {
371        piece.addStyle(style);
372      }
373      return piece;
374    }
375    
376    public Cell addStyle(String style) {
377      for (Piece p : pieces)
378        p.addStyle(style);
379      return this;
380    }
381    public void addToHint(String text) {
382      for (Piece p : pieces)
383        p.addToHint(text);            
384    }
385    public Piece addStyledText(String hint, String alt, String fgColor, String bgColor, String link, boolean border) {
386      Piece p = new Piece(link, alt, hint);
387      p.addStyle("padding-left: 3px");
388      p.addStyle("padding-right: 3px");
389      if (border) {
390        p.addStyle("border: 1px grey solid");
391        p.addStyle("font-weight: bold");
392      }
393      if (fgColor != null) {
394        p.addStyle("color: "+fgColor);
395        p.addStyle("background-color: "+bgColor);
396      } else {
397        p.addStyle("color: black");
398        p.addStyle("background-color: "+bgColor != null ? bgColor : "white");       
399      }
400      pieces.add(p);
401      return p;
402    }
403    public String text() {
404      StringBuilder b = new StringBuilder();
405      for (Piece p : pieces)
406        b.append(p.text);
407      return b.toString();
408    }
409    @Override
410    public String toString() {
411      if (span != 1) {
412        return text()+" {"+span+"}";
413      } else {
414        return text();
415      }
416    }
417    public Cell setStyle(String value) {
418      cellStyle = value;
419      return this;
420    }
421    
422    public Cell span(int value) {
423      span = value;
424      return this;
425    }
426    public Cell center() {
427      alignment = TextAlignment.CENTER;
428      return this;
429    }
430    
431    
432  }
433
434  public class Title extends Cell {
435    private int width;
436
437    public Title(String prefix, String reference, String text, String hint, String suffix, int width) {
438      super(prefix, reference, text, hint, suffix);
439      this.width = width;
440    }
441
442    public Title(String prefix, String reference, String text, String hint, String suffix, int width, int span) {
443      super(prefix, reference, text, hint, suffix);
444      this.width = width;
445      this.span = span;
446    }
447
448    public Title setStyle(String value) {
449      super.setStyle(value);
450      return this;
451    }
452  }
453  
454  public class Row {
455    private List<Row> subRows = new ArrayList<HierarchicalTableGenerator.Row>();
456    private List<Cell> cells = new ArrayList<HierarchicalTableGenerator.Cell>();
457    private String icon;
458    private String anchor;
459    private String hint;
460    private String color;
461    private int lineColor;
462    private String id;
463    private String opacity;
464    
465    public List<Row> getSubRows() {
466      return subRows;
467    }
468    public List<Cell> getCells() {
469      return cells;
470    }
471    public String getIcon() {
472      return icon;
473    }
474    public void setIcon(String icon, String hint) {
475      this.icon = icon;
476      this.hint = hint;
477    }
478    public String getAnchor() {
479      return anchor;
480    }
481    public void setAnchor(String anchor) {
482      this.anchor = anchor;
483    }
484    public String getHint() {
485      return hint;
486    }
487    public String getColor() {
488      return color;
489    }
490    public void setColor(String color) {
491      this.color = color;
492    }
493    public int getLineColor() {
494      return lineColor;
495    }
496    public void setLineColor(int lineColor) {
497      assert lineColor >= 0;
498      assert lineColor <= 2;
499      this.lineColor = lineColor;
500    }
501    public String getId() {
502      return id;
503    }
504    public void setId(String id) {
505      this.id = id;
506    }
507    public String getOpacity() {
508      return opacity;
509    }
510    public void setOpacity(String opacity) {
511      this.opacity = opacity;
512    }
513    
514  }
515
516  public class TableModel {
517    private String id;
518    private boolean active;
519    private List<Title> titles = new ArrayList<HierarchicalTableGenerator.Title>();
520    private List<Row> rows = new ArrayList<HierarchicalTableGenerator.Row>();
521    private String docoRef;
522    private String docoImg;
523    private boolean alternating;
524        
525    public TableModel(String id, boolean active) {
526      super();
527      this.id = id;
528      this.active = active;
529    }
530    public List<Title> getTitles() {
531      return titles;
532    }
533    public List<Row> getRows() {
534      return rows;
535    }
536    public String getDocoRef() {
537      return docoRef;
538    }
539    public String getDocoImg() {
540      return docoImg;
541    }
542    public void setDocoRef(String docoRef) {
543      this.docoRef = docoRef;
544    }
545    public void setDocoImg(String docoImg) {
546      this.docoImg = docoImg;
547    }
548    public String getId() {
549      return id;
550    }
551    
552    public void setId(String id) {
553      this.id = id;
554    }
555    public boolean isActive() {
556      return active && ACTIVE_TABLES;
557    }
558    public boolean isAlternating() {
559      return alternating;
560    }
561    public void setAlternating(boolean alternating) {
562      this.alternating = alternating;
563    }
564    
565  }
566
567
568  private String dest;
569  private boolean makeTargets;
570  
571  /**
572   * There are circumstances where the table has to present in the absence of a stable supporting infrastructure.
573   * and the file paths cannot be guaranteed. For these reasons, you can tell the builder to inline all the graphics
574   * (all the styles are inlined anyway, since the table fbuiler has even less control over the styling
575   *  
576   */
577  private boolean inLineGraphics;  
578  
579  public HierarchicalTableGenerator() {
580    super();
581  }
582
583  public HierarchicalTableGenerator(String dest, boolean inlineGraphics) {
584    super();
585    this.dest = dest;
586    this.inLineGraphics = inlineGraphics;
587    this.makeTargets = true;
588  }
589
590  public HierarchicalTableGenerator(String dest, boolean inlineGraphics, boolean makeTargets) {
591    super();
592    this.dest = dest;
593    this.inLineGraphics = inlineGraphics;
594    this.makeTargets = makeTargets;
595  }
596
597  public TableModel initNormalTable(String prefix, boolean isLogical, boolean alternating, String id, boolean isActive) {
598    TableModel model = new TableModel(id, isActive);
599    
600    model.setAlternating(alternating);
601    model.setDocoImg(Utilities.pathURL(prefix, "help16.png"));
602    model.setDocoRef(Utilities.pathURL(prefix, "formats.html#table"));
603    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Name"), translate("sd.hint", "The logical name of the element"), null, 0));
604    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Flags"), translate("sd.hint", "Information about the use of the element"), null, 0));
605    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));
606    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Type"), translate("sd.hint", "Reference to the type of the element"), null, 100));
607    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Description & Constraints"), translate("sd.hint", "Additional information about the element"), null, 0));
608    if (isLogical) {
609      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));
610    }
611    return model;
612  }
613
614  public TableModel initComparisonTable(String prefix, String id) {
615    TableModel model = new TableModel(id, true);
616    
617    model.setAlternating(true);
618    model.setDocoImg(Utilities.pathURL(prefix, "help16.png"));
619    model.setDocoRef(Utilities.pathURL(prefix, "formats.html#table"));    
620    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Name"), translate("sd.hint", "The logical name of the element"), null, 0));
621    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "L Flags"), translate("sd.hint", "Information about the use of the element - Left Structure"), null, 0).setStyle("border-left: 1px grey solid"));
622    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "L Card."), translate("sd.hint", "Minimum and Maximum # of times the the element can appear in the instance - Left Structure"), null, 0));
623    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "L Type"), translate("sd.hint", "Reference to the type of the element - Left Structure"), null, 100));
624    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "L Description & Constraints"), translate("sd.hint", "Additional information about the element - Left Structure"), null, 0).setStyle("border-right: 1px grey solid"));
625    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "R Flags"), translate("sd.hint", "Information about the use of the element - Left Structure"), null, 0).setStyle("border-left: 1px grey solid"));
626    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "R Card."), translate("sd.hint", "Minimum and Maximum # of times the the element can appear in the instance - Left Structure"), null, 0));
627    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "L Type"), translate("sd.hint", "Reference to the type of the element - Left Structure"), null, 100));
628    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "L Description & Constraints"), translate("sd.hint", "Additional information about the element - Left Structure"), null, 0).setStyle("border-right: 1px grey solid"));
629    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Comments"), translate("sd.hint", "Comments about the comparison"), null, 0));
630    return model;
631  }
632
633
634
635  public TableModel initGridTable(String prefix, String id) {
636    TableModel model = new TableModel(id, false);
637    
638    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));
639    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));
640    model.getTitles().add(new Title(null, model.getDocoRef(), translate("sd.head", "Type"), translate("sd.hint", "Reference to the type of the element"), null, 100));
641    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));
642    return model;
643  }
644
645  public XhtmlNode generate(TableModel model, String imagePath, int border, Set<String> outputTracker) throws IOException, FHIRException  {
646    checkModel(model);
647    XhtmlNode table = new XhtmlNode(NodeType.Element, "table").setAttribute("border", Integer.toString(border)).setAttribute("cellspacing", "0").setAttribute("cellpadding", "0");
648    
649    if (model.isActive()) {      
650      table.setAttribute("id", model.getId());
651    }
652    table.setAttribute("style", "border: " + border + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top;");
653    XhtmlNode tr = table.addTag("tr");
654    tr.setAttribute("style", "border: " + Integer.toString(1 + border) + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top");
655    XhtmlNode tc = null;
656    for (Title t : model.getTitles()) {
657      tc = renderCell(tr, t, "th", null, null, null, false, null, "white", 0, imagePath, border, outputTracker, model, null);
658      if (t.width != 0)
659        tc.setAttribute("style", "width: "+Integer.toString(t.width)+"px");
660    }
661    if (tc != null && model.getDocoRef() != null) {
662      XhtmlNode img = tc.addTag("span").setAttribute("style", "float: right").addTag("a").setAttribute("title", "Legend for this format").setAttribute("href", model.getDocoRef()).addTag("img");
663      img.setAttribute("alt", "doco").setAttribute("style", "background-color: inherit").setAttribute("src", model.getDocoImg());
664      if (model.isActive()) {
665        img.setAttribute("onload", "fhirTableInit(this)");
666      }
667    }
668      
669    Counter counter = new Counter();
670    for (Row r : model.getRows()) {
671      renderRow(table, r, 0, new ArrayList<Integer>(), imagePath, border, outputTracker, counter, model);
672    }
673    if (model.getDocoRef() != null) {
674      tr = table.addTag("tr");
675      tc = tr.addTag("td");
676      tc.setAttribute("class", "hierarchy");
677      tc.setAttribute("colspan", Integer.toString(model.getTitles().size()));
678      tc.addTag("br");
679      XhtmlNode a = tc.addTag("a").setAttribute("title", translate("sd.doco", "Legend for this format")).setAttribute("href", model.getDocoRef());
680      if (model.getDocoImg() != null)
681        a.addTag("img").setAttribute("alt", "doco").setAttribute("style", "background-color: inherit").setAttribute("src", model.getDocoImg());
682      a.addText(" "+translate("sd.doco", "Documentation for this format"));
683    }
684    return table;
685  }
686
687
688  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  {
689    counter.row();
690    XhtmlNode tr = table.addTag("tr");
691    String color = "white";
692    if (r.getColor() != null)
693      color = r.getColor();
694    else if (model.isAlternating()  && counter.isOdd())
695      color = BACKGROUND_ALT_COLOR;
696    
697    tr.setAttribute("style", "border: " + border + "px #F0F0F0 solid; padding:0px; vertical-align: top; background-color: "+color+(r.getOpacity() == null ? "" : "; opacity: "+r.getOpacity()));
698    if (model.isActive()) {
699      tr.setAttribute("id", r.getId());
700    }
701    boolean first = true;
702    for (Cell t : r.getCells()) {
703      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);
704      first = false;
705    }
706    table.addText("\r\n");
707    
708    for (int i = 0; i < r.getSubRows().size(); i++) {
709      Row c = r.getSubRows().get(i);
710      List<Integer> ind = new ArrayList<Integer>();
711      ind.addAll(indents);
712      if (i == r.getSubRows().size() - 1) {
713        ind.add(r.getLineColor()*2);
714      } else {
715        ind.add(r.getLineColor()*2+1);
716      }
717      renderRow(table, c, indent+1, ind, imagePath, border, outputTracker, counter, model);
718    }
719  }
720
721
722  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  {
723    XhtmlNode tc = tr.addTag(name);
724    tc.setAttribute("class", "hierarchy");
725    if (c.span > 1) {
726      tc.colspan(Integer.toString(c.span));
727    }
728    if (indents != null) {
729      tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_spacer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
730      tc.setAttribute("style", "vertical-align: top; text-align : left; "+(c.cellStyle != null  && c.cellStyle.contains("background-color") ? "" : "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)+")"+(c.cellStyle != null ? ";"+c.cellStyle : ""));
731      for (int i = 0; i < indents.size()-1; i++) {
732        switch (indents.get(i)) {
733          case NEW_REGULAR:
734          case NEW_SLICER:
735          case NEW_SLICE:
736            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_blank.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
737            break;
738          case CONTINUE_REGULAR:
739            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
740            break;
741          case CONTINUE_SLICER:
742            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline_slicer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
743            break;
744          case CONTINUE_SLICE:
745            tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline_slice.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
746            break;
747          default:
748            throw new Error("Unrecognized indent level: " + indents.get(i));
749        }
750      }
751      if (!indents.isEmpty()) {
752        String sfx = table.isActive() && hasChildren ? "-open" : "";
753        XhtmlNode img = tc.addTag("img");
754        switch (indents.get(indents.size()-1)) {
755        case NEW_REGULAR:
756          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin_end"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
757          break;
758        case NEW_SLICER:
759          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin_end_slicer"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
760          break;
761        case NEW_SLICE:
762          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin_end_slice"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
763          break;
764        case CONTINUE_REGULAR:
765          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
766          break;
767        case CONTINUE_SLICER:
768          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin_slicer"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
769          break;
770        case CONTINUE_SLICE:
771          img.setAttribute("src", srcFor(imagePath, "tbl_vjoin_slice"+sfx+".png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
772          break;
773        default:
774          throw new Error("Unrecognized indent level: " + indents.get(indents.size()-1));
775        }
776        if (table.isActive() && hasChildren) {
777          img.setAttribute("onClick", "tableRowAction(this)");
778        }
779      }
780    }
781    else
782      tc.setAttribute("style", "vertical-align: top; text-align : left; "+(c.cellStyle != null  && c.cellStyle.contains("background-color") ? "" : "background-color: "+color+"; ")+"border: "+ border +"px #F0F0F0 solid; padding:0px 4px 0px 4px"+(c.cellStyle != null ? ";"+c.cellStyle : ""));
783    if (!Utilities.noString(icon)) {
784      XhtmlNode img = tc.addTag("img").setAttribute("src", srcFor(imagePath, icon)).setAttribute("class", "hierarchy").setAttribute("style", "background-color: "+color+"; background-color: inherit").setAttribute("alt", ".");
785      if (hint != null)
786        img.setAttribute("title", hint);
787      tc.addText(" ");
788    }
789    for (Piece p : c.pieces) {
790      if (!Utilities.noString(p.getTag())) {
791        XhtmlNode tag = tc.addTag(p.getTag());
792        if (p.attributes != null)
793          for (String n : p.attributes.keySet())
794            tag.setAttribute(n, p.attributes.get(n));
795        if (p.getHint() != null)
796          tag.setAttribute("title", p.getHint());
797        addStyle(tag, p);
798        if (p.hasChildren())
799          tag.getChildNodes().addAll(p.getChildren());
800      } else if (!Utilities.noString(p.getReference())) {
801        XhtmlNode a = addStyle(tc.addTag("a"), p);
802        a.setAttribute("href", p.getReference());
803        if (!Utilities.noString(p.getHint()))
804          a.setAttribute("title", p.getHint());
805        if (p.getText() != null) {
806          a.addText(p.getText());
807        } else {
808          a.addChildren(p.getChildren());
809        }
810        addStyle(a, p);
811      } else { 
812        if (!Utilities.noString(p.getHint())) {
813          XhtmlNode s = addStyle(tc.addTag("span"), p);
814          s.setAttribute("title", p.getHint());
815          s.addText(p.getText());
816        } else if (p.getStyle() != null) {
817          XhtmlNode s = addStyle(tc.addTag("span"), p);
818          s.addText(p.getText());
819        } else
820          tc.addText(p.getText());
821      }
822    }
823    if (makeTargets && !Utilities.noString(anchor))
824      tc.addTag("a").setAttribute("name", nmTokenize(anchor)).addText(" ");
825    return tc;
826  }
827
828
829  private XhtmlNode addStyle(XhtmlNode node, Piece p) {
830    if (p.getStyle() != null)
831      node.setAttribute("style", p.getStyle());
832    return node;
833  }
834
835  private String nmTokenize(String anchor) {
836    return anchor.replace("[", "_").replace("]", "_");
837  }
838  
839  private String srcFor(String corePrefix, String filename) throws IOException {
840    if (inLineGraphics) {
841      if (files.containsKey(filename))
842        return files.get(filename);
843      StringBuilder b = new StringBuilder();
844      b.append("data:image/png;base64,");
845      byte[] bytes;
846      File file = new File(Utilities.path(dest, filename));
847      if (!file.exists()) // because sometime this is called real early before the files exist. it will be built again later because of this
848        bytes = new byte[0]; 
849      else
850        bytes = FileUtils.readFileToByteArray(file);
851      b.append(new String(Base64.encodeBase64(bytes)));
852//      files.put(filename, b.toString());
853      return b.toString();
854    } else
855      return corePrefix+filename;
856  }
857
858
859  private void checkModel(TableModel model) throws FHIRException  {
860    check(!model.getTitles().isEmpty(), "Must have titles");
861    int tc = 0;
862    for (Cell c : model.getTitles()) {
863      check(c);
864      tc = tc + c.span;
865    }
866    int i = 0;
867    for (Row r : model.getRows()) { 
868      check(r, "rows", tc, "", i, model.getRows().size());
869      i++;
870    }
871  }
872
873
874  private void check(Cell c) throws FHIRException  {  
875    boolean hasText = false;
876    for (Piece p : c.pieces)
877      if (!Utilities.noString(p.getText()))
878        hasText = true;
879    check(hasText, "Title cells must have text");    
880  }
881
882
883  private void check(Row r, String string, int size, String path, int index, int total) throws FHIRException  {
884    String id = Integer.toString(index)+".";
885    if (total <= 26) {
886      char c = (char) ('a'+index);
887      id = Character.toString(c);
888    }
889    path = path + id;
890    r.setId(path);
891    int tc = 0;
892    for (Cell c : r.getCells()) {
893      tc = tc + c.span;
894    }
895    check(tc == size, "All rows must have the same number of columns as the titles  ("+Integer.toString(size)+") but row "+path+" doesn't - it has "+tc+" ("+r.getCells().get(0).text()+"): "+r.getCells());
896    int i = 0;
897    for (Row c : r.getSubRows()) {
898      check(c, "rows", size, path, i, r.getSubRows().size());
899      i++;
900    }
901  }
902
903
904  private String checkExists(List<Integer> indents, boolean hasChildren, int lineColor, Set<String> outputTracker) throws IOException  {
905    String filename = makeName(indents);
906    
907    StringBuilder b = new StringBuilder();
908    if (inLineGraphics) {
909      if (files.containsKey(filename))
910        return files.get(filename);
911      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
912      genImage(indents, hasChildren, lineColor, bytes);
913      b.append("data:image/png;base64,");
914      byte[] encodeBase64 = Base64.encodeBase64(bytes.toByteArray());
915      b.append(new String(encodeBase64));
916      files.put(filename, b.toString());
917      return b.toString();
918    } else {
919      b.append("tbl_bck");
920      for (Integer i : indents)
921        b.append(Integer.toString(i));
922      int indent = lineColor*2 + (hasChildren?1:0);
923      b.append(Integer.toString(indent));
924      b.append(".png");
925      String file = Utilities.path(dest, b.toString());
926      if (!new File(file).exists()) {
927        File newFile = new File(file);
928        newFile.getParentFile().mkdirs();
929        newFile.createNewFile();
930        FileOutputStream stream = new FileOutputStream(file);
931        genImage(indents, hasChildren, lineColor, stream);
932        if (outputTracker!=null)
933          outputTracker.add(file);
934      }
935      return b.toString();
936    }
937  }
938
939
940  private void genImage(List<Integer> indents, boolean hasChildren, int lineColor, OutputStream stream) throws IOException {
941    BufferedImage bi = new BufferedImage(800, 2, BufferedImage.TYPE_INT_ARGB);
942    // i have no idea why this works to make these pixels transparent. It defies logic. 
943    // But this combination of INT_ARGB and filling with grey magically worked when nothing else did. So it stays as is.
944    Color grey = new Color(99,99,99,0); 
945    for (int i = 0; i < 800; i++) {
946      bi.setRGB(i, 0, grey.getRGB());
947      bi.setRGB(i, 1, grey.getRGB());
948    }
949    Color black = new Color(0, 0, 0);
950    Color green = new Color(14,209,69);
951    Color gold = new Color(212,168,21);
952    for (int i = 0; i < indents.size(); i++) {
953      int indent = indents.get(i).intValue();
954      if (indent == CONTINUE_REGULAR)
955        bi.setRGB(12+(i*16), 0, black.getRGB());
956      else if (indent == CONTINUE_SLICER)
957        bi.setRGB(12+(i*16), 0, green.getRGB());
958      else if (indent == CONTINUE_SLICE)
959        bi.setRGB(12+(i*16), 0, gold.getRGB());
960    }
961    if (hasChildren) {
962      if (lineColor==0)
963        bi.setRGB(12+(indents.size()*16), 0, black.getRGB());
964      else if (lineColor==1)
965        bi.setRGB(12+(indents.size()*16), 0, green.getRGB());
966      else if (lineColor==2)
967        bi.setRGB(12+(indents.size()*16), 0, gold.getRGB());
968    }
969    ImageIO.write(bi, "PNG", stream);
970  }
971
972  private String makeName(List<Integer> indents) {
973    StringBuilder b = new StringBuilder();
974    b.append("indents:");
975    for (Integer i : indents)
976      b.append(Integer.toString(i));
977    return b.toString();
978  }
979
980  private void check(boolean check, String message) throws FHIRException  {
981    if (!check)
982      throw new FHIRException(message);
983  }
984
985  public void emptyRow(TableModel model, int cellCount) {
986    Row r = new Row();
987    model.rows.add(r);
988    for (int i = 0; i < cellCount; i++) {
989      r.getCells().add(new Cell());
990    }
991  }
992}