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}