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