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