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}