001/*
002Copyright (c) 2011+, HL7, Inc
003All rights reserved.
004
005Redistribution and use in source and binary forms, with or without modification, 
006are permitted provided that the following conditions are met:
007
008 * Redistributions of source code must retain the above copyright notice, this 
009   list of conditions and the following disclaimer.
010 * Redistributions in binary form must reproduce the above copyright notice, 
011   this list of conditions and the following disclaimer in the documentation 
012   and/or other materials provided with the distribution.
013 * Neither the name of HL7 nor the names of its contributors may be used to 
014   endorse or promote products derived from this software without specific 
015   prior written permission.
016
017THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
018ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
019WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
020IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
021INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
022NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
023PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
024WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
025ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
026POSSIBILITY OF SUCH DAMAGE.
027
028*/
029package org.hl7.fhir.utilities.xhtml;
030
031import java.io.FileOutputStream;
032import java.io.IOException;
033import java.io.OutputStream;
034import java.io.OutputStreamWriter;
035import java.io.StringWriter;
036import java.io.Writer;
037
038import org.hl7.fhir.utilities.Utilities;
039import org.hl7.fhir.utilities.xml.IXMLWriter;
040import org.w3c.dom.Element;
041
042public class XhtmlComposer {
043
044  public static final String XHTML_NS = "http://www.w3.org/1999/xhtml";
045  private boolean pretty;
046  private boolean xml; 
047  
048  public static final boolean XML = true; 
049  public static final boolean HTML = false; 
050  
051  public XhtmlComposer(boolean xml, boolean pretty) {
052    super();
053    this.pretty = pretty;
054    this.xml = xml;
055  }
056
057  public XhtmlComposer(boolean xml) {
058    super();
059    this.pretty = false;
060    this.xml = xml;
061  }
062
063  private Writer dst;
064
065  public String compose(XhtmlDocument doc) throws IOException  {
066    StringWriter sdst = new StringWriter();
067    dst = sdst;
068    composeDoc(doc);
069    return sdst.toString();
070  }
071
072  public String compose(XhtmlNode node) throws IOException  {
073    StringWriter sdst = new StringWriter();
074    dst = sdst;
075    writeNode("", node, false);
076    return sdst.toString();
077  }
078
079  public void compose(OutputStream stream, XhtmlDocument doc) throws IOException  {
080    byte[] bom = new byte[] { (byte)0xEF, (byte)0xBB, (byte)0xBF };
081    stream.write(bom);
082    dst = new OutputStreamWriter(stream, "UTF-8");
083    composeDoc(doc);
084    dst.flush();
085  }
086
087  private void composeDoc(XhtmlDocument doc) throws IOException  {
088    // headers....
089//    dst.append("<html>" + (pretty ? "\r\n" : ""));
090    for (XhtmlNode c : doc.getChildNodes())
091      writeNode("  ", c, false);
092//    dst.append("</html>" + (pretty ? "\r\n" : ""));
093  }
094
095  private void writeNode(String indent, XhtmlNode node, boolean noPrettyOverride) throws IOException  {
096    if (node.getNodeType() == NodeType.Comment)
097      writeComment(indent, node, noPrettyOverride);
098    else if (node.getNodeType() == NodeType.DocType)
099      writeDocType(node);
100    else if (node.getNodeType() == NodeType.Instruction)
101      writeInstruction(node);
102    else if (node.getNodeType() == NodeType.Element)
103      writeElement(indent, node, noPrettyOverride);
104    else if (node.getNodeType() == NodeType.Document)
105      writeDocument(indent, node);
106    else if (node.getNodeType() == NodeType.Text)
107      writeText(node);
108    else if (node.getNodeType() == null)
109      throw new IOException("Null node type");
110    else
111      throw new IOException("Unknown node type: "+node.getNodeType().toString());
112  }
113
114  private void writeText(XhtmlNode node) throws IOException  {
115    for (char c : node.getContent().toCharArray())
116    {
117      if (c == '&')
118        dst.append("&amp;");
119      else if (c == '<')
120        dst.append("&lt;");
121      else if (c == '>')
122        dst.append("&gt;");
123      else if (xml) {
124        if (c == '"')
125          dst.append("&quot;");
126        else 
127          dst.append(c);
128      } else {
129        if (c == XhtmlNode.NBSP.charAt(0))
130          dst.append("&nbsp;");
131        else if (c == (char) 0xA7)
132          dst.append("&sect;");
133        else if (c == (char) 169)
134          dst.append("&copy;");
135        else if (c == (char) 8482)
136          dst.append("&trade;");
137        else if (c == (char) 956)
138          dst.append("&mu;");
139        else if (c == (char) 174)
140          dst.append("&reg;");
141        else 
142          dst.append(c);
143      }
144    }
145  }
146
147  private void writeComment(String indent, XhtmlNode node, boolean noPrettyOverride) throws IOException {
148    dst.append(indent + "<!-- " + node.getContent().trim() + " -->" + (pretty && !noPrettyOverride ? "\r\n" : ""));
149}
150
151  private void writeDocType(XhtmlNode node) throws IOException {
152    dst.append("<!" + node.getContent() + ">\r\n");
153}
154
155  private void writeInstruction(XhtmlNode node) throws IOException {
156    dst.append("<?" + node.getContent() + "?>\r\n");
157}
158
159  private String escapeHtml(String s)  {
160    if (s == null || s.equals(""))
161      return null;
162    StringBuilder b = new StringBuilder();
163    for (char c : s.toCharArray())
164      if (c == '<')
165        b.append("&lt;");
166      else if (c == '>')
167        b.append("&gt;");
168      else if (c == '"')
169        b.append("&quot;");
170      else if (c == '&')
171        b.append("&amp;");
172      else
173        b.append(c);
174    return b.toString();
175  }
176  
177  private String attributes(XhtmlNode node) {
178    StringBuilder s = new StringBuilder();
179    for (String n : node.getAttributes().keySet())
180      s.append(" " + n + "=\"" + escapeHtml(node.getAttributes().get(n)) + "\"");
181    return s.toString();
182  }
183  
184  private void writeElement(String indent, XhtmlNode node, boolean noPrettyOverride) throws IOException  {
185    if (!pretty || noPrettyOverride)
186      indent = "";
187
188    // html self closing tags: http://xahlee.info/js/html5_non-closing_tag.html 
189    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")))
190      dst.append(indent + "<" + node.getName() + attributes(node) + "/>" + (pretty && !noPrettyOverride ? "\r\n" : ""));
191    else {
192    boolean act = node.allChildrenAreText();
193    if (act || !pretty ||  noPrettyOverride)
194      dst.append(indent + "<" + node.getName() + attributes(node)+">");
195    else
196      dst.append(indent + "<" + node.getName() + attributes(node) + ">\r\n");
197    if (node.getName() == "head" && node.getElement("meta") == null)
198      dst.append(indent + "  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>" + (pretty && !noPrettyOverride ? "\r\n" : ""));
199
200
201    for (XhtmlNode c : node.getChildNodes())
202      writeNode(indent + "  ", c, noPrettyOverride || node.isNoPretty());
203    if (act)
204      dst.append("</" + node.getName() + ">" + (pretty && !noPrettyOverride ? "\r\n" : ""));
205    else if (node.getChildNodes().get(node.getChildNodes().size() - 1).getNodeType() == NodeType.Text)
206      dst.append((pretty && !noPrettyOverride ? "\r\n"+ indent : "")  + "</" + node.getName() + ">" + (pretty && !noPrettyOverride ? "\r\n" : ""));
207    else
208      dst.append(indent + "</" + node.getName() + ">" + (pretty && !noPrettyOverride ? "\r\n" : ""));
209    }
210  }
211
212  private void writeDocument(String indent, XhtmlNode node) throws IOException  {
213    indent = "";
214    for (XhtmlNode c : node.getChildNodes())
215      writeNode(indent, c, false);
216  }
217
218
219  public void compose(IXMLWriter xml, XhtmlNode node) throws IOException  {
220    compose(xml, node, false);
221  }
222  
223  public void compose(IXMLWriter xml, XhtmlNode node, boolean noPrettyOverride) throws IOException  {
224    if (node.getNodeType() == NodeType.Comment)
225      xml.comment(node.getContent(), pretty && !noPrettyOverride);
226    else if (node.getNodeType() == NodeType.Element)
227      composeElement(xml, node, noPrettyOverride);
228    else if (node.getNodeType() == NodeType.Text)
229      xml.text(node.getContent());
230    else
231      throw new Error("Unhandled node type: "+node.getNodeType().toString());
232  }
233
234  private void composeElement(IXMLWriter xml, XhtmlNode node, boolean noPrettyOverride) throws IOException  {
235    for (String n : node.getAttributes().keySet()) {
236      if (n.equals("xmlns")) 
237        xml.setDefaultNamespace(node.getAttributes().get(n));
238      else if (n.startsWith("xmlns:")) 
239        xml.namespace(n.substring(6), node.getAttributes().get(n));
240      else
241      xml.attribute(n, node.getAttributes().get(n));
242    }
243    xml.enter(XHTML_NS, node.getName());
244    for (XhtmlNode n : node.getChildNodes())
245      compose(xml, n, noPrettyOverride || node.isNoPretty());
246    xml.exit(XHTML_NS, node.getName());
247  }
248
249  public String composePlainText(XhtmlNode x) {
250    StringBuilder b = new StringBuilder();
251    composePlainText(x, b, false);
252    return b.toString().trim();
253  }
254
255  private boolean composePlainText(XhtmlNode x, StringBuilder b, boolean lastWS) {
256    if (x.getNodeType() == NodeType.Text) {
257      String s = x.getContent();
258      if (!lastWS & (s.startsWith(" ") || s.startsWith("\r") || s.startsWith("\n") || s.endsWith("\t"))) {
259        b.append(" ");
260        lastWS = true;
261      }
262      String st = s.trim().replace("\r", " ").replace("\n", " ").replace("\t", " ");
263      while (st.contains("  "))
264        st = st.replace("  ", " ");
265      if (!Utilities.noString(st)) {
266        b.append(st);
267        lastWS = false;
268        if (!lastWS & (s.endsWith(" ") || s.endsWith("\r") || s.endsWith("\n") || s.endsWith("\t"))) {
269          b.append(" ");
270          lastWS = true;
271        }
272      }
273      return lastWS;
274    } else if (x.getNodeType() == NodeType.Element) {
275      if (x.getName().equals("li")) {
276        b.append("* ");
277        lastWS = true;
278      }
279      
280      for (XhtmlNode n : x.getChildNodes()) {
281        lastWS = composePlainText(n, b, lastWS);
282      }
283      if (x.getName().equals("p")) {
284        b.append("\r\n\r\n");
285        lastWS = true;
286      }
287      if (x.getName().equals("br") || x.getName().equals("li")) {
288        b.append("\r\n");
289        lastWS = true;
290      }
291      return lastWS;
292    } else
293      return lastWS;
294  }
295
296  public void compose(Element div, XhtmlNode x) {
297    for (XhtmlNode child : x.getChildNodes()) {
298      appendChild(div, child);
299    }
300  }
301
302  private void appendChild(Element e, XhtmlNode node) {
303    if (node.getNodeType() == NodeType.Comment)
304      e.appendChild(e.getOwnerDocument().createComment(node.getContent()));
305    else if (node.getNodeType() == NodeType.DocType)
306      throw new Error("not done yet");
307    else if (node.getNodeType() == NodeType.Instruction)
308      e.appendChild(e.getOwnerDocument().createProcessingInstruction("", node.getContent()));
309    else if (node.getNodeType() == NodeType.Text)
310      e.appendChild(e.getOwnerDocument().createTextNode(node.getContent()));
311    else if (node.getNodeType() == NodeType.Element) {
312      Element child = e.getOwnerDocument().createElementNS(XHTML_NS, node.getName());
313      e.appendChild(child);
314      for (XhtmlNode c : node.getChildNodes()) {
315        appendChild(child, c);
316      }
317    } else
318      throw new Error("Unknown node type: "+node.getNodeType().toString());
319  }
320
321  public void compose(OutputStream stream, XhtmlNode x) throws IOException {
322    byte[] bom = new byte[] { (byte)0xEF, (byte)0xBB, (byte)0xBF };
323    stream.write(bom);
324    dst = new OutputStreamWriter(stream, "UTF-8");
325    dst.append("<html><head><link rel=\"stylesheet\" href=\"fhir.css\"/></head><body>\r\n");
326    writeNode("", x, false);
327    dst.append("</body></html>\r\n");
328    dst.flush();
329  }
330
331  public void composeDocument(FileOutputStream f, XhtmlNode xhtml) throws IOException {
332    byte[] bom = new byte[] { (byte)0xEF, (byte)0xBB, (byte)0xBF };
333    f.write(bom);
334    dst = new OutputStreamWriter(f, "UTF-8");
335    writeNode("", xhtml, false);
336    dst.flush();
337    dst.close();
338  }
339
340  public String composeEx(XhtmlNode node) {
341    try {
342      return compose(node);
343    } catch (IOException e) {
344      throw new Error(e);
345    }
346  }
347  
348}