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