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