001package org.hl7.fhir.r5.elementmodel;
002
003import java.io.ByteArrayInputStream;
004
005/*
006  Copyright (c) 2011+, HL7, Inc.
007  All rights reserved.
008
009  Redistribution and use in source and binary forms, with or without modification, 
010  are permitted provided that the following conditions are met:
011
012 * Redistributions of source code must retain the above copyright notice, this 
013     list of conditions and the following disclaimer.
014 * Redistributions in binary form must reproduce the above copyright notice, 
015     this list of conditions and the following disclaimer in the documentation 
016     and/or other materials provided with the distribution.
017 * Neither the name of HL7 nor the names of its contributors may be used to 
018     endorse or promote products derived from this software without specific 
019     prior written permission.
020
021  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
022  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
023  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
024  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
025  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
026  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
027  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
028  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
029  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
030  POSSIBILITY OF SUCH DAMAGE.
031
032 */
033
034
035import java.io.IOException;
036import java.io.InputStream;
037import java.io.OutputStream;
038import java.util.ArrayList;
039import java.util.Collections;
040import java.util.Comparator;
041import java.util.HashSet;
042import java.util.List;
043import java.util.Set;
044
045import javax.xml.parsers.DocumentBuilder;
046import javax.xml.parsers.DocumentBuilderFactory;
047import javax.xml.parsers.SAXParserFactory;
048import javax.xml.transform.Transformer;
049import javax.xml.transform.TransformerFactory;
050import javax.xml.transform.dom.DOMResult;
051import javax.xml.transform.sax.SAXSource;
052
053import org.hl7.fhir.exceptions.DefinitionException;
054import org.hl7.fhir.exceptions.FHIRException;
055import org.hl7.fhir.exceptions.FHIRFormatError;
056import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
057import org.hl7.fhir.r5.context.IWorkerContext;
058import org.hl7.fhir.r5.elementmodel.Element.SpecialElement;
059import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat;
060import org.hl7.fhir.r5.extensions.ExtensionDefinitions;
061import org.hl7.fhir.r5.extensions.ExtensionUtilities;
062import org.hl7.fhir.r5.formats.FormatUtilities;
063import org.hl7.fhir.r5.formats.IParser.OutputStyle;
064import org.hl7.fhir.r5.model.Constants;
065import org.hl7.fhir.r5.model.DateTimeType;
066import org.hl7.fhir.r5.model.ElementDefinition;
067import org.hl7.fhir.r5.model.ElementDefinition.PropertyRepresentation;
068import org.hl7.fhir.r5.model.Enumeration;
069import org.hl7.fhir.r5.model.StructureDefinition;
070
071import org.hl7.fhir.r5.utils.UserDataNames;
072import org.hl7.fhir.r5.utils.formats.XmlLocationAnnotator;
073import org.hl7.fhir.r5.utils.formats.XmlLocationData;
074import org.hl7.fhir.utilities.ElementDecoration;
075import org.hl7.fhir.utilities.StringPair;
076import org.hl7.fhir.utilities.FileUtilities;
077import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
078import org.hl7.fhir.utilities.Utilities;
079import org.hl7.fhir.utilities.i18n.I18nConstants;
080import org.hl7.fhir.utilities.validation.ValidationMessage;
081import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
082import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
083import org.hl7.fhir.utilities.xhtml.CDANarrativeFormat;
084import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
085import org.hl7.fhir.utilities.xhtml.XhtmlNode;
086import org.hl7.fhir.utilities.xhtml.XhtmlParser;
087import org.hl7.fhir.utilities.xml.IXMLWriter;
088import org.hl7.fhir.utilities.xml.XMLUtil;
089import org.hl7.fhir.utilities.xml.XMLWriter;
090import org.w3c.dom.Document;
091import org.w3c.dom.Node;
092import org.xml.sax.ErrorHandler;
093import org.xml.sax.InputSource;
094import org.xml.sax.SAXParseException;
095import org.xml.sax.XMLReader;
096
097@MarkedToMoveToAdjunctPackage
098public class XmlParser extends ParserBase {
099  private boolean allowXsiLocation;
100  private String version;
101  private boolean elideElements;
102
103  public XmlParser(IWorkerContext context) {
104    super(context);
105  }
106
107  private String schemaPath;
108  private boolean markedXhtml;
109
110  public String getSchemaPath() {
111    return schemaPath;
112  }
113  public void setSchemaPath(String schemaPath) {
114    this.schemaPath = schemaPath;
115  }
116
117  public boolean isAllowXsiLocation() {
118    return allowXsiLocation;
119  }
120
121  public void setAllowXsiLocation(boolean allowXsiLocation) {
122    this.allowXsiLocation = allowXsiLocation;
123  }
124
125  public List<ValidatedFragment> parse(InputStream inStream) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
126
127    byte[] content = FileUtilities.streamToBytes(inStream);
128    ValidatedFragment focusFragment = new ValidatedFragment(ValidatedFragment.FOCUS_NAME, "xml", content, false);
129
130    ByteArrayInputStream stream = new ByteArrayInputStream(content);
131    Document doc = null;
132    try {
133      DocumentBuilderFactory factory = XMLUtil.newXXEProtectedDocumentBuilderFactory();
134      // xxe protection
135      factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
136      factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
137      factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
138      factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
139      factory.setXIncludeAware(false);
140      factory.setExpandEntityReferences(false);
141
142      factory.setNamespaceAware(true);
143      if (policy == ValidationPolicy.EVERYTHING) {
144        // The SAX interface appears to not work when reporting the correct version/encoding.
145        // if we can, we'll inspect the header/encoding ourselves 
146
147        stream.mark(1024);
148        version = checkHeader(focusFragment.getErrors(), stream);
149        stream.reset();
150
151        // use a slower parser that keeps location data
152        TransformerFactory transformerFactory = XMLUtil.newXXEProtectedTransformerFactory();
153        Transformer nullTransformer = transformerFactory.newTransformer();
154        DocumentBuilder docBuilder = factory.newDocumentBuilder();
155        doc = docBuilder.newDocument();
156        DOMResult domResult = new DOMResult(doc);
157        SAXParserFactory spf = XMLUtil.newXXEProtectedSaxParserFactory();
158        spf.setNamespaceAware(true);
159        spf.setValidating(false);
160
161        XMLReader xmlReader = XMLUtil.getXXEProtectedXMLReader(spf);
162
163        XmlLocationAnnotator locationAnnotator = new XmlLocationAnnotator(xmlReader, doc);
164        InputSource inputSource = new InputSource(stream);
165        SAXSource saxSource = new SAXSource(locationAnnotator, inputSource);
166        nullTransformer.transform(saxSource, domResult);
167      } else {
168        DocumentBuilder builder = factory.newDocumentBuilder();
169        builder.setErrorHandler(new NullErrorHandler());
170        doc = builder.parse(stream);
171      }
172    } catch (Exception e) {
173      if (e.getMessage().contains("lineNumber:") && e.getMessage().contains("columnNumber:")) {
174        int line = Utilities.parseInt(extractVal(e.getMessage(), "lineNumber"), 0); 
175        int col = Utilities.parseInt(extractVal(e.getMessage(), "columnNumber"), 0); 
176        logError(focusFragment.getErrors(), ValidationMessage.NO_RULE_DATE, line, col, "(xml)", IssueType.INVALID, e.getMessage().substring(e.getMessage().lastIndexOf(";")+1).trim(), IssueSeverity.FATAL);
177      } else {
178        logError(focusFragment.getErrors(), ValidationMessage.NO_RULE_DATE, 0, 0, "(xml)", IssueType.INVALID, e.getMessage(), IssueSeverity.FATAL);
179      }
180      doc = null;
181    }
182    if (doc != null) {
183      focusFragment.setElement(parse(focusFragment.getErrors(), doc));
184    }
185    List<ValidatedFragment> res = new ArrayList<>();
186    res.add(focusFragment);
187    return res;
188  }
189
190
191  private String extractVal(String src, String name) {
192    src = src.substring(src.indexOf(name)+name.length()+1);
193    src = src.substring(0, src.indexOf(";")).trim();
194    return src;
195  }
196  private void checkForProcessingInstruction(List<ValidationMessage> errors, Document document) throws FHIRFormatError {
197    if (policy == ValidationPolicy.EVERYTHING && FormatUtilities.FHIR_NS.equals(document.getDocumentElement().getNamespaceURI())) {
198      Node node = document.getFirstChild();
199      while (node != null) {
200        if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE)
201          logError(errors, ValidationMessage.NO_RULE_DATE, line(document, false), col(document, false), "(document)", IssueType.INVALID, context.formatMessage(
202              I18nConstants.NO_PROCESSING_INSTRUCTIONS_ALLOWED_IN_RESOURCES), IssueSeverity.ERROR);
203        node = node.getNextSibling();
204      }
205    }
206  }
207
208
209  private int line(Node node, boolean end) {
210    XmlLocationData loc = node == null ? null : (XmlLocationData) node.getUserData(XmlLocationData.LOCATION_DATA_KEY);
211    return loc == null ? 0 : end ? loc.getEndLine() : loc.getStartLine();
212  }
213
214  private int col(Node node, boolean end) {
215    XmlLocationData loc = node == null ? null : (XmlLocationData) node.getUserData(XmlLocationData.LOCATION_DATA_KEY);
216    return loc == null ? 0 : end ? loc.getEndColumn() : loc.getStartColumn();
217  }
218
219  public Element parse(List<ValidationMessage> errors, Document doc) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
220    checkForProcessingInstruction(errors, doc);
221    org.w3c.dom.Element element = doc.getDocumentElement();
222    return parse(errors, element);
223  }
224
225  public Element parse(List<ValidationMessage> errors, org.w3c.dom.Element element) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
226    String ns = element.getNamespaceURI();
227    String name = element.getLocalName();
228    String path = "/"+pathPrefix(ns)+name;
229
230    StructureDefinition sd = getDefinition(errors, line(element, false), col(element, false), (ns == null ? "noNamespace" : ns), name);
231    if (sd == null)
232      return null;
233
234    Element result = new Element(element.getLocalName(), new Property(context, sd.getSnapshot().getElement().get(0), sd, getProfileUtilities(), getContextUtilities())).setFormat(FhirFormat.XML);
235    result.setPath(element.getLocalName());
236    checkElement(errors, element, result, path, result.getProperty(), false);
237    result.markLocation(line(element, false), col(element, false));
238    result.setType(element.getLocalName());
239    parseChildren(errors, path, element, result);
240    result.numberChildren();
241    return result;
242  }
243
244  private String pathPrefix(String ns) {
245    if (Utilities.noString(ns))
246      return "";
247    if (ns.equals(FormatUtilities.FHIR_NS))
248      return "f:";
249    if (ns.equals(FormatUtilities.XHTML_NS))
250      return "h:";
251    if (ns.equals("urn:hl7-org:v3"))
252      return "v3:";
253    if (ns.equals("urn:hl7-org:sdtc")) 
254      return "sdtc:";
255    if (ns.equals("urn:ihe:pharm"))
256      return "pharm:";
257    if (ns.equals("http://ns.electronichealth.net.au/Ci/Cda/Extensions/3.0"))
258      return "ext:";
259    return "?:";
260  }
261
262  private boolean empty(org.w3c.dom.Element element) {
263    for (int i = 0; i < element.getAttributes().getLength(); i++) {
264      String n = element.getAttributes().item(i).getNodeName();
265      if (!n.equals("xmlns") && !n.startsWith("xmlns:"))
266        return false;
267    }
268    if (!Utilities.noString(element.getTextContent().trim()))
269      return false;
270
271    Node n = element.getFirstChild();
272    while (n != null) {
273      if (n.getNodeType() == Node.ELEMENT_NODE)
274        return false;
275      n = n.getNextSibling();
276    }
277    return true;
278  }
279
280  private void checkElement(List<ValidationMessage> errors, org.w3c.dom.Element element, Element e, String path, Property prop, boolean xsiTypeChecked) throws FHIRFormatError {
281    if (policy == ValidationPolicy.EVERYTHING) {
282      if (empty(element) && FormatUtilities.FHIR_NS.equals(element.getNamespaceURI())) // this rule only applies to FHIR Content
283        logError(errors, ValidationMessage.NO_RULE_DATE, line(element, false), col(element, false), path, IssueType.INVALID, context.formatMessage(I18nConstants.ELEMENT_MUST_HAVE_SOME_CONTENT), IssueSeverity.ERROR);
284      String ns = prop.getXmlNamespace();
285      String elementNs = element.getNamespaceURI();
286      if (elementNs == null) {
287        elementNs = "noNamespace";
288      }
289      if (!elementNs.equals(ns)) {
290        logError(errors, ValidationMessage.NO_RULE_DATE, line(element, false), col(element, false), path, IssueType.INVALID, context.formatMessage(I18nConstants.WRONG_NAMESPACE__EXPECTED_, ns), IssueSeverity.ERROR);
291      }
292      if (!xsiTypeChecked) {
293        String xsiType = element.getAttributeNS(FormatUtilities.NS_XSI, "type");
294        if (!Utilities.noString(xsiType)) {
295          String actualType = prop.getXmlTypeName();
296          if (xsiType.equals(actualType)) {
297            logError(errors, "2023-10-12", line(element, false), col(element, false), path, IssueType.INVALID, context.formatMessage(I18nConstants.XSI_TYPE_UNNECESSARY), IssueSeverity.INFORMATION);            
298          } else {
299            StructureDefinition sd = findLegalConstraint(xsiType, actualType);
300            if (sd != null) {
301              e.setType(sd.getType());
302              e.setExplicitType(xsiType);
303            } else {
304              logError(errors, "2023-10-12", line(element, false), col(element, false), path, IssueType.INVALID, context.formatMessage(I18nConstants.XSI_TYPE_WRONG, xsiType, actualType), IssueSeverity.ERROR);           
305            }  
306          }
307        }
308      }
309    }
310  }
311
312  private StructureDefinition findLegalConstraint(String xsiType, String actualType) {
313    StructureDefinition sdA = context.fetchTypeDefinition(actualType);
314    StructureDefinition sd = context.fetchTypeDefinition(xsiType);
315    while (sd != null) {
316      if (sd == sdA) {
317        return sd;
318      }
319      sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition());
320    }
321    return null;
322  }
323
324  public Element parse(List<ValidationMessage> errors, org.w3c.dom.Element base, String type) throws Exception {
325    StructureDefinition sd = getDefinition(errors, 0, 0, FormatUtilities.FHIR_NS, type);
326    Element result = new Element(base.getLocalName(), new Property(context, sd.getSnapshot().getElement().get(0), sd, getProfileUtilities(), getContextUtilities())).setFormat(FhirFormat.XML).setNativeObject(base);
327    result.setPath(base.getLocalName());
328    String path = "/"+pathPrefix(base.getNamespaceURI())+base.getLocalName();
329    checkElement(errors, base, result, path, result.getProperty(), false);
330    result.setType(base.getLocalName());
331    parseChildren(errors, path, base, result);
332    result.numberChildren();
333    return result;
334  }
335
336  private void parseChildren(List<ValidationMessage> errors, String path, org.w3c.dom.Element node, Element element) throws FHIRFormatError, FHIRException, IOException, DefinitionException {
337    // this parsing routine retains the original order in a the XML file, to support validation
338    reapComments(node, element);
339    List<Property> properties = element.getProperty().getChildProperties(element.getName(), XMLUtil.getXsiType(node));
340    Property cgProp = getChoiceGroupProp(properties);
341    Property mtProp = cgProp == null ? null : getTextProp(cgProp.getChildProperties(null, null));
342
343    String text = mtProp == null ? XMLUtil.getDirectText(node).trim() : null;
344    int line = line(node, false);
345    int col = col(node, false);
346    if (!Utilities.noString(text)) {
347      Property property = getTextProp(properties);
348      if (property != null) {
349        if ("ED.data[x]".equals(property.getDefinition().getId()) || (property.getDefinition()!=null && property.getDefinition().getBase()!=null && "ED.data[x]".equals(property.getDefinition().getBase().getPath()))) {
350          if ("B64".equals(node.getAttribute("representation"))) {
351            Element n = new Element("dataBase64Binary", property, "base64Binary", text).markLocation(line, col).setFormat(FhirFormat.XML);
352            n.setPath(element.getPath()+"."+property.getName());
353            element.getChildren().add(n);
354          } else {
355            Element n = new Element("dataString", property, "string", text).markLocation(line, col).setFormat(FhirFormat.XML);
356            n.setPath(element.getPath()+"."+property.getName());
357            element.getChildren().add(n);
358          }
359        } else {
360          Element n = new Element(property.getName(), property, property.getType(), text).markLocation(line, col).setFormat(FhirFormat.XML);
361          n.setPath(element.getPath()+"."+property.getName());
362          element.getChildren().add(n);
363        }
364      } else {
365        Node n = node.getFirstChild();
366        while (n != null) {
367          if (n.getNodeType() == Node.TEXT_NODE && !Utilities.noString(n.getTextContent().trim())) {
368            Node nt = n; // try to find the nearest element for a line/col location
369            boolean end = false;
370            while (nt.getPreviousSibling() != null && nt.getNodeType() != Node.ELEMENT_NODE) {
371              nt = nt.getPreviousSibling();
372              end = true;
373            }
374            while (nt.getNextSibling() != null && nt.getNodeType() != Node.ELEMENT_NODE) {
375              nt = nt.getNextSibling();
376              end = false;
377            }
378            line = line(nt, end);
379            col = col(nt, end);
380            logError(errors, ValidationMessage.NO_RULE_DATE, line, col, path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.TEXT_SHOULD_NOT_BE_PRESENT, Utilities.makeSingleLine(n.getTextContent().trim())), IssueSeverity.ERROR);
381          }
382          n = n.getNextSibling();
383        }
384      }                 
385    }
386
387    for (int i = 0; i < node.getAttributes().getLength(); i++) {
388      Node attr = node.getAttributes().item(i);
389      String value = attr.getNodeValue();
390      if (!validAttrValue(value)) {
391        logError(errors, ValidationMessage.NO_RULE_DATE, line, col, path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.XML_ATTR_VALUE_INVALID, attr.getNodeName()), IssueSeverity.ERROR);
392      }
393      if (!(attr.getNodeName().equals("xmlns") || attr.getNodeName().startsWith("xmlns:"))) {
394        Property property = getAttrProp(properties, attr.getLocalName(), attr.getNamespaceURI());
395        if (property != null) {
396          String av = attr.getNodeValue();
397          if (ExtensionUtilities.hasExtension(property.getDefinition(), ExtensionDefinitions.EXT_DATE_FORMAT))
398            av = convertForDateFormatFromExternal(ExtensionUtilities.readStringExtension(property.getDefinition(), ExtensionDefinitions.EXT_DATE_FORMAT), av);          
399          if (property.getName().equals("value") && element.isPrimitive())
400            element.setValue(av);
401          else {
402            String[] vl = {av};
403            if (property.isList() && av.contains(" ")) {
404              vl = av.split(" ");
405            }
406            for (String v : vl) {
407              Element n = new Element(property.getName(), property, property.getType(), v).markLocation(line, col).setFormat(FhirFormat.XML);
408              n.setPath(element.getPath()+"."+property.getName());
409              element.getChildren().add(n);
410            }
411          }
412        } else {
413          boolean ok = false;
414          if (FormatUtilities.FHIR_NS.equals(node.getNamespaceURI())) {
415            if (attr.getLocalName().equals("schemaLocation") && FormatUtilities.NS_XSI.equals(attr.getNamespaceURI())) {
416              ok = ok || allowXsiLocation; 
417            }
418          } else
419            ok = ok || (attr.getLocalName().equals("schemaLocation")); // xsi:schemalocation allowed for non FHIR content
420          ok = ok || (hasTypeAttr(element) && attr.getLocalName().equals("type") && FormatUtilities.NS_XSI.equals(attr.getNamespaceURI())); // xsi:type allowed if element says so
421          if (!ok) { 
422            logError(errors, 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);
423          }
424        }
425      }
426    }
427
428    String lastName = null;
429    int repeatCount = 0;
430    Node child = node.getFirstChild();
431    while (child != null) {
432      if (child.getNodeType() == Node.ELEMENT_NODE) {
433        Property property = getElementProp(properties, child.getLocalName(), child.getNamespaceURI());
434
435        if (property != null) {
436          if (property.getName().equals(lastName)) {
437            repeatCount++;
438          } else {
439            lastName = property.getName();
440            repeatCount = 0;
441          }
442          if (!property.isChoice() && "xhtml".equals(property.getType())) {
443            XhtmlNode xhtml;
444            if (property.getDefinition().hasRepresentation(PropertyRepresentation.CDATEXT))
445              xhtml = new CDANarrativeFormat().convert((org.w3c.dom.Element) child);
446            else {
447              XhtmlParser xp = new XhtmlParser();
448              xhtml = xp.parseHtmlNode((org.w3c.dom.Element) child);
449              if (policy == ValidationPolicy.EVERYTHING) {
450                for (StringPair s : xp.getValidationIssues()) {
451                  logError(errors, "2022-11-17", line(child, false), col(child, false), path, IssueType.INVALID, context.formatMessage(s.getName(), s.getValue()), IssueSeverity.ERROR);                
452                }
453              }
454            }
455            Element n = new Element(property.getName(), property, "xhtml", new XhtmlComposer(XhtmlComposer.XML, false).compose(xhtml)).setXhtml(xhtml).markLocation(line(child, false), col(child, false)).setFormat(FhirFormat.XML).setNativeObject(child);
456            n.setPath(element.getPath()+"."+property.getName());
457            element.getChildren().add(n);
458          } else {
459            String npath = path+"/"+pathPrefix(child.getNamespaceURI())+child.getLocalName();
460            String name = child.getLocalName();
461            if (!property.isChoice() && !name.equals(property.getName())) {
462              name = property.getName();
463            }
464            Element n = new Element(name, property).markLocation(line(child, false), col(child, false)).setFormat(FhirFormat.XML).setNativeObject(child);
465            if (property.isList()) {
466              n.setPath(element.getPath()+"."+property.getName()+"["+repeatCount+"]");                                    
467            } else {
468              n.setPath(element.getPath()+"."+property.getName());
469            }
470            boolean xsiTypeChecked = false;
471            boolean ok = true;
472            if (property.isChoice()) {
473              if (property.getDefinition().hasRepresentation(PropertyRepresentation.TYPEATTR)) {
474                String xsiType = ((org.w3c.dom.Element) child).getAttributeNS(FormatUtilities.NS_XSI, "type");
475                if (Utilities.noString(xsiType)) {
476                  if (ExtensionUtilities.hasExtension(property.getDefinition(), "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype")) {
477                    xsiType = ExtensionUtilities.readStringExtension(property.getDefinition(), "http://hl7.org/fhir/StructureDefinition/elementdefinition-defaulttype");
478                    n.setType(xsiType);
479                  } else {
480                    logError(errors, ValidationMessage.NO_RULE_DATE, line(child, false), col(child, false), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.NO_TYPE_FOUND_ON_, child.getLocalName()), IssueSeverity.ERROR);
481                    ok = false;
482                  }
483                } else {
484                  if (xsiType.contains(":"))
485                    xsiType = xsiType.substring(xsiType.indexOf(":")+1);
486                  n.setType(xsiType);
487                  n.setExplicitType(xsiType);
488                }
489                xsiTypeChecked = true;
490              } else
491                n.setType(n.getType());
492            }
493            checkElement(errors, (org.w3c.dom.Element) child, n, npath, n.getProperty(), xsiTypeChecked);
494            element.getChildren().add(n);
495            if (ok) {
496              if (property.isResource())
497                parseResource(errors, npath, (org.w3c.dom.Element) child, n, property);
498              else
499                parseChildren(errors, npath, (org.w3c.dom.Element) child, n);
500            }
501          }
502        } else {
503          if (cgProp != null) {
504            property = getElementProp(cgProp.getChildProperties(null, null), child.getLocalName(), child.getNamespaceURI());
505            if (property != null) {
506              if (cgProp.getName().equals(lastName)) {
507                repeatCount++;
508              } else {
509                lastName = cgProp.getName();
510                repeatCount = 0;
511              }
512
513              String npath = path+"/"+pathPrefix(cgProp.getXmlNamespace())+cgProp.getName();
514              String name = cgProp.getName();
515              Element cgn = new Element(cgProp.getName(), cgProp).setFormat(FhirFormat.XML);
516              cgn.setPath(element.getPath()+"."+cgProp.getName()+"["+repeatCount+"]"); 
517              element.getChildren().add(cgn);
518
519              npath = npath+"/"+pathPrefix(child.getNamespaceURI())+child.getLocalName();
520              name = child.getLocalName();
521              Element n = new Element(name, property).markLocation(line(child, false), col(child, false)).setFormat(FhirFormat.XML).setNativeObject(child);
522              cgn.getChildren().add(n);
523              n.setPath(element.getPath()+"."+property.getName());
524              checkElement(errors, (org.w3c.dom.Element) child, n, npath, n.getProperty(), false);
525              parseChildren(errors, npath, (org.w3c.dom.Element) child, n);
526            }
527          }
528          if (property == null) {
529            logError(errors, ValidationMessage.NO_RULE_DATE, line(child, false), col(child, false), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.UNDEFINED_ELEMENT_, child.getLocalName(), path), IssueSeverity.ERROR);
530          }
531        }
532      } else if (child.getNodeType() == Node.TEXT_NODE && !Utilities.noString(child.getTextContent().trim()) && mtProp != null) {
533        if (cgProp.getName().equals(lastName)) {
534          repeatCount++;
535        } else {
536          lastName = cgProp.getName();
537          repeatCount = 0;
538        }
539
540        String npath = path+"/"+pathPrefix(cgProp.getXmlNamespace())+cgProp.getName();
541        String name = cgProp.getName();
542        Element cgn = new Element(cgProp.getName(), cgProp).setFormat(FhirFormat.XML);
543        cgn.setPath(element.getPath()+"."+cgProp.getName()+"["+repeatCount+"]"); 
544        element.getChildren().add(cgn);
545
546        npath = npath+"/text()";
547        name = mtProp.getName();
548        Element n = new Element(name, mtProp, mtProp.getType(), child.getTextContent().trim()).markLocation(line(child, false), col(child, false)).setFormat(FhirFormat.XML).setNativeObject(child);
549        cgn.getChildren().add(n);
550        n.setPath(element.getPath()+"."+mtProp.getName());
551
552
553      } else if (child.getNodeType() == Node.CDATA_SECTION_NODE) {
554        logError(errors, ValidationMessage.NO_RULE_DATE, line(child, false), col(child, false), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.CDATA_IS_NOT_ALLOWED), IssueSeverity.ERROR);
555      } else if (!Utilities.existsInList(child.getNodeType(), 3, 8)) {
556        logError(errors, 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);
557      }
558      child = child.getNextSibling();
559    }
560  }
561
562  private Property getChoiceGroupProp(List<Property> properties) {
563    for (Property p : properties) {
564      if (p.getDefinition().hasExtension(ExtensionDefinitions.EXT_ID_CHOICE_GROUP)) {
565        return p;
566      }
567    }
568    return null;
569  }
570
571  private boolean validAttrValue(String value) {
572    if (version == null) {
573      return true;
574    }
575    if (version.equals("1.0")) {
576      boolean ok = true;
577      for (char ch : value.toCharArray()) {
578        if (ch <= 0x1F && !Utilities.existsInList(ch, '\r', '\n', '\t')) {
579          ok = false;
580        }
581      }
582      return ok;
583    } else
584      return true;
585  }
586
587
588  private Property getElementProp(List<Property> properties, String nodeName, String namespace) {
589    List<Property> propsSortedByLongestFirst = new ArrayList<Property>(properties);
590    // sort properties according to their name longest first, so .requestOrganizationReference comes first before .request[x]
591    // and therefore the longer property names get evaluated first
592    Collections.sort(propsSortedByLongestFirst, new Comparator<Property>() {
593      @Override
594      public int compare(Property o1, Property o2) {
595        return o2.getName().length() - o1.getName().length();
596      }
597    });
598    // first scan, by namespace
599    for (Property p : propsSortedByLongestFirst) {
600      if (!p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR) && !p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) {
601        if (p.getXmlName().equals(nodeName) && p.getXmlNamespace().equals(namespace)) 
602          return p;
603      }
604    }
605    for (Property p : propsSortedByLongestFirst) {
606      if (!p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR) && !p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) {
607        if (p.getXmlName().equals(nodeName)) 
608          return p;
609        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))) 
610          return p;
611      }
612    }
613
614
615    return null;
616  }
617
618  private Property getAttrProp(List<Property> properties, String nodeName, String namespace) {
619    for (Property p : properties) {
620      if (p.getXmlName().equals(nodeName) && p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR) && p.getXmlNamespace().equals(namespace)) {
621        return p;
622      }
623    }
624    if (namespace == null) {
625      for (Property p : properties) {
626        if (p.getXmlName().equals(nodeName) && p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR)) {
627          return p;
628        }    
629      }
630    }
631    return null;
632  }
633
634  private Property getTextProp(List<Property> properties) {
635    for (Property p : properties)
636      if (p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) 
637        return p;
638    return null;
639  }
640
641  private String convertForDateFormatFromExternal(String fmt, String av) throws FHIRException {
642    if ("v3".equals(fmt) || "YYYYMMDDHHMMSS.UUUU[+|-ZZzz]".equals(fmt)) {
643      try {
644        DateTimeType d = DateTimeType.parseV3(av);
645        return d.asStringValue();
646      } catch (Exception e) {
647        return av; // not at all clear what to do in this case.
648      }
649    }
650    throw new FHIRException(context.formatMessage(I18nConstants.UNKNOWN_DATA_FORMAT_, fmt));
651  }
652
653  private String convertForDateFormatToExternal(String fmt, String av) throws FHIRException {
654    if ("v3".equals(fmt) || "YYYYMMDDHHMMSS.UUUU[+|-ZZzz]".equals(fmt)) {
655      DateTimeType d = new DateTimeType(av);
656      return d.getAsV3();
657    } else
658      throw new FHIRException(context.formatMessage(I18nConstants.UNKNOWN_DATE_FORMAT_, fmt));
659  }
660
661  private void parseResource(List<ValidationMessage> errors, String string, org.w3c.dom.Element container, Element parent, Property elementProperty) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
662    org.w3c.dom.Element res = XMLUtil.getFirstChild(container);
663    String name = res.getLocalName();
664    StructureDefinition sd = context.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(name, null));
665    if (sd == null)
666      throw new FHIRFormatError(context.formatMessage(I18nConstants.CONTAINED_RESOURCE_DOES_NOT_APPEAR_TO_BE_A_FHIR_RESOURCE_UNKNOWN_NAME_, res.getLocalName()));
667    parent.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd, getProfileUtilities(), getContextUtilities()), SpecialElement.fromProperty(parent.getProperty()), elementProperty);
668    parent.setType(name);
669    parseChildren(errors, res.getLocalName(), res, parent);
670  }
671
672  private void reapComments(org.w3c.dom.Element element, Element context) {
673    Node node = element.getPreviousSibling();
674    while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
675      if (node.getNodeType() == Node.COMMENT_NODE)
676        context.getComments().add(0, node.getTextContent());
677      node = node.getPreviousSibling();
678    }
679    node = element.getLastChild();
680    while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
681      node = node.getPreviousSibling();
682    }
683    while (node != null) {
684      if (node.getNodeType() == Node.COMMENT_NODE)
685        context.getComments().add(node.getTextContent());
686      node = node.getNextSibling();
687    }
688  }
689
690  private boolean isAttr(Property property) {
691    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
692      if (r.getValue() == PropertyRepresentation.XMLATTR) {
693        return true;
694      }
695    }
696    return false;
697  }
698
699  private boolean isCdaText(Property property) {
700    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
701      if (r.getValue() == PropertyRepresentation.CDATEXT) {
702        return true;
703      }
704    }
705    return false;
706  }
707
708  private boolean isTypeAttr(Property property) {
709    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
710      if (r.getValue() == PropertyRepresentation.TYPEATTR) {
711        return true;
712      }
713    }
714    return false;
715  }
716
717  private boolean isText(Property property) {
718    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
719      if (r.getValue() == PropertyRepresentation.XMLTEXT) {
720        return true;
721      }
722    }
723    return false;
724  }
725
726  @Override
727  public void compose(Element e, OutputStream stream, OutputStyle style, String base) throws IOException, FHIRException {
728    markedXhtml = false;
729    XMLWriter xml = new XMLWriter(stream, "UTF-8");
730    xml.setPretty(style == OutputStyle.PRETTY);
731    if (style == OutputStyle.CANONICAL) {
732      xml.setXmlHeader(false);
733      xml.setIgnoreComments(true);
734      xml.setCanonical(true);
735    } else {
736      xml.setCanonical(false);
737    }
738    xml.start();
739    if (e.getPath() == null) {
740      e.populatePaths(null);
741    }
742    String ns = e.getProperty().getXmlNamespace();
743    if (ns!=null && !"noNamespace".equals(ns)) {
744      xml.setDefaultNamespace(ns);
745    }
746    if (hasTypeAttr(e))
747      xml.namespace("http://www.w3.org/2001/XMLSchema-instance", "xsi");
748    if (Utilities.isAbsoluteUrl(e.getType())) {
749      xml.namespace(urlRoot(e.getType()), "et");
750    }
751    addNamespaces(xml, e);
752    composeElement(xml, e, e.getType(), true);
753    xml.end();
754  }
755
756  private void addNamespaces(IXMLWriter xml, Element e) throws IOException {
757    String ns = e.getProperty().getXmlNamespace();
758    if (ns!=null && xml.getDefaultNamespace()!=null && !xml.getDefaultNamespace().equals(ns)){
759      if (!xml.namespaceDefined(ns)) {
760        String prefix = pathPrefix(ns);
761        if (prefix.endsWith(":")) {
762          prefix = prefix.substring(0, prefix.length()-1);
763        }
764        if ("?".equals(prefix)) {
765          xml.namespace(ns);
766        } else {
767          xml.namespace(ns, prefix);
768        }
769      }
770    }
771    for (Element c : e.getChildren()) {
772      addNamespaces(xml, c);
773    }
774  }
775
776  private boolean hasTypeAttr(Element e) {
777    if (isTypeAttr(e.getProperty()))
778      return true;
779    for (Element c : e.getChildren()) {
780      if (hasTypeAttr(c))
781        return true;
782    }
783    // xsi_type is always allowed on CDA elements. right now, I'm not sure where to indicate this in the model, 
784    // so it's just hardcoded here 
785    if (e.getType() != null && e.getType().startsWith(Constants.NS_CDA_ROOT)) {
786      return true;
787    }
788    return false;
789  }
790
791  private void setXsiTypeIfIsTypeAttr(IXMLWriter xml, Element element) throws IOException, FHIRException {
792    if (isTypeAttr(element.getProperty()) && !Utilities.noString(element.getType())) {
793      String type = element.getType();
794      if (Utilities.isAbsoluteUrl(type)) {
795        type = type.substring(type.lastIndexOf("/")+1);
796      }
797      xml.attribute("xsi:type",type);    
798    }
799  }
800
801  public void compose(Element e, IXMLWriter xml) throws Exception {
802    if (e.getPath() == null) {
803      e.populatePaths(null);
804    }
805    markedXhtml = false;
806    xml.start();
807    xml.setDefaultNamespace(e.getProperty().getXmlNamespace());
808
809    if (Utilities.isAbsoluteUrl(e.getType())) {
810      xml.namespace(urlRoot(e.getType()), "et");
811    }
812
813    if (schemaPath != null) {
814      xml.setSchemaLocation(FormatUtilities.FHIR_NS, Utilities.pathURL(schemaPath, e.fhirType()+".xsd"));
815    }
816    composeElement(xml, e, e.getType(), true);
817    xml.end();
818  }
819
820  private void composeElement(IXMLWriter xml, Element element, String elementName, boolean root) throws IOException, FHIRException {
821    if (canonicalFilter.contains(element.getPath())) {
822      return;
823    }
824    if (!(isElideElements() && element.isElided())) {
825      if (showDecorations) {
826        @SuppressWarnings("unchecked")
827        List<ElementDecoration> decorations = (List<ElementDecoration>) element.getUserData(UserDataNames.rendering_xml_decorations);
828        if (decorations != null)
829          for (ElementDecoration d : decorations)
830            xml.decorate(d);
831      }
832      for (String s : element.getComments()) {
833        xml.comment(s, true);
834      }
835    }
836    if (isText(element.getProperty())) {
837      if (isElideElements() && element.isElided() && xml.canElide())
838        xml.elide();
839      else {
840        if (linkResolver != null) {
841          xml.link(linkResolver.resolveProperty(element.getProperty()));
842        }
843        xml.enter(element.getProperty().getXmlNamespace(),elementName);
844        if (linkResolver != null && element.getProperty().isReference()) {
845          String ref = linkResolver.resolveReference(getReferenceForElement(element));
846          if (ref != null) {
847            xml.externalLink(ref);
848          }
849        }
850        xml.text(element.getValue());
851        if (linkResolver != null) {
852          xml.link(linkResolver.resolveProperty(element.getProperty()));
853        }
854        xml.exit(element.getProperty().getXmlNamespace(),elementName);
855      }
856    } else if (!element.hasChildren() && !element.hasValue() && !element.hasXhtml()) {
857      if (isElideElements() && element.isElided() && xml.canElide())
858        xml.elide();
859      else {
860        if (element.getExplicitType() != null)
861          xml.attribute("xsi:type", element.getExplicitType());
862        xml.element(elementName);
863      }
864    } else if (element.isPrimitive() || (element.hasType() && isPrimitive(element.getType()))) {
865      if (element.getType().equals("xhtml")) {
866        if (isElideElements() && element.isElided() && xml.canElide())
867          xml.elide();
868        else {
869          if ((element.getXhtml()==null) && (element.getValue() != null)) {
870            XhtmlParser xhtml = new XhtmlParser();
871            element.setXhtml(xhtml.setXmlMode(true).parse(element.getValue(), null).getDocumentElement());
872          }
873          if (isCdaText(element.getProperty())) {
874            new CDANarrativeFormat().convert(xml, element.getXhtml());
875          } else {
876            String xhtml = new XhtmlComposer(XhtmlComposer.XML, false).setCanonical(xml.isCanonical()).compose(element.getXhtml());
877            xml.escapedText(xhtml);
878            if (!markedXhtml) {
879              xml.anchor("end-xhtml");
880              markedXhtml = true;
881            }
882          }
883        }
884      } else if (isText(element.getProperty())) {
885        if (isElideElements() && element.isElided() && xml.canElide())
886          xml.elide();
887        else {
888          if (linkResolver != null)
889            xml.link(linkResolver.resolveProperty(element.getProperty()));
890          xml.text(element.getValue());
891        }
892      } else {
893        if (isElideElements() && element.isElided())
894          xml.attributeElide();
895        else {
896          setXsiTypeIfIsTypeAttr(xml, element);
897          if (element.hasValue()) {
898            if (linkResolver != null)
899              xml.link(linkResolver.resolveType(element.getType()));
900            xml.attribute("value", element.getValue());
901          }
902          if (linkResolver != null) {
903            xml.link(linkResolver.resolveProperty(element.getProperty()));
904          }
905          if (element.hasChildren()) {
906            xml.enter(element.getProperty().getXmlNamespace(), elementName);
907            if (linkResolver != null && element.getProperty().isReference()) {
908              String ref = linkResolver.resolveReference(getReferenceForElement(element));
909              if (ref != null) {
910                xml.externalLink(ref);
911              }
912            }
913            for (Element child : element.getChildren())
914              composeElement(xml, child, child.getName(), false);
915            if (linkResolver != null) {
916              xml.link(linkResolver.resolveProperty(element.getProperty()));
917            }
918            xml.exit(element.getProperty().getXmlNamespace(),elementName);
919          } else
920            xml.element(elementName);
921        }
922      }
923    } else {
924      if (isElideElements() && element.isElided() && xml.canElide())
925        xml.elide();
926      else {
927        setXsiTypeIfIsTypeAttr(xml, element);
928        Set<String> handled = new HashSet<>();
929      for (Element child : element.getChildren()) {
930        if (!handled.contains(child.getName()) && isAttr(child.getProperty()) && wantCompose(element.getPath(), child)) {
931          handled.add(child.getName());
932          if (isElideElements() && child.isElided())
933            xml.attributeElide();
934          else {
935            String av = child.getValue();
936            if (child.getProperty().isList()) {
937              for (Element c2 : element.getChildren()) {
938                if (c2 != child && c2.getName().equals(child.getName())) {
939                  if (c2.isElided())
940                    av = av + " ...";
941                  else
942                    av = av + " " + c2.getValue();
943                }
944              }
945            }
946            if (linkResolver != null)
947              xml.link(linkResolver.resolveType(child.getType()));
948            if (ExtensionUtilities.hasExtension(child.getProperty().getDefinition(), ExtensionDefinitions.EXT_DATE_FORMAT))
949              av = convertForDateFormatToExternal(ExtensionUtilities.readStringExtension(child.getProperty().getDefinition(), ExtensionDefinitions.EXT_DATE_FORMAT), av);
950            xml.attribute(child.getProperty().getXmlNamespace(), child.getProperty().getXmlName(), av);
951          }
952        }
953      }
954      }
955      if (!element.getProperty().getDefinition().hasExtension(ExtensionDefinitions.EXT_ID_CHOICE_GROUP)) {
956        if (linkResolver != null)
957          xml.link(linkResolver.resolveProperty(element.getProperty()));
958        if (!xml.namespaceDefined(element.getProperty().getXmlNamespace())) {
959          String abbrev = makeNamespaceAbbrev(element.getProperty(), xml);
960          xml.namespace(element.getProperty().getXmlNamespace(), abbrev);
961        }
962        if (Utilities.isAbsoluteUrl(elementName)) {
963          xml.enter(urlRoot(elementName), urlTail(elementName));
964        } else {
965          xml.enter(element.getProperty().getXmlNamespace(), elementName);
966        }
967      }
968
969      if (!root && element.getSpecial() != null) {
970        if (linkResolver != null)
971          xml.link(linkResolver.resolveProperty(element.getProperty()));
972        xml.enter(element.getProperty().getXmlNamespace(),element.getType());
973      }
974      if (linkResolver != null && element.getProperty().isReference()) {
975        String ref = linkResolver.resolveReference(getReferenceForElement(element));
976        if (ref != null) {
977          xml.externalLink(ref);
978        }
979      }
980      for (Element child : element.getChildren()) {
981        if (wantCompose(element.getPath(), child)) {
982          if (isElideElements() && child.isElided() && xml.canElide())
983            xml.elide();
984          else {
985            if (isText(child.getProperty())) {
986              if (linkResolver != null)
987                xml.link(linkResolver.resolveProperty(element.getProperty()));
988              xml.text(child.getValue());
989            } else if (!isAttr(child.getProperty())) {
990              composeElement(xml, child, child.getName(), false);
991            }
992          }
993        }
994      }
995      if (!element.getProperty().getDefinition().hasExtension(ExtensionDefinitions.EXT_ID_CHOICE_GROUP)) {
996
997        if (linkResolver != null) {
998          xml.link(linkResolver.resolveProperty(element.getProperty()));
999        }
1000        if (!root && element.getSpecial() != null) {
1001          xml.exit(element.getProperty().getXmlNamespace(), element.getType());
1002        }
1003
1004        if (Utilities.isAbsoluteUrl(elementName)) {
1005          xml.exit(urlRoot(elementName), urlTail(elementName));
1006        } else {
1007          xml.exit(element.getProperty().getXmlNamespace(),elementName);
1008        }
1009      }
1010    }
1011  }
1012
1013  private String urlRoot(String elementName) {
1014    return elementName.substring(0, elementName.lastIndexOf("/"));
1015  }
1016  
1017  private String makeNamespaceAbbrev(Property property, IXMLWriter xml) {
1018    // it's a cosmetic thing, but we're going to try to come up with a nice namespace
1019
1020    ElementDefinition ed = property.getDefinition();
1021    String ns = property.getXmlNamespace();
1022    String n = property.getXmlName();
1023
1024    String diff = property.getName().toLowerCase().replace(n.toLowerCase(), "");
1025    if (!Utilities.noString(diff) && diff.length() <= 5 && Utilities.isToken(diff) && !xml.abbreviationDefined(diff)) {
1026      return diff;
1027    }
1028
1029    int i = ns.length()-1;
1030    while (i > 0) {
1031      if (Character.isAlphabetic(ns.charAt(i)) || Character.isDigit(ns.charAt(i))) {
1032        i--;
1033      } else {
1034        break;
1035      }
1036    }
1037    String tail = ns.substring(i+1);
1038    if (!Utilities.noString(tail) && tail.length() <= 5 && Utilities.isToken(tail) && !xml.abbreviationDefined(tail)) {
1039      return tail;
1040    }
1041
1042    i = 0;
1043    while (xml.abbreviationDefined("ns"+i)) {
1044      i++;
1045    }
1046    return "ns"+i;
1047  }
1048  private String checkHeader(List<ValidationMessage> errors, InputStream stream) throws IOException {
1049    try {
1050      // the stream will either start with the UTF-8 BOF or with <xml
1051      int i0 = stream.read();
1052      int i1 = stream.read();
1053      int i2 = stream.read();
1054
1055      StringBuilder b = new StringBuilder();
1056      if (i0 == 0xEF && i1 == 0xBB && i2 == 0xBF) {
1057        // ok, it's UTF-8
1058      } else if (i0 == 0x3C && i1 == 0x3F && i2 == 0x78) { // <xm
1059        b.append((char) i0);
1060        b.append((char) i1);
1061        b.append((char) i2);
1062      } else if (i0 == 60) { // just plain old XML with no header
1063        return "1.0";        
1064      } else {
1065        throw new Exception(context.formatMessage(I18nConstants.XML_ENCODING_INVALID));
1066      }
1067      int i = stream.read();
1068      do {
1069        b.append((char) i);
1070        i = stream.read();
1071      } while (i != 0x3E);
1072      String header = b.toString();
1073      String e = null;
1074      i = header.indexOf("encoding=\"");
1075      if (i > -1) {
1076        e = header.substring(i+10, i+15);
1077      } else {
1078        i = header.indexOf("encoding='");
1079        if (i > -1) {
1080          e = header.substring(i+10, i+15);
1081        } 
1082      }
1083      if (e != null && !"UTF-8".equalsIgnoreCase(e)) {
1084        logError(errors, ValidationMessage.NO_RULE_DATE, 0, 0, "XML", IssueType.INVALID, context.formatMessage(I18nConstants.XML_ENCODING_INVALID, e), IssueSeverity.ERROR);
1085      }
1086
1087      i = header.indexOf("version=\"");
1088      if (i > -1) {
1089        return header.substring(i+9, i+12);
1090      } else {
1091        i = header.indexOf("version='");
1092        if (i > -1) {
1093          return header.substring(i+9, i+12);          
1094        } 
1095      }
1096      return "?xml-p1?";
1097    } catch (Exception e) {
1098      // suppress this error 
1099      logError(errors, ValidationMessage.NO_RULE_DATE, 0, 0, "XML", IssueType.INVALID, e.getMessage(), IssueSeverity.ERROR);
1100    }
1101    return "?xml-p2?";
1102  }
1103
1104  class NullErrorHandler implements ErrorHandler {
1105    @Override
1106    public void fatalError(SAXParseException e) {
1107      // do nothing
1108    }
1109
1110    @Override
1111    public void error(SAXParseException e) {
1112      // do nothing
1113    }
1114
1115    @Override
1116    public void warning(SAXParseException e) {
1117      // do nothing
1118    }
1119  }
1120
1121  public boolean isElideElements() {
1122    return elideElements;
1123  }
1124
1125  public void setElideElements(boolean elideElements) {
1126    this.elideElements = elideElements;
1127  }
1128
1129}