001package org.hl7.fhir.utilities.xhtml;
002
003import static org.apache.commons.lang3.StringUtils.isNotBlank;
004
005/*
006  Copyright (c) 2011+, HL7, Inc.
007  All rights reserved.
008  
009  Redistribution and use in source and binary forms, with or without modification, 
010  are permitted provided that the following conditions are met:
011    
012   * Redistributions of source code must retain the above copyright notice, this 
013     list of conditions and the following disclaimer.
014   * Redistributions in binary form must reproduce the above copyright notice, 
015     this list of conditions and the following disclaimer in the documentation 
016     and/or other materials provided with the distribution.
017   * Neither the name of HL7 nor the names of its contributors may be used to 
018     endorse or promote products derived from this software without specific 
019     prior written permission.
020  
021  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
022  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
023  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
024  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
025  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
026  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
027  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
028  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
029  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
030  POSSIBILITY OF SUCH DAMAGE.
031  
032 */
033
034
035
036import java.io.IOException;
037import java.io.Serializable;
038import java.util.HashMap;
039import java.util.List;
040import java.util.Map;
041
042import org.hl7.fhir.exceptions.FHIRException;
043import org.hl7.fhir.exceptions.FHIRFormatError;
044import org.hl7.fhir.instance.model.api.IBaseXhtml;
045import org.hl7.fhir.utilities.MarkDownProcessor;
046import org.hl7.fhir.utilities.MarkDownProcessor.Dialect;
047import org.hl7.fhir.utilities.Utilities;
048
049import ca.uhn.fhir.model.primitive.XhtmlDt;
050
051@ca.uhn.fhir.model.api.annotation.DatatypeDef(name="xhtml")
052public class XhtmlNode extends XhtmlFluent implements IBaseXhtml {
053  private static final long serialVersionUID = -4362547161441436492L;
054
055
056  public static class Location implements Serializable {
057    private static final long serialVersionUID = -4079302502900219721L;
058    private int line;
059    private int column;
060    public Location(int line, int column) {
061      super();
062      this.line = line;
063      this.column = column;
064    }
065    public int getLine() {
066      return line;
067    }
068    public int getColumn() {
069      return column;
070    }
071    @Override
072    public String toString() {
073      return "Line "+Integer.toString(line)+", column "+Integer.toString(column);
074    }
075  }
076
077  public static final String NBSP = Character.toString((char)0xa0);
078  public static final String XMLNS = "http://www.w3.org/1999/xhtml";
079  private static final String DECL_XMLNS = " xmlns=\""+XMLNS+"\"";
080
081  private Location location;
082  private NodeType nodeType;
083  private String name;
084  private Map<String, String> attributes = new HashMap<String, String>();
085  private XhtmlNodeList childNodes = new XhtmlNodeList();
086  private String content;
087  private boolean notPretty;
088  private boolean seperated;  
089  private Boolean emptyExpanded;
090
091  public XhtmlNode() {
092    super();
093  }
094
095
096  public XhtmlNode(NodeType nodeType, String name) {
097    super();
098    this.nodeType = nodeType;
099    this.name = name;
100  }
101
102  public XhtmlNode(NodeType nodeType) {
103    super();
104    this.nodeType = nodeType;
105  }
106
107  public NodeType getNodeType() {
108    return nodeType;
109  }
110
111  public void setNodeType(NodeType nodeType) {
112    this.nodeType = nodeType;
113  }
114
115  public String getName() {
116    return name;
117  }
118
119  public XhtmlNode setName(String name) {
120    assert name.contains(":") == false : "Name should not contain any : but was " + name;
121    this.name = name;
122    return this;
123  }
124
125  public Map<String, String> getAttributes() {
126    return attributes;
127  }
128
129  public XhtmlNodeList getChildNodes() {
130    return childNodes;
131  }
132
133  public String getContent() {
134    return content;
135  }
136
137  public XhtmlNode setContent(String content) {
138    if (!(nodeType != NodeType.Text || nodeType != NodeType.Comment)) 
139      throw new Error("Wrong node type");
140    this.content = content;
141    return this;
142  }
143
144  public void validate(List<String> errors, String path, boolean inResource, boolean inPara, boolean inLink) {
145    if (nodeType == NodeType.Element || nodeType == NodeType.Document) {
146      path = Utilities.noString(path) ? name : path+"/"+name;
147      if (inResource) {
148        if (!Utilities.existsInList(name, "p", "br", "div", "h1", "h2", "h3", "h4", "h5", "h6", "a", "span", "b", "em", "i", "strong",
149            "small", "big", "tt", "small", "dfn", "q", "var", "abbr", "acronym", "cite", "blockquote", "hr", "address", "bdo", "kbd", "q", "sub", "sup",
150            "ul", "ol", "li", "dl", "dt", "dd", "pre", "table", "caption", "colgroup", "col", "thead", "tr", "tfoot", "tbody", "th", "td",
151            "code", "samp", "img", "map", "area")) {
152          errors.add("Error at "+path+": Found "+name+" in a resource");          
153        }      
154        for (String an : attributes.keySet()) {
155          boolean ok = an.startsWith("xmlns") || Utilities.existsInList(an,
156              "title", "style", "class", "ID", "lang", "xml:lang", "dir", "accesskey", "tabindex",
157              // tables
158              "span", "width", "align", "valign", "char", "charoff", "abbr", "axis", "headers", "scope", "rowspan", "colspan") ||
159              Utilities.existsInList(name + "." + an, "a.href", "a.name", "img.src", "img.border", "div.xmlns", "blockquote.cite", "q.cite",
160                  "a.charset", "a.type", "a.name", "a.href", "a.hreflang", "a.rel", "a.rev", "a.shape", "a.coords", "img.src",
161                  "img.alt", "img.longdesc", "img.height", "img.width", "img.usemap", "img.ismap", "map.name", "area.shape",
162                  "area.coords", "area.href", "area.nohref", "area.alt", "table.summary", "table.width", "table.border",
163                  "table.frame", "table.rules", "table.cellspacing", "table.cellpadding", "pre.space", "td.nowrap"
164                  );
165          if (!ok)
166            errors.add("Error at "+path+": Found attribute "+name+"."+an+" in a resource");          
167        }
168      }
169      if (inPara && Utilities.existsInList(name, "div",  "blockquote", "table", "ol", "ul", "p")) {
170        errors.add("Error at "+path+": Found "+name+" inside an html paragraph");
171      }
172      if (inLink && Utilities.existsInList(name, "a")) {
173        errors.add("Error at "+path+": Found an <a> inside an <a> paragraph");
174      }
175
176      if (childNodes != null) {
177        if ("p".equals(name)) {
178          inPara = true;
179        }
180        if ("a".equals(name)) {
181          inLink = true;
182        }
183        for (XhtmlNode child : childNodes) {
184          child.validate(errors, path, inResource, inPara, inLink);
185        }
186      }
187    }
188  }
189  
190  public XhtmlNode addTag(String name)
191  {
192
193    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document))  {
194      throw new Error("Wrong node type - node is "+nodeType.toString()+" ('"+getName()+"/"+getContent()+"')");
195    }
196    
197//    if (inPara && name.equals("p")) {
198//      throw new FHIRException("nested Para");
199//    }
200//    if (inLink && name.equals("a")) {
201//      throw new FHIRException("Nested Link");
202//    }
203    XhtmlNode node = new XhtmlNode(NodeType.Element);
204    node.setName(name);
205    if (childNodes.isInPara() || name.equals("p")) {
206      node.getChildNodes().setInPara(true);
207    }
208    if (childNodes.isInLink() || name.equals("a")) {
209      node.getChildNodes().setInLink(true);
210    }
211    childNodes.add(node);
212    return node;
213  }
214
215  public XhtmlNode addTag(int index, String name)
216  {
217
218    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
219      throw new Error("Wrong node type. is "+nodeType.toString());
220    XhtmlNode node = new XhtmlNode(NodeType.Element);
221    if (childNodes.isInPara() || name.equals("p")) {
222      node.getChildNodes().setInPara(true);
223    }
224    if (childNodes.isInLink() || name.equals("a")) {
225      node.getChildNodes().setInLink(true);
226    }
227    node.setName(name);
228    childNodes.add(index, node);
229    return node;
230  }
231
232  public XhtmlNode addComment(String content)
233  {
234    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
235      throw new Error("Wrong node type");
236    XhtmlNode node = new XhtmlNode(NodeType.Comment);
237    node.setContent(content);
238    childNodes.add(node);
239    return node;
240  }
241
242  public XhtmlNode addDocType(String content)
243  {
244    if (!(nodeType == NodeType.Document)) 
245      throw new Error("Wrong node type");
246    XhtmlNode node = new XhtmlNode(NodeType.DocType);
247    node.setContent(content);
248    childNodes.add(node);
249    return node;
250  }
251
252  public XhtmlNode addInstruction(String content)
253  {
254    if (!(nodeType == NodeType.Document)) 
255      throw new Error("Wrong node type");
256    XhtmlNode node = new XhtmlNode(NodeType.Instruction);
257    node.setContent(content);
258    childNodes.add(node);
259    return node;
260  }
261  public XhtmlNode addText(String content)
262  {
263    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
264      throw new Error("Wrong node type");
265    if (content != null) {
266      XhtmlNode node = new XhtmlNode(NodeType.Text);
267      node.setContent(content);
268      childNodes.add(node);
269      return node;
270    } else 
271      return null;
272  }
273
274  public XhtmlNode addText(int index, String content)
275  {
276    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
277      throw new Error("Wrong node type");
278    if (content == null)
279      throw new Error("Content cannot be null");
280
281    XhtmlNode node = new XhtmlNode(NodeType.Text);
282    node.setContent(content);
283    childNodes.add(index, node);
284    return node;
285  }
286
287  public boolean allChildrenAreText()
288  {
289    boolean res = true;
290    for (XhtmlNode n : childNodes)
291      res = res && n.getNodeType() == NodeType.Text;
292    return res;
293  }
294
295  public XhtmlNode getElement(String name) {
296    for (XhtmlNode n : childNodes)
297      if (n.getNodeType() == NodeType.Element && name.equals(n.getName())) 
298        return n;
299    return null;
300  }
301
302  public XhtmlNode getFirstElement() {
303    for (XhtmlNode n : childNodes)
304      if (n.getNodeType() == NodeType.Element) 
305        return n;
306    return null;
307  }
308
309  public String allText() {
310    if (childNodes == null || childNodes.isEmpty())
311      return getContent();
312    
313    StringBuilder b = new StringBuilder();
314    for (XhtmlNode n : childNodes)
315      if (n.getNodeType() == NodeType.Text)
316        b.append(n.getContent());
317      else if (n.getNodeType() == NodeType.Element)
318        b.append(n.allText());
319    return b.toString();
320  }
321
322  public XhtmlNode attribute(String name, String value) {
323    if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 
324      throw new Error("Wrong node type");
325    if (name == null)
326      throw new Error("name is null");
327    if (value == null)
328      throw new Error("value is null");
329    attributes.put(name, value);
330    return this;
331  }
332
333  public boolean hasAttribute(String name) {
334    return getAttributes().containsKey(name);
335  }
336
337  public String getAttribute(String name) {
338    return getAttributes().get(name);
339  }
340
341  public XhtmlNode setAttribute(String name, String value) {
342    if (nodeType != NodeType.Element) {
343      throw new Error("Attempt to set an attribute on something that is not an element");
344    }
345    getAttributes().put(name, value);
346    return this;    
347  }
348
349  public XhtmlNode copy() {
350    XhtmlNode dst = new XhtmlNode(nodeType);
351    dst.name = name;
352    for (String n : attributes.keySet()) {
353      dst.attributes.put(n, attributes.get(n));
354    }
355    for (XhtmlNode n : childNodes)
356      dst.childNodes.add(n.copy());
357    dst.content = content;
358    return dst;
359  }
360
361  @Override
362  public boolean isEmpty() {
363    return (childNodes == null || childNodes.isEmpty()) && content == null;
364  }
365
366  public boolean equalsDeep(XhtmlNode other) {
367    if (other == null) {
368      return false;
369    }
370
371    if (!(nodeType == other.nodeType) || !compare(name, other.name) || !compare(content, other.content))
372      return false;
373    if (attributes.size() != other.attributes.size())
374      return false;
375    for (String an : attributes.keySet())
376      if (!attributes.get(an).equals(other.attributes.get(an)))
377        return false;
378    if (childNodes.size() != other.childNodes.size())
379      return false;
380    for (int i = 0; i < childNodes.size(); i++) {
381      if (!compareDeep(childNodes.get(i), other.childNodes.get(i)))
382        return false;
383    }
384    return true;
385  }
386
387  private boolean compare(String s1, String s2) {
388    if (s1 == null && s2 == null)
389      return true;
390    if (s1 == null || s2 == null)
391      return false;
392    return s1.equals(s2);
393  }
394
395  private static boolean compareDeep(XhtmlNode e1, XhtmlNode e2) {
396    if (e1 == null && e2 == null)
397      return true;
398    if (e1 == null || e2 == null)
399      return false;
400    return e1.equalsDeep(e2);
401  }
402
403  public String getNsDecl() {
404    for (String an : attributes.keySet()) {
405      if (an.equals("xmlns")) {
406        return attributes.get(an);
407      }
408    }
409    return null;
410  }
411
412
413  public Boolean getEmptyExpanded() {
414    return emptyExpanded;
415  }
416
417  public boolean hasEmptyExpanded() {
418    return emptyExpanded != null;
419  }
420
421  public void setEmptyExpanded(Boolean emptyExpanded) {
422    this.emptyExpanded = emptyExpanded;
423  }
424
425
426  @Override
427  public String getValueAsString() {
428    if (isEmpty()) {
429      return null;
430    }
431    try {
432      String retVal = new XhtmlComposer(XhtmlComposer.XML).compose(this);
433      retVal = XhtmlDt.preprocessXhtmlNamespaceDeclaration(retVal);
434      return retVal;
435    } catch (Exception e) {
436      // TODO: composer shouldn't throw exception like this
437      throw new RuntimeException(e);
438    }
439  }
440
441  @Override
442  public void setValueAsString(String theValue) throws IllegalArgumentException {
443    this.attributes = null;
444    this.childNodes = null;
445    this.content = null;
446    this.name = null;
447    this.nodeType= null;
448    if (theValue == null || theValue.length() == 0) {
449      return;
450    }
451
452    String val = theValue.trim();
453
454    if (!val.startsWith("<")) {
455      val = "<div" + DECL_XMLNS +">" + val + "</div>";
456    }
457    if (val.startsWith("<?") && val.endsWith("?>")) {
458      return;
459    }
460
461    val = XhtmlDt.preprocessXhtmlNamespaceDeclaration(val);
462
463    try {
464      XhtmlDocument fragment = new XhtmlParser().parse(val, "div");
465      this.attributes = fragment.getAttributes();
466      this.childNodes = fragment.getChildNodes();
467      // Strip the <? .. ?> declaration if one was present
468      if (childNodes.size() > 0 && childNodes.get(0) != null && childNodes.get(0).getNodeType() == NodeType.Instruction) {
469        childNodes.remove(0);
470      }
471      this.content = fragment.getContent();
472      this.name = fragment.getName();
473      this.nodeType= fragment.getNodeType();
474    } catch (Exception e) {
475      // TODO: composer shouldn't throw exception like this
476      throw new RuntimeException(e);
477    }
478
479  }
480
481  public XhtmlNode getElementByIndex(int i) {
482    int c = 0;
483    for (XhtmlNode n : childNodes)
484      if (n.getNodeType() == NodeType.Element) {
485        if (c == i)
486          return n;
487        else
488          c++;
489      }
490    return null;
491  }
492
493  @Override
494  public String getValue() {
495    return getValueAsString();
496  }
497
498  public boolean hasValue() {
499    return isNotBlank(getValueAsString());
500  }
501
502  @Override
503  public XhtmlNode setValue(String theValue) throws IllegalArgumentException {
504    setValueAsString(theValue);
505    return this;
506  }
507
508  /**
509   * Returns false
510   */
511  public boolean hasFormatComment() {
512    return false;
513  }
514
515  /**
516   * NOT SUPPORTED - Throws {@link UnsupportedOperationException}
517   */
518  public List<String> getFormatCommentsPre() {
519    throw new UnsupportedOperationException();
520  }
521
522  /**
523   * NOT SUPPORTED - Throws {@link UnsupportedOperationException}
524   */
525  public List<String> getFormatCommentsPost() {
526    throw new UnsupportedOperationException();
527  }
528
529  /**
530   * NOT SUPPORTED - Throws {@link UnsupportedOperationException}
531   */
532  public Object getUserData(String theName) {
533    throw new UnsupportedOperationException();
534  }
535
536  /**
537   * NOT SUPPORTED - Throws {@link UnsupportedOperationException}
538   */
539  public void setUserData(String theName, Object theValue) {
540    throw new UnsupportedOperationException();
541  }
542
543
544  public Location getLocation() {
545    return location;
546  }
547
548
549  public void setLocation(Location location) {
550    this.location = location;
551  }
552
553  // xhtml easy adders -----------------------------------------------
554  
555
556  @Override
557  public String toString() {
558    switch (nodeType) {
559    case Document: 
560    case Element:
561      try {
562        return new XhtmlComposer(XhtmlComposer.HTML).compose(this);
563      } catch (IOException e) {
564        return super.toString();
565      }
566    case Text:
567      return this.content;
568    case Comment:
569      return "<!-- "+this.content+" -->";
570    case DocType: 
571      return "<? "+this.content+" />";
572    case Instruction:
573      return "<? "+this.content+" />";
574    }
575    return super.toString();
576  }
577
578
579  public XhtmlNode getNextElement(XhtmlNode c) {
580    boolean f = false;
581    for (XhtmlNode n : childNodes) {
582      if (n == c)
583        f = true;
584      else if (f && n.getNodeType() == NodeType.Element) 
585        return n;
586    }
587    return null;
588  }
589
590
591  public XhtmlNode notPretty() {
592    notPretty = true;
593    return this;
594  }
595
596
597  public boolean isNoPretty() {
598    return notPretty;
599  }
600
601
602  public XhtmlNode style(String style) {
603    if (hasAttribute("style")) {
604      setAttribute("style", getAttribute("style")+"; "+style);
605    } else {
606      setAttribute("style", style);
607    }
608    return this;
609  }
610
611
612  public XhtmlNode nbsp() {
613    addText(NBSP);
614    return this;
615  }
616
617
618  public XhtmlNode para(String text) {
619    XhtmlNode p = para();
620    p.addText(text);
621    return p;
622    
623  }
624
625  public XhtmlNode add(XhtmlNode n) {
626    getChildNodes().add(n);
627    return this;
628  }
629
630
631  public XhtmlNode addChildren(List<XhtmlNode> children) {
632    getChildNodes().addAll(children);
633    return this;
634  }
635
636  public XhtmlNode addChildren(XhtmlNode x) {
637    if (x != null) {
638      getChildNodes().addAll(x.getChildNodes());
639    }
640    return this;
641  }
642
643
644  public XhtmlNode input(String name, String type, String placeholder, int size) {
645    XhtmlNode p = new XhtmlNode(NodeType.Element, "input");
646    p.attribute("name", name);
647    p.attribute("type", type);
648    p.attribute("placeholder", placeholder);
649    p.attribute("size", Integer.toString(size));
650    getChildNodes().add(p);
651    return p;
652  }
653
654  public XhtmlNode select(String name) {
655    XhtmlNode p = new XhtmlNode(NodeType.Element, "select");
656    p.attribute("name", name);
657    p.attribute("size", "1");
658    getChildNodes().add(p);
659    return p;
660  }
661  
662  public XhtmlNode option(String value, String text, boolean selected) {
663    XhtmlNode p = new XhtmlNode(NodeType.Element, "option");
664    p.attribute("value", value);
665    p.attribute("selected", Boolean.toString(selected));
666    p.tx(text);
667    getChildNodes().add(p);
668    return p;
669  }
670
671
672  public XhtmlNode remove(XhtmlNode x) {
673    getChildNodes().remove(x);
674    return this;
675    
676  }
677
678
679  public void clear() {
680    getChildNodes().clear();
681    
682  }
683
684
685  public XhtmlNode backgroundColor(String color) {
686    style("background-color: "+color);
687    return this;
688  }
689
690
691  public boolean isPara() {
692    return "p".equals(name);
693  }
694
695  public XhtmlNode sep(String separator) {
696    // if there's already text, add the separator. otherwise, we'll add it next time
697    if (!seperated) {
698      seperated = true;
699      return this;
700    }
701    return tx(separator);
702  }
703
704
705  // more fluent
706  
707  public XhtmlNode colspan(String n) {
708    return setAttribute("colspan", n);
709  }
710  
711  // differs from tx because it returns the owner node, not the created text
712  public XhtmlNode txN(String cnt) {
713    addText(cnt);
714    return this;
715  }
716
717
718  @Override
719  protected void addChildren(XhtmlNodeList childNodes) {
720    this.childNodes.addAll(childNodes);    
721  }
722
723  
724  
725}