001package org.hl7.fhir.utilities.xhtml;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2017 University Health Network
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;
064
065import javax.imageio.ImageIO;
066
067import org.apache.commons.codec.binary.Base64;
068import org.apache.commons.io.FileUtils;
069import org.hl7.fhir.exceptions.FHIRException;
070import org.hl7.fhir.utilities.Utilities;
071
072
073public class HierarchicalTableGenerator  {
074  public static final String TEXT_ICON_REFERENCE = "Reference to another Resource";
075  public static final String TEXT_ICON_PRIMITIVE = "Primitive Data Type";
076  public static final String TEXT_ICON_DATATYPE = "Data Type";
077  public static final String TEXT_ICON_RESOURCE = "Resource";
078  public static final String TEXT_ICON_ELEMENT = "Element";
079  public static final String TEXT_ICON_REUSE = "Reference to another Element";
080  public static final String TEXT_ICON_EXTENSION = "Extension";
081  public static final String TEXT_ICON_CHOICE = "Choice of Types";
082  public static final String TEXT_ICON_SLICE = "Slice Definition";
083  public static final String TEXT_ICON_EXTENSION_SIMPLE = "Simple Extension";
084  public static final String TEXT_ICON_PROFILE = "Profile";
085  public static final String TEXT_ICON_EXTENSION_COMPLEX = "Complex Extension";
086
087  private static Map<String, String> files = new HashMap<String, String>();
088
089  public class Piece {
090    private String tag;
091    private String reference;
092    private String text;
093    private String hint;
094    private String style;
095    private Map<String, String> attributes;
096    
097    public Piece(String tag) {
098      super();
099      this.tag = tag;
100    }
101    
102    public Piece(String reference, String text, String hint) {
103      super();
104      this.reference = reference;
105      this.text = text;
106      this.hint = hint;
107    }
108    public String getReference() {
109      return reference;
110    }
111    public void setReference(String value) {
112      reference = value;
113    }
114    public String getText() {
115      return text;
116    }
117    public String getHint() {
118      return hint;
119    }
120
121    public String getTag() {
122      return tag;
123    }
124
125    public String getStyle() {
126      return style;
127    }
128
129    public void setTag(String tag) {
130      this.tag = tag;
131    }
132
133    public void setText(String text) {
134      this.text = text;
135    }
136
137    public void setHint(String hint) {
138      this.hint = hint;
139    }
140
141    public Piece setStyle(String style) {
142      this.style = style;
143      return this;
144    }
145
146    public Piece addStyle(String style) {
147      if (this.style != null)
148        this.style = this.style+"; "+style;
149      else
150        this.style = style;
151      return this;
152    }
153
154    public void addToHint(String text) {
155      if (this.hint == null)
156        this.hint = text;
157      else
158        this.hint += (this.hint.endsWith(".") || this.hint.endsWith("?") ? " " : ". ")+text;
159    }
160  }
161  
162  public class Cell {
163    private List<Piece> pieces = new ArrayList<HierarchicalTableGenerator.Piece>();
164
165    public Cell() {
166      
167    }
168    public Cell(String prefix, String reference, String text, String hint, String suffix) {
169      super();
170      if (!Utilities.noString(prefix))
171        pieces.add(new Piece(null, prefix, null));
172      pieces.add(new Piece(reference, text, hint));
173      if (!Utilities.noString(suffix))
174        pieces.add(new Piece(null, suffix, null));
175    }
176    public List<Piece> getPieces() {
177      return pieces;
178    }
179    public Cell addPiece(Piece piece) {
180      pieces.add(piece);
181      return this;
182    }
183//    private List<Piece> htmlToParagraphPieces(String html) {
184//      List<Piece> myPieces = new ArrayList<Piece>();
185//      String[] paragraphs = html.replace("<p>", "").split("<\\/p>|<br  \\/>");
186//      for (int i=0;i<paragraphs.length;i++) {
187//        if (!paragraphs[i].isEmpty()) {
188//          if (i!=0) {
189//            myPieces.add(new Piece("br"));
190//            myPieces.add(new Piece("br"));
191//          }
192//          myPieces.addAll(htmlFormattingToPieces(paragraphs[i]));
193//        }
194//      }
195//      
196//      return myPieces;
197//    }
198//    private List<Piece> htmlFormattingToPieces(String html) {
199//      List<Piece> myPieces = new ArrayList<Piece>();
200//      //Todo: At least handle bold and italics and turn them into formatted spans.  (Will need to handle nesting though)
201//      myPieces.add(new Piece(null, html, null));
202//      
203//      return myPieces;
204//    }
205    public void addStyle(String style) {
206      for (Piece p : pieces)
207        p.addStyle(style);      
208    }
209    public void addToHint(String text) {
210      for (Piece p : pieces)
211        p.addToHint(text);            
212    }
213    public Piece addImage(String src, String hint, String alt, String fgColor, String bgColor) {
214//      Piece img = new Piece("img");
215      Piece img = new Piece(null, alt, hint);
216      img.addStyle("padding: 3px");
217      if (fgColor != null) {
218        img.addStyle("color: "+fgColor);
219        img.addStyle("background-color: "+bgColor);
220      }
221        
222//      img.attributes = new HashMap<String, String>();
223//      img.attributes.put("src", src);
224//      img.attributes.put("alt", alt);
225//      img.hint = hint;
226      pieces.add(img);
227      return img;
228    }
229    public String text() {
230      StringBuilder b = new StringBuilder();
231      for (Piece p : pieces)
232        b.append(p.text);
233      return b.toString();
234    }
235    @Override
236    public String toString() {
237      return text();
238    }
239    
240    
241  }
242
243  public class Title extends Cell {
244    private int width;
245
246    public Title(String prefix, String reference, String text, String hint, String suffix, int width) {
247      super(prefix, reference, text, hint, suffix);
248      this.width = width;
249    }
250  }
251  
252  public class Row {
253    private List<Row> subRows = new ArrayList<HierarchicalTableGenerator.Row>();
254    private List<Cell> cells = new ArrayList<HierarchicalTableGenerator.Cell>();
255    private String icon;
256    private String anchor;
257    private String hint;
258    private String color;
259    
260    public List<Row> getSubRows() {
261      return subRows;
262    }
263    public List<Cell> getCells() {
264      return cells;
265    }
266    public String getIcon() {
267      return icon;
268    }
269    public void setIcon(String icon, String hint) {
270      this.icon = icon;
271      this.hint = hint;
272    }
273    public String getAnchor() {
274      return anchor;
275    }
276    public void setAnchor(String anchor) {
277      this.anchor = anchor;
278    }
279    public String getHint() {
280      return hint;
281    }
282    public String getColor() {
283      return color;
284    }
285    public void setColor(String color) {
286      this.color = color;
287    }
288    
289    
290  }
291
292  public class TableModel {
293    private List<Title> titles = new ArrayList<HierarchicalTableGenerator.Title>();
294    private List<Row> rows = new ArrayList<HierarchicalTableGenerator.Row>();
295    private String docoRef;
296    private String docoImg;
297    public List<Title> getTitles() {
298      return titles;
299    }
300    public List<Row> getRows() {
301      return rows;
302    }
303    public String getDocoRef() {
304      return docoRef;
305    }
306    public String getDocoImg() {
307      return docoImg;
308    }
309    public void setDocoRef(String docoRef) {
310      this.docoRef = docoRef;
311    }
312    public void setDocoImg(String docoImg) {
313      this.docoImg = docoImg;
314    }
315    
316  }
317
318
319  private String dest;
320  
321  /**
322   * There are circumstances where the table has to present in the absence of a stable supporting infrastructure.
323   * and the file paths cannot be guaranteed. For these reasons, you can tell the builder to inline all the graphics
324   * (all the styles are inlined anyway, since the table fbuiler has even less control over the styling
325   *  
326   */
327  private boolean inLineGraphics;
328  
329  
330  public HierarchicalTableGenerator() {
331    super();
332  }
333
334  public HierarchicalTableGenerator(String dest, boolean inlineGraphics) {
335    super();
336    this.dest = dest;
337    this.inLineGraphics = inlineGraphics;
338  }
339
340  public TableModel initNormalTable(String prefix, boolean isLogical) {
341    TableModel model = new TableModel();
342    
343    model.setDocoImg(prefix+"help16.png");
344    model.setDocoRef(prefix+"formats.html#table");
345    model.getTitles().add(new Title(null, model.getDocoRef(), "Name", "The logical name of the element", null, 0));
346    model.getTitles().add(new Title(null, model.getDocoRef(), "Flags", "Information about the use of the element", null, 0));
347    model.getTitles().add(new Title(null, model.getDocoRef(), "Card.", "Minimum and Maximum # of times the the element can appear in the instance", null, 0));
348    model.getTitles().add(new Title(null, model.getDocoRef(), "Type", "Reference to the type of the element", null, 100));
349    model.getTitles().add(new Title(null, model.getDocoRef(), "Description & Constraints", "Additional information about the element", null, 0));
350    if (isLogical) {
351      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));
352    }
353    return model;
354  }
355
356  public TableModel initGridTable(String prefix) {
357    TableModel model = new TableModel();
358    
359    model.getTitles().add(new Title(null, model.getDocoRef(), "Name", "The name of the element (Slice name in brackets).  Mouse-over provides definition", null, 0));
360    model.getTitles().add(new Title(null, model.getDocoRef(), "Card.", "Minimum and Maximum # of times the the element can appear in the instance. Super-scripts indicate additional constraints on appearance", null, 0));
361    model.getTitles().add(new Title(null, model.getDocoRef(), "Type", "Reference to the type of the element", null, 100));
362    model.getTitles().add(new Title(null, model.getDocoRef(), "Constraints and Usage", "Fixed values, length limits, vocabulary bindings and other usage notes", null, 0));
363    return model;
364  }
365
366  public XhtmlNode generate(TableModel model, String imagePath, int border) throws IOException, FHIRException  {
367    checkModel(model);
368    XhtmlNode table = new XhtmlNode(NodeType.Element, "table").setAttribute("border", Integer.toString(border)).setAttribute("cellspacing", "0").setAttribute("cellpadding", "0");
369    table.setAttribute("style", "border: " + border + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top;");
370    XhtmlNode tr = table.addTag("tr");
371    tr.setAttribute("style", "border: " + Integer.toString(1 + border) + "px #F0F0F0 solid; font-size: 11px; font-family: verdana; vertical-align: top;");
372    XhtmlNode tc = null;
373    for (Title t : model.getTitles()) {
374      tc = renderCell(tr, t, "th", null, null, null, false, null, "white", imagePath, border);
375      if (t.width != 0)
376        tc.setAttribute("style", "width: "+Integer.toString(t.width)+"px");
377    }
378    if (tc != null && model.getDocoRef() != null)
379      tc.addTag("span").setAttribute("style", "float: right").addTag("a").setAttribute("title", "Legend for this format").setAttribute("href", model.getDocoRef()).addTag("img").setAttribute("alt", "doco").setAttribute("style", "background-color: inherit").setAttribute("src", model.getDocoImg());
380      
381    for (Row r : model.getRows()) {
382      renderRow(table, r, 0, new ArrayList<Boolean>(), imagePath, border);
383    }
384    if (model.getDocoRef() != null) {
385      tr = table.addTag("tr");
386      tc = tr.addTag("td");
387      tc.setAttribute("class", "hierarchy");
388      tc.setAttribute("colspan", Integer.toString(model.getTitles().size()));
389      tc.addTag("br");
390      XhtmlNode a = tc.addTag("a").setAttribute("title", "Legend for this format").setAttribute("href", model.getDocoRef());
391      if (model.getDocoImg() != null)
392        a.addTag("img").setAttribute("alt", "doco").setAttribute("style", "background-color: inherit").setAttribute("src", model.getDocoImg());
393      a.addText(" Documentation for this format");
394    }
395    return table;
396  }
397
398
399  private void renderRow(XhtmlNode table, Row r, int indent, List<Boolean> indents, String imagePath, int border) throws IOException  {
400    XhtmlNode tr = table.addTag("tr");
401    String color = "white";
402    if (r.getColor() != null)
403      color = r.getColor();
404    tr.setAttribute("style", "border: " + border + "px #F0F0F0 solid; padding:0px; vertical-align: top; background-color: "+color+";");
405    boolean first = true;
406    for (Cell t : r.getCells()) {
407      renderCell(tr, t, "td", first ? r.getIcon() : null, first ? r.getHint() : null, first ? indents : null, !r.getSubRows().isEmpty(), first ? r.getAnchor() : null, color, imagePath, border);
408      first = false;
409    }
410    table.addText("\r\n");
411    
412    for (int i = 0; i < r.getSubRows().size(); i++) {
413      Row c = r.getSubRows().get(i);
414      List<Boolean> ind = new ArrayList<Boolean>();
415      ind.addAll(indents);
416      if (i == r.getSubRows().size() - 1)
417        ind.add(true);
418      else
419        ind.add(false);
420      renderRow(table, c, indent+1, ind, imagePath, border);
421    }
422  }
423
424
425  private XhtmlNode renderCell(XhtmlNode tr, Cell c, String name, String icon, String hint, List<Boolean> indents, boolean hasChildren, String anchor, String color, String imagePath, int border) throws IOException  {
426    XhtmlNode tc = tr.addTag(name);
427    tc.setAttribute("class", "hierarchy");
428    if (indents != null) {
429      tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_spacer.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
430      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)+")");
431      for (int i = 0; i < indents.size()-1; i++) { 
432        if (indents.get(i))
433          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_blank.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
434        else
435          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vline.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
436      }
437      if (!indents.isEmpty())
438        if (indents.get(indents.size()-1))
439          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin_end.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
440        else
441          tc.addTag("img").setAttribute("src", srcFor(imagePath, "tbl_vjoin.png")).setAttribute("style", "background-color: inherit").setAttribute("class", "hierarchy").setAttribute("alt", ".");
442    }
443    else
444      tc.setAttribute("style", "vertical-align: top; text-align : left; background-color: "+color+"; border: "+ border +"px #F0F0F0 solid; padding:0px 4px 0px 4px");
445    if (!Utilities.noString(icon)) {
446      XhtmlNode img = tc.addTag("img").setAttribute("src", srcFor(imagePath, icon)).setAttribute("class", "hierarchy").setAttribute("style", "background-color: "+color+"; background-color: inherit").setAttribute("alt", ".");
447      if (hint != null)
448        img.setAttribute("title", hint);
449      tc.addText(" ");
450    }
451    for (Piece p : c.pieces) {
452      if (!Utilities.noString(p.getTag())) {
453        XhtmlNode tag = tc.addTag(p.getTag());
454        if (p.attributes != null)
455          for (String n : p.attributes.keySet())
456            tag.setAttribute(n, p.attributes.get(n));
457        if (p.getHint() != null)
458          tag.setAttribute("title", p.getHint());
459        addStyle(tag, p);
460      } else if (!Utilities.noString(p.getReference())) {
461        XhtmlNode a = addStyle(tc.addTag("a"), p);
462        a.setAttribute("href", p.getReference());
463        if (!Utilities.noString(p.getHint()))
464          a.setAttribute("title", p.getHint());
465        a.addText(p.getText());
466        addStyle(a, p);
467      } else { 
468        if (!Utilities.noString(p.getHint())) {
469          XhtmlNode s = addStyle(tc.addTag("span"), p);
470          s.setAttribute("title", p.getHint());
471          s.addText(p.getText());
472        } else if (p.getStyle() != null) {
473          XhtmlNode s = addStyle(tc.addTag("span"), p);
474          s.addText(p.getText());
475        } else
476          tc.addText(p.getText());
477      }
478    }
479    if (!Utilities.noString(anchor))
480      tc.addTag("a").setAttribute("name", nmTokenize(anchor)).addText(" ");
481    return tc;
482  }
483
484
485  private XhtmlNode addStyle(XhtmlNode node, Piece p) {
486    if (p.getStyle() != null)
487      node.setAttribute("style", p.getStyle());
488    return node;
489  }
490
491  private String nmTokenize(String anchor) {
492    return anchor.replace("[", "_").replace("]", "_");
493  }
494  
495  private String srcFor(String corePrefix, String filename) throws IOException {
496    if (inLineGraphics) {
497      if (files.containsKey(filename))
498        return files.get(filename);
499      StringBuilder b = new StringBuilder();
500      b.append("data: image/png;base64,");
501      byte[] bytes;
502      File file = new File(Utilities.path(dest, filename));
503      if (!file.exists()) // because sometime this is called real early before the files exist. it will be built again later because of this
504        bytes = new byte[0]; 
505      else
506        bytes = FileUtils.readFileToByteArray(file);
507      b.append(new String(Base64.encodeBase64(bytes)));
508//      files.put(filename, b.toString());
509      return b.toString();
510    } 
511    return corePrefix+filename;
512  }
513
514
515  private void checkModel(TableModel model) throws FHIRException  {
516    check(!model.getRows().isEmpty(), "Must have rows");
517    check(!model.getTitles().isEmpty(), "Must have titles");
518    for (Cell c : model.getTitles())
519      check(c);
520    int i = 0;
521    for (Row r : model.getRows()) { 
522      check(r, "rows", model.getTitles().size(), Integer.toString(i));
523      i++;
524    }
525  }
526
527
528  private void check(Cell c) throws FHIRException  {  
529    boolean hasText = false;
530    for (Piece p : c.pieces)
531      if (!Utilities.noString(p.getText()))
532        hasText = true;
533    check(hasText, "Title cells must have text");    
534  }
535
536
537  private void check(Row r, String string, int size, String path) throws FHIRException  {    
538    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());
539    int i = 0;
540    for (Row c : r.getSubRows()) {
541      check(c, "rows", size, path+"."+Integer.toString(i));
542      i++;
543    }
544  }
545
546
547  private String checkExists(List<Boolean> indents, boolean hasChildren) throws IOException  {
548    String filename = makeName(indents);
549    
550    StringBuilder b = new StringBuilder();
551    if (inLineGraphics) {
552      if (files.containsKey(filename))
553        return files.get(filename);
554      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
555      genImage(indents, hasChildren, bytes);
556      b.append("data: image/png;base64,");
557      byte[] encodeBase64 = Base64.encodeBase64(bytes.toByteArray());
558      b.append(new String(encodeBase64));
559      files.put(filename, b.toString());
560      return b.toString();
561    }
562    b.append("tbl_bck");
563    for (Boolean i : indents)
564        b.append(i ? "0" : "1");
565    if (hasChildren)
566        b.append("1");
567    else
568        b.append("0");
569    b.append(".png");
570    String file = Utilities.path(dest, b.toString());
571    if (!new File(file).exists()) {
572        //FIXME resource leak
573        FileOutputStream stream = new FileOutputStream(file);
574        genImage(indents, hasChildren, stream);
575    }
576    return b.toString();
577  }
578
579
580  private void genImage(List<Boolean> indents, boolean hasChildren, OutputStream stream) throws IOException {
581    BufferedImage bi = new BufferedImage(800, 2, BufferedImage.TYPE_INT_ARGB);
582    // i have no idea why this works to make these pixels transparent. It defies logic. 
583    // But this combination of INT_ARGB and filling with grey magically worked when nothing else did. So it stays as is.
584    Color grey = new Color(99,99,99,0); 
585    for (int i = 0; i < 800; i++) {
586      bi.setRGB(i, 0, grey.getRGB());
587      bi.setRGB(i, 1, grey.getRGB());
588    }
589    Color black = new Color(0, 0, 0);
590    for (int i = 0; i < indents.size(); i++) {
591      if (!indents.get(i))
592        bi.setRGB(12+(i*16), 0, black.getRGB());
593    }
594    if (hasChildren)
595      bi.setRGB(12+(indents.size()*16), 0, black.getRGB());
596    ImageIO.write(bi, "PNG", stream);
597  }
598
599  private String makeName(List<Boolean> indents) {
600    StringBuilder b = new StringBuilder();
601    b.append("indents:");
602    for (Boolean i : indents)
603      b.append(i ? "1" : "0");
604    return b.toString();
605  }
606
607  private void check(boolean check, String message) throws FHIRException  {
608    if (!check)
609      throw new FHIRException(message);
610  }
611}