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