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.InputSource;
082import org.xml.sax.XMLReader;
083
084public class XmlParser extends ParserBase {
085  private boolean allowXsiLocation;
086  private String version;
087
088  public XmlParser(IWorkerContext context) {
089    super(context);
090  }
091
092  private String schemaPath;
093
094  public String getSchemaPath() {
095    return schemaPath;
096  }
097  public void setSchemaPath(String schemaPath) {
098    this.schemaPath = schemaPath;
099  }
100
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        doc = builder.parse(stream);
158      }
159    } catch (Exception e) {
160      if (e.getMessage().contains("lineNumber:") && e.getMessage().contains("columnNumber:")) {
161        int line = Utilities.parseInt(extractVal(e.getMessage(), "lineNumber"), 0); 
162        int col = Utilities.parseInt(extractVal(e.getMessage(), "columnNumber"), 0); 
163        logError(ValidationMessage.NO_RULE_DATE, line, col, "(xml)", IssueType.INVALID, e.getMessage().substring(e.getMessage().lastIndexOf(";")+1).trim(), IssueSeverity.FATAL);
164      } else {
165        logError(ValidationMessage.NO_RULE_DATE, 0, 0, "(xml)", IssueType.INVALID, e.getMessage(), IssueSeverity.FATAL);
166      }
167      doc = null;
168    }
169    if (doc != null) {
170      Element e = parse(doc);
171      if (e != null) {
172        res.add(new NamedElement(null, e));
173      }
174    }
175    return res;
176  }
177
178
179  private String extractVal(String src, String name) {
180    src = src.substring(src.indexOf(name)+name.length()+1);
181    src = src.substring(0, src.indexOf(";")).trim();
182    return src;
183  }
184  private void checkForProcessingInstruction(Document document) throws FHIRFormatError {
185    if (policy == ValidationPolicy.EVERYTHING && FormatUtilities.FHIR_NS.equals(document.getDocumentElement().getNamespaceURI())) {
186      Node node = document.getFirstChild();
187      while (node != null) {
188        if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE)
189          logError(ValidationMessage.NO_RULE_DATE, line(document, false), col(document, false), "(document)", IssueType.INVALID, context.formatMessage(
190              I18nConstants.NO_PROCESSING_INSTRUCTIONS_ALLOWED_IN_RESOURCES), IssueSeverity.ERROR);
191        node = node.getNextSibling();
192      }
193    }
194  }
195
196
197  private int line(Node node, boolean end) {
198    XmlLocationData loc = node == null ? null : (XmlLocationData) node.getUserData(XmlLocationData.LOCATION_DATA_KEY);
199    return loc == null ? 0 : end ? loc.getEndLine() : loc.getStartLine();
200  }
201
202  private int col(Node node, boolean end) {
203    XmlLocationData loc = node == null ? null : (XmlLocationData) node.getUserData(XmlLocationData.LOCATION_DATA_KEY);
204    return loc == null ? 0 : end ? loc.getEndColumn() : loc.getStartColumn();
205  }
206
207  public Element parse(Document doc) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
208    checkForProcessingInstruction(doc);
209    org.w3c.dom.Element element = doc.getDocumentElement();
210    return parse(element);
211  }
212
213  public Element parse(org.w3c.dom.Element element) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
214    String ns = element.getNamespaceURI();
215    String name = element.getLocalName();
216    String path = "/"+pathPrefix(ns)+name;
217
218    StructureDefinition sd = getDefinition(line(element, false), col(element, false), (ns == null ? "noNamespace" : ns), name);
219    if (sd == null)
220      return null;
221
222    Element result = new Element(element.getLocalName(), new Property(context, sd.getSnapshot().getElement().get(0), sd));
223    result.setPath(element.getLocalName());
224    checkElement(element, path, result.getProperty());
225    result.markLocation(line(element, false), col(element, false));
226    result.setType(element.getLocalName());
227    parseChildren(path, element, result);
228    result.numberChildren();
229    return result;
230  }
231
232  private String pathPrefix(String ns) {
233    if (Utilities.noString(ns))
234      return "";
235    if (ns.equals(FormatUtilities.FHIR_NS))
236      return "f:";
237    if (ns.equals(FormatUtilities.XHTML_NS))
238      return "h:";
239    if (ns.equals("urn:hl7-org:v3"))
240      return "v3:";
241    if (ns.equals("urn:hl7-org:sdtc")) 
242      return "sdtc:";
243    if (ns.equals("urn:ihe:pharm"))
244      return "pharm:";
245    return "?:";
246  }
247
248  private boolean empty(org.w3c.dom.Element element) {
249    for (int i = 0; i < element.getAttributes().getLength(); i++) {
250      String n = element.getAttributes().item(i).getNodeName();
251      if (!n.equals("xmlns") && !n.startsWith("xmlns:"))
252        return false;
253    }
254    if (!Utilities.noString(element.getTextContent().trim()))
255      return false;
256
257    Node n = element.getFirstChild();
258    while (n != null) {
259      if (n.getNodeType() == Node.ELEMENT_NODE)
260        return false;
261      n = n.getNextSibling();
262    }
263    return true;
264  }
265
266  private void checkElement(org.w3c.dom.Element element, String path, Property prop) throws FHIRFormatError {
267    if (policy == ValidationPolicy.EVERYTHING) {
268      if (empty(element) && FormatUtilities.FHIR_NS.equals(element.getNamespaceURI())) // this rule only applies to FHIR Content
269        logError(ValidationMessage.NO_RULE_DATE, line(element, false), col(element, false), path, IssueType.INVALID, context.formatMessage(I18nConstants.ELEMENT_MUST_HAVE_SOME_CONTENT), IssueSeverity.ERROR);
270      String ns = prop.getXmlNamespace();
271      String elementNs = element.getNamespaceURI();
272      if (elementNs == null) {
273        elementNs = "noNamespace";
274      }
275      if (!elementNs.equals(ns))
276        logError(ValidationMessage.NO_RULE_DATE, line(element, false), col(element, false), path, IssueType.INVALID, context.formatMessage(I18nConstants.WRONG_NAMESPACE__EXPECTED_, ns), IssueSeverity.ERROR);
277    }
278  }
279
280  public Element parse(org.w3c.dom.Element base, String type) throws Exception {
281    StructureDefinition sd = getDefinition(0, 0, FormatUtilities.FHIR_NS, type);
282    Element result = new Element(base.getLocalName(), new Property(context, sd.getSnapshot().getElement().get(0), sd));
283    result.setPath(base.getLocalName());
284    String path = "/"+pathPrefix(base.getNamespaceURI())+base.getLocalName();
285    checkElement(base, path, result.getProperty());
286    result.setType(base.getLocalName());
287    parseChildren(path, base, result);
288    result.numberChildren();
289    return result;
290  }
291
292  private void parseChildren(String path, org.w3c.dom.Element node, Element element) throws FHIRFormatError, FHIRException, IOException, DefinitionException {
293    // this parsing routine retains the original order in a the XML file, to support validation
294    reapComments(node, element);
295    List<Property> properties = element.getProperty().getChildProperties(element.getName(), XMLUtil.getXsiType(node));
296
297    String text = XMLUtil.getDirectText(node).trim();
298    int line = line(node, false);
299    int col = col(node, false);
300    if (!Utilities.noString(text)) {
301      Property property = getTextProp(properties);
302      if (property != null) {
303        if ("ED.data[x]".equals(property.getDefinition().getId()) || (property.getDefinition()!=null && property.getDefinition().getBase()!=null && "ED.data[x]".equals(property.getDefinition().getBase().getPath()))) {
304          if ("B64".equals(node.getAttribute("representation"))) {
305            Element n = new Element("dataBase64Binary", property, "base64Binary", text).markLocation(line, col);
306            n.setPath(element.getPath()+"."+property.getName());
307            element.getChildren().add(n);
308          } else {
309            Element n = new Element("dataString", property, "string", text).markLocation(line, col);
310            n.setPath(element.getPath()+"."+property.getName());
311            element.getChildren().add(n);
312          }
313        } else {
314          Element n = new Element(property.getName(), property, property.getType(), text).markLocation(line, col);
315          n.setPath(element.getPath()+"."+property.getName());
316          element.getChildren().add(n);
317        }
318      } 
319      else {
320        Node n = node.getFirstChild();
321        while (n != null) {
322          if (n.getNodeType() == Node.TEXT_NODE && !Utilities.noString(n.getTextContent().trim())) {
323            Node nt = n; // try to find the nearest element for a line/col location
324            boolean end = false;
325            while (nt.getPreviousSibling() != null && nt.getNodeType() != Node.ELEMENT_NODE) {
326              nt = nt.getPreviousSibling();
327              end = true;
328            }
329            while (nt.getNextSibling() != null && nt.getNodeType() != Node.ELEMENT_NODE) {
330              nt = nt.getNextSibling();
331              end = false;
332            }
333            line = line(nt, end);
334            col = col(nt, end);
335            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);
336          }
337          n = n.getNextSibling();
338        }
339      }                 
340    }
341
342    for (int i = 0; i < node.getAttributes().getLength(); i++) {
343      Node attr = node.getAttributes().item(i);
344      String value = attr.getNodeValue();
345      if (!validAttrValue(value)) {
346        logError(ValidationMessage.NO_RULE_DATE, line, col, path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.XML_ATTR_VALUE_INVALID, attr.getNodeName()), IssueSeverity.ERROR);
347      }
348      if (!(attr.getNodeName().equals("xmlns") || attr.getNodeName().startsWith("xmlns:"))) {
349        Property property = getAttrProp(properties, attr.getLocalName(), attr.getNamespaceURI());
350        if (property != null) {
351          String av = attr.getNodeValue();
352          if (ToolingExtensions.hasExtension(property.getDefinition(), ToolingExtensions.EXT_DATE_FORMAT))
353            av = convertForDateFormatFromExternal(ToolingExtensions.readStringExtension(property.getDefinition(), ToolingExtensions.EXT_DATE_FORMAT), av);
354          if (property.getName().equals("value") && element.isPrimitive())
355            element.setValue(av);
356          else {
357            Element n = new Element(property.getName(), property, property.getType(), av).markLocation(line, col);
358            n.setPath(element.getPath()+"."+property.getName());
359            element.getChildren().add(n);
360          }
361        } else {
362          boolean ok = false;
363          if (FormatUtilities.FHIR_NS.equals(node.getNamespaceURI())) {
364            if (attr.getLocalName().equals("schemaLocation") && FormatUtilities.NS_XSI.equals(attr.getNamespaceURI())) {
365              ok = ok || allowXsiLocation; 
366            }
367          } else
368            ok = ok || (attr.getLocalName().equals("schemaLocation")); // xsi:schemalocation allowed for non FHIR content
369          ok = ok || (hasTypeAttr(element) && attr.getLocalName().equals("type") && FormatUtilities.NS_XSI.equals(attr.getNamespaceURI())); // xsi:type allowed if element says so
370          if (!ok) { 
371            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);
372          }
373        }
374      }
375    }
376
377    String lastName = null;
378    int repeatCount = 0;
379    Node child = node.getFirstChild();
380    while (child != null) {
381      if (child.getNodeType() == Node.ELEMENT_NODE) {
382        Property property = getElementProp(properties, child.getLocalName(), child.getNamespaceURI());
383        if (property != null) {
384          if (property.getName().equals(lastName)) {
385            repeatCount++;
386          } else {
387            lastName = property.getName();
388            repeatCount = 0;
389          }
390          if (!property.isChoice() && "xhtml".equals(property.getType())) {
391            XhtmlNode xhtml;
392            if (property.getDefinition().hasRepresentation(PropertyRepresentation.CDATEXT))
393              xhtml = new CDANarrativeFormat().convert((org.w3c.dom.Element) child);
394            else {
395              XhtmlParser xp = new XhtmlParser();
396              xhtml = xp.parseHtmlNode((org.w3c.dom.Element) child);
397              if (policy == ValidationPolicy.EVERYTHING) {
398                for (StringPair s : xp.getValidationIssues()) {
399                  logError("2022-11-17", line(child, false), col(child, false), path, IssueType.INVALID, context.formatMessage(s.getName(), s.getValue()), IssueSeverity.ERROR);                
400                }
401              }
402            }
403            Element n = new Element(property.getName(), property, "xhtml", new XhtmlComposer(XhtmlComposer.XML, false).compose(xhtml)).setXhtml(xhtml).markLocation(line(child, false), col(child, false));
404            n.setPath(element.getPath()+"."+property.getName());
405            element.getChildren().add(n);
406          } else {
407            String npath = path+"/"+pathPrefix(child.getNamespaceURI())+child.getLocalName();
408            Element n = new Element(child.getLocalName(), property).markLocation(line(child, false), col(child, false));
409            if (property.isList()) {
410              n.setPath(element.getPath()+"."+property.getName()+"["+repeatCount+"]");                                    
411            } else {
412              n.setPath(element.getPath()+"."+property.getName());
413            }
414            checkElement((org.w3c.dom.Element) child, npath, n.getProperty());
415            boolean ok = true;
416            if (property.isChoice()) {
417              if (property.getDefinition().hasRepresentation(PropertyRepresentation.TYPEATTR)) {
418                String xsiType = ((org.w3c.dom.Element) child).getAttributeNS(FormatUtilities.NS_XSI, "type");
419                if (Utilities.noString(xsiType)) {
420                  if (ToolingExtensions.hasExtension(property.getDefinition(), "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype")) {
421                    xsiType = ToolingExtensions.readStringExtension(property.getDefinition(), "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype");
422                    n.setType(xsiType);
423                  } else {
424                    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);
425                    ok = false;
426                  }
427                } else {
428                  if (xsiType.contains(":"))
429                    xsiType = xsiType.substring(xsiType.indexOf(":")+1);
430                  n.setType(xsiType);
431                  n.setExplicitType(xsiType);
432                }
433              } else
434                n.setType(n.getType());
435            }
436            element.getChildren().add(n);
437            if (ok) {
438              if (property.isResource())
439                parseResource(npath, (org.w3c.dom.Element) child, n, property);
440              else
441                parseChildren(npath, (org.w3c.dom.Element) child, n);
442            }
443          }
444        } else
445          logError(ValidationMessage.NO_RULE_DATE, line(child, false), col(child, false), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.UNDEFINED_ELEMENT_, child.getLocalName()), IssueSeverity.ERROR);
446      } else if (child.getNodeType() == Node.CDATA_SECTION_NODE){
447        logError(ValidationMessage.NO_RULE_DATE, line(child, false), col(child, false), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.CDATA_IS_NOT_ALLOWED), IssueSeverity.ERROR);
448      } else if (!Utilities.existsInList(child.getNodeType(), 3, 8)) {
449        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);
450      }
451      child = child.getNextSibling();
452    }
453  }
454
455  private boolean validAttrValue(String value) {
456    if (version == null) {
457      return true;
458    }
459    if (version.equals("1.0")) {
460      boolean ok = true;
461      for (char ch : value.toCharArray()) {
462        if (ch <= 0x1F && !Utilities.existsInList(ch, '\r', '\n', '\t')) {
463          ok = false;
464        }
465      }
466      return ok;
467    } else
468      return true;
469  }
470
471
472  private Property getElementProp(List<Property> properties, String nodeName, String namespace) {
473    List<Property> propsSortedByLongestFirst = new ArrayList<Property>(properties);
474    // sort properties according to their name longest first, so .requestOrganizationReference comes first before .request[x]
475    // and therefore the longer property names get evaluated first
476    Collections.sort(propsSortedByLongestFirst, new Comparator<Property>() {
477      @Override
478      public int compare(Property o1, Property o2) {
479        return o2.getName().length() - o1.getName().length();
480      }
481    });
482    // first scan, by namespace
483    for (Property p : propsSortedByLongestFirst) {
484      if (!p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR) && !p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) {
485        if (p.getXmlName().equals(nodeName) && p.getXmlNamespace().equals(namespace)) 
486          return p;
487      }
488    }
489    for (Property p : propsSortedByLongestFirst) {
490      if (!p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR) && !p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) {
491        if (p.getXmlName().equals(nodeName)) 
492          return p;
493        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))) 
494          return p;
495      }
496    }
497    return null;
498  }
499
500  private Property getAttrProp(List<Property> properties, String nodeName, String namespace) {
501    for (Property p : properties) {
502      if (p.getXmlName().equals(nodeName) && p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR) && p.getXmlNamespace().equals(namespace)) {
503        return p;
504      }
505    }
506    if (namespace == null) {
507      for (Property p : properties) {
508        if (p.getXmlName().equals(nodeName) && p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR)) {
509          return p;
510        }    
511      }
512    }
513    return null;
514  }
515
516  private Property getTextProp(List<Property> properties) {
517    for (Property p : properties)
518      if (p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) 
519        return p;
520    return null;
521  }
522
523  private String convertForDateFormatFromExternal(String fmt, String av) throws FHIRException {
524    if ("v3".equals(fmt) || "YYYYMMDDHHMMSS.UUUU[+|-ZZzz]".equals(fmt)) {
525      try {
526        DateTimeType d = DateTimeType.parseV3(av);
527        return d.asStringValue();
528      } catch (Exception e) {
529        return av; // not at all clear what to do in this case.
530      }
531    }
532    throw new FHIRException(context.formatMessage(I18nConstants.UNKNOWN_DATA_FORMAT_, fmt));
533  }
534
535  private String convertForDateFormatToExternal(String fmt, String av) throws FHIRException {
536    if ("v3".equals(fmt)) {
537      DateTimeType d = new DateTimeType(av);
538      return d.getAsV3();
539    } else
540      throw new FHIRException(context.formatMessage(I18nConstants.UNKNOWN_DATE_FORMAT_, fmt));
541  }
542
543  private void parseResource(String string, org.w3c.dom.Element container, Element parent, Property elementProperty) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
544    org.w3c.dom.Element res = XMLUtil.getFirstChild(container);
545    String name = res.getLocalName();
546    StructureDefinition sd = context.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(name, null));
547    if (sd == null)
548      throw new FHIRFormatError(context.formatMessage(I18nConstants.CONTAINED_RESOURCE_DOES_NOT_APPEAR_TO_BE_A_FHIR_RESOURCE_UNKNOWN_NAME_, res.getLocalName()));
549    parent.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd), SpecialElement.fromProperty(parent.getProperty()), elementProperty);
550    parent.setType(name);
551    parseChildren(res.getLocalName(), res, parent);
552  }
553
554  private void reapComments(org.w3c.dom.Element element, Element context) {
555    Node node = element.getPreviousSibling();
556    while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
557      if (node.getNodeType() == Node.COMMENT_NODE)
558        context.getComments().add(0, node.getTextContent());
559      node = node.getPreviousSibling();
560    }
561    node = element.getLastChild();
562    while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
563      node = node.getPreviousSibling();
564    }
565    while (node != null) {
566      if (node.getNodeType() == Node.COMMENT_NODE)
567        context.getComments().add(node.getTextContent());
568      node = node.getNextSibling();
569    }
570  }
571
572  private boolean isAttr(Property property) {
573    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
574      if (r.getValue() == PropertyRepresentation.XMLATTR) {
575        return true;
576      }
577    }
578    return false;
579  }
580
581  private boolean isCdaText(Property property) {
582    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
583      if (r.getValue() == PropertyRepresentation.CDATEXT) {
584        return true;
585      }
586    }
587    return false;
588  }
589
590  private boolean isTypeAttr(Property property) {
591    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
592      if (r.getValue() == PropertyRepresentation.TYPEATTR) {
593        return true;
594      }
595    }
596    return false;
597  }
598
599  private boolean isText(Property property) {
600    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
601      if (r.getValue() == PropertyRepresentation.XMLTEXT) {
602        return true;
603      }
604    }
605    return false;
606  }
607
608  @Override
609  public void compose(Element e, OutputStream stream, OutputStyle style, String base) throws IOException, FHIRException {
610    XMLWriter xml = new XMLWriter(stream, "UTF-8");
611    xml.setSortAttributes(false);
612    xml.setPretty(style == OutputStyle.PRETTY);
613    xml.start();
614    if (e.getPath() == null) {
615      e.populatePaths(null);
616    }
617    String ns = e.getProperty().getXmlNamespace();
618    if (ns!=null && !"noNamespace".equals(ns)) {
619      xml.setDefaultNamespace(ns);
620    }
621    if (hasTypeAttr(e))
622      xml.namespace("http://www.w3.org/2001/XMLSchema-instance", "xsi");
623    addNamespaces(xml, e);
624    composeElement(xml, e, e.getType(), true);
625    xml.end();
626  }
627
628  private void addNamespaces(IXMLWriter xml, Element e) throws IOException {
629    String ns = e.getProperty().getXmlNamespace();
630    if (ns!=null && xml.getDefaultNamespace()!=null && !xml.getDefaultNamespace().equals(ns)){
631      if (!xml.namespaceDefined(ns)) {
632        String prefix = pathPrefix(ns);
633        if (prefix.endsWith(":")) {
634          prefix = prefix.substring(0, prefix.length()-1);
635        }
636        if ("?".equals(prefix)) {
637          xml.namespace(ns);
638        } else {
639          xml.namespace(ns, prefix);
640        }
641      }
642    }
643    for (Element c : e.getChildren()) {
644      addNamespaces(xml, c);
645    }
646  }
647
648  private boolean hasTypeAttr(Element e) {
649    if (isTypeAttr(e.getProperty()))
650      return true;
651    for (Element c : e.getChildren()) {
652      if (hasTypeAttr(c))
653        return true;
654    }
655    return false;
656  }
657
658  private void setXsiTypeIfIsTypeAttr(IXMLWriter xml, Element element) throws IOException, FHIRException {
659    if (isTypeAttr(element.getProperty()) && !Utilities.noString(element.getType())) {
660      String type = element.getType();
661      if (Utilities.isAbsoluteUrl(type)) {
662        type = type.substring(type.lastIndexOf("/")+1);
663      }
664      xml.attribute("xsi:type",type);    
665    }
666  }
667
668  public void compose(Element e, IXMLWriter xml) throws Exception {
669    if (e.getPath() == null) {
670      e.populatePaths(null);
671    }
672    xml.start();
673    xml.setDefaultNamespace(e.getProperty().getXmlNamespace());
674    if (schemaPath != null) {
675      xml.setSchemaLocation(FormatUtilities.FHIR_NS, Utilities.pathURL(schemaPath, e.fhirType()+".xsd"));
676    }
677    composeElement(xml, e, e.getType(), true);
678    xml.end();
679  }
680
681  private void composeElement(IXMLWriter xml, Element element, String elementName, boolean root) throws IOException, FHIRException {
682    if (showDecorations) {
683      @SuppressWarnings("unchecked")
684      List<ElementDecoration> decorations = (List<ElementDecoration>) element.getUserData("fhir.decorations");
685      if (decorations != null)
686        for (ElementDecoration d : decorations)
687          xml.decorate(d);
688    }
689    for (String s : element.getComments()) {
690      xml.comment(s, true);
691    }
692    if (isText(element.getProperty())) {
693      if (linkResolver != null)
694        xml.link(linkResolver.resolveProperty(element.getProperty()));
695      xml.enter(element.getProperty().getXmlNamespace(),elementName);
696      xml.text(element.getValue());
697      xml.exit(element.getProperty().getXmlNamespace(),elementName);   
698    } else if (!element.hasChildren() && !element.hasValue()) {
699      if (element.getExplicitType() != null)
700        xml.attribute("xsi:type", element.getExplicitType());
701      xml.element(elementName);
702    } else if (element.isPrimitive() || (element.hasType() && isPrimitive(element.getType()))) {
703      if (element.getType().equals("xhtml")) {
704        String rawXhtml = element.getValue();
705        if (isCdaText(element.getProperty())) {
706          new CDANarrativeFormat().convert(xml, new XhtmlParser().parseFragment(rawXhtml));
707        } else {
708          xml.escapedText(rawXhtml);
709          xml.anchor("end-xhtml");
710        }
711      } else if (isText(element.getProperty())) {
712        if (linkResolver != null)
713          xml.link(linkResolver.resolveProperty(element.getProperty()));
714        xml.text(element.getValue());
715      } else {
716        setXsiTypeIfIsTypeAttr(xml, element);
717        if (element.hasValue()) {
718          if (linkResolver != null)
719            xml.link(linkResolver.resolveType(element.getType()));
720          xml.attribute("value", element.getValue());
721        }
722        if (linkResolver != null)
723          xml.link(linkResolver.resolveProperty(element.getProperty()));
724        if (element.hasChildren()) {
725          xml.enter(element.getProperty().getXmlNamespace(), elementName);
726          for (Element child : element.getChildren()) 
727            composeElement(xml, child, child.getName(), false);
728          xml.exit(element.getProperty().getXmlNamespace(),elementName);
729        } else
730          xml.element(elementName);
731      }
732    } else {
733      setXsiTypeIfIsTypeAttr(xml, element);
734      for (Element child : element.getChildren()) {
735        if (isAttr(child.getProperty()) && wantCompose(element.getPath(), child)) {
736          if (linkResolver != null)
737            xml.link(linkResolver.resolveType(child.getType()));
738          String av = child.getValue();
739          if (ToolingExtensions.hasExtension(child.getProperty().getDefinition(), ToolingExtensions.EXT_DATE_FORMAT))
740            av = convertForDateFormatToExternal(ToolingExtensions.readStringExtension(child.getProperty().getDefinition(), ToolingExtensions.EXT_DATE_FORMAT), av);
741          xml.attribute(child.getProperty().getXmlNamespace(),child.getProperty().getXmlName(), av);
742        }
743      }
744      if (linkResolver != null)
745        xml.link(linkResolver.resolveProperty(element.getProperty()));
746      xml.enter(element.getProperty().getXmlNamespace(),elementName);
747      if (!root && element.getSpecial() != null) {
748        if (linkResolver != null)
749          xml.link(linkResolver.resolveProperty(element.getProperty()));
750        xml.enter(element.getProperty().getXmlNamespace(),element.getType());
751      }
752      for (Element child : element.getChildren()) {
753        if (wantCompose(element.getPath(), child)) {
754          if (isText(child.getProperty())) {
755            if (linkResolver != null)
756              xml.link(linkResolver.resolveProperty(element.getProperty()));
757            xml.text(child.getValue());
758          } else if (!isAttr(child.getProperty()))
759            composeElement(xml, child, child.getName(), false);
760        }
761      }
762      if (!root && element.getSpecial() != null)
763        xml.exit(element.getProperty().getXmlNamespace(),element.getType());
764      xml.exit(element.getProperty().getXmlNamespace(),elementName);
765    }
766  }
767
768  private String checkHeader(InputStream stream) throws IOException {
769    try {
770      // the stream will either start with the UTF-8 BOF or with <xml
771      int i0 = stream.read();
772      int i1 = stream.read();
773      int i2 = stream.read();
774
775      StringBuilder b = new StringBuilder();
776      if (i0 == 0xEF && i1 == 0xBB && i2 == 0xBF) {
777        // ok, it's UTF-8
778      } else if (i0 == 0x3C && i1 == 0x3F && i2 == 0x78) { // <xm
779        b.append((char) i0);
780        b.append((char) i1);
781        b.append((char) i2);
782      } else if (i0 == 60) { // just plain old XML with no header
783        return "1.0";        
784      } else {
785        throw new Exception(context.formatMessage(I18nConstants.XML_ENCODING_INVALID));
786      }
787      int i = stream.read();
788      do {
789        b.append((char) i);
790        i = stream.read();
791      } while (i != 0x3E);
792      String header = b.toString();
793      String e = null;
794      i = header.indexOf("encoding=\"");
795      if (i > -1) {
796        e = header.substring(i+10, i+15);
797      } else {
798        i = header.indexOf("encoding='");
799        if (i > -1) {
800          e = header.substring(i+10, i+15);
801        } 
802      }
803      if (e != null && !"UTF-8".equalsIgnoreCase(e)) {
804        logError(ValidationMessage.NO_RULE_DATE, 0, 0, "XML", IssueType.INVALID, context.formatMessage(I18nConstants.XML_ENCODING_INVALID), IssueSeverity.ERROR);
805      }
806
807      i = header.indexOf("version=\"");
808      if (i > -1) {
809        return header.substring(i+9, i+12);
810      } else {
811        i = header.indexOf("version='");
812        if (i > -1) {
813          return header.substring(i+9, i+12);          
814        } 
815      }
816      return "?xml-p1?";
817    } catch (Exception e) {
818      // suppress this error 
819      logError(ValidationMessage.NO_RULE_DATE, 0, 0, "XML", IssueType.INVALID, e.getMessage(), IssueSeverity.ERROR);
820    }
821    return "?xml-p2?";
822  }
823
824}