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 * HAPI FHIR - Core Library 034 * %% 035 * Copyright (C) 2014 - 2017 University Health Network 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 static org.apache.commons.lang3.StringUtils.isBlank; 053 054import java.util.ArrayList; 055import java.util.HashMap; 056import java.util.List; 057import java.util.Map; 058 059import org.hl7.fhir.instance.model.api.IBaseXhtml; 060 061import ca.uhn.fhir.model.primitive.XhtmlDt; 062 063@ca.uhn.fhir.model.api.annotation.DatatypeDef(name="xhtml") 064public class XhtmlNode implements IBaseXhtml { 065 private static final long serialVersionUID = -4362547161441436492L; 066 067 068 public static class Location { 069 private int line; 070 private int column; 071 public Location(int line, int column) { 072 super(); 073 this.line = line; 074 this.column = column; 075 } 076 public int getLine() { 077 return line; 078 } 079 public int getColumn() { 080 return column; 081 } 082 @Override 083 public String toString() { 084 return "Line "+Integer.toString(line)+", column "+Integer.toString(column); 085 } 086 } 087 088 public static final String NBSP = Character.toString((char)0xa0); 089 private static final String DECL_XMLNS = " xmlns=\"http://www.w3.org/1999/xhtml\""; 090 091 092 private Location location; 093 private NodeType nodeType; 094 private String name; 095 private Map<String, String> attributes = new HashMap<String, String>(); 096 private List<XhtmlNode> childNodes = new ArrayList<XhtmlNode>(); 097 private String content; 098 099 public XhtmlNode() { 100 super(); 101 } 102 103 104 public XhtmlNode(NodeType nodeType, String name) { 105 super(); 106 this.nodeType = nodeType; 107 this.name = name; 108 } 109 110 public XhtmlNode(NodeType nodeType) { 111 super(); 112 this.nodeType = nodeType; 113 } 114 115 public NodeType getNodeType() { 116 return nodeType; 117 } 118 119 public void setNodeType(NodeType nodeType) { 120 this.nodeType = nodeType; 121 } 122 123 public String getName() { 124 return name; 125 } 126 127 public void setName(String name) { 128 assert name.contains(":") == false : "Name should not contain any : but was " + name; 129 this.name = name; 130 } 131 132 public Map<String, String> getAttributes() { 133 return attributes; 134 } 135 136 public List<XhtmlNode> getChildNodes() { 137 return childNodes; 138 } 139 140 public String getContent() { 141 return content; 142 } 143 144 public XhtmlNode setContent(String content) { 145 if (!(nodeType != NodeType.Text || nodeType != NodeType.Comment)) 146 throw new Error("Wrong node type"); 147 this.content = content; 148 return this; 149 } 150 151 public XhtmlNode addTag(String name) 152 { 153 154 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 155 throw new Error("Wrong node type. is "+nodeType.toString()); 156 XhtmlNode node = new XhtmlNode(NodeType.Element); 157 node.setName(name); 158 childNodes.add(node); 159 return node; 160 } 161 162 public XhtmlNode addTag(int index, String name) 163 { 164 165 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 166 throw new Error("Wrong node type. is "+nodeType.toString()); 167 XhtmlNode node = new XhtmlNode(NodeType.Element); 168 node.setName(name); 169 childNodes.add(index, node); 170 return node; 171 } 172 173 public XhtmlNode addComment(String content) 174 { 175 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 176 throw new Error("Wrong node type"); 177 XhtmlNode node = new XhtmlNode(NodeType.Comment); 178 node.setContent(content); 179 childNodes.add(node); 180 return node; 181 } 182 183 public XhtmlNode addDocType(String content) 184 { 185 if (!(nodeType == NodeType.Document)) 186 throw new Error("Wrong node type"); 187 XhtmlNode node = new XhtmlNode(NodeType.DocType); 188 node.setContent(content); 189 childNodes.add(node); 190 return node; 191 } 192 193 public XhtmlNode addInstruction(String content) 194 { 195 if (!(nodeType == NodeType.Document)) 196 throw new Error("Wrong node type"); 197 XhtmlNode node = new XhtmlNode(NodeType.Instruction); 198 node.setContent(content); 199 childNodes.add(node); 200 return node; 201 } 202 203 204 205 206 public XhtmlNode addText(String content) 207 { 208 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 209 throw new Error("Wrong node type"); 210 if (content != null) { 211 XhtmlNode node = new XhtmlNode(NodeType.Text); 212 node.setContent(content); 213 childNodes.add(node); 214 return node; 215 } 216 return null; 217 } 218 219 public XhtmlNode addText(int index, String content) 220 { 221 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 222 throw new Error("Wrong node type"); 223 if (content == null) 224 throw new Error("Content cannot be null"); 225 226 XhtmlNode node = new XhtmlNode(NodeType.Text); 227 node.setContent(content); 228 childNodes.add(index, node); 229 return node; 230 } 231 232 public boolean allChildrenAreText() 233 { 234 boolean res = true; 235 for (XhtmlNode n : childNodes) 236 res = res && n.getNodeType() == NodeType.Text; 237 return res; 238 } 239 240 public XhtmlNode getElement(String name) 241 { 242 for (XhtmlNode n : childNodes) 243 if (n.getNodeType() == NodeType.Element && name.equals(n.getName())) 244 return n; 245 return null; 246 } 247 248 public XhtmlNode getFirstElement() 249 { 250 for (XhtmlNode n : childNodes) 251 if (n.getNodeType() == NodeType.Element) 252 return n; 253 return null; 254 } 255 256 public String allText() { 257 StringBuilder b = new StringBuilder(); 258 for (XhtmlNode n : childNodes) 259 if (n.getNodeType() == NodeType.Text) 260 b.append(n.getContent()); 261 else if (n.getNodeType() == NodeType.Element) 262 b.append(n.allText()); 263 return b.toString(); 264 } 265 266 public XhtmlNode attribute(String name, String value) { 267 if (!(nodeType == NodeType.Element || nodeType == NodeType.Document)) 268 throw new Error("Wrong node type"); 269 if (name == null) 270 throw new Error("name is null"); 271 if (value == null) 272 throw new Error("value is null"); 273 attributes.put(name, value); 274 return this; 275 } 276 277 public boolean hasAttribute(String name) { 278 return getAttributes().containsKey(name); 279 } 280 281 public String getAttribute(String name) { 282 return getAttributes().get(name); 283 } 284 285 public XhtmlNode setAttribute(String name, String value) { 286 getAttributes().put(name, value); 287 return this; 288 } 289 290 public XhtmlNode copy() { 291 XhtmlNode dst = new XhtmlNode(nodeType); 292 dst.name = name; 293 for (String n : attributes.keySet()) { 294 dst.attributes.put(n, attributes.get(n)); 295 } 296 for (XhtmlNode n : childNodes) 297 dst.childNodes.add(n.copy()); 298 dst.content = content; 299 return dst; 300 } 301 302 @Override 303 public boolean isEmpty() { 304 return (childNodes == null || childNodes.isEmpty()) && content == null; 305 } 306 307 public boolean equalsDeep(XhtmlNode other) { 308 if (other == null) { 309 return false; 310 } 311 312 if (!(nodeType == other.nodeType) || !compare(name, other.name) || !compare(content, other.content)) 313 return false; 314 if (attributes.size() != other.attributes.size()) 315 return false; 316 for (String an : attributes.keySet()) 317 if (!attributes.get(an).equals(other.attributes.get(an))) 318 return false; 319 if (childNodes.size() != other.childNodes.size()) 320 return false; 321 for (int i = 0; i < childNodes.size(); i++) { 322 if (!compareDeep(childNodes.get(i), other.childNodes.get(i))) 323 return false; 324 } 325 return true; 326 } 327 328 private boolean compare(String s1, String s2) { 329 if (s1 == null && s2 == null) 330 return true; 331 if (s1 == null || s2 == null) 332 return false; 333 return s1.equals(s2); 334 } 335 336 private static boolean compareDeep(XhtmlNode e1, XhtmlNode e2) { 337 if (e1 == null && e2 == null) 338 return true; 339 if (e1 == null || e2 == null) 340 return false; 341 return e1.equalsDeep(e2); 342 } 343 344 public String getNsDecl() { 345 for (String an : attributes.keySet()) { 346 if (an.equals("xmlns")) { 347 return attributes.get(an); 348 } 349 } 350 return null; 351 } 352 353 354 @Override 355 public String getValueAsString() { 356 if (isEmpty()) { 357 return null; 358 } 359 try { 360 String retVal = new XhtmlComposer().compose(this); 361 retVal = XhtmlDt.preprocessXhtmlNamespaceDeclaration(retVal); 362 return retVal; 363 } catch (Exception e) { 364 // TODO: composer shouldn't throw exception like this 365 throw new RuntimeException(e); 366 } 367 } 368 369 @Override 370 public void setValueAsString(String theValue) throws IllegalArgumentException { 371 this.attributes = null; 372 this.childNodes = null; 373 this.content = null; 374 this.name = null; 375 this.nodeType= null; 376 if (isBlank(theValue)) { 377 return; 378 } 379 380 String val = theValue.trim(); 381 382 if (!val.startsWith("<")) { 383 val = "<div" + DECL_XMLNS +">" + val + "</div>"; 384 } 385 if (val.startsWith("<?") && val.endsWith("?>")) { 386 return; 387 } 388 389 val = XhtmlDt.preprocessXhtmlNamespaceDeclaration(val); 390 391 try { 392 // TODO: this is ugly 393 XhtmlNode fragment = new XhtmlParser().parseFragment(val); 394 this.attributes = fragment.attributes; 395 this.childNodes = fragment.childNodes; 396 this.content = fragment.content; 397 this.name = fragment.name; 398 this.nodeType= fragment.nodeType; 399 } catch (Exception e) { 400 // TODO: composer shouldn't throw exception like this 401 throw new RuntimeException(e); 402 } 403 404 } 405 406 public XhtmlNode getElementByIndex(int i) { 407 int c = 0; 408 for (XhtmlNode n : childNodes) 409 if (n.getNodeType() == NodeType.Element) { 410 if (c == i){ 411 return n; 412 } 413 c++; 414 } 415 return null; 416 } 417 418 @Override 419 public String getValue() { 420 return getValueAsString(); 421 } 422 423 @Override 424 public XhtmlNode setValue(String theValue) throws IllegalArgumentException { 425 setValueAsString(theValue); 426 return this; 427 } 428 429 /** 430 * Returns false 431 */ 432 @Override 433 public boolean hasFormatComment() { 434 return false; 435 } 436 437 /** 438 * NOT SUPPORTED - Throws {@link UnsupportedOperationException} 439 */ 440 @Override 441 public List<String> getFormatCommentsPre() { 442 throw new UnsupportedOperationException(); 443 } 444 445 /** 446 * NOT SUPPORTED - Throws {@link UnsupportedOperationException} 447 */ 448 @Override 449 public List<String> getFormatCommentsPost() { 450 throw new UnsupportedOperationException(); 451 } 452 453 454 public Location getLocation() { 455 return location; 456 } 457 458 459 public void setLocation(Location location) { 460 this.location = location; 461 } 462 463}