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