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.IOException;
032import java.util.ArrayList;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036
037import org.hl7.fhir.instance.model.api.IBaseXhtml;
038import org.hl7.fhir.utilities.Utilities;
039
040import ca.uhn.fhir.model.primitive.XhtmlDt;
041
042@ca.uhn.fhir.model.api.annotation.DatatypeDef(name="xhtml")
043public class XhtmlNode implements IBaseXhtml {
044  private static final long serialVersionUID = -4362547161441436492L;
045
046
047  public static class Location {
048    private int line;
049    private int column;
050    public Location(int line, int column) {
051      super();
052      this.line = line;
053      this.column = column;
054    }
055    public int getLine() {
056      return line;
057    }
058    public int getColumn() {
059      return column;
060    }
061    @Override
062    public String toString() {
063      return "Line "+Integer.toString(line)+", column "+Integer.toString(column);
064    }
065  }
066
067  public static final String NBSP = Character.toString((char)0xa0);
068  private static final String DECL_XMLNS = " xmlns=\"http://www.w3.org/1999/xhtml\"";
069
070
071  private Location location;
072  private NodeType nodeType;
073  private String name;
074  private Map<String, String> attributes = new HashMap<String, String>();
075  private List<XhtmlNode> childNodes = new ArrayList<XhtmlNode>();
076  private String content;
077
078  public XhtmlNode() {
079    super();
080  }
081
082
083  public XhtmlNode(NodeType nodeType, String name) {
084    super();
085    this.nodeType = nodeType;
086    this.name = name;
087  }
088
089  public XhtmlNode(NodeType nodeType) {
090    super();
091    this.nodeType = nodeType;
092  }
093
094  public NodeType getNodeType() {
095    return nodeType;
096  }
097
098  public void setNodeType(NodeType nodeType) {
099    this.nodeType = nodeType;
100  }
101
102  public String getName() {
103    return name;
104  }
105
106  public XhtmlNode setName(String name) {
107    assert name.contains(":") == false : "Name should not contain any : but was " + name;
108    this.name = name;
109    return this;
110  }
111
112  public Map<String, String> getAttributes() {
113    return attributes;
114  }
115
116  public List<XhtmlNode> getChildNodes() {
117    return childNodes;
118  }
119
120  public String getContent() {
121    return content;
122  }
123
124  public XhtmlNode setContent(String content) {
125    if (!(nodeType != NodeType.Text || nodeType != NodeType.Comment)) 
126      throw new Error("Wrong node type");
127    this.content = content;
128    return this;
129  }
130
131  public XhtmlNode addTag(String name)
132  {
133
134    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
135      throw new Error("Wrong node type. is "+nodeType.toString());
136    XhtmlNode node = new XhtmlNode(NodeType.Element);
137    node.setName(name);
138    childNodes.add(node);
139    return node;
140  }
141
142  public XhtmlNode addTag(int index, String name)
143  {
144
145    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
146      throw new Error("Wrong node type. is "+nodeType.toString());
147    XhtmlNode node = new XhtmlNode(NodeType.Element);
148    node.setName(name);
149    childNodes.add(index, node);
150    return node;
151  }
152
153  public XhtmlNode addComment(String content)
154  {
155    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
156      throw new Error("Wrong node type");
157    XhtmlNode node = new XhtmlNode(NodeType.Comment);
158    node.setContent(content);
159    childNodes.add(node);
160    return node;
161  }
162
163  public XhtmlNode addDocType(String content)
164  {
165    if (!(nodeType == NodeType.Document)) 
166      throw new Error("Wrong node type");
167    XhtmlNode node = new XhtmlNode(NodeType.DocType);
168    node.setContent(content);
169    childNodes.add(node);
170    return node;
171  }
172
173  public XhtmlNode addInstruction(String content)
174  {
175    if (!(nodeType == NodeType.Document)) 
176      throw new Error("Wrong node type");
177    XhtmlNode node = new XhtmlNode(NodeType.Instruction);
178    node.setContent(content);
179    childNodes.add(node);
180    return node;
181  }
182
183
184
185
186  public XhtmlNode addText(String content)
187  {
188    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
189      throw new Error("Wrong node type");
190    if (content != null) {
191      XhtmlNode node = new XhtmlNode(NodeType.Text);
192      node.setContent(content);
193      childNodes.add(node);
194      return node;
195    } else 
196      return null;
197  }
198
199  public XhtmlNode addText(int index, String content)
200  {
201    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
202      throw new Error("Wrong node type");
203    if (content == null)
204      throw new Error("Content cannot be null");
205
206    XhtmlNode node = new XhtmlNode(NodeType.Text);
207    node.setContent(content);
208    childNodes.add(index, node);
209    return node;
210  }
211
212  public boolean allChildrenAreText()
213  {
214    boolean res = true;
215    for (XhtmlNode n : childNodes)
216      res = res && n.getNodeType() == NodeType.Text;
217    return res;
218  }
219
220  public XhtmlNode getElement(String name) {
221    for (XhtmlNode n : childNodes)
222      if (n.getNodeType() == NodeType.Element && name.equals(n.getName())) 
223        return n;
224    return null;
225  }
226
227  public XhtmlNode getFirstElement() {
228    for (XhtmlNode n : childNodes)
229      if (n.getNodeType() == NodeType.Element) 
230        return n;
231    return null;
232  }
233
234  public String allText() {
235    if (childNodes == null || childNodes.isEmpty())
236      return getContent();
237    
238    StringBuilder b = new StringBuilder();
239    for (XhtmlNode n : childNodes)
240      if (n.getNodeType() == NodeType.Text)
241        b.append(n.getContent());
242      else if (n.getNodeType() == NodeType.Element)
243        b.append(n.allText());
244    return b.toString();
245  }
246
247  public XhtmlNode attribute(String name, String value) {
248    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
249      throw new Error("Wrong node type");
250    if (name == null)
251      throw new Error("name is null");
252    if (value == null)
253      throw new Error("value is null");
254    attributes.put(name, value);
255    return this;
256  }
257
258  public boolean hasAttribute(String name) {
259    return getAttributes().containsKey(name);
260  }
261
262  public String getAttribute(String name) {
263    return getAttributes().get(name);
264  }
265
266  public XhtmlNode setAttribute(String name, String value) {
267    getAttributes().put(name, value);
268    return this;    
269  }
270
271  public XhtmlNode copy() {
272    XhtmlNode dst = new XhtmlNode(nodeType);
273    dst.name = name;
274    for (String n : attributes.keySet()) {
275      dst.attributes.put(n, attributes.get(n));
276    }
277    for (XhtmlNode n : childNodes)
278      dst.childNodes.add(n.copy());
279    dst.content = content;
280    return dst;
281  }
282
283  @Override
284  public boolean isEmpty() {
285    return (childNodes == null || childNodes.isEmpty()) && content == null;
286  }
287
288  public boolean equalsDeep(XhtmlNode other) {
289    if (other == null) {
290      return false;
291    }
292
293    if (!(nodeType == other.nodeType) || !compare(name, other.name) || !compare(content, other.content))
294      return false;
295    if (attributes.size() != other.attributes.size())
296      return false;
297    for (String an : attributes.keySet())
298      if (!attributes.get(an).equals(other.attributes.get(an)))
299        return false;
300    if (childNodes.size() != other.childNodes.size())
301      return false;
302    for (int i = 0; i < childNodes.size(); i++) {
303      if (!compareDeep(childNodes.get(i), other.childNodes.get(i)))
304        return false;
305    }
306    return true;
307  }
308
309  private boolean compare(String s1, String s2) {
310    if (s1 == null && s2 == null)
311      return true;
312    if (s1 == null || s2 == null)
313      return false;
314    return s1.equals(s2);
315  }
316
317  private static boolean compareDeep(XhtmlNode e1, XhtmlNode e2) {
318    if (e1 == null && e2 == null)
319      return true;
320    if (e1 == null || e2 == null)
321      return false;
322    return e1.equalsDeep(e2);
323  }
324
325  public String getNsDecl() {
326    for (String an : attributes.keySet()) {
327      if (an.equals("xmlns")) {
328        return attributes.get(an);
329      }
330    }
331    return null;
332  }
333
334
335  @Override
336  public String getValueAsString() {
337    if (isEmpty()) {
338      return null;
339    }
340    try {
341      String retVal = new XhtmlComposer(XhtmlComposer.HTML).compose(this);
342      retVal = XhtmlDt.preprocessXhtmlNamespaceDeclaration(retVal);
343      return retVal;
344    } catch (Exception e) {
345      // TODO: composer shouldn't throw exception like this
346      throw new RuntimeException(e);
347    }
348  }
349
350  @Override
351  public void setValueAsString(String theValue) throws IllegalArgumentException {
352    this.attributes = null;
353    this.childNodes = null;
354    this.content = null;
355    this.name = null;
356    this.nodeType= null;
357    if (theValue == null || theValue.length() == 0) {
358      return;
359    }
360
361    String val = theValue.trim();
362
363    if (!val.startsWith("<")) {
364      val = "<div" + DECL_XMLNS +">" + val + "</div>";
365    }
366    if (val.startsWith("<?") && val.endsWith("?>")) {
367      return;
368    }
369
370    val = XhtmlDt.preprocessXhtmlNamespaceDeclaration(val);
371
372    try {
373      // TODO: this is ugly
374      XhtmlNode fragment = new XhtmlParser().parseFragment(val);
375      this.attributes = fragment.attributes;
376      this.childNodes = fragment.childNodes;
377      this.content = fragment.content;
378      this.name = fragment.name;
379      this.nodeType= fragment.nodeType;
380    } catch (Exception e) {
381      // TODO: composer shouldn't throw exception like this
382      throw new RuntimeException(e);
383    }
384
385  }
386
387  public XhtmlNode getElementByIndex(int i) {
388    int c = 0;
389    for (XhtmlNode n : childNodes)
390      if (n.getNodeType() == NodeType.Element) {
391        if (c == i)
392          return n;
393        else
394          c++;
395      }
396    return null;
397  }
398
399  @Override
400  public String getValue() {
401    return getValueAsString();
402  }
403
404  @Override
405  public XhtmlNode setValue(String theValue) throws IllegalArgumentException {
406    setValueAsString(theValue);
407    return this;
408  }
409
410  /**
411   * Returns false
412   */
413  public boolean hasFormatComment() {
414    return false;
415  }
416
417  /**
418   * NOT SUPPORTED - Throws {@link UnsupportedOperationException}
419   */
420  public List<String> getFormatCommentsPre() {
421    throw new UnsupportedOperationException();
422  }
423
424  /**
425   * NOT SUPPORTED - Throws {@link UnsupportedOperationException}
426   */
427  public List<String> getFormatCommentsPost() {
428    throw new UnsupportedOperationException();
429  }
430
431
432  public Location getLocation() {
433    return location;
434  }
435
436
437  public void setLocation(Location location) {
438    this.location = location;
439  }
440
441  // xhtml easy adders -----------------------------------------------
442  public XhtmlNode h1() {
443    return addTag("h1");
444  }
445  
446  public XhtmlNode h2() {
447    return addTag("h2");
448  }
449  
450  public XhtmlNode h3() {
451    return addTag("h3");
452  }
453  
454  public XhtmlNode h4() {
455    return addTag("h4");
456  }
457  
458  public XhtmlNode table(String clss) {
459    XhtmlNode res = addTag("table");
460    if (!Utilities.noString(clss))
461      res.setAttribute("class", clss);
462    return res;
463  }
464  
465  public XhtmlNode tr() {
466    return addTag("tr");
467  }
468  
469  public XhtmlNode th() {
470    return addTag("th");
471  }
472  
473  public XhtmlNode td() {
474    return addTag("td");
475  }
476  
477  public XhtmlNode colspan(String n) {
478    return setAttribute("colspan", n);
479  }
480  
481  public XhtmlNode para() {
482    return addTag("p");
483  }
484
485  public XhtmlNode pre() {
486    return addTag("pre");
487  }
488
489  public void br() {
490    addTag("br");
491  }
492
493  public void hr() {
494    addTag("hr");
495  }
496
497  public XhtmlNode ul() {
498    return addTag("ul");
499  }
500
501  public XhtmlNode li() {
502    return addTag("li");
503  }
504
505  public XhtmlNode b() {
506    return addTag("b");
507  }
508
509  public XhtmlNode i() {
510    return addTag("i");
511  }
512  public XhtmlNode tx(String cnt) {
513    return addText(cnt);
514  }
515  public XhtmlNode ah(String href) {
516    return addTag("a").attribute("href", href);
517  }
518
519  public void an(String href) {
520    addTag("a").attribute("name", href).tx(" ");
521  }
522
523  public XhtmlNode span(String style, String title) {
524    XhtmlNode res = addTag("span");
525    if (!Utilities.noString(style))
526      res.attribute("style", style);
527    if (!Utilities.noString(title))
528      res.attribute("title", title);
529    return res;
530  }
531
532
533  public void code(String text) {
534    addTag("code").tx(text);
535  }
536
537
538  public XhtmlNode blockquote() {
539    return addTag("blockquote");
540  }
541
542
543  @Override
544  public String toString() {
545    switch (nodeType) {
546    case Document: 
547    case Element:
548      try {
549        return new XhtmlComposer(XhtmlComposer.HTML).compose(this);
550      } catch (IOException e) {
551        return super.toString();
552      }
553    case Text:
554      return this.content;
555    case Comment:
556      return "<!-- "+this.content+" -->";
557    case DocType: 
558      return "<? "+this.content+" />";
559    case Instruction:
560      return "<? "+this.content+" />";
561    }
562    return super.toString();
563  }
564
565
566  public XhtmlNode getNextElement(XhtmlNode c) {
567    boolean f = false;
568    for (XhtmlNode n : childNodes) {
569      if (n == c)
570        f = true;
571      else if (f && n.getNodeType() == NodeType.Element) 
572        return n;
573    }
574    return null;
575  }
576
577}