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}