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}