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