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}