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