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
034import java.io.FileOutputStream;
035import java.io.IOException;
036import java.io.OutputStream;
037import java.io.OutputStreamWriter;
038import java.io.StringWriter;
039import java.io.Writer;
040
041import org.hl7.fhir.utilities.Utilities;
042import org.hl7.fhir.utilities.xml.IXMLWriter;
043import org.w3c.dom.Element;
044
045public class XhtmlComposer {
046
047  public static final String XHTML_NS = "http://www.w3.org/1999/xhtml";
048  private boolean pretty;
049  private boolean xml; 
050  
051  public static final boolean XML = true; 
052  public static final boolean HTML = false; 
053  
054  public XhtmlComposer(boolean xml, boolean pretty) {
055    super();
056    this.pretty = pretty;
057    this.xml = xml;
058  }
059
060  public XhtmlComposer(boolean xml) {
061    super();
062    this.pretty = false;
063    this.xml = xml;
064  }
065
066  private Writer dst;
067
068  public String compose(XhtmlDocument doc) throws IOException  {
069    StringWriter sdst = new StringWriter();
070    dst = sdst;
071    composeDoc(doc);
072    return sdst.toString();
073  }
074
075  public String compose(XhtmlNode node) throws IOException  {
076    StringWriter sdst = new StringWriter();
077    dst = sdst;
078    writeNode("", node, false);
079    return sdst.toString();
080  }
081
082  public void compose(OutputStream stream, XhtmlDocument doc) throws IOException  {
083    byte[] bom = new byte[] { (byte)0xEF, (byte)0xBB, (byte)0xBF };
084    stream.write(bom);
085    dst = new OutputStreamWriter(stream, "UTF-8");
086    composeDoc(doc);
087    dst.flush();
088  }
089
090  private void composeDoc(XhtmlDocument doc) throws IOException  {
091    // headers....
092//    dst.append("<html>" + (pretty ? "\r\n" : ""));
093    for (XhtmlNode c : doc.getChildNodes()) {
094      writeNode("  ", c, false);
095    }
096//    dst.append("</html>" + (pretty ? "\r\n" : ""));
097  }
098
099  private void writeNode(String indent, XhtmlNode node, boolean noPrettyOverride) throws IOException  {
100    if (node.getNodeType() == NodeType.Comment) {
101      writeComment(indent, node, noPrettyOverride);
102    } else if (node.getNodeType() == NodeType.DocType) {
103      writeDocType(node);
104    } else if (node.getNodeType() == NodeType.Instruction) {
105      writeInstruction(node);
106    } else if (node.getNodeType() == NodeType.Element) {
107      writeElement(indent, node, noPrettyOverride);
108    } else if (node.getNodeType() == NodeType.Document) {
109      writeDocument(indent, node);
110    } else if (node.getNodeType() == NodeType.Text) {
111      writeText(node);
112    } else if (node.getNodeType() == null) {
113      throw new IOException("Null node type");
114    } else {
115      throw new IOException("Unknown node type: "+node.getNodeType().toString());
116    }
117  }
118
119  private void writeText(XhtmlNode node) throws IOException  {
120    for (char c : node.getContent().toCharArray())
121    {
122      if (c == '&') {
123        dst.append("&amp;");
124      } else if (c == '<') {
125        dst.append("&lt;");
126      } else if (c == '>') {
127        dst.append("&gt;");
128      } else if (xml) {
129        if (c == '"')
130          dst.append("&quot;");
131        else 
132          dst.append(c);
133      } else {
134        if (c == XhtmlNode.NBSP.charAt(0))
135          dst.append("&nbsp;");
136        else if (c == (char) 0xA7)
137          dst.append("&sect;");
138        else if (c == (char) 169)
139          dst.append("&copy;");
140        else if (c == (char) 8482)
141          dst.append("&trade;");
142        else if (c == (char) 956)
143          dst.append("&mu;");
144        else if (c == (char) 174)
145          dst.append("&reg;");
146        else 
147          dst.append(c);
148      }
149    }
150  }
151
152  private void writeComment(String indent, XhtmlNode node, boolean noPrettyOverride) throws IOException {
153    dst.append(indent + "<!-- " + node.getContent().trim() + " -->" + (pretty && !noPrettyOverride ? "\r\n" : ""));
154}
155
156  private void writeDocType(XhtmlNode node) throws IOException {
157    dst.append("<!" + node.getContent() + ">\r\n");
158}
159
160  private void writeInstruction(XhtmlNode node) throws IOException {
161    dst.append("<?" + node.getContent() + "?>\r\n");
162}
163
164  private String escapeHtml(String s)  {
165    if (s == null || s.equals(""))
166      return null;
167    StringBuilder b = new StringBuilder();
168    for (char c : s.toCharArray())
169      if (c == '<')
170        b.append("&lt;");
171      else if (c == '>')
172        b.append("&gt;");
173      else if (c == '"')
174        b.append("&quot;");
175      else if (c == '&')
176        b.append("&amp;");
177      else
178        b.append(c);
179    return b.toString();
180  }
181  
182  private String attributes(XhtmlNode node) {
183    StringBuilder s = new StringBuilder();
184    for (String n : node.getAttributes().keySet())
185      s.append(" " + n + "=\"" + escapeHtml(node.getAttributes().get(n)) + "\"");
186    return s.toString();
187  }
188  
189  private void writeElement(String indent, XhtmlNode node, boolean noPrettyOverride) throws IOException  {
190    if (!pretty || noPrettyOverride)
191      indent = "";
192
193    // html self closing tags: http://xahlee.info/js/html5_non-closing_tag.html 
194    boolean concise = node.getChildNodes().size() == 0;
195    if (node.hasEmptyExpanded() && node.getEmptyExpanded()) {
196      concise = false;
197    }
198    if (!xml && Utilities.existsInList(node.getName(), "area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr")) {
199      concise = true;
200    }
201
202    if (concise)
203      dst.append(indent + "<" + node.getName() + attributes(node) + "/>" + (pretty && !noPrettyOverride ? "\r\n" : ""));
204    else {
205      boolean act = node.allChildrenAreText();
206      if (act || !pretty ||  noPrettyOverride)
207        dst.append(indent + "<" + node.getName() + attributes(node)+">");
208      else
209        dst.append(indent + "<" + node.getName() + attributes(node) + ">\r\n");
210      if (node.getName() == "head" && node.getElement("meta") == null)
211        dst.append(indent + "  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>" + (pretty && !noPrettyOverride ? "\r\n" : ""));
212
213
214      for (XhtmlNode c : node.getChildNodes())
215        writeNode(indent + "  ", c, noPrettyOverride || node.isNoPretty());
216      if (act)
217        dst.append("</" + node.getName() + ">" + (pretty && !noPrettyOverride ? "\r\n" : ""));
218      else if (node.getChildNodes().get(node.getChildNodes().size() - 1).getNodeType() == NodeType.Text)
219        dst.append((pretty && !noPrettyOverride ? "\r\n"+ indent : "")  + "</" + node.getName() + ">" + (pretty && !noPrettyOverride ? "\r\n" : ""));
220      else
221        dst.append(indent + "</" + node.getName() + ">" + (pretty && !noPrettyOverride ? "\r\n" : ""));
222    }
223  }
224
225  private void writeDocument(String indent, XhtmlNode node) throws IOException  {
226    indent = "";
227    for (XhtmlNode c : node.getChildNodes())
228      writeNode(indent, c, false);
229  }
230
231
232  public void compose(IXMLWriter xml, XhtmlNode node) throws IOException  {
233    compose(xml, node, false);
234  }
235  
236  public void compose(IXMLWriter xml, XhtmlNode node, boolean noPrettyOverride) throws IOException  {
237    if (node.getNodeType() == NodeType.Comment)
238      xml.comment(node.getContent(), pretty && !noPrettyOverride);
239    else if (node.getNodeType() == NodeType.Element)
240      composeElement(xml, node, noPrettyOverride);
241    else if (node.getNodeType() == NodeType.Text)
242      xml.text(node.getContent());
243    else
244      throw new Error("Unhandled node type: "+node.getNodeType().toString());
245  }
246
247  private void composeElement(IXMLWriter xml, XhtmlNode node, boolean noPrettyOverride) throws IOException  {
248    for (String n : node.getAttributes().keySet()) {
249      if (n.equals("xmlns")) 
250        xml.setDefaultNamespace(node.getAttributes().get(n));
251      else if (n.startsWith("xmlns:")) 
252        xml.namespace(n.substring(6), node.getAttributes().get(n));
253      else
254      xml.attribute(n, node.getAttributes().get(n));
255    }
256    xml.enter(XHTML_NS, node.getName());
257    for (XhtmlNode n : node.getChildNodes())
258      compose(xml, n, noPrettyOverride || node.isNoPretty());
259    xml.exit(XHTML_NS, node.getName());
260  }
261
262  public String composePlainText(XhtmlNode x) {
263    StringBuilder b = new StringBuilder();
264    composePlainText(x, b, false);
265    return b.toString().trim();
266  }
267
268  private boolean composePlainText(XhtmlNode x, StringBuilder b, boolean lastWS) {
269    if (x.getNodeType() == NodeType.Text) {
270      String s = x.getContent();
271      if (!lastWS & (s.startsWith(" ") || s.startsWith("\r") || s.startsWith("\n") || s.endsWith("\t"))) {
272        b.append(" ");
273        lastWS = true;
274      }
275      String st = s.trim().replace("\r", " ").replace("\n", " ").replace("\t", " ");
276      while (st.contains("  "))
277        st = st.replace("  ", " ");
278      if (!Utilities.noString(st)) {
279        b.append(st);
280        lastWS = false;
281        if (!lastWS & (s.endsWith(" ") || s.endsWith("\r") || s.endsWith("\n") || s.endsWith("\t"))) {
282          b.append(" ");
283          lastWS = true;
284        }
285      }
286      return lastWS;
287    } else if (x.getNodeType() == NodeType.Element) {
288      if (x.getName().equals("li")) {
289        b.append("* ");
290        lastWS = true;
291      }
292      
293      for (XhtmlNode n : x.getChildNodes()) {
294        lastWS = composePlainText(n, b, lastWS);
295      }
296      if (x.getName().equals("p")) {
297        b.append("\r\n\r\n");
298        lastWS = true;
299      }
300      if (x.getName().equals("br") || x.getName().equals("li")) {
301        b.append("\r\n");
302        lastWS = true;
303      }
304      return lastWS;
305    } else
306      return lastWS;
307  }
308
309  public void compose(Element div, XhtmlNode x) {
310    for (XhtmlNode child : x.getChildNodes()) {
311      appendChild(div, child);
312    }
313  }
314
315  private void appendChild(Element e, XhtmlNode node) {
316    if (node.getNodeType() == NodeType.Comment)
317      e.appendChild(e.getOwnerDocument().createComment(node.getContent()));
318    else if (node.getNodeType() == NodeType.DocType)
319      throw new Error("not done yet");
320    else if (node.getNodeType() == NodeType.Instruction)
321      e.appendChild(e.getOwnerDocument().createProcessingInstruction("", node.getContent()));
322    else if (node.getNodeType() == NodeType.Text)
323      e.appendChild(e.getOwnerDocument().createTextNode(node.getContent()));
324    else if (node.getNodeType() == NodeType.Element) {
325      Element child = e.getOwnerDocument().createElementNS(XHTML_NS, node.getName());
326      e.appendChild(child);
327      for (String n : node.getAttributes().keySet()) {
328        child.setAttribute(n,  node.getAttribute(n));
329      }
330      for (XhtmlNode c : node.getChildNodes()) {
331        appendChild(child, c);
332      }
333    } else
334      throw new Error("Unknown node type: "+node.getNodeType().toString());
335  }
336
337  public void compose(OutputStream stream, XhtmlNode x) throws IOException {
338    byte[] bom = new byte[] { (byte)0xEF, (byte)0xBB, (byte)0xBF };
339    stream.write(bom);
340    dst = new OutputStreamWriter(stream, "UTF-8");
341    dst.append("<html><head><link rel=\"stylesheet\" href=\"fhir.css\"/></head><body>\r\n");
342    writeNode("", x, false);
343    dst.append("</body></html>\r\n");
344    dst.flush();
345  }
346
347  public void composeDocument(FileOutputStream f, XhtmlNode xhtml) throws IOException {
348    byte[] bom = new byte[] { (byte)0xEF, (byte)0xBB, (byte)0xBF };
349    f.write(bom);
350    dst = new OutputStreamWriter(f, "UTF-8");
351    writeNode("", xhtml, false);
352    dst.flush();
353    dst.close();
354  }
355
356  public String composeEx(XhtmlNode node) {
357    try {
358      return compose(node);
359    } catch (IOException e) {
360      throw new Error(e);
361    }
362  }
363  
364}