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