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 078 public XhtmlNode() { 079 super(); 080 } 081 082 083 public XhtmlNode(NodeType nodeType, String name) { 084 super(); 085 this.nodeType = nodeType; 086 this.name = name; 087 } 088 089 public XhtmlNode(NodeType nodeType) { 090 super(); 091 this.nodeType = nodeType; 092 } 093 094 public NodeType getNodeType() { 095 return nodeType; 096 } 097 098 public void setNodeType(NodeType nodeType) { 099 this.nodeType = nodeType; 100 } 101 102 public String getName() { 103 return name; 104 } 105 106 public XhtmlNode setName(String name) { 107 assert name.contains(":") == false : "Name should not contain any : but was " + name; 108 this.name = name; 109 return this; 110 } 111 112 public Map<String, String> getAttributes() { 113 return attributes; 114 } 115 116 public List<XhtmlNode> getChildNodes() { 117 return childNodes; 118 } 119 120 public String getContent() { 121 return content; 122 } 123 124 public XhtmlNode setContent(String content) { 125 if (!(nodeType != NodeType.Text || nodeType != NodeType.Comment)) 126 throw new Error("Wrong node type"); 127 this.content = content; 128 return this; 129 } 130 131 public XhtmlNode addTag(String name) 132 { 133 134 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 135 throw new Error("Wrong node type. is "+nodeType.toString()); 136 XhtmlNode node = new XhtmlNode(NodeType.Element); 137 node.setName(name); 138 childNodes.add(node); 139 return node; 140 } 141 142 public XhtmlNode addTag(int index, String name) 143 { 144 145 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 146 throw new Error("Wrong node type. is "+nodeType.toString()); 147 XhtmlNode node = new XhtmlNode(NodeType.Element); 148 node.setName(name); 149 childNodes.add(index, node); 150 return node; 151 } 152 153 public XhtmlNode addComment(String content) 154 { 155 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 156 throw new Error("Wrong node type"); 157 XhtmlNode node = new XhtmlNode(NodeType.Comment); 158 node.setContent(content); 159 childNodes.add(node); 160 return node; 161 } 162 163 public XhtmlNode addDocType(String content) 164 { 165 if (!(nodeType == NodeType.Document)) 166 throw new Error("Wrong node type"); 167 XhtmlNode node = new XhtmlNode(NodeType.DocType); 168 node.setContent(content); 169 childNodes.add(node); 170 return node; 171 } 172 173 public XhtmlNode addInstruction(String content) 174 { 175 if (!(nodeType == NodeType.Document)) 176 throw new Error("Wrong node type"); 177 XhtmlNode node = new XhtmlNode(NodeType.Instruction); 178 node.setContent(content); 179 childNodes.add(node); 180 return node; 181 } 182 183 184 185 186 public XhtmlNode addText(String content) 187 { 188 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 189 throw new Error("Wrong node type"); 190 if (content != null) { 191 XhtmlNode node = new XhtmlNode(NodeType.Text); 192 node.setContent(content); 193 childNodes.add(node); 194 return node; 195 } else 196 return null; 197 } 198 199 public XhtmlNode addText(int index, String content) 200 { 201 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 202 throw new Error("Wrong node type"); 203 if (content == null) 204 throw new Error("Content cannot be null"); 205 206 XhtmlNode node = new XhtmlNode(NodeType.Text); 207 node.setContent(content); 208 childNodes.add(index, node); 209 return node; 210 } 211 212 public boolean allChildrenAreText() 213 { 214 boolean res = true; 215 for (XhtmlNode n : childNodes) 216 res = res && n.getNodeType() == NodeType.Text; 217 return res; 218 } 219 220 public XhtmlNode getElement(String name) { 221 for (XhtmlNode n : childNodes) 222 if (n.getNodeType() == NodeType.Element && name.equals(n.getName())) 223 return n; 224 return null; 225 } 226 227 public XhtmlNode getFirstElement() { 228 for (XhtmlNode n : childNodes) 229 if (n.getNodeType() == NodeType.Element) 230 return n; 231 return null; 232 } 233 234 public String allText() { 235 if (childNodes == null || childNodes.isEmpty()) 236 return getContent(); 237 238 StringBuilder b = new StringBuilder(); 239 for (XhtmlNode n : childNodes) 240 if (n.getNodeType() == NodeType.Text) 241 b.append(n.getContent()); 242 else if (n.getNodeType() == NodeType.Element) 243 b.append(n.allText()); 244 return b.toString(); 245 } 246 247 public XhtmlNode attribute(String name, String value) { 248 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 249 throw new Error("Wrong node type"); 250 if (name == null) 251 throw new Error("name is null"); 252 if (value == null) 253 throw new Error("value is null"); 254 attributes.put(name, value); 255 return this; 256 } 257 258 public boolean hasAttribute(String name) { 259 return getAttributes().containsKey(name); 260 } 261 262 public String getAttribute(String name) { 263 return getAttributes().get(name); 264 } 265 266 public XhtmlNode setAttribute(String name, String value) { 267 getAttributes().put(name, value); 268 return this; 269 } 270 271 public XhtmlNode copy() { 272 XhtmlNode dst = new XhtmlNode(nodeType); 273 dst.name = name; 274 for (String n : attributes.keySet()) { 275 dst.attributes.put(n, attributes.get(n)); 276 } 277 for (XhtmlNode n : childNodes) 278 dst.childNodes.add(n.copy()); 279 dst.content = content; 280 return dst; 281 } 282 283 @Override 284 public boolean isEmpty() { 285 return (childNodes == null || childNodes.isEmpty()) && content == null; 286 } 287 288 public boolean equalsDeep(XhtmlNode other) { 289 if (other == null) { 290 return false; 291 } 292 293 if (!(nodeType == other.nodeType) || !compare(name, other.name) || !compare(content, other.content)) 294 return false; 295 if (attributes.size() != other.attributes.size()) 296 return false; 297 for (String an : attributes.keySet()) 298 if (!attributes.get(an).equals(other.attributes.get(an))) 299 return false; 300 if (childNodes.size() != other.childNodes.size()) 301 return false; 302 for (int i = 0; i < childNodes.size(); i++) { 303 if (!compareDeep(childNodes.get(i), other.childNodes.get(i))) 304 return false; 305 } 306 return true; 307 } 308 309 private boolean compare(String s1, String s2) { 310 if (s1 == null && s2 == null) 311 return true; 312 if (s1 == null || s2 == null) 313 return false; 314 return s1.equals(s2); 315 } 316 317 private static boolean compareDeep(XhtmlNode e1, XhtmlNode e2) { 318 if (e1 == null && e2 == null) 319 return true; 320 if (e1 == null || e2 == null) 321 return false; 322 return e1.equalsDeep(e2); 323 } 324 325 public String getNsDecl() { 326 for (String an : attributes.keySet()) { 327 if (an.equals("xmlns")) { 328 return attributes.get(an); 329 } 330 } 331 return null; 332 } 333 334 335 @Override 336 public String getValueAsString() { 337 if (isEmpty()) { 338 return null; 339 } 340 try { 341 String retVal = new XhtmlComposer(XhtmlComposer.HTML).compose(this); 342 retVal = XhtmlDt.preprocessXhtmlNamespaceDeclaration(retVal); 343 return retVal; 344 } catch (Exception e) { 345 // TODO: composer shouldn't throw exception like this 346 throw new RuntimeException(e); 347 } 348 } 349 350 @Override 351 public void setValueAsString(String theValue) throws IllegalArgumentException { 352 this.attributes = null; 353 this.childNodes = null; 354 this.content = null; 355 this.name = null; 356 this.nodeType= null; 357 if (theValue == null || theValue.length() == 0) { 358 return; 359 } 360 361 String val = theValue.trim(); 362 363 if (!val.startsWith("<")) { 364 val = "<div" + DECL_XMLNS +">" + val + "</div>"; 365 } 366 if (val.startsWith("<?") && val.endsWith("?>")) { 367 return; 368 } 369 370 val = XhtmlDt.preprocessXhtmlNamespaceDeclaration(val); 371 372 try { 373 // TODO: this is ugly 374 XhtmlNode fragment = new XhtmlParser().parseFragment(val); 375 this.attributes = fragment.attributes; 376 this.childNodes = fragment.childNodes; 377 this.content = fragment.content; 378 this.name = fragment.name; 379 this.nodeType= fragment.nodeType; 380 } catch (Exception e) { 381 // TODO: composer shouldn't throw exception like this 382 throw new RuntimeException(e); 383 } 384 385 } 386 387 public XhtmlNode getElementByIndex(int i) { 388 int c = 0; 389 for (XhtmlNode n : childNodes) 390 if (n.getNodeType() == NodeType.Element) { 391 if (c == i) 392 return n; 393 else 394 c++; 395 } 396 return null; 397 } 398 399 @Override 400 public String getValue() { 401 return getValueAsString(); 402 } 403 404 @Override 405 public XhtmlNode setValue(String theValue) throws IllegalArgumentException { 406 setValueAsString(theValue); 407 return this; 408 } 409 410 /** 411 * Returns false 412 */ 413 public boolean hasFormatComment() { 414 return false; 415 } 416 417 /** 418 * NOT SUPPORTED - Throws {@link UnsupportedOperationException} 419 */ 420 public List<String> getFormatCommentsPre() { 421 throw new UnsupportedOperationException(); 422 } 423 424 /** 425 * NOT SUPPORTED - Throws {@link UnsupportedOperationException} 426 */ 427 public List<String> getFormatCommentsPost() { 428 throw new UnsupportedOperationException(); 429 } 430 431 432 public Location getLocation() { 433 return location; 434 } 435 436 437 public void setLocation(Location location) { 438 this.location = location; 439 } 440 441 // xhtml easy adders ----------------------------------------------- 442 public XhtmlNode h1() { 443 return addTag("h1"); 444 } 445 446 public XhtmlNode h2() { 447 return addTag("h2"); 448 } 449 450 public XhtmlNode h3() { 451 return addTag("h3"); 452 } 453 454 public XhtmlNode h4() { 455 return addTag("h4"); 456 } 457 458 public XhtmlNode table(String clss) { 459 XhtmlNode res = addTag("table"); 460 if (!Utilities.noString(clss)) 461 res.setAttribute("class", clss); 462 return res; 463 } 464 465 public XhtmlNode tr() { 466 return addTag("tr"); 467 } 468 469 public XhtmlNode th() { 470 return addTag("th"); 471 } 472 473 public XhtmlNode td() { 474 return addTag("td"); 475 } 476 477 public XhtmlNode colspan(String n) { 478 return setAttribute("colspan", n); 479 } 480 481 public XhtmlNode para() { 482 return addTag("p"); 483 } 484 485 public XhtmlNode pre() { 486 return addTag("pre"); 487 } 488 489 public void br() { 490 addTag("br"); 491 } 492 493 public void hr() { 494 addTag("hr"); 495 } 496 497 public XhtmlNode ul() { 498 return addTag("ul"); 499 } 500 501 public XhtmlNode li() { 502 return addTag("li"); 503 } 504 505 public XhtmlNode b() { 506 return addTag("b"); 507 } 508 509 public XhtmlNode i() { 510 return addTag("i"); 511 } 512 public XhtmlNode tx(String cnt) { 513 return addText(cnt); 514 } 515 public XhtmlNode ah(String href) { 516 return addTag("a").attribute("href", href); 517 } 518 519 public void an(String href) { 520 addTag("a").attribute("name", href).tx(" "); 521 } 522 523 public XhtmlNode span(String style, String title) { 524 XhtmlNode res = addTag("span"); 525 if (!Utilities.noString(style)) 526 res.attribute("style", style); 527 if (!Utilities.noString(title)) 528 res.attribute("title", title); 529 return res; 530 } 531 532 533 public void code(String text) { 534 addTag("code").tx(text); 535 } 536 537 538 public XhtmlNode blockquote() { 539 return addTag("blockquote"); 540 } 541 542 543 @Override 544 public String toString() { 545 switch (nodeType) { 546 case Document: 547 case Element: 548 try { 549 return new XhtmlComposer(XhtmlComposer.HTML).compose(this); 550 } catch (IOException e) { 551 return super.toString(); 552 } 553 case Text: 554 return this.content; 555 case Comment: 556 return "<!-- "+this.content+" -->"; 557 case DocType: 558 return "<? "+this.content+" />"; 559 case Instruction: 560 return "<? "+this.content+" />"; 561 } 562 return super.toString(); 563 } 564 565 566 public XhtmlNode getNextElement(XhtmlNode c) { 567 boolean f = false; 568 for (XhtmlNode n : childNodes) { 569 if (n == c) 570 f = true; 571 else if (f && n.getNodeType() == NodeType.Element) 572 return n; 573 } 574 return null; 575 } 576 577}