001package org.hl7.fhir.dstu3.elementmodel; 002 003/*- 004 * #%L 005 * org.hl7.fhir.dstu3 006 * %% 007 * Copyright (C) 2014 - 2019 Health Level 7 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.OutputStream; 027import java.util.ArrayList; 028import java.util.Collections; 029import java.util.Comparator; 030import java.util.List; 031 032import javax.xml.parsers.DocumentBuilder; 033import javax.xml.parsers.DocumentBuilderFactory; 034import javax.xml.parsers.SAXParser; 035import javax.xml.parsers.SAXParserFactory; 036import javax.xml.transform.Transformer; 037import javax.xml.transform.TransformerFactory; 038import javax.xml.transform.dom.DOMResult; 039import javax.xml.transform.sax.SAXSource; 040 041import org.hl7.fhir.dstu3.context.IWorkerContext; 042import org.hl7.fhir.dstu3.elementmodel.Element.SpecialElement; 043import org.hl7.fhir.dstu3.formats.FormatUtilities; 044import org.hl7.fhir.dstu3.formats.IParser.OutputStyle; 045import org.hl7.fhir.dstu3.model.DateTimeType; 046import org.hl7.fhir.dstu3.model.ElementDefinition.PropertyRepresentation; 047import org.hl7.fhir.dstu3.model.Enumeration; 048import org.hl7.fhir.dstu3.model.StructureDefinition; 049import org.hl7.fhir.dstu3.utils.ToolingExtensions; 050import org.hl7.fhir.dstu3.utils.formats.XmlLocationAnnotator; 051import org.hl7.fhir.dstu3.utils.formats.XmlLocationData; 052import org.hl7.fhir.exceptions.DefinitionException; 053import org.hl7.fhir.exceptions.FHIRException; 054import org.hl7.fhir.exceptions.FHIRFormatError; 055import org.hl7.fhir.utilities.Utilities; 056import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 057import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 058import org.hl7.fhir.utilities.xhtml.XhtmlComposer; 059import org.hl7.fhir.utilities.xhtml.XhtmlNode; 060import org.hl7.fhir.utilities.xhtml.XhtmlParser; 061import org.hl7.fhir.utilities.xml.IXMLWriter; 062import org.hl7.fhir.utilities.xml.XMLUtil; 063import org.hl7.fhir.utilities.xml.XMLWriter; 064import org.w3c.dom.Document; 065import org.w3c.dom.Node; 066import org.xml.sax.InputSource; 067import org.xml.sax.XMLReader; 068 069public class XmlParser extends ParserBase { 070 private boolean allowXsiLocation; 071 072 public XmlParser(IWorkerContext context) { 073 super(context); 074 } 075 076 077 public boolean isAllowXsiLocation() { 078 return allowXsiLocation; 079 } 080 081 public void setAllowXsiLocation(boolean allowXsiLocation) { 082 this.allowXsiLocation = allowXsiLocation; 083 } 084 085 086 public Element parse(InputStream stream) throws FHIRFormatError, DefinitionException, FHIRException, IOException { 087 Document doc = null; 088 try { 089 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 090 // xxe protection 091 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 092 factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 093 factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 094 factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 095 factory.setXIncludeAware(false); 096 factory.setExpandEntityReferences(false); 097 098 factory.setNamespaceAware(true); 099 if (policy == ValidationPolicy.EVERYTHING) { 100 // use a slower parser that keeps location data 101 TransformerFactory transformerFactory = TransformerFactory.newInstance(); 102 Transformer nullTransformer = transformerFactory.newTransformer(); 103 DocumentBuilder docBuilder = factory.newDocumentBuilder(); 104 doc = docBuilder.newDocument(); 105 DOMResult domResult = new DOMResult(doc); 106 SAXParserFactory spf = SAXParserFactory.newInstance(); 107 spf.setNamespaceAware(true); 108 spf.setValidating(false); 109 // xxe protection 110 spf.setFeature("http://xml.org/sax/features/external-general-entities", false); 111 spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 112 SAXParser saxParser = spf.newSAXParser(); 113 XMLReader xmlReader = saxParser.getXMLReader(); 114 // xxe protection 115 xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false); 116 xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 117 118 XmlLocationAnnotator locationAnnotator = new XmlLocationAnnotator(xmlReader, doc); 119 InputSource inputSource = new InputSource(stream); 120 SAXSource saxSource = new SAXSource(locationAnnotator, inputSource); 121 nullTransformer.transform(saxSource, domResult); 122 } else { 123 DocumentBuilder builder = factory.newDocumentBuilder(); 124 doc = builder.parse(stream); 125 } 126 } catch (Exception e) { 127 logError(0, 0, "(syntax)", IssueType.INVALID, e.getMessage(), IssueSeverity.FATAL); 128 doc = null; 129 } 130 if (doc == null) 131 return null; 132 else 133 return parse(doc); 134 } 135 136 private void checkForProcessingInstruction(Document document) throws FHIRFormatError { 137 if (policy == ValidationPolicy.EVERYTHING) { 138 Node node = document.getFirstChild(); 139 while (node != null) { 140 if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) 141 logError(line(document), col(document), "(document)", IssueType.INVALID, "No processing instructions allowed in resources", IssueSeverity.ERROR); 142 node = node.getNextSibling(); 143 } 144 } 145 } 146 147 148 private int line(Node node) { 149 XmlLocationData loc = (XmlLocationData) node.getUserData(XmlLocationData.LOCATION_DATA_KEY); 150 return loc == null ? 0 : loc.getStartLine(); 151 } 152 153 private int col(Node node) { 154 XmlLocationData loc = (XmlLocationData) node.getUserData(XmlLocationData.LOCATION_DATA_KEY); 155 return loc == null ? 0 : loc.getStartColumn(); 156 } 157 158 public Element parse(Document doc) throws FHIRFormatError, DefinitionException, FHIRException, IOException { 159 checkForProcessingInstruction(doc); 160 org.w3c.dom.Element element = doc.getDocumentElement(); 161 return parse(element); 162 } 163 164 public Element parse(org.w3c.dom.Element element) throws FHIRFormatError, DefinitionException, FHIRException, IOException { 165 String ns = element.getNamespaceURI(); 166 String name = element.getLocalName(); 167 String path = "/"+pathPrefix(ns)+name; 168 169 StructureDefinition sd = getDefinition(line(element), col(element), ns, name); 170 if (sd == null) 171 return null; 172 173 Element result = new Element(element.getLocalName(), new Property(context, sd.getSnapshot().getElement().get(0), sd)); 174 checkElement(element, path, result.getProperty()); 175 result.markLocation(line(element), col(element)); 176 result.setType(element.getLocalName()); 177 parseChildren(path, element, result); 178 result.numberChildren(); 179 return result; 180 } 181 182 private String pathPrefix(String ns) { 183 if (Utilities.noString(ns)) 184 return ""; 185 if (ns.equals(FormatUtilities.FHIR_NS)) 186 return "f:"; 187 if (ns.equals(FormatUtilities.XHTML_NS)) 188 return "h:"; 189 if (ns.equals("urn:hl7-org:v3")) 190 return "v3:"; 191 return "?:"; 192 } 193 194 private boolean empty(org.w3c.dom.Element element) { 195 for (int i = 0; i < element.getAttributes().getLength(); i++) { 196 String n = element.getAttributes().item(i).getNodeName(); 197 if (!n.equals("xmlns") && !n.startsWith("xmlns:")) 198 return false; 199 } 200 if (!Utilities.noString(element.getTextContent().trim())) 201 return false; 202 203 Node n = element.getFirstChild(); 204 while (n != null) { 205 if (n.getNodeType() == Node.ELEMENT_NODE) 206 return false; 207 n = n.getNextSibling(); 208 } 209 return true; 210 } 211 212 private void checkElement(org.w3c.dom.Element element, String path, Property prop) throws FHIRFormatError { 213 if (policy == ValidationPolicy.EVERYTHING) { 214 if (empty(element)) 215 logError(line(element), col(element), path, IssueType.INVALID, "Element must have some content", IssueSeverity.ERROR); 216 String ns = FormatUtilities.FHIR_NS; 217 if (ToolingExtensions.hasExtension(prop.getDefinition(), "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace")) 218 ns = ToolingExtensions.readStringExtension(prop.getDefinition(), "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace"); 219 else if (ToolingExtensions.hasExtension(prop.getStructure(), "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace")) 220 ns = ToolingExtensions.readStringExtension(prop.getStructure(), "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace"); 221 if (!element.getNamespaceURI().equals(ns)) 222 logError(line(element), col(element), path, IssueType.INVALID, "Wrong namespace - expected '"+ns+"'", IssueSeverity.ERROR); 223 } 224 } 225 226 public Element parse(org.w3c.dom.Element base, String type) throws Exception { 227 StructureDefinition sd = getDefinition(0, 0, FormatUtilities.FHIR_NS, type); 228 Element result = new Element(base.getLocalName(), new Property(context, sd.getSnapshot().getElement().get(0), sd)); 229 String path = "/"+pathPrefix(base.getNamespaceURI())+base.getLocalName(); 230 checkElement(base, path, result.getProperty()); 231 result.setType(base.getLocalName()); 232 parseChildren(path, base, result); 233 result.numberChildren(); 234 return result; 235 } 236 237 private void parseChildren(String path, org.w3c.dom.Element node, Element context) throws FHIRFormatError, FHIRException, IOException, DefinitionException { 238 // this parsing routine retains the original order in a the XML file, to support validation 239 reapComments(node, context); 240 List<Property> properties = context.getProperty().getChildProperties(context.getName(), XMLUtil.getXsiType(node)); 241 242 String text = XMLUtil.getDirectText(node).trim(); 243 if (!Utilities.noString(text)) { 244 Property property = getTextProp(properties); 245 if (property != null) { 246 context.getChildren().add(new Element(property.getName(), property, property.getType(), text).markLocation(line(node), col(node))); 247 } else { 248 logError(line(node), col(node), path, IssueType.STRUCTURE, "Text should not be present", IssueSeverity.ERROR); 249 } 250 } 251 252 for (int i = 0; i < node.getAttributes().getLength(); i++) { 253 Node attr = node.getAttributes().item(i); 254 if (!(attr.getNodeName().equals("xmlns") || attr.getNodeName().startsWith("xmlns:"))) { 255 Property property = getAttrProp(properties, attr.getNodeName()); 256 if (property != null) { 257 String av = attr.getNodeValue(); 258 if (ToolingExtensions.hasExtension(property.getDefinition(), "http://www.healthintersections.com.au/fhir/StructureDefinition/elementdefinition-dateformat")) 259 av = convertForDateFormat(ToolingExtensions.readStringExtension(property.getDefinition(), "http://www.healthintersections.com.au/fhir/StructureDefinition/elementdefinition-dateformat"), av); 260 if (property.getName().equals("value") && context.isPrimitive()) 261 context.setValue(av); 262 else 263 context.getChildren().add(new Element(property.getName(), property, property.getType(), av).markLocation(line(node), col(node))); 264 } else if (!allowXsiLocation || !attr.getNodeName().endsWith(":schemaLocation") ) { 265 logError(line(node), col(node), path, IssueType.STRUCTURE, "Undefined attribute '@"+attr.getNodeName()+"' on "+node.getNodeName(), IssueSeverity.ERROR); 266 } 267 } 268 } 269 270 Node child = node.getFirstChild(); 271 while (child != null) { 272 if (child.getNodeType() == Node.ELEMENT_NODE) { 273 Property property = getElementProp(properties, child.getLocalName()); 274 if (property != null) { 275 if (!property.isChoice() && "xhtml".equals(property.getType())) { 276 XhtmlNode xhtml = new XhtmlParser().setValidatorMode(true).parseHtmlNode((org.w3c.dom.Element) child); 277 context.getChildren().add(new Element("div", property, "xhtml", new XhtmlComposer(XhtmlComposer.XML).compose(xhtml)).setXhtml(xhtml).markLocation(line(child), col(child))); 278 } else { 279 String npath = path+"/"+pathPrefix(child.getNamespaceURI())+child.getLocalName(); 280 Element n = new Element(child.getLocalName(), property).markLocation(line(child), col(child)); 281 checkElement((org.w3c.dom.Element) child, npath, n.getProperty()); 282 boolean ok = true; 283 if (property.isChoice()) { 284 if (property.getDefinition().hasRepresentation(PropertyRepresentation.TYPEATTR)) { 285 String xsiType = ((org.w3c.dom.Element) child).getAttributeNS(FormatUtilities.NS_XSI, "type"); 286 if (xsiType == null) { 287 logError(line(child), col(child), path, IssueType.STRUCTURE, "No type found on '"+child.getLocalName()+'"', IssueSeverity.ERROR); 288 ok = false; 289 } else { 290 if (xsiType.contains(":")) 291 xsiType = xsiType.substring(xsiType.indexOf(":")+1); 292 n.setType(xsiType); 293 } 294 } else 295 n.setType(n.getType()); 296 } 297 context.getChildren().add(n); 298 if (ok) { 299 if (property.isResource()) 300 parseResource(npath, (org.w3c.dom.Element) child, n, property); 301 else 302 parseChildren(npath, (org.w3c.dom.Element) child, n); 303 } 304 } 305 } else 306 logError(line(child), col(child), path, IssueType.STRUCTURE, "Undefined element '"+child.getLocalName()+"'", IssueSeverity.ERROR); 307 } else if (child.getNodeType() == Node.CDATA_SECTION_NODE){ 308 logError(line(child), col(child), path, IssueType.STRUCTURE, "CDATA is not allowed", IssueSeverity.ERROR); 309 } else if (!Utilities.existsInList(child.getNodeType(), 3, 8)) { 310 logError(line(child), col(child), path, IssueType.STRUCTURE, "Node type "+Integer.toString(child.getNodeType())+" is not allowed", IssueSeverity.ERROR); 311 } 312 child = child.getNextSibling(); 313 } 314 } 315 316 private Property getElementProp(List<Property> properties, String nodeName) { 317 List<Property> propsSortedByLongestFirst = new ArrayList<Property>(properties); 318 // sort properties according to their name longest first, so .requestOrganizationReference comes first before .request[x] 319 // and therefore the longer property names get evaluated first 320 Collections.sort(propsSortedByLongestFirst, new Comparator<Property>() { 321 @Override 322 public int compare(Property o1, Property o2) { 323 return o2.getName().length() - o1.getName().length(); 324 } 325 }); 326 for (Property p : propsSortedByLongestFirst) 327 if (!p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR) && !p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) { 328 if (p.getName().equals(nodeName)) 329 return p; 330 if (p.getName().endsWith("[x]") && nodeName.length() > p.getName().length()-3 && p.getName().substring(0, p.getName().length()-3).equals(nodeName.substring(0, p.getName().length()-3))) 331 return p; 332 } 333 return null; 334 } 335 336 private Property getAttrProp(List<Property> properties, String nodeName) { 337 for (Property p : properties) 338 if (p.getName().equals(nodeName) && p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR)) 339 return p; 340 return null; 341 } 342 343 private Property getTextProp(List<Property> properties) { 344 for (Property p : properties) 345 if (p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) 346 return p; 347 return null; 348 } 349 350 private String convertForDateFormat(String fmt, String av) throws FHIRException { 351 if ("v3".equals(fmt)) { 352 DateTimeType d = DateTimeType.parseV3(av); 353 return d.asStringValue(); 354 } else 355 throw new FHIRException("Unknown Data format '"+fmt+"'"); 356 } 357 358 private void parseResource(String string, org.w3c.dom.Element container, Element parent, Property elementProperty) throws FHIRFormatError, DefinitionException, FHIRException, IOException { 359 org.w3c.dom.Element res = XMLUtil.getFirstChild(container); 360 String name = res.getLocalName(); 361 StructureDefinition sd = context.fetchTypeDefinition(name); 362 if (sd == null) 363 throw new FHIRFormatError("Contained resource does not appear to be a FHIR resource (unknown name '"+res.getLocalName()+"')"); 364 parent.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd), SpecialElement.fromProperty(parent.getProperty()), elementProperty); 365 parent.setType(name); 366 parseChildren(res.getLocalName(), res, parent); 367 } 368 369 private void reapComments(org.w3c.dom.Element element, Element context) { 370 Node node = element.getPreviousSibling(); 371 while (node != null && node.getNodeType() != Node.ELEMENT_NODE) { 372 if (node.getNodeType() == Node.COMMENT_NODE) 373 context.getComments().add(0, node.getTextContent()); 374 node = node.getPreviousSibling(); 375 } 376 node = element.getLastChild(); 377 while (node != null && node.getNodeType() != Node.ELEMENT_NODE) { 378 node = node.getPreviousSibling(); 379 } 380 while (node != null) { 381 if (node.getNodeType() == Node.COMMENT_NODE) 382 context.getComments().add(node.getTextContent()); 383 node = node.getNextSibling(); 384 } 385 } 386 387 private boolean isAttr(Property property) { 388 for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) { 389 if (r.getValue() == PropertyRepresentation.XMLATTR) { 390 return true; 391 } 392 } 393 return false; 394 } 395 396 private boolean isText(Property property) { 397 for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) { 398 if (r.getValue() == PropertyRepresentation.XMLTEXT) { 399 return true; 400 } 401 } 402 return false; 403 } 404 405 @Override 406 public void compose(Element e, OutputStream stream, OutputStyle style, String base) throws IOException { 407 XMLWriter xml = new XMLWriter(stream, "UTF-8"); 408 xml.setPretty(style == OutputStyle.PRETTY); 409 xml.start(); 410 xml.setDefaultNamespace(e.getProperty().getNamespace()); 411 composeElement(xml, e, e.getType()); 412 xml.end(); 413 414 } 415 416 public void compose(Element e, IXMLWriter xml) throws Exception { 417 xml.start(); 418 xml.setDefaultNamespace(e.getProperty().getNamespace()); 419 composeElement(xml, e, e.getType()); 420 xml.end(); 421 } 422 423 private void composeElement(IXMLWriter xml, Element element, String elementName) throws IOException { 424 for (String s : element.getComments()) { 425 xml.comment(s, true); 426 } 427 if (isText(element.getProperty())) { 428 if (linkResolver != null) 429 xml.link(linkResolver.resolveProperty(element.getProperty())); 430 xml.enter(elementName); 431 xml.text(element.getValue()); 432 xml.exit(elementName); 433 } else if (element.isPrimitive() || (element.hasType() && isPrimitive(element.getType()))) { 434 if (element.getType().equals("xhtml")) { 435 xml.escapedText(element.getValue()); 436 } else if (isText(element.getProperty())) { 437 if (linkResolver != null) 438 xml.link(linkResolver.resolveProperty(element.getProperty())); 439 xml.text(element.getValue()); 440 } else { 441 if (element.hasValue()) { 442 if (linkResolver != null) 443 xml.link(linkResolver.resolveType(element.getType())); 444 xml.attribute("value", element.getValue()); 445 } 446 if (linkResolver != null) 447 xml.link(linkResolver.resolveProperty(element.getProperty())); 448 if (element.hasChildren()) { 449 xml.enter(elementName); 450 for (Element child : element.getChildren()) 451 composeElement(xml, child, child.getName()); 452 xml.exit(elementName); 453 } else 454 xml.element(elementName); 455 } 456 } else { 457 for (Element child : element.getChildren()) { 458 if (isAttr(child.getProperty())) { 459 if (linkResolver != null) 460 xml.link(linkResolver.resolveType(child.getType())); 461 xml.attribute(child.getName(), child.getValue()); 462 } 463 } 464 if (linkResolver != null) 465 xml.link(linkResolver.resolveProperty(element.getProperty())); 466 xml.enter(elementName); 467 if (element.getSpecial() != null) { 468 if (linkResolver != null) 469 xml.link(linkResolver.resolveProperty(element.getProperty())); 470 xml.enter(element.getType()); 471 } 472 for (Element child : element.getChildren()) { 473 if (isText(child.getProperty())) { 474 if (linkResolver != null) 475 xml.link(linkResolver.resolveProperty(element.getProperty())); 476 xml.text(child.getValue()); 477 } else if (!isAttr(child.getProperty())) 478 composeElement(xml, child, child.getName()); 479 } 480 if (element.getSpecial() != null) 481 xml.exit(element.getType()); 482 xml.exit(elementName); 483 } 484 } 485 486}