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}