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