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.io.InputStream; 054import java.io.InputStreamReader; 055import java.io.Reader; 056import java.io.StringReader; 057import java.nio.charset.StandardCharsets; 058import java.util.ArrayList; 059import java.util.HashMap; 060import java.util.HashSet; 061import java.util.List; 062import java.util.Map; 063import java.util.Set; 064 065import org.hl7.fhir.exceptions.FHIRException; 066import org.hl7.fhir.exceptions.FHIRFormatError; 067import org.hl7.fhir.utilities.xhtml.XhtmlNode.Location; 068import org.w3c.dom.Attr; 069import org.w3c.dom.Element; 070import org.w3c.dom.Node; 071import org.xmlpull.v1.XmlPullParser; 072import org.xmlpull.v1.XmlPullParserException; 073 074public class XhtmlParser { 075 public static final String XHTML_NS = "http://www.w3.org/1999/xhtml"; 076 077 public class NSMap { 078 private Map<String, String> nslist = new HashMap<String, String>(); 079 080 public NSMap(NSMap nsm) { 081 if (nsm != null) 082 nslist.putAll(nsm.nslist); 083 } 084 085 public void def(String ns) { 086 nslist.put("", ns); 087 } 088 089 public void ns(String abbrev, String ns) { 090 nslist.put(abbrev, ns); 091 } 092 093 public String def() { 094 return nslist.get(""); 095 } 096 097 public boolean hasDef() { 098 return nslist.containsKey(""); 099 } 100 101 public String get(String abbrev) { 102 return nslist.containsKey(abbrev) ? nslist.get(abbrev) : "http://error/undefined-namespace"; 103 } 104 } 105 106 public class QName { 107 private String ns; 108 private String name; 109 110 public QName(String src) { 111 if (src.contains(":")) { 112 ns = src.substring(0, src.indexOf(":")); 113 name = src.substring(src.indexOf(":")+1); 114 } else { 115 ns = null; 116 name = src; 117 } 118 } 119 120 public String getName() { 121 return name; 122 } 123 124 public boolean hasNs() { 125 return ns != null; 126 } 127 128 public String getNs() { 129 return ns; 130 } 131 132 @Override 133 public String toString() { 134 return ns+"::"+name; 135 } 136 137 } 138 139 private Set<String> elements = new HashSet<String>(); 140 private Set<String> attributes = new HashSet<String>(); 141 142 143 public XhtmlParser() { 144 super(); 145 policy = ParserSecurityPolicy.Accept; // for general parsing 146 147 // set up sets 148 elements.add("p"); 149 elements.add("br"); 150 elements.add("div"); 151 elements.add("h1"); 152 elements.add("h2"); 153 elements.add("h3"); 154 elements.add("h4"); 155 elements.add("h5"); 156 elements.add("h6"); 157 elements.add("a"); 158 elements.add("span"); 159 elements.add("b"); 160 elements.add("em"); 161 elements.add("i"); 162 elements.add("strong"); 163 elements.add("small"); 164 elements.add("big"); 165 elements.add("tt"); 166 elements.add("small"); 167 elements.add("dfn"); 168 elements.add("q"); 169 elements.add("var"); 170 elements.add("abbr"); 171 elements.add("acronym"); 172 elements.add("cite"); 173 elements.add("blockquote"); 174 elements.add("hr"); 175 elements.add("address"); 176 elements.add("bdo"); 177 elements.add("kbd"); 178 elements.add("q"); 179 elements.add("sub"); 180 elements.add("sup"); 181 elements.add("ul"); 182 elements.add("ol"); 183 elements.add("li"); 184 elements.add("dl"); 185 elements.add("dt"); 186 elements.add("dd"); 187 elements.add("pre"); 188 elements.add("table"); 189 elements.add("caption"); 190 elements.add("colgroup"); 191 elements.add("col"); 192 elements.add("thead"); 193 elements.add("tr"); 194 elements.add("tfoot"); 195 elements.add("tbody"); 196 elements.add("th"); 197 elements.add("td"); 198 elements.add("code"); 199 elements.add("samp"); 200 elements.add("img"); 201 elements.add("map"); 202 elements.add("area"); 203 204 attributes.add("title"); 205 attributes.add("style"); 206 attributes.add("class"); 207 attributes.add("id"); 208 attributes.add("lang"); 209 attributes.add("xml:lang"); 210 attributes.add("dir"); 211 attributes.add("accesskey"); 212 attributes.add("tabindex"); 213 // tables: 214 attributes.add("span"); 215 attributes.add("width"); 216 attributes.add("align"); 217 attributes.add("valign"); 218 attributes.add("char"); 219 attributes.add("charoff"); 220 attributes.add("abbr"); 221 attributes.add("axis"); 222 attributes.add("headers"); 223 attributes.add("scope"); 224 attributes.add("rowspan"); 225 attributes.add("colspan"); 226 227 attributes.add("a.href"); 228 attributes.add("a.name"); 229 attributes.add("img.src"); 230 attributes.add("img.border"); 231 attributes.add("div.xmlns"); 232 attributes.add("blockquote.cite"); 233 attributes.add("q.cite"); 234 attributes.add("a.charset"); 235 attributes.add("a.type"); 236 attributes.add("a.name"); 237 attributes.add("a.href"); 238 attributes.add("a.hreflang"); 239 attributes.add("a.rel"); 240 attributes.add("a.rev"); 241 attributes.add("a.shape"); 242 attributes.add("a.coords"); 243 attributes.add("img.src"); 244 attributes.add("img.alt"); 245 attributes.add("img.longdesc"); 246 attributes.add("img.height"); 247 attributes.add("img.width"); 248 attributes.add("img.usemap"); 249 attributes.add("img.ismap"); 250 attributes.add("map.name"); 251 attributes.add("area.shape"); 252 attributes.add("area.coords"); 253 attributes.add("area.href"); 254 attributes.add("area.nohref"); 255 attributes.add("area.alt"); 256 attributes.add("table.summary"); 257 attributes.add("table.width"); 258 attributes.add("table.border"); 259 attributes.add("table.frame"); 260 attributes.add("table.rules"); 261 attributes.add("table.cellspacing"); 262 attributes.add("table.cellpadding"); 263} 264 265public enum ParserSecurityPolicy { 266 Accept, 267 Drop, 268 Reject 269 } 270 271 private ParserSecurityPolicy policy; 272 273 private boolean trimWhitespace; 274 private boolean mustBeWellFormed = true; 275 private boolean validatorMode; 276 277 public boolean isTrimWhitespace() { 278 return trimWhitespace; 279 } 280 281 public void setTrimWhitespace(boolean trimWhitespace) { 282 this.trimWhitespace = trimWhitespace; 283 } 284 285 public boolean isMustBeWellFormed() { 286 return mustBeWellFormed; 287 } 288 289 public XhtmlParser setMustBeWellFormed(boolean mustBeWellFormed) { 290 this.mustBeWellFormed = mustBeWellFormed; 291 return this; 292 } 293 294 295 public boolean isValidatorMode() { 296 return validatorMode; 297 } 298 299 public XhtmlParser setValidatorMode(boolean validatorMode) { 300 this.validatorMode = validatorMode; 301 return this; 302 } 303 304 public ParserSecurityPolicy getPolicy() { 305 return policy; 306 } 307 308 public void setPolicy(ParserSecurityPolicy policy) { 309 this.policy = policy; 310 } 311 312 public XhtmlNode parseHtmlNode(Element node) throws FHIRFormatError { 313 return parseHtmlNode(node, null); 314 } 315 316 public XhtmlNode parseHtmlNode(Element node, String defaultNS) throws FHIRFormatError { 317 XhtmlNode res = parseNode(node, defaultNS); 318 if (res.getNsDecl() == null) 319 res.getAttributes().put("xmlns", XHTML_NS); 320 return res; 321 } 322 323 private XhtmlNode parseNode(Element node, String defaultNS) throws FHIRFormatError { 324 XhtmlNode res = new XhtmlNode(NodeType.Element); 325 res.setName(node.getLocalName()); 326 defaultNS = checkNS(res, node, defaultNS); 327 for (int i = 0; i < node.getAttributes().getLength(); i++) { 328 Attr attr = (Attr) node.getAttributes().item(i); 329 if (attributeIsOk(res.getName(), attr.getName(), attr.getValue()) && !attr.getLocalName().startsWith("xmlns")) 330 res.getAttributes().put(attr.getName(), attr.getValue()); 331 } 332 Node child = node.getFirstChild(); 333 while (child != null) { 334 if (child.getNodeType() == Node.TEXT_NODE) { 335 res.addText(child.getTextContent()); 336 } else if (child.getNodeType() == Node.COMMENT_NODE) { 337 res.addComment(child.getTextContent()); 338 } else if (child.getNodeType() == Node.ELEMENT_NODE) { 339 if (elementIsOk(child.getLocalName())) 340 res.getChildNodes().add(parseNode((Element) child, defaultNS)); 341 } else 342 throw new FHIRFormatError("Unhandled XHTML feature: "+Integer.toString(child.getNodeType())+descLoc()); 343 child = child.getNextSibling(); 344 } 345 return res; 346 } 347 348 private String checkNS(XhtmlNode res, Element node, String defaultNS) { 349 if (!validatorMode) 350 return null; 351 String ns = node.getNamespaceURI(); 352 if (ns == null) 353 return null; 354 if (!ns.equals(defaultNS)) { 355 res.getAttributes().put("xmlns", ns); 356 return ns; 357 } 358 return defaultNS; 359 } 360 361 public XhtmlNode parseHtmlNode(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError { 362 XhtmlNode res = parseNode(xpp); 363 if (res.getNsDecl() == null) 364 res.getAttributes().put("xmlns", XHTML_NS); 365 return res; 366 367 } 368 private XhtmlNode parseNode(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError { 369 XhtmlNode res = new XhtmlNode(NodeType.Element); 370 res.setName(xpp.getName()); 371 372 for (int i = 0; i < xpp.getAttributeCount(); i++) { 373 if (attributeIsOk(xpp.getName(), xpp.getAttributeName(i), xpp.getAttributeValue(i))) 374 res.getAttributes().put(xpp.getAttributeName(i), xpp.getAttributeValue(i)); 375 } 376 int eventType = xpp.next(); 377 while (eventType != XmlPullParser.END_TAG) { 378 if (eventType == XmlPullParser.TEXT) { 379 res.addText(xpp.getText()); 380 xpp.next(); 381 } else if (eventType == XmlPullParser.COMMENT) { 382 res.addComment(xpp.getText()); 383 xpp.next(); 384 } else if (eventType == XmlPullParser.START_TAG) { 385 if (elementIsOk(xpp.getName())) 386 res.getChildNodes().add(parseNode(xpp)); 387 } else 388 throw new FHIRFormatError("Unhandled XHTML feature: "+Integer.toString(eventType)+descLoc()); 389 eventType = xpp.getEventType(); 390 } 391 xpp.next(); 392 return res; 393 } 394 395 private boolean attributeIsOk(String elem, String attr, String value) throws FHIRFormatError { 396 if (validatorMode) 397 return true; 398 boolean ok = attributes.contains(attr) || attributes.contains(elem+"."+attr); 399 if (ok) 400 return true; 401 else switch (policy) { 402 case Accept: return true; 403 case Drop: return false; 404 case Reject: throw new FHIRFormatError("Illegal HTML attribute "+elem+"."+attr); 405 } 406 407 if ((elem+"."+attr).equals("img.src") && !(value.startsWith("#") || value.startsWith("http:") || value.startsWith("https:"))) { 408 switch (policy) { 409 case Accept: return true; 410 case Drop: return false; 411 case Reject: throw new FHIRFormatError("Illegal Image Reference "+value); 412 } 413 } 414 return false; 415 } 416 417private boolean elementIsOk(String name) throws FHIRFormatError { 418 if (validatorMode) 419 return true; 420 boolean ok = elements.contains(name); 421 if (ok) 422 return true; 423 else switch (policy) { 424 case Accept: return true; 425 case Drop: return false; 426 case Reject: throw new FHIRFormatError("Illegal HTML element "+name); 427 } 428 return false; 429} 430 431 private String descLoc() { 432 return " at line "+Integer.toString(line)+" column "+Integer.toString(col); 433 } 434 435 private Reader rdr; 436 private String cache = ""; 437 private XhtmlNode unwindPoint; 438 private String lastText = ""; 439 private int line = 1; 440 private int col = 0; 441 private char lastChar; 442 private Location lastLoc; 443 444 public XhtmlDocument parse(String source, String entryName) throws FHIRFormatError, IOException { 445 rdr = new StringReader(source); 446 return parse(entryName); 447 } 448 449 public XhtmlDocument parse(InputStream input, String entryName) throws FHIRFormatError, IOException { 450 rdr = new InputStreamReader(input, StandardCharsets.UTF_8); 451 return parse(entryName); 452 } 453 454 private XhtmlDocument parse(String entryName) throws FHIRFormatError, IOException 455 { 456 XhtmlDocument result = new XhtmlDocument(); 457 skipWhiteSpaceAndComments(result); 458 if (peekChar() != '<') 459 throw new FHIRFormatError("Unable to Parse HTML - does not start with tag. Found "+peekChar()+descLoc()); 460 readChar(); 461 markLocation(); 462 QName n = new QName(readName().toLowerCase()); 463 if ((entryName != null) && !n.getName().equals(entryName)) 464 throw new FHIRFormatError("Unable to Parse HTML - starts with '"+n+"' not '"+entryName+"'"+descLoc()); 465 XhtmlNode root = result.addTag(n.getName()); 466 root.setLocation(markLocation()); 467 parseAttributes(root); 468 markLocation(); 469 NSMap nsm = checkNamespaces(n, root, null, true); 470 if (readChar() == '/') { 471 if (peekChar() != '>') 472 throw new FHIRFormatError("unexpected non-end of element "+n+" "+descLoc()); 473 readChar(); 474 } else { 475 unwindPoint = null; 476 List<XhtmlNode> p = new ArrayList<>(); 477 parseElementInner(root, p, nsm, true); 478 } 479 return result; 480 } 481 482 private Location markLocation() { 483 Location res = lastLoc; 484 lastLoc = new Location(line, col); 485 return res; 486 } 487 488 private NSMap checkNamespaces(QName n, XhtmlNode node, NSMap nsm, boolean root) { 489 // what we do here is strip out any stated namespace attributes, putting them in the namesapce map 490 // then we figure out what the namespace of this element is, and state it explicitly if it's not the default 491 492 NSMap result = new NSMap(nsm); 493 List<String> nsattrs = new ArrayList<String>(); 494 for (String an : node.getAttributes().keySet()) { 495 if (an.equals("xmlns")) { 496 result.def(node.getAttribute(an)); 497 nsattrs.add(an); 498 } 499 if (an.startsWith("xmlns:")) { 500 result.ns(an.substring(6), node.getAttribute(an)); 501 nsattrs.add(an); 502 } 503 } 504 for (String s : nsattrs) 505 node.getAttributes().remove(s); 506 if (n.hasNs()) { 507 String nns = result.get(n.getNs()); 508 if (!nns.equals(result.def())) { 509 node.getAttributes().put("xmlns", nns); 510 result.def(nns); 511 } 512 } else if (root && result.hasDef()) { 513 node.getAttributes().put("xmlns", result.def()); 514 } 515 return result; 516 } 517 518 private void addTextNode(XhtmlNode node, StringBuilder s) 519 { 520 String t = isTrimWhitespace() ? s.toString().trim() : s.toString(); 521 if (t.length() > 0) 522 { 523 lastText = t; 524 // System.out.println(t); 525 node.addText(t).setLocation(markLocation()); 526 s.setLength(0); 527 } 528 } 529 private void parseElementInner(XhtmlNode node, List<XhtmlNode> parents, NSMap nsm, boolean escaping) throws FHIRFormatError, IOException 530 { 531 StringBuilder s = new StringBuilder(); 532 while (peekChar() != '\0' && !parents.contains(unwindPoint) && !(node == unwindPoint)) 533 { 534 if (peekChar() == '<') 535 { 536 addTextNode(node, s); 537 readChar(); 538 if (peekChar() == '!') { 539 String sc = readToCommentEnd(); 540 if (sc.startsWith("DOCTYPE")) 541 throw new FHIRFormatError("Malformed XHTML: Found a DocType declaration, and these are not allowed (XXE security vulnerability protection)"); 542 node.addComment(sc).setLocation(markLocation()); 543 } else if (peekChar() == '?') 544 node.addComment(readToTagEnd()).setLocation(markLocation()); 545 else if (peekChar() == '/') { 546 readChar(); 547 QName n = new QName(readToTagEnd()); 548 if (node.getName().equals(n.getName())) 549 return; 550 else 551 { 552 if (mustBeWellFormed) 553 throw new FHIRFormatError("Malformed XHTML: Found \"</"+n.getName()+">\" expecting \"</"+node.getName()+">\""+descLoc()); 554 for (int i = parents.size() - 1; i >= 0; i--) 555 { 556 if (parents.get(i).getName().equals(n)) 557 unwindPoint = parents.get(i); 558 } 559 if (unwindPoint != null) 560 { 561 for (int i = parents.size(); i > 0; i--) 562 { 563 if (i < parents.size() && parents.get(i) == unwindPoint) 564 return; 565 if (i == parents.size()) 566 { 567 parents.get(i - 1).getChildNodes().addAll(node.getChildNodes()); 568 node.getChildNodes().clear(); 569 } 570 else 571 { 572 parents.get(i - 1).getChildNodes().addAll(parents.get(i).getChildNodes()); 573 parents.get(i).getChildNodes().clear(); 574 } 575 } 576 } 577 } 578 } 579 else if (Character.isLetterOrDigit(peekChar())) 580 { 581 parseElement(node, parents, nsm); 582 } 583 else 584 throw new FHIRFormatError("Unable to Parse HTML - node '" + node.getName() + "' has unexpected content '"+peekChar()+"' (last text = '"+lastText+"'"+descLoc()); 585 } 586 else if (peekChar() == '&') // escaping && 587 { 588 parseLiteral(s); 589 } 590 else 591 s.append(readChar()); 592 } 593 addTextNode(node, s); 594 } 595 596 private void parseElement(XhtmlNode parent, List<XhtmlNode> parents, NSMap nsm) throws IOException, FHIRFormatError 597 { 598 markLocation(); 599 QName name = new QName(readName()); 600 XhtmlNode node = parent.addTag(name.getName()); 601 node.setLocation(markLocation()); 602 List<XhtmlNode> newParents = new ArrayList<XhtmlNode>(); 603 newParents.addAll(parents); 604 newParents.add(parent); 605 parseAttributes(node); 606 markLocation(); 607 nsm = checkNamespaces(name, node, nsm, false); 608 if (readChar() == '/') { 609 if (peekChar() != '>') 610 throw new FHIRFormatError("unexpected non-end of element "+name+" "+descLoc()); 611 readChar(); 612 } else { 613 parseElementInner(node, newParents, nsm, "script".equals(name.getName())); 614 } 615 } 616 617 private void parseAttributes(XhtmlNode node) throws FHIRFormatError, IOException 618 { 619 while (Character.isWhitespace(peekChar())) 620 readChar(); 621 while (peekChar() != '>' && peekChar() != '/' && peekChar() != '\0') 622 { 623 String name = readName(); 624 if (name.length() == 0) 625 { 626 throw new FHIRFormatError("Unable to read attribute on <"+node.getName()+">"+descLoc()); 627 } 628 while (Character.isWhitespace(peekChar())) 629 readChar(); 630 631 if (isNameChar(peekChar()) || peekChar() == '>' || peekChar() == '/') 632 node.getAttributes().put(name, null); 633 else if (peekChar() != '=') 634 { 635 throw new FHIRFormatError("Unable to read attribute '"+name+"' value on <"+node.getName()+">"+descLoc()); 636 } 637 else 638 { 639 readChar(); 640 while (Character.isWhitespace(peekChar())) 641 readChar(); 642 if (peekChar() == '"' || peekChar() == '\'') 643 node.getAttributes().put(name, parseAttributeValue(readChar())); 644 else 645 node.getAttributes().put(name, parseAttributeValue('\0')); 646 } 647 while (Character.isWhitespace(peekChar())) 648 readChar(); 649 } 650 } 651 652 private String parseAttributeValue(char term) throws IOException, FHIRFormatError 653 { 654 StringBuilder b = new StringBuilder(); 655 while (peekChar() != '\0' && peekChar() != '>' && (term != '\0' || peekChar() != '/') && peekChar() != term) 656 { 657 if (peekChar() == '&') 658 { 659 parseLiteral(b); 660 } 661 else 662 b.append(readChar()); 663 } 664 if (peekChar() == term) 665 readChar(); 666 return b.toString(); 667 } 668 669 670 private void skipWhiteSpaceAndComments(XhtmlNode focus) throws IOException, FHIRFormatError { 671 while (Character.isWhitespace(peekChar()) || (peekChar() == 0xfeff)) 672 readChar(); 673 if (peekChar() == '<') 674 { 675 char ch = readChar(); 676 if (peekChar() == '!') { 677 readChar(); 678 if (peekChar() == '-') { 679 readChar(); 680 if (peekChar() == '-') { 681 readChar(); 682 if (peekChar() == ' ') 683 readChar(); 684 focus.addComment(readToCommentEnd()); 685 } else 686 throw new FHIRFormatError("unrecognised element type <!"+peekChar()+descLoc()); 687 } else 688 focus.addDocType(readToDocTypeEnd()); 689 skipWhiteSpaceAndComments(focus); 690 } else if (peekChar() == '?') { 691 String r = readToTagEnd(); 692 focus.addInstruction(r.substring(1, r.length()-1)); 693 skipWhiteSpaceAndComments(focus); 694 } else 695 pushChar(ch); 696 } 697 } 698 699 private void skipWhiteSpace() throws IOException { 700 if (trimWhitespace) 701 while (Character.isWhitespace(peekChar()) || (peekChar() == 0xfeff)) 702 readChar(); 703 } 704 705 private void pushChar(char ch) { 706 cache = Character.toString(ch)+cache; 707 } 708 709 private char peekChar() throws IOException 710 { 711 if (cache.length() > 0) 712 return cache.charAt(0); 713 else if (!rdr.ready()) 714 return '\0'; 715 else 716 { 717 char c = (char)rdr.read(); 718 if (c == (char)-1) 719 { 720 cache = ""; 721 return '\0'; 722 } 723 cache = Character.toString(c); 724 return c; 725 } 726 } 727 728 private char readChar() throws IOException 729 { 730 char c; 731 if (cache.length() > 0) 732 { 733 c = cache.charAt(0); 734 cache = cache.length() == 1 ? "" : cache.substring(1); 735 } 736 else if (!rdr.ready()) 737 c = '\0'; 738 else 739 c = (char)rdr.read(); 740 if (c == '\r' || c == '\n') { 741 if (c == '\r' || lastChar != '\r') { 742 line++; 743 col = 0; 744 } 745 lastChar = c; 746 } 747 col++; 748 return c; 749 } 750 751 private String readToTagEnd() throws IOException, FHIRFormatError 752 { 753 StringBuilder s = new StringBuilder(); 754 while (peekChar() != '>' && peekChar() != '\0') 755 s.append(readChar()); 756 if (peekChar() != '\0') 757 { 758 readChar(); 759 skipWhiteSpace(); 760 } else if (mustBeWellFormed) 761 throw new FHIRFormatError("Unexpected termination of html source"+descLoc()); 762 return s.toString(); 763 } 764 765 private String readToDocTypeEnd() throws IOException, FHIRFormatError 766 { 767 StringBuilder s = new StringBuilder(); 768 769 boolean done = false; 770 while (!done) { 771 char c = peekChar(); 772 if (c == '>') { 773 done = true; 774 readChar(); 775 } else if (c != '\0') 776 s.append(readChar()); 777 else if (mustBeWellFormed) 778 throw new FHIRFormatError("Unexpected termination of html source"+descLoc()); 779 } 780 return s.toString(); 781 } 782 783 private String readToCommentEnd() throws IOException, FHIRFormatError 784 { 785 if (peekChar() == '!') 786 readChar(); 787 StringBuilder s = new StringBuilder(); 788 789 boolean simple = true; 790 if (peekChar() == '-') { 791 readChar(); 792 simple = peekChar() != '-'; 793 if (simple) 794 s.append('-'); 795 else 796 readChar(); 797 } 798 799 boolean done = false; 800 while (!done) { 801 char c = peekChar(); 802 if (c == '-') { 803 readChar(); 804 if (peekChar() == '-') { 805 readChar(); 806 if (peekChar() == '>') { 807 done = true; 808 } else 809 s.append("--"); 810 } else 811 s.append('-'); 812 } else if (simple && peekChar() == '>') { 813 done = true; 814 } else if (c != '\0') 815 s.append(readChar()); 816 else if (mustBeWellFormed) 817 throw new FHIRFormatError("Unexpected termination of html source"+descLoc()); 818 } 819 if (peekChar() != '\0') 820 { 821 readChar(); 822 skipWhiteSpace(); 823 } 824 return s.toString(); 825 } 826 827 private boolean isNameChar(char ch) 828 { 829 return Character.isLetterOrDigit(ch) || ch == '_' || ch == '-' || ch == ':'; 830 } 831 832 private String readName() throws IOException 833 { 834 StringBuilder s = new StringBuilder(); 835 while (isNameChar(peekChar())) 836 s.append(readChar()); 837 return s.toString(); 838 } 839 840 private String readUntil(char ch) throws IOException 841 { 842 StringBuilder s = new StringBuilder(); 843 while (peekChar() != 0 && peekChar() != ch) 844 s.append(readChar()); 845 readChar(); 846 return s.toString(); 847 } 848 849 private void parseLiteral(StringBuilder s) throws IOException, FHIRFormatError { 850 // UInt16 w; 851 readChar(); 852 String c = readUntil(';'); 853 if (c.equals("apos")) 854 s.append('\''); 855 else if (c.equals("quot")) 856 s.append('"'); 857 else if (c.equals("nbsp")) 858 s.append(XhtmlNode.NBSP); 859 else if (c.equals("amp")) 860 s.append('&'); 861 else if (c.equals("lsquo")) 862 s.append((char) 8216); // right single quotation, U+2019 ISOnum 863 else if (c.equals("rsquo")) 864 s.append((char) 8217); // right single quotation, U+2019 ISOnum 865 //s.append((char)0x60); // right single quote 866 //s.append('’'); 867 else if (c.equals("gt")) 868 s.append('>'); 869 else if (c.equals("lt")) 870 s.append('<'); 871 else if (c.equals("copy")) 872 s.append((char) 169); 873 else if (c.equals("reg")) 874 s.append((char) 174); 875 else if (c.equals("sect")) 876 s.append((char) 0xA7); 877 else if (c.charAt(0) == '#') { 878 if (isInteger(c.substring(1), 10)) 879 s.append((char) Integer.parseInt(c.substring(1))); 880 else if (c.charAt(1) == 'x' && isInteger(c.substring(2), 16)) 881 s.append((char) Integer.parseInt(c.substring(2), 16)); 882 } else if (c.equals("fnof")) 883 s.append((char) 402); // latin small f with hook = function = florin, U+0192 ISOtech --> 884 else if (c.equals("Alpha")) 885 s.append((char) 913); // greek capital letter alpha, U+0391 886 else if (c.equals("Beta")) 887 s.append((char) 914); // greek capital letter beta, U+0392 888 else if (c.equals("Gamma")) 889 s.append((char) 915); // greek capital letter gamma, U+0393 ISOgrk3 890 else if (c.equals("Delta")) 891 s.append((char) 916); // greek capital letter delta, U+0394 ISOgrk3 892 else if (c.equals("Epsilon")) 893 s.append((char) 917); // greek capital letter epsilon, U+0395 894 else if (c.equals("Zeta")) 895 s.append((char) 918); // greek capital letter zeta, U+0396 896 else if (c.equals("Eta")) 897 s.append((char) 919); // greek capital letter eta, U+0397 898 else if (c.equals("Theta")) 899 s.append((char) 920); // greek capital letter theta, U+0398 ISOgrk3 900 else if (c.equals("Iota")) 901 s.append((char) 921); // greek capital letter iota, U+0399 902 else if (c.equals("Kappa")) 903 s.append((char) 922); // greek capital letter kappa, U+039A 904 else if (c.equals("Lambda")) 905 s.append((char) 923); // greek capital letter lambda, U+039B ISOgrk3 906 else if (c.equals("Mu")) 907 s.append((char) 924); // greek capital letter mu, U+039C 908 else if (c.equals("Nu")) 909 s.append((char) 925); // greek capital letter nu, U+039D 910 else if (c.equals("Xi")) 911 s.append((char) 926); // greek capital letter xi, U+039E ISOgrk3 912 else if (c.equals("Omicron")) 913 s.append((char) 927); // greek capital letter omicron, U+039F 914 else if (c.equals("Pi")) 915 s.append((char) 928); // greek capital letter pi, U+03A0 ISOgrk3 916 else if (c.equals("Rho")) 917 s.append((char) 929); // greek capital letter rho, U+03A1 918 else if (c.equals("Sigma")) 919 s.append((char) 931); // greek capital letter sigma, U+03A3 ISOgrk3 920 else if (c.equals("Tau")) 921 s.append((char) 932); // greek capital letter tau, U+03A4 922 else if (c.equals("Upsilon")) 923 s.append((char) 933); // greek capital letter upsilon, U+03A5 ISOgrk3 924 else if (c.equals("Phi")) 925 s.append((char) 934); // greek capital letter phi, U+03A6 ISOgrk3 926 else if (c.equals("Chi")) 927 s.append((char) 935); // greek capital letter chi, U+03A7 928 else if (c.equals("Psi")) 929 s.append((char) 936); // greek capital letter psi, U+03A8 ISOgrk3 930 else if (c.equals("Omega")) 931 s.append((char) 937); // greek capital letter omega, U+03A9 ISOgrk3 932 else if (c.equals("alpha")) 933 s.append((char) 945); // greek small letter alpha, U+03B1 ISOgrk3 934 else if (c.equals("beta")) 935 s.append((char) 946); // greek small letter beta, U+03B2 ISOgrk3 936 else if (c.equals("gamma")) 937 s.append((char) 947); // greek small letter gamma, U+03B3 ISOgrk3 938 else if (c.equals("delta")) 939 s.append((char) 948); // greek small letter delta, U+03B4 ISOgrk3 940 else if (c.equals("epsilon")) 941 s.append((char) 949); // greek small letter epsilon, U+03B5 ISOgrk3 942 else if (c.equals("zeta")) 943 s.append((char) 950); // greek small letter zeta, U+03B6 ISOgrk3 944 else if (c.equals("eta")) 945 s.append((char) 951); // greek small letter eta, U+03B7 ISOgrk3 946 else if (c.equals("theta")) 947 s.append((char) 952); // greek small letter theta, U+03B8 ISOgrk3 948 else if (c.equals("iota")) 949 s.append((char) 953); // greek small letter iota, U+03B9 ISOgrk3 950 else if (c.equals("kappa")) 951 s.append((char) 954); // greek small letter kappa, U+03BA ISOgrk3 952 else if (c.equals("lambda")) 953 s.append((char) 955); // greek small letter lambda, U+03BB ISOgrk3 954 else if (c.equals("mu")) 955 s.append((char) 956); // greek small letter mu, U+03BC ISOgrk3 956 else if (c.equals("nu")) 957 s.append((char) 957); // greek small letter nu, U+03BD ISOgrk3 958 else if (c.equals("xi")) 959 s.append((char) 958); // greek small letter xi, U+03BE ISOgrk3 960 else if (c.equals("omicron")) 961 s.append((char) 959); // greek small letter omicron, U+03BF NEW 962 else if (c.equals("pi")) 963 s.append((char) 960); // greek small letter pi, U+03C0 ISOgrk3 964 else if (c.equals("rho")) 965 s.append((char) 961); // greek small letter rho, U+03C1 ISOgrk3 966 else if (c.equals("sigmaf")) 967 s.append((char) 962); // greek small letter final sigma, U+03C2 ISOgrk3 968 else if (c.equals("sigma")) 969 s.append((char) 963); // greek small letter sigma, U+03C3 ISOgrk3 970 else if (c.equals("tau")) 971 s.append((char) 964); // greek small letter tau, U+03C4 ISOgrk3 972 else if (c.equals("upsilon")) 973 s.append((char) 965); // greek small letter upsilon, U+03C5 ISOgrk3 974 else if (c.equals("phi")) 975 s.append((char) 966); // greek small letter phi, U+03C6 ISOgrk3 976 else if (c.equals("chi")) 977 s.append((char) 967); // greek small letter chi, U+03C7 ISOgrk3 978 else if (c.equals("psi")) 979 s.append((char) 968); // greek small letter psi, U+03C8 ISOgrk3 980 else if (c.equals("omega")) 981 s.append((char) 969); // greek small letter omega, U+03C9 ISOgrk3 982 else if (c.equals("thetasym")) 983 s.append((char) 977); // greek small letter theta symbol, U+03D1 NEW 984 else if (c.equals("upsih")) 985 s.append((char) 978); // greek upsilon with hook symbol, U+03D2 NEW 986 else if (c.equals("piv")) 987 s.append((char) 982); // greek pi symbol, U+03D6 ISOgrk3 988 else if (c.equals("bull")) 989 s.append((char) 8226); // bullet = black small circle, U+2022 ISOpub 990 else if (c.equals("hellip")) 991 s.append((char) 8230); // horizontal ellipsis = three dot leader, U+2026 ISOpub 992 else if (c.equals("prime")) 993 s.append((char) 8242); // prime = minutes = feet, U+2032 ISOtech 994 else if (c.equals("Prime")) 995 s.append((char) 8243); // double prime = seconds = inches, U+2033 ISOtech 996 else if (c.equals("oline")) 997 s.append((char) 8254); // overline = spacing overscore, U+203E NEW 998 else if (c.equals("frasl")) 999 s.append((char) 8260); // fraction slash, U+2044 NEW 1000 else if (c.equals("weierp")) 1001 s.append((char) 8472); // script capital P = power set = Weierstrass p, U+2118 ISOamso 1002 else if (c.equals("image")) 1003 s.append((char) 8465); // blackletter capital I = imaginary part, U+2111 ISOamso 1004 else if (c.equals("real")) 1005 s.append((char) 8476); // blackletter capital R = real part symbol, U+211C ISOamso 1006 else if (c.equals("trade")) 1007 s.append((char) 8482); // trade mark sign, U+2122 ISOnum 1008 else if (c.equals("alefsym")) 1009 s.append((char) 8501); // alef symbol = first transfinite cardinal, U+2135 NEW 1010 else if (c.equals("larr")) 1011 s.append((char) 8592); // leftwards arrow, U+2190 ISOnum 1012 else if (c.equals("uarr")) 1013 s.append((char) 8593); // upwards arrow, U+2191 ISOnum 1014 else if (c.equals("rarr")) 1015 s.append((char) 8594); // rightwards arrow, U+2192 ISOnum 1016 else if (c.equals("darr")) 1017 s.append((char) 8595); // downwards arrow, U+2193 ISOnum 1018 else if (c.equals("harr")) 1019 s.append((char) 8596); // left right arrow, U+2194 ISOamsa 1020 else if (c.equals("crarr")) 1021 s.append((char) 8629); // downwards arrow with corner leftwards = carriage return, U+21B5 NEW 1022 else if (c.equals("lArr")) 1023 s.append((char) 8656); // leftwards double arrow, U+21D0 ISOtech 1024 else if (c.equals("uArr")) 1025 s.append((char) 8657); // upwards double arrow, U+21D1 ISOamsa 1026 else if (c.equals("rArr")) 1027 s.append((char) 8658); // rightwards double arrow, U+21D2 ISOtech 1028 else if (c.equals("dArr")) 1029 s.append((char) 8659); // downwards double arrow, U+21D3 ISOamsa 1030 else if (c.equals("hArr")) 1031 s.append((char) 8660); // left right double arrow, U+21D4 ISOamsa 1032 else if (c.equals("forall")) 1033 s.append((char) 8704); // for all, U+2200 ISOtech 1034 else if (c.equals("part")) 1035 s.append((char) 8706); // partial differential, U+2202 ISOtech 1036 else if (c.equals("exist")) 1037 s.append((char) 8707); // there exists, U+2203 ISOtech 1038 else if (c.equals("empty")) 1039 s.append((char) 8709); // empty set = null set = diameter, U+2205 ISOamso 1040 else if (c.equals("nabla")) 1041 s.append((char) 8711); // nabla = backward difference, U+2207 ISOtech 1042 else if (c.equals("isin")) 1043 s.append((char) 8712); // element of, U+2208 ISOtech 1044 else if (c.equals("notin")) 1045 s.append((char) 8713); // not an element of, U+2209 ISOtech 1046 else if (c.equals("ni")) 1047 s.append((char) 8715); // contains as member, U+220B ISOtech 1048 else if (c.equals("prod")) 1049 s.append((char) 8719); // n-ary product = product sign, U+220F ISOamsb 1050 else if (c.equals("sum")) 1051 s.append((char) 8721); // n-ary sumation, U+2211 ISOamsb 1052 else if (c.equals("minus")) 1053 s.append((char) 8722); // minus sign, U+2212 ISOtech 1054 else if (c.equals("lowast")) 1055 s.append((char) 8727); // asterisk operator, U+2217 ISOtech 1056 else if (c.equals("radic")) 1057 s.append((char) 8730); // square root = radical sign, U+221A ISOtech 1058 else if (c.equals("prop")) 1059 s.append((char) 8733); // proportional to, U+221D ISOtech 1060 else if (c.equals("infin")) 1061 s.append((char) 8734); // infinity, U+221E ISOtech --> 1062 else if (c.equals("ang")) 1063 s.append((char) 8736); // angle, U+2220 ISOamso 1064 else if (c.equals("and")) 1065 s.append((char) 8743); // logical and = wedge, U+2227 ISOtech 1066 else if (c.equals("or")) 1067 s.append((char) 8744); // logical or = vee, U+2228 ISOtech 1068 else if (c.equals("cap")) 1069 s.append((char) 8745); // intersection = cap, U+2229 ISOtech 1070 else if (c.equals("cup")) 1071 s.append((char) 8746); // union = cup, U+222A ISOtech 1072 else if (c.equals("int")) 1073 s.append((char) 8747); // integral, U+222B ISOtech 1074 else if (c.equals("there4")) 1075 s.append((char) 8756); // therefore, U+2234 ISOtech 1076 else if (c.equals("sim")) 1077 s.append((char) 8764); // tilde operator = varies with = similar t U+223C ISOtech 1078 else if (c.equals("cong")) 1079 s.append((char) 8773); // approximately equal to, U+2245 ISOtec 1080 else if (c.equals("asymp")) 1081 s.append((char) 8776); // almost equal to = asymptotic to, U+2248 ISOamsr 1082 else if (c.equals("ne")) 1083 s.append((char) 8800); // not equal to, U+2260 ISOtech 1084 else if (c.equals("equiv")) 1085 s.append((char) 8801); // identical to, U+2261 ISOtech 1086 else if (c.equals("le")) 1087 s.append((char) 8804); // less-than or equal to, U+2264 ISOtech 1088 else if (c.equals("ge")) 1089 s.append((char) 8805); // greater-than or equal to, U+2265 ISOtech 1090 else if (c.equals("sub")) 1091 s.append((char) 8834); // subset of, U+2282 ISOtech 1092 else if (c.equals("sup")) 1093 s.append((char) 8835); // superset of, U+2283 ISOtech 1094 else if (c.equals("nsub")) 1095 s.append((char) 8836); // not a subset of, U+2284 ISOamsn 1096 else if (c.equals("sube")) 1097 s.append((char) 8838); // subset of or equal to, U+2286 ISOtech 1098 else if (c.equals("supe")) 1099 s.append((char) 8839); // superset of or equal to, U+2287 ISOtech 1100 else if (c.equals("oplus")) 1101 s.append((char) 8853); // circled plus = direct sum, U+2295 ISOamsb 1102 else if (c.equals("otimes")) 1103 s.append((char) 8855); // circled times = vector product, U+2297 ISOamsb --> 1104 else if (c.equals("perp")) 1105 s.append((char) 8869); // up tack = orthogonal to = perpendicular, U+22A5 ISOtech 1106 else if (c.equals("sdot")) 1107 s.append((char) 8901); // dot operator, U+22C5 ISOamsb 1108 else if (c.equals("lceil")) 1109 s.append((char) 8968); // left ceiling = apl upstile, U+2308 ISOamsc 1110 else if (c.equals("rceil")) 1111 s.append((char) 8969); // right ceiling, U+2309 ISOamsc 1112 else if (c.equals("lfloor")) 1113 s.append((char) 8970); // left floor = apl downstile, U+230A ISOamsc 1114 else if (c.equals("rfloor")) 1115 s.append((char) 8971); // right floor, U+230B ISOamsc 1116 else if (c.equals("lang")) 1117 s.append((char) 9001); // left-pointing angle bracket = bra, U+2329 ISOtech 1118 else if (c.equals("rang")) 1119 s.append((char) 9002); // right-pointing angle bracket = ket, U+232A ISOtech 1120 else if (c.equals("loz")) 1121 s.append((char) 9674); // lozenge, U+25CA ISOpub 1122 else if (c.equals("spades")) 1123 s.append((char) 9824); // black spade suit, U+2660 ISOpub 1124 else if (c.equals("clubs")) 1125 s.append((char) 9827); // black club suit = shamrock, U+2663 ISOpub 1126 else if (c.equals("hearts")) 1127 s.append((char) 9829); // black heart suit = valentine, U+2665 ISOpub 1128 else if (c.equals("diams")) 1129 s.append((char) 9830); // black diamond suit, U+2666 ISOpub -- 1130 else if (c.equals("ndash")) 1131 s.append((char) 8211); 1132 else if (c.equals("mdash")) 1133 s.append((char) 8212); 1134 else if (c.equals("ldquo")) 1135 s.append((char) 8221); 1136 else if (c.equals("rdquo")) 1137 s.append((char) 201D); 1138 else 1139 throw new FHIRFormatError("unable to parse character reference '" + c + "'' (last text = '" + lastText + "'" + descLoc()); 1140 } 1141 1142 private boolean isInteger(String s, int base) { 1143 try { 1144 Integer.parseInt(s, base); 1145 return true; 1146 } catch (Exception e) { 1147 return false; 1148 } 1149 } 1150 1151 public XhtmlNode parseFragment(String source) throws IOException, FHIRException { 1152 rdr = new StringReader(source); 1153 return parseFragment(); 1154 } 1155 1156 public XhtmlNode parseFragment(InputStream input) throws IOException, FHIRException { 1157 rdr = new InputStreamReader(input); 1158 return parseFragment(); 1159 } 1160 1161 private XhtmlNode parseFragment() throws IOException, FHIRException 1162 { 1163 skipWhiteSpace(); 1164 if (peekChar() != '<') 1165 throw new FHIRException("Unable to Parse HTML - does not start with tag. Found "+peekChar()+descLoc()); 1166 readChar(); 1167 if (peekChar() == '?') { 1168 readToTagEnd(); 1169 skipWhiteSpace(); 1170 if (peekChar() != '<') 1171 throw new FHIRException("Unable to Parse HTML - does not start with tag after processing instruction. Found "+peekChar()+descLoc()); 1172 readChar(); 1173 } 1174 String n = readName().toLowerCase(); 1175 readToTagEnd(); 1176 XhtmlNode result = new XhtmlNode(NodeType.Element); 1177 1178 int colonIndex = n.indexOf(':'); 1179 if (colonIndex != -1) { 1180 n = n.substring(colonIndex + 1); 1181 } 1182 1183 result.setName(n); 1184 unwindPoint = null; 1185 List<XhtmlNode> p = new ArrayList<>(); 1186 parseElementInner(result, p, null, true); 1187 1188 return result; 1189 } 1190 1191 1192}