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