001package org.hl7.fhir.r5.elementmodel;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009
010 * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012 * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015 * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029
030 */
031
032
033import java.io.IOException;
034import java.io.InputStream;
035import java.io.OutputStream;
036import java.util.ArrayList;
037import java.util.Collections;
038import java.util.Comparator;
039import java.util.List;
040
041import javax.xml.parsers.DocumentBuilder;
042import javax.xml.parsers.DocumentBuilderFactory;
043import javax.xml.parsers.SAXParser;
044import javax.xml.parsers.SAXParserFactory;
045import javax.xml.transform.Transformer;
046import javax.xml.transform.TransformerFactory;
047import javax.xml.transform.dom.DOMResult;
048import javax.xml.transform.sax.SAXSource;
049
050import org.hl7.fhir.exceptions.DefinitionException;
051import org.hl7.fhir.exceptions.FHIRException;
052import org.hl7.fhir.exceptions.FHIRFormatError;
053import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
054import org.hl7.fhir.r5.context.IWorkerContext;
055import org.hl7.fhir.r5.elementmodel.Element.SpecialElement;
056import org.hl7.fhir.r5.formats.FormatUtilities;
057import org.hl7.fhir.r5.formats.IParser.OutputStyle;
058import org.hl7.fhir.r5.model.DateTimeType;
059import org.hl7.fhir.r5.model.ElementDefinition.PropertyRepresentation;
060import org.hl7.fhir.r5.model.Enumeration;
061import org.hl7.fhir.r5.model.StructureDefinition;
062import org.hl7.fhir.r5.utils.ToolingExtensions;
063import org.hl7.fhir.r5.utils.formats.XmlLocationAnnotator;
064import org.hl7.fhir.r5.utils.formats.XmlLocationData;
065import org.hl7.fhir.utilities.ElementDecoration;
066import org.hl7.fhir.utilities.StringPair;
067import org.hl7.fhir.utilities.Utilities;
068import org.hl7.fhir.utilities.i18n.I18nConstants;
069import org.hl7.fhir.utilities.validation.ValidationMessage;
070import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
071import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
072import org.hl7.fhir.utilities.xhtml.CDANarrativeFormat;
073import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
074import org.hl7.fhir.utilities.xhtml.XhtmlNode;
075import org.hl7.fhir.utilities.xhtml.XhtmlParser;
076import org.hl7.fhir.utilities.xml.IXMLWriter;
077import org.hl7.fhir.utilities.xml.XMLUtil;
078import org.hl7.fhir.utilities.xml.XMLWriter;
079import org.w3c.dom.Document;
080import org.w3c.dom.Node;
081import org.xml.sax.ErrorHandler;
082import org.xml.sax.InputSource;
083import org.xml.sax.SAXParseException;
084import org.xml.sax.XMLReader;
085
086public class XmlParser extends ParserBase {
087  private boolean allowXsiLocation;
088  private String version;
089
090  public XmlParser(IWorkerContext context) {
091    super(context);
092  }
093
094  private String schemaPath;
095
096  public String getSchemaPath() {
097    return schemaPath;
098  }
099  public void setSchemaPath(String schemaPath) {
100    this.schemaPath = schemaPath;
101  }
102
103  public boolean isAllowXsiLocation() {
104    return allowXsiLocation;
105  }
106
107  public void setAllowXsiLocation(boolean allowXsiLocation) {
108    this.allowXsiLocation = allowXsiLocation;
109  }
110
111  public List<NamedElement> parse(InputStream stream) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
112    List<NamedElement> res = new ArrayList<>();
113    Document doc = null;
114    try {
115      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
116      // xxe protection
117      factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
118      factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
119      factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
120      factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
121      factory.setXIncludeAware(false);
122      factory.setExpandEntityReferences(false);
123
124      factory.setNamespaceAware(true);
125      if (policy == ValidationPolicy.EVERYTHING) {
126        // The SAX interface appears to not work when reporting the correct version/encoding.
127        // if we can, we'll inspect the header/encoding ourselves 
128        if (stream.markSupported()) {
129          stream.mark(1024);
130          version = checkHeader(stream);
131          stream.reset();
132        }
133        // use a slower parser that keeps location data
134        TransformerFactory transformerFactory = TransformerFactory.newInstance();
135        Transformer nullTransformer = transformerFactory.newTransformer();
136        DocumentBuilder docBuilder = factory.newDocumentBuilder();
137        doc = docBuilder.newDocument();
138        DOMResult domResult = new DOMResult(doc);
139        SAXParserFactory spf = SAXParserFactory.newInstance();
140        spf.setNamespaceAware(true);
141        spf.setValidating(false);
142        // xxe protection
143        spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
144        spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
145        SAXParser saxParser = spf.newSAXParser();
146        XMLReader xmlReader = saxParser.getXMLReader();
147        // xxe protection
148        xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
149        xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
150
151        XmlLocationAnnotator locationAnnotator = new XmlLocationAnnotator(xmlReader, doc);
152        InputSource inputSource = new InputSource(stream);
153        SAXSource saxSource = new SAXSource(locationAnnotator, inputSource);
154        nullTransformer.transform(saxSource, domResult);
155      } else {
156        DocumentBuilder builder = factory.newDocumentBuilder();
157        builder.setErrorHandler(new NullErrorHandler());
158        doc = builder.parse(stream);
159      }
160    } catch (Exception e) {
161      if (e.getMessage().contains("lineNumber:") && e.getMessage().contains("columnNumber:")) {
162        int line = Utilities.parseInt(extractVal(e.getMessage(), "lineNumber"), 0); 
163        int col = Utilities.parseInt(extractVal(e.getMessage(), "columnNumber"), 0); 
164        logError(ValidationMessage.NO_RULE_DATE, line, col, "(xml)", IssueType.INVALID, e.getMessage().substring(e.getMessage().lastIndexOf(";")+1).trim(), IssueSeverity.FATAL);
165      } else {
166        logError(ValidationMessage.NO_RULE_DATE, 0, 0, "(xml)", IssueType.INVALID, e.getMessage(), IssueSeverity.FATAL);
167      }
168      doc = null;
169    }
170    if (doc != null) {
171      Element e = parse(doc);
172      if (e != null) {
173        res.add(new NamedElement(null, e));
174      }
175    }
176    return res;
177  }
178
179
180  private String extractVal(String src, String name) {
181    src = src.substring(src.indexOf(name)+name.length()+1);
182    src = src.substring(0, src.indexOf(";")).trim();
183    return src;
184  }
185  private void checkForProcessingInstruction(Document document) throws FHIRFormatError {
186    if (policy == ValidationPolicy.EVERYTHING && FormatUtilities.FHIR_NS.equals(document.getDocumentElement().getNamespaceURI())) {
187      Node node = document.getFirstChild();
188      while (node != null) {
189        if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE)
190          logError(ValidationMessage.NO_RULE_DATE, line(document, false), col(document, false), "(document)", IssueType.INVALID, context.formatMessage(
191              I18nConstants.NO_PROCESSING_INSTRUCTIONS_ALLOWED_IN_RESOURCES), IssueSeverity.ERROR);
192        node = node.getNextSibling();
193      }
194    }
195  }
196
197
198  private int line(Node node, boolean end) {
199    XmlLocationData loc = node == null ? null : (XmlLocationData) node.getUserData(XmlLocationData.LOCATION_DATA_KEY);
200    return loc == null ? 0 : end ? loc.getEndLine() : loc.getStartLine();
201  }
202
203  private int col(Node node, boolean end) {
204    XmlLocationData loc = node == null ? null : (XmlLocationData) node.getUserData(XmlLocationData.LOCATION_DATA_KEY);
205    return loc == null ? 0 : end ? loc.getEndColumn() : loc.getStartColumn();
206  }
207
208  public Element parse(Document doc) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
209    checkForProcessingInstruction(doc);
210    org.w3c.dom.Element element = doc.getDocumentElement();
211    return parse(element);
212  }
213
214  public Element parse(org.w3c.dom.Element element) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
215    String ns = element.getNamespaceURI();
216    String name = element.getLocalName();
217    String path = "/"+pathPrefix(ns)+name;
218
219    StructureDefinition sd = getDefinition(line(element, false), col(element, false), (ns == null ? "noNamespace" : ns), name);
220    if (sd == null)
221      return null;
222
223    Element result = new Element(element.getLocalName(), new Property(context, sd.getSnapshot().getElement().get(0), sd));
224    result.setPath(element.getLocalName());
225    checkElement(element, path, result.getProperty());
226    result.markLocation(line(element, false), col(element, false));
227    result.setType(element.getLocalName());
228    parseChildren(path, element, result);
229    result.numberChildren();
230    return result;
231  }
232
233  private String pathPrefix(String ns) {
234    if (Utilities.noString(ns))
235      return "";
236    if (ns.equals(FormatUtilities.FHIR_NS))
237      return "f:";
238    if (ns.equals(FormatUtilities.XHTML_NS))
239      return "h:";
240    if (ns.equals("urn:hl7-org:v3"))
241      return "v3:";
242    if (ns.equals("urn:hl7-org:sdtc")) 
243      return "sdtc:";
244    if (ns.equals("urn:ihe:pharm"))
245      return "pharm:";
246    return "?:";
247  }
248
249  private boolean empty(org.w3c.dom.Element element) {
250    for (int i = 0; i < element.getAttributes().getLength(); i++) {
251      String n = element.getAttributes().item(i).getNodeName();
252      if (!n.equals("xmlns") && !n.startsWith("xmlns:"))
253        return false;
254    }
255    if (!Utilities.noString(element.getTextContent().trim()))
256      return false;
257
258    Node n = element.getFirstChild();
259    while (n != null) {
260      if (n.getNodeType() == Node.ELEMENT_NODE)
261        return false;
262      n = n.getNextSibling();
263    }
264    return true;
265  }
266
267  private void checkElement(org.w3c.dom.Element element, String path, Property prop) throws FHIRFormatError {
268    if (policy == ValidationPolicy.EVERYTHING) {
269      if (empty(element) && FormatUtilities.FHIR_NS.equals(element.getNamespaceURI())) // this rule only applies to FHIR Content
270        logError(ValidationMessage.NO_RULE_DATE, line(element, false), col(element, false), path, IssueType.INVALID, context.formatMessage(I18nConstants.ELEMENT_MUST_HAVE_SOME_CONTENT), IssueSeverity.ERROR);
271      String ns = prop.getXmlNamespace();
272      String elementNs = element.getNamespaceURI();
273      if (elementNs == null) {
274        elementNs = "noNamespace";
275      }
276      if (!elementNs.equals(ns))
277        logError(ValidationMessage.NO_RULE_DATE, line(element, false), col(element, false), path, IssueType.INVALID, context.formatMessage(I18nConstants.WRONG_NAMESPACE__EXPECTED_, ns), IssueSeverity.ERROR);
278    }
279  }
280
281  public Element parse(org.w3c.dom.Element base, String type) throws Exception {
282    StructureDefinition sd = getDefinition(0, 0, FormatUtilities.FHIR_NS, type);
283    Element result = new Element(base.getLocalName(), new Property(context, sd.getSnapshot().getElement().get(0), sd));
284    result.setPath(base.getLocalName());
285    String path = "/"+pathPrefix(base.getNamespaceURI())+base.getLocalName();
286    checkElement(base, path, result.getProperty());
287    result.setType(base.getLocalName());
288    parseChildren(path, base, result);
289    result.numberChildren();
290    return result;
291  }
292
293  private void parseChildren(String path, org.w3c.dom.Element node, Element element) throws FHIRFormatError, FHIRException, IOException, DefinitionException {
294    // this parsing routine retains the original order in a the XML file, to support validation
295    reapComments(node, element);
296    List<Property> properties = element.getProperty().getChildProperties(element.getName(), XMLUtil.getXsiType(node));
297
298    String text = XMLUtil.getDirectText(node).trim();
299    int line = line(node, false);
300    int col = col(node, false);
301    if (!Utilities.noString(text)) {
302      Property property = getTextProp(properties);
303      if (property != null) {
304        if ("ED.data[x]".equals(property.getDefinition().getId()) || (property.getDefinition()!=null && property.getDefinition().getBase()!=null && "ED.data[x]".equals(property.getDefinition().getBase().getPath()))) {
305          if ("B64".equals(node.getAttribute("representation"))) {
306            Element n = new Element("dataBase64Binary", property, "base64Binary", text).markLocation(line, col);
307            n.setPath(element.getPath()+"."+property.getName());
308            element.getChildren().add(n);
309          } else {
310            Element n = new Element("dataString", property, "string", text).markLocation(line, col);
311            n.setPath(element.getPath()+"."+property.getName());
312            element.getChildren().add(n);
313          }
314        } else {
315          Element n = new Element(property.getName(), property, property.getType(), text).markLocation(line, col);
316          n.setPath(element.getPath()+"."+property.getName());
317          element.getChildren().add(n);
318        }
319      } 
320      else {
321        Node n = node.getFirstChild();
322        while (n != null) {
323          if (n.getNodeType() == Node.TEXT_NODE && !Utilities.noString(n.getTextContent().trim())) {
324            Node nt = n; // try to find the nearest element for a line/col location
325            boolean end = false;
326            while (nt.getPreviousSibling() != null && nt.getNodeType() != Node.ELEMENT_NODE) {
327              nt = nt.getPreviousSibling();
328              end = true;
329            }
330            while (nt.getNextSibling() != null && nt.getNodeType() != Node.ELEMENT_NODE) {
331              nt = nt.getNextSibling();
332              end = false;
333            }
334            line = line(nt, end);
335            col = col(nt, end);
336            logError(ValidationMessage.NO_RULE_DATE, line, col, path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.TEXT_SHOULD_NOT_BE_PRESENT, Utilities.makeSingleLine(n.getTextContent().trim())), IssueSeverity.ERROR);
337          }
338          n = n.getNextSibling();
339        }
340      }                 
341    }
342
343    for (int i = 0; i < node.getAttributes().getLength(); i++) {
344      Node attr = node.getAttributes().item(i);
345      String value = attr.getNodeValue();
346      if (!validAttrValue(value)) {
347        logError(ValidationMessage.NO_RULE_DATE, line, col, path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.XML_ATTR_VALUE_INVALID, attr.getNodeName()), IssueSeverity.ERROR);
348      }
349      if (!(attr.getNodeName().equals("xmlns") || attr.getNodeName().startsWith("xmlns:"))) {
350        Property property = getAttrProp(properties, attr.getLocalName(), attr.getNamespaceURI());
351        if (property != null) {
352          String av = attr.getNodeValue();
353          if (ToolingExtensions.hasExtension(property.getDefinition(), ToolingExtensions.EXT_DATE_FORMAT))
354            av = convertForDateFormatFromExternal(ToolingExtensions.readStringExtension(property.getDefinition(), ToolingExtensions.EXT_DATE_FORMAT), av);
355          if (property.getName().equals("value") && element.isPrimitive())
356            element.setValue(av);
357          else {
358            Element n = new Element(property.getName(), property, property.getType(), av).markLocation(line, col);
359            n.setPath(element.getPath()+"."+property.getName());
360            element.getChildren().add(n);
361          }
362        } else {
363          boolean ok = false;
364          if (FormatUtilities.FHIR_NS.equals(node.getNamespaceURI())) {
365            if (attr.getLocalName().equals("schemaLocation") && FormatUtilities.NS_XSI.equals(attr.getNamespaceURI())) {
366              ok = ok || allowXsiLocation; 
367            }
368          } else
369            ok = ok || (attr.getLocalName().equals("schemaLocation")); // xsi:schemalocation allowed for non FHIR content
370          ok = ok || (hasTypeAttr(element) && attr.getLocalName().equals("type") && FormatUtilities.NS_XSI.equals(attr.getNamespaceURI())); // xsi:type allowed if element says so
371          if (!ok) { 
372            logError(ValidationMessage.NO_RULE_DATE, line(node, false), col(node, false), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.UNDEFINED_ATTRIBUTE__ON__FOR_TYPE__PROPERTIES__, attr.getNodeName(), node.getNodeName(), element.fhirType(), properties), IssueSeverity.ERROR);
373          }
374        }
375      }
376    }
377
378    String lastName = null;
379    int repeatCount = 0;
380    Node child = node.getFirstChild();
381    while (child != null) {
382      if (child.getNodeType() == Node.ELEMENT_NODE) {
383        Property property = getElementProp(properties, child.getLocalName(), child.getNamespaceURI());
384        if (property != null) {
385          if (property.getName().equals(lastName)) {
386            repeatCount++;
387          } else {
388            lastName = property.getName();
389            repeatCount = 0;
390          }
391          if (!property.isChoice() && "xhtml".equals(property.getType())) {
392            XhtmlNode xhtml;
393            if (property.getDefinition().hasRepresentation(PropertyRepresentation.CDATEXT))
394              xhtml = new CDANarrativeFormat().convert((org.w3c.dom.Element) child);
395            else {
396              XhtmlParser xp = new XhtmlParser();
397              xhtml = xp.parseHtmlNode((org.w3c.dom.Element) child);
398              if (policy == ValidationPolicy.EVERYTHING) {
399                for (StringPair s : xp.getValidationIssues()) {
400                  logError("2022-11-17", line(child, false), col(child, false), path, IssueType.INVALID, context.formatMessage(s.getName(), s.getValue()), IssueSeverity.ERROR);                
401                }
402              }
403            }
404            Element n = new Element(property.getName(), property, "xhtml", new XhtmlComposer(XhtmlComposer.XML, false).compose(xhtml)).setXhtml(xhtml).markLocation(line(child, false), col(child, false));
405            n.setPath(element.getPath()+"."+property.getName());
406            element.getChildren().add(n);
407          } else {
408            String npath = path+"/"+pathPrefix(child.getNamespaceURI())+child.getLocalName();
409            Element n = new Element(child.getLocalName(), property).markLocation(line(child, false), col(child, false));
410            if (property.isList()) {
411              n.setPath(element.getPath()+"."+property.getName()+"["+repeatCount+"]");                                    
412            } else {
413              n.setPath(element.getPath()+"."+property.getName());
414            }
415            checkElement((org.w3c.dom.Element) child, npath, n.getProperty());
416            boolean ok = true;
417            if (property.isChoice()) {
418              if (property.getDefinition().hasRepresentation(PropertyRepresentation.TYPEATTR)) {
419                String xsiType = ((org.w3c.dom.Element) child).getAttributeNS(FormatUtilities.NS_XSI, "type");
420                if (Utilities.noString(xsiType)) {
421                  if (ToolingExtensions.hasExtension(property.getDefinition(), "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype")) {
422                    xsiType = ToolingExtensions.readStringExtension(property.getDefinition(), "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype");
423                    n.setType(xsiType);
424                  } else {
425                    logError(ValidationMessage.NO_RULE_DATE, line(child, false), col(child, false), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.NO_TYPE_FOUND_ON_, child.getLocalName()), IssueSeverity.ERROR);
426                    ok = false;
427                  }
428                } else {
429                  if (xsiType.contains(":"))
430                    xsiType = xsiType.substring(xsiType.indexOf(":")+1);
431                  n.setType(xsiType);
432                  n.setExplicitType(xsiType);
433                }
434              } else
435                n.setType(n.getType());
436            }
437            element.getChildren().add(n);
438            if (ok) {
439              if (property.isResource())
440                parseResource(npath, (org.w3c.dom.Element) child, n, property);
441              else
442                parseChildren(npath, (org.w3c.dom.Element) child, n);
443            }
444          }
445        } else {
446          logError(ValidationMessage.NO_RULE_DATE, line(child, false), col(child, false), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.UNDEFINED_ELEMENT_, child.getLocalName(), path), IssueSeverity.ERROR);
447        }
448      } else if (child.getNodeType() == Node.CDATA_SECTION_NODE){
449        logError(ValidationMessage.NO_RULE_DATE, line(child, false), col(child, false), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.CDATA_IS_NOT_ALLOWED), IssueSeverity.ERROR);
450      } else if (!Utilities.existsInList(child.getNodeType(), 3, 8)) {
451        logError(ValidationMessage.NO_RULE_DATE, line(child, false), col(child, false), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.NODE_TYPE__IS_NOT_ALLOWED, Integer.toString(child.getNodeType())), IssueSeverity.ERROR);
452      }
453      child = child.getNextSibling();
454    }
455  }
456
457  private boolean validAttrValue(String value) {
458    if (version == null) {
459      return true;
460    }
461    if (version.equals("1.0")) {
462      boolean ok = true;
463      for (char ch : value.toCharArray()) {
464        if (ch <= 0x1F && !Utilities.existsInList(ch, '\r', '\n', '\t')) {
465          ok = false;
466        }
467      }
468      return ok;
469    } else
470      return true;
471  }
472
473
474  private Property getElementProp(List<Property> properties, String nodeName, String namespace) {
475    List<Property> propsSortedByLongestFirst = new ArrayList<Property>(properties);
476    // sort properties according to their name longest first, so .requestOrganizationReference comes first before .request[x]
477    // and therefore the longer property names get evaluated first
478    Collections.sort(propsSortedByLongestFirst, new Comparator<Property>() {
479      @Override
480      public int compare(Property o1, Property o2) {
481        return o2.getName().length() - o1.getName().length();
482      }
483    });
484    // first scan, by namespace
485    for (Property p : propsSortedByLongestFirst) {
486      if (!p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR) && !p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) {
487        if (p.getXmlName().equals(nodeName) && p.getXmlNamespace().equals(namespace)) 
488          return p;
489      }
490    }
491    for (Property p : propsSortedByLongestFirst) {
492      if (!p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR) && !p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) {
493        if (p.getXmlName().equals(nodeName)) 
494          return p;
495        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))) 
496          return p;
497      }
498    }
499    return null;
500  }
501
502  private Property getAttrProp(List<Property> properties, String nodeName, String namespace) {
503    for (Property p : properties) {
504      if (p.getXmlName().equals(nodeName) && p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR) && p.getXmlNamespace().equals(namespace)) {
505        return p;
506      }
507    }
508    if (namespace == null) {
509      for (Property p : properties) {
510        if (p.getXmlName().equals(nodeName) && p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR)) {
511          return p;
512        }    
513      }
514    }
515    return null;
516  }
517
518  private Property getTextProp(List<Property> properties) {
519    for (Property p : properties)
520      if (p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) 
521        return p;
522    return null;
523  }
524
525  private String convertForDateFormatFromExternal(String fmt, String av) throws FHIRException {
526    if ("v3".equals(fmt) || "YYYYMMDDHHMMSS.UUUU[+|-ZZzz]".equals(fmt)) {
527      try {
528        DateTimeType d = DateTimeType.parseV3(av);
529        return d.asStringValue();
530      } catch (Exception e) {
531        return av; // not at all clear what to do in this case.
532      }
533    }
534    throw new FHIRException(context.formatMessage(I18nConstants.UNKNOWN_DATA_FORMAT_, fmt));
535  }
536
537  private String convertForDateFormatToExternal(String fmt, String av) throws FHIRException {
538    if ("v3".equals(fmt)) {
539      DateTimeType d = new DateTimeType(av);
540      return d.getAsV3();
541    } else
542      throw new FHIRException(context.formatMessage(I18nConstants.UNKNOWN_DATE_FORMAT_, fmt));
543  }
544
545  private void parseResource(String string, org.w3c.dom.Element container, Element parent, Property elementProperty) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
546    org.w3c.dom.Element res = XMLUtil.getFirstChild(container);
547    String name = res.getLocalName();
548    StructureDefinition sd = context.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(name, null));
549    if (sd == null)
550      throw new FHIRFormatError(context.formatMessage(I18nConstants.CONTAINED_RESOURCE_DOES_NOT_APPEAR_TO_BE_A_FHIR_RESOURCE_UNKNOWN_NAME_, res.getLocalName()));
551    parent.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd), SpecialElement.fromProperty(parent.getProperty()), elementProperty);
552    parent.setType(name);
553    parseChildren(res.getLocalName(), res, parent);
554  }
555
556  private void reapComments(org.w3c.dom.Element element, Element context) {
557    Node node = element.getPreviousSibling();
558    while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
559      if (node.getNodeType() == Node.COMMENT_NODE)
560        context.getComments().add(0, node.getTextContent());
561      node = node.getPreviousSibling();
562    }
563    node = element.getLastChild();
564    while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
565      node = node.getPreviousSibling();
566    }
567    while (node != null) {
568      if (node.getNodeType() == Node.COMMENT_NODE)
569        context.getComments().add(node.getTextContent());
570      node = node.getNextSibling();
571    }
572  }
573
574  private boolean isAttr(Property property) {
575    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
576      if (r.getValue() == PropertyRepresentation.XMLATTR) {
577        return true;
578      }
579    }
580    return false;
581  }
582
583  private boolean isCdaText(Property property) {
584    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
585      if (r.getValue() == PropertyRepresentation.CDATEXT) {
586        return true;
587      }
588    }
589    return false;
590  }
591
592  private boolean isTypeAttr(Property property) {
593    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
594      if (r.getValue() == PropertyRepresentation.TYPEATTR) {
595        return true;
596      }
597    }
598    return false;
599  }
600
601  private boolean isText(Property property) {
602    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
603      if (r.getValue() == PropertyRepresentation.XMLTEXT) {
604        return true;
605      }
606    }
607    return false;
608  }
609
610  @Override
611  public void compose(Element e, OutputStream stream, OutputStyle style, String base) throws IOException, FHIRException {
612    XMLWriter xml = new XMLWriter(stream, "UTF-8");
613    xml.setSortAttributes(false);
614    xml.setPretty(style == OutputStyle.PRETTY);
615    xml.start();
616    if (e.getPath() == null) {
617      e.populatePaths(null);
618    }
619    String ns = e.getProperty().getXmlNamespace();
620    if (ns!=null && !"noNamespace".equals(ns)) {
621      xml.setDefaultNamespace(ns);
622    }
623    if (hasTypeAttr(e))
624      xml.namespace("http://www.w3.org/2001/XMLSchema-instance", "xsi");
625    addNamespaces(xml, e);
626    composeElement(xml, e, e.getType(), true);
627    xml.end();
628  }
629
630  private void addNamespaces(IXMLWriter xml, Element e) throws IOException {
631    String ns = e.getProperty().getXmlNamespace();
632    if (ns!=null && xml.getDefaultNamespace()!=null && !xml.getDefaultNamespace().equals(ns)){
633      if (!xml.namespaceDefined(ns)) {
634        String prefix = pathPrefix(ns);
635        if (prefix.endsWith(":")) {
636          prefix = prefix.substring(0, prefix.length()-1);
637        }
638        if ("?".equals(prefix)) {
639          xml.namespace(ns);
640        } else {
641          xml.namespace(ns, prefix);
642        }
643      }
644    }
645    for (Element c : e.getChildren()) {
646      addNamespaces(xml, c);
647    }
648  }
649
650  private boolean hasTypeAttr(Element e) {
651    if (isTypeAttr(e.getProperty()))
652      return true;
653    for (Element c : e.getChildren()) {
654      if (hasTypeAttr(c))
655        return true;
656    }
657    return false;
658  }
659
660  private void setXsiTypeIfIsTypeAttr(IXMLWriter xml, Element element) throws IOException, FHIRException {
661    if (isTypeAttr(element.getProperty()) && !Utilities.noString(element.getType())) {
662      String type = element.getType();
663      if (Utilities.isAbsoluteUrl(type)) {
664        type = type.substring(type.lastIndexOf("/")+1);
665      }
666      xml.attribute("xsi:type",type);    
667    }
668  }
669
670  public void compose(Element e, IXMLWriter xml) throws Exception {
671    if (e.getPath() == null) {
672      e.populatePaths(null);
673    }
674    xml.start();
675    xml.setDefaultNamespace(e.getProperty().getXmlNamespace());
676    if (schemaPath != null) {
677      xml.setSchemaLocation(FormatUtilities.FHIR_NS, Utilities.pathURL(schemaPath, e.fhirType()+".xsd"));
678    }
679    composeElement(xml, e, e.getType(), true);
680    xml.end();
681  }
682
683  private void composeElement(IXMLWriter xml, Element element, String elementName, boolean root) throws IOException, FHIRException {
684    if (showDecorations) {
685      @SuppressWarnings("unchecked")
686      List<ElementDecoration> decorations = (List<ElementDecoration>) element.getUserData("fhir.decorations");
687      if (decorations != null)
688        for (ElementDecoration d : decorations)
689          xml.decorate(d);
690    }
691    for (String s : element.getComments()) {
692      xml.comment(s, true);
693    }
694    if (isText(element.getProperty())) {
695      if (linkResolver != null)
696        xml.link(linkResolver.resolveProperty(element.getProperty()));
697      xml.enter(element.getProperty().getXmlNamespace(),elementName);
698      xml.text(element.getValue());
699      xml.exit(element.getProperty().getXmlNamespace(),elementName);   
700    } else if (!element.hasChildren() && !element.hasValue()) {
701      if (element.getExplicitType() != null)
702        xml.attribute("xsi:type", element.getExplicitType());
703      xml.element(elementName);
704    } else if (element.isPrimitive() || (element.hasType() && isPrimitive(element.getType()))) {
705      if (element.getType().equals("xhtml")) {
706        String rawXhtml = element.getValue();
707        if (isCdaText(element.getProperty())) {
708          new CDANarrativeFormat().convert(xml, new XhtmlParser().parseFragment(rawXhtml));
709        } else {
710          xml.escapedText(rawXhtml);
711          xml.anchor("end-xhtml");
712        }
713      } else if (isText(element.getProperty())) {
714        if (linkResolver != null)
715          xml.link(linkResolver.resolveProperty(element.getProperty()));
716        xml.text(element.getValue());
717      } else {
718        setXsiTypeIfIsTypeAttr(xml, element);
719        if (element.hasValue()) {
720          if (linkResolver != null)
721            xml.link(linkResolver.resolveType(element.getType()));
722          xml.attribute("value", element.getValue());
723        }
724        if (linkResolver != null)
725          xml.link(linkResolver.resolveProperty(element.getProperty()));
726        if (element.hasChildren()) {
727          xml.enter(element.getProperty().getXmlNamespace(), elementName);
728          for (Element child : element.getChildren()) 
729            composeElement(xml, child, child.getName(), false);
730          xml.exit(element.getProperty().getXmlNamespace(),elementName);
731        } else
732          xml.element(elementName);
733      }
734    } else {
735      setXsiTypeIfIsTypeAttr(xml, element);
736      for (Element child : element.getChildren()) {
737        if (isAttr(child.getProperty()) && wantCompose(element.getPath(), child)) {
738          if (linkResolver != null)
739            xml.link(linkResolver.resolveType(child.getType()));
740          String av = child.getValue();
741          if (ToolingExtensions.hasExtension(child.getProperty().getDefinition(), ToolingExtensions.EXT_DATE_FORMAT))
742            av = convertForDateFormatToExternal(ToolingExtensions.readStringExtension(child.getProperty().getDefinition(), ToolingExtensions.EXT_DATE_FORMAT), av);
743          xml.attribute(child.getProperty().getXmlNamespace(),child.getProperty().getXmlName(), av);
744        }
745      }
746      if (linkResolver != null)
747        xml.link(linkResolver.resolveProperty(element.getProperty()));
748      xml.enter(element.getProperty().getXmlNamespace(),elementName);
749      if (!root && element.getSpecial() != null) {
750        if (linkResolver != null)
751          xml.link(linkResolver.resolveProperty(element.getProperty()));
752        xml.enter(element.getProperty().getXmlNamespace(),element.getType());
753      }
754      for (Element child : element.getChildren()) {
755        if (wantCompose(element.getPath(), child)) {
756          if (isText(child.getProperty())) {
757            if (linkResolver != null)
758              xml.link(linkResolver.resolveProperty(element.getProperty()));
759            xml.text(child.getValue());
760          } else if (!isAttr(child.getProperty()))
761            composeElement(xml, child, child.getName(), false);
762        }
763      }
764      if (!root && element.getSpecial() != null)
765        xml.exit(element.getProperty().getXmlNamespace(),element.getType());
766      xml.exit(element.getProperty().getXmlNamespace(),elementName);
767    }
768  }
769
770  private String checkHeader(InputStream stream) throws IOException {
771    try {
772      // the stream will either start with the UTF-8 BOF or with <xml
773      int i0 = stream.read();
774      int i1 = stream.read();
775      int i2 = stream.read();
776
777      StringBuilder b = new StringBuilder();
778      if (i0 == 0xEF && i1 == 0xBB && i2 == 0xBF) {
779        // ok, it's UTF-8
780      } else if (i0 == 0x3C && i1 == 0x3F && i2 == 0x78) { // <xm
781        b.append((char) i0);
782        b.append((char) i1);
783        b.append((char) i2);
784      } else if (i0 == 60) { // just plain old XML with no header
785        return "1.0";        
786      } else {
787        throw new Exception(context.formatMessage(I18nConstants.XML_ENCODING_INVALID));
788      }
789      int i = stream.read();
790      do {
791        b.append((char) i);
792        i = stream.read();
793      } while (i != 0x3E);
794      String header = b.toString();
795      String e = null;
796      i = header.indexOf("encoding=\"");
797      if (i > -1) {
798        e = header.substring(i+10, i+15);
799      } else {
800        i = header.indexOf("encoding='");
801        if (i > -1) {
802          e = header.substring(i+10, i+15);
803        } 
804      }
805      if (e != null && !"UTF-8".equalsIgnoreCase(e)) {
806        logError(ValidationMessage.NO_RULE_DATE, 0, 0, "XML", IssueType.INVALID, context.formatMessage(I18nConstants.XML_ENCODING_INVALID), IssueSeverity.ERROR);
807      }
808
809      i = header.indexOf("version=\"");
810      if (i > -1) {
811        return header.substring(i+9, i+12);
812      } else {
813        i = header.indexOf("version='");
814        if (i > -1) {
815          return header.substring(i+9, i+12);          
816        } 
817      }
818      return "?xml-p1?";
819    } catch (Exception e) {
820      // suppress this error 
821      logError(ValidationMessage.NO_RULE_DATE, 0, 0, "XML", IssueType.INVALID, e.getMessage(), IssueSeverity.ERROR);
822    }
823    return "?xml-p2?";
824  }
825
826  class NullErrorHandler implements ErrorHandler {
827    @Override
828    public void fatalError(SAXParseException e) {
829        // do nothing
830    }
831
832    @Override
833    public void error(SAXParseException e) {
834        // do nothing
835    }
836    
837    @Override
838    public void warning(SAXParseException e) {
839        // do nothing
840    }
841}
842}