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("&"); 120 else if (c == '<') 121 dst.append("<"); 122 else if (c == '>') 123 dst.append(">"); 124 else if (xml) { 125 if (c == '"') 126 dst.append("""); 127 else 128 dst.append(c); 129 } else { 130 if (c == XhtmlNode.NBSP.charAt(0)) 131 dst.append(" "); 132 else if (c == (char) 0xA7) 133 dst.append("§"); 134 else if (c == (char) 169) 135 dst.append("©"); 136 else if (c == (char) 8482) 137 dst.append("™"); 138 else if (c == (char) 956) 139 dst.append("μ"); 140 else if (c == (char) 174) 141 dst.append("®"); 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("<"); 167 else if (c == '>') 168 b.append(">"); 169 else if (c == '"') 170 b.append("""); 171 else if (c == '&') 172 b.append("&"); 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}