001package org.hl7.fhir.r4.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
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.OutputStream;
035import java.util.ArrayList;
036import java.util.Collections;
037import java.util.Comparator;
038import java.util.List;
039
040import javax.xml.parsers.DocumentBuilder;
041import javax.xml.parsers.DocumentBuilderFactory;
042import javax.xml.parsers.SAXParserFactory;
043import javax.xml.transform.Transformer;
044import javax.xml.transform.TransformerFactory;
045import javax.xml.transform.dom.DOMResult;
046import javax.xml.transform.sax.SAXSource;
047
048import org.hl7.fhir.exceptions.DefinitionException;
049import org.hl7.fhir.exceptions.FHIRException;
050import org.hl7.fhir.exceptions.FHIRFormatError;
051import org.hl7.fhir.r4.conformance.ProfileUtilities;
052import org.hl7.fhir.r4.context.IWorkerContext;
053import org.hl7.fhir.r4.elementmodel.Element.SpecialElement;
054import org.hl7.fhir.r4.formats.FormatUtilities;
055import org.hl7.fhir.r4.formats.IParser.OutputStyle;
056import org.hl7.fhir.r4.model.DateTimeType;
057import org.hl7.fhir.r4.model.ElementDefinition.PropertyRepresentation;
058import org.hl7.fhir.r4.model.Enumeration;
059import org.hl7.fhir.r4.model.StructureDefinition;
060import org.hl7.fhir.r4.utils.ToolingExtensions;
061import org.hl7.fhir.r4.utils.formats.XmlLocationAnnotator;
062import org.hl7.fhir.r4.utils.formats.XmlLocationData;
063import org.hl7.fhir.utilities.ElementDecoration;
064import org.hl7.fhir.utilities.StringPair;
065import org.hl7.fhir.utilities.Utilities;
066import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
067import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
068import org.hl7.fhir.utilities.xhtml.CDANarrativeFormat;
069import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
070import org.hl7.fhir.utilities.xhtml.XhtmlNode;
071import org.hl7.fhir.utilities.xhtml.XhtmlParser;
072import org.hl7.fhir.utilities.xml.IXMLWriter;
073import org.hl7.fhir.utilities.xml.XMLUtil;
074import org.hl7.fhir.utilities.xml.XMLWriter;
075import org.w3c.dom.Document;
076import org.w3c.dom.Node;
077import org.xml.sax.InputSource;
078import org.xml.sax.XMLReader;
079
080public class XmlParser extends ParserBase {
081  private boolean allowXsiLocation;
082
083  public XmlParser(IWorkerContext context) {
084    super(context);
085  }
086
087  public boolean isAllowXsiLocation() {
088    return allowXsiLocation;
089  }
090
091  public void setAllowXsiLocation(boolean allowXsiLocation) {
092    this.allowXsiLocation = allowXsiLocation;
093  }
094
095  public Element parse(InputStream stream) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
096    Document doc = null;
097    try {
098      DocumentBuilderFactory factory = XMLUtil.newXXEProtectedDocumentBuilderFactory();
099      // xxe protection
100      factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
101      factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
102      factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
103      factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
104      factory.setXIncludeAware(false);
105      factory.setExpandEntityReferences(false);
106
107      factory.setNamespaceAware(true);
108      if (policy == ValidationPolicy.EVERYTHING) {
109        // use a slower parser that keeps location data
110        TransformerFactory transformerFactory = XMLUtil.newXXEProtectedTransformerFactory();
111        Transformer nullTransformer = transformerFactory.newTransformer();
112        DocumentBuilder docBuilder = factory.newDocumentBuilder();
113        doc = docBuilder.newDocument();
114        DOMResult domResult = new DOMResult(doc);
115        SAXParserFactory spf = XMLUtil.newXXEProtectedSaxParserFactory();
116        spf.setNamespaceAware(true);
117        spf.setValidating(false);
118        XMLReader xmlReader = XMLUtil.getXXEProtectedXMLReader(spf);
119
120        XmlLocationAnnotator locationAnnotator = new XmlLocationAnnotator(xmlReader, doc);
121        InputSource inputSource = new InputSource(stream);
122        SAXSource saxSource = new SAXSource(locationAnnotator, inputSource);
123        nullTransformer.transform(saxSource, domResult);
124      } else {
125        DocumentBuilder builder = factory.newDocumentBuilder();
126        doc = builder.parse(stream);
127      }
128    } catch (Exception e) {
129      logError(0, 0, "(syntax)", IssueType.INVALID, e.getMessage(), IssueSeverity.FATAL);
130      doc = null;
131    }
132    if (doc == null)
133      return null;
134    else
135      return parse(doc);
136  }
137
138  private void checkForProcessingInstruction(Document document) throws FHIRFormatError {
139    if (policy == ValidationPolicy.EVERYTHING
140        && FormatUtilities.FHIR_NS.equals(document.getDocumentElement().getNamespaceURI())) {
141      Node node = document.getFirstChild();
142      while (node != null) {
143        if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE)
144          logError(line(document), col(document), "(document)", IssueType.INVALID,
145              "No processing instructions allowed in resources", IssueSeverity.ERROR);
146        node = node.getNextSibling();
147      }
148    }
149  }
150
151  private int line(Node node) {
152    XmlLocationData loc = (XmlLocationData) node.getUserData(XmlLocationData.LOCATION_DATA_KEY);
153    return loc == null ? 0 : loc.getStartLine();
154  }
155
156  private int col(Node node) {
157    XmlLocationData loc = (XmlLocationData) node.getUserData(XmlLocationData.LOCATION_DATA_KEY);
158    return loc == null ? 0 : loc.getStartColumn();
159  }
160
161  public Element parse(Document doc) throws FHIRFormatError, DefinitionException, FHIRException, IOException {
162    checkForProcessingInstruction(doc);
163    org.w3c.dom.Element element = doc.getDocumentElement();
164    return parse(element);
165  }
166
167  public Element parse(org.w3c.dom.Element element)
168      throws FHIRFormatError, DefinitionException, FHIRException, IOException {
169    String ns = element.getNamespaceURI();
170    String name = element.getLocalName();
171    String path = "/" + pathPrefix(ns) + name;
172
173    StructureDefinition sd = getDefinition(line(element), col(element), ns, name);
174    if (sd == null)
175      return null;
176
177    Element result = new Element(element.getLocalName(),
178        new Property(context, sd.getSnapshot().getElement().get(0), sd));
179    checkElement(element, path, result.getProperty());
180    result.markLocation(line(element), col(element));
181    result.setType(element.getLocalName());
182    parseChildren(path, element, result);
183    result.numberChildren();
184    return result;
185  }
186
187  private String pathPrefix(String ns) {
188    if (Utilities.noString(ns))
189      return "";
190    if (ns.equals(FormatUtilities.FHIR_NS))
191      return "f:";
192    if (ns.equals(FormatUtilities.XHTML_NS))
193      return "h:";
194    if (ns.equals("urn:hl7-org:v3"))
195      return "v3:";
196    return "?:";
197  }
198
199  private boolean empty(org.w3c.dom.Element element) {
200    for (int i = 0; i < element.getAttributes().getLength(); i++) {
201      String n = element.getAttributes().item(i).getNodeName();
202      if (!n.equals("xmlns") && !n.startsWith("xmlns:"))
203        return false;
204    }
205    if (!Utilities.noString(element.getTextContent().trim()))
206      return false;
207
208    Node n = element.getFirstChild();
209    while (n != null) {
210      if (n.getNodeType() == Node.ELEMENT_NODE)
211        return false;
212      n = n.getNextSibling();
213    }
214    return true;
215  }
216
217  private void checkElement(org.w3c.dom.Element element, String path, Property prop) throws FHIRFormatError {
218    if (policy == ValidationPolicy.EVERYTHING) {
219      if (empty(element) && FormatUtilities.FHIR_NS.equals(element.getNamespaceURI())) // this rule only applies to FHIR
220                                                                                       // Content
221        logError(line(element), col(element), path, IssueType.INVALID, "Element must have some content",
222            IssueSeverity.ERROR);
223      String ns = FormatUtilities.FHIR_NS;
224      if (ToolingExtensions.hasExtension(prop.getDefinition(),
225          "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace"))
226        ns = ToolingExtensions.readStringExtension(prop.getDefinition(),
227            "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace");
228      else if (ToolingExtensions.hasExtension(prop.getStructure(),
229          "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace"))
230        ns = ToolingExtensions.readStringExtension(prop.getStructure(),
231            "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace");
232      if (!element.getNamespaceURI().equals(ns))
233        logError(line(element), col(element), path, IssueType.INVALID, "Wrong namespace - expected '" + ns + "'",
234            IssueSeverity.ERROR);
235    }
236  }
237
238  public Element parse(org.w3c.dom.Element base, String type) throws Exception {
239    StructureDefinition sd = getDefinition(0, 0, FormatUtilities.FHIR_NS, type);
240    Element result = new Element(base.getLocalName(), new Property(context, sd.getSnapshot().getElement().get(0), sd));
241    String path = "/" + pathPrefix(base.getNamespaceURI()) + base.getLocalName();
242    checkElement(base, path, result.getProperty());
243    result.setType(base.getLocalName());
244    parseChildren(path, base, result);
245    result.numberChildren();
246    return result;
247  }
248
249  private void parseChildren(String path, org.w3c.dom.Element node, Element context)
250      throws FHIRFormatError, FHIRException, IOException, DefinitionException {
251    // this parsing routine retains the original order in a the XML file, to support
252    // validation
253    reapComments(node, context);
254    List<Property> properties = context.getProperty().getChildProperties(context.getName(), XMLUtil.getXsiType(node));
255
256    String text = XMLUtil.getDirectText(node).trim();
257    if (!Utilities.noString(text)) {
258      Property property = getTextProp(properties);
259      if (property != null) {
260        context.getChildren().add(
261            new Element(property.getName(), property, property.getType(), text).markLocation(line(node), col(node)));
262      } else {
263        logError(line(node), col(node), path, IssueType.STRUCTURE, "Text should not be present", IssueSeverity.ERROR);
264      }
265    }
266
267    for (int i = 0; i < node.getAttributes().getLength(); i++) {
268      Node attr = node.getAttributes().item(i);
269      if (!(attr.getNodeName().equals("xmlns") || attr.getNodeName().startsWith("xmlns:"))) {
270        Property property = getAttrProp(properties, attr.getNodeName());
271        if (property != null) {
272          String av = attr.getNodeValue();
273          if (ToolingExtensions.hasExtension(property.getDefinition(),
274              "http://www.healthintersections.com.au/fhir/StructureDefinition/elementdefinition-dateformat"))
275            av = convertForDateFormatFromExternal(ToolingExtensions.readStringExtension(property.getDefinition(),
276                "http://www.healthintersections.com.au/fhir/StructureDefinition/elementdefinition-dateformat"), av);
277          if (property.getName().equals("value") && context.isPrimitive())
278            context.setValue(av);
279          else
280            context.getChildren().add(
281                new Element(property.getName(), property, property.getType(), av).markLocation(line(node), col(node)));
282        } else {
283          boolean ok = false;
284          if (FormatUtilities.FHIR_NS.equals(node.getNamespaceURI())) {
285            if (attr.getLocalName().equals("schemaLocation") && FormatUtilities.NS_XSI.equals(attr.getNamespaceURI())) {
286              ok = ok || allowXsiLocation;
287            }
288          } else
289            ok = ok || (attr.getLocalName().equals("schemaLocation")); // xsi:schemalocation allowed for non FHIR
290                                                                       // content
291          ok = ok || (hasTypeAttr(context) && attr.getLocalName().equals("type")
292              && FormatUtilities.NS_XSI.equals(attr.getNamespaceURI())); // xsi:type allowed if element says so
293          if (!ok)
294            logError(line(node), col(node), path, IssueType.STRUCTURE,
295                "Undefined attribute '@" + attr.getNodeName() + "' on " + node.getNodeName() + " for type "
296                    + context.fhirType() + " (properties = " + properties + ")",
297                IssueSeverity.ERROR);
298        }
299      }
300    }
301
302    Node child = node.getFirstChild();
303    while (child != null) {
304      if (child.getNodeType() == Node.ELEMENT_NODE) {
305        Property property = getElementProp(properties, child.getLocalName());
306        if (property != null) {
307          if (!property.isChoice() && "xhtml".equals(property.getType())) {
308            XhtmlNode xhtml;
309            if (property.getDefinition().hasRepresentation(PropertyRepresentation.CDATEXT))
310              xhtml = new CDANarrativeFormat().convert((org.w3c.dom.Element) child);
311            else {
312              XhtmlParser xp = new XhtmlParser();
313              xhtml = xp.parseHtmlNode((org.w3c.dom.Element) child);
314              if (policy == ValidationPolicy.EVERYTHING) {
315                for (StringPair s : xp.getValidationIssues()) {
316                  logError(line(child), col(child), path, IssueType.INVALID, s.getName() + " " + s.getValue(),
317                      IssueSeverity.ERROR);
318                }
319              }
320            }
321            context.getChildren()
322                .add(new Element(property.getName(), property, "xhtml",
323                    new XhtmlComposer(XhtmlComposer.XML, false).compose(xhtml)).setXhtml(xhtml)
324                        .markLocation(line(child), col(child)));
325          } else {
326            String npath = path + "/" + pathPrefix(child.getNamespaceURI()) + child.getLocalName();
327            Element n = new Element(child.getLocalName(), property).markLocation(line(child), col(child));
328            checkElement((org.w3c.dom.Element) child, npath, n.getProperty());
329            boolean ok = true;
330            if (property.isChoice()) {
331              if (property.getDefinition().hasRepresentation(PropertyRepresentation.TYPEATTR)) {
332                String xsiType = ((org.w3c.dom.Element) child).getAttributeNS(FormatUtilities.NS_XSI, "type");
333                if (Utilities.noString(xsiType)) {
334                  logError(line(child), col(child), path, IssueType.STRUCTURE,
335                      "No type found on '" + child.getLocalName() + '"', IssueSeverity.ERROR);
336                  ok = false;
337                } else {
338                  if (xsiType.contains(":"))
339                    xsiType = xsiType.substring(xsiType.indexOf(":") + 1);
340                  n.setType(xsiType);
341                  n.setExplicitType(xsiType);
342                }
343              } else
344                n.setType(n.getType());
345            }
346            context.getChildren().add(n);
347            if (ok) {
348              if (property.isResource())
349                parseResource(npath, (org.w3c.dom.Element) child, n, property);
350              else
351                parseChildren(npath, (org.w3c.dom.Element) child, n);
352            }
353          }
354        } else
355          logError(line(child), col(child), path, IssueType.STRUCTURE,
356              "Undefined element '" + child.getLocalName() + "'", IssueSeverity.ERROR);
357      } else if (child.getNodeType() == Node.CDATA_SECTION_NODE) {
358        logError(line(child), col(child), path, IssueType.STRUCTURE, "CDATA is not allowed", IssueSeverity.ERROR);
359      } else if (!Utilities.existsInList(child.getNodeType(), 3, 8)) {
360        logError(line(child), col(child), path, IssueType.STRUCTURE,
361            "Node type " + Integer.toString(child.getNodeType()) + " is not allowed", IssueSeverity.ERROR);
362      }
363      child = child.getNextSibling();
364    }
365  }
366
367  private Property getElementProp(List<Property> properties, String nodeName) {
368    List<Property> propsSortedByLongestFirst = new ArrayList<Property>(properties);
369    // sort properties according to their name longest first, so
370    // .requestOrganizationReference comes first before .request[x]
371    // and therefore the longer property names get evaluated first
372    Collections.sort(propsSortedByLongestFirst, new Comparator<Property>() {
373      @Override
374      public int compare(Property o1, Property o2) {
375        return o2.getName().length() - o1.getName().length();
376      }
377    });
378    for (Property p : propsSortedByLongestFirst)
379      if (!p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR)
380          && !p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT)) {
381        if (p.getName().equals(nodeName))
382          return p;
383        if (p.getName().endsWith("[x]") && nodeName.length() > p.getName().length() - 3 && p.getName()
384            .substring(0, p.getName().length() - 3).equals(nodeName.substring(0, p.getName().length() - 3)))
385          return p;
386      }
387    return null;
388  }
389
390  private Property getAttrProp(List<Property> properties, String nodeName) {
391    for (Property p : properties)
392      if (p.getName().equals(nodeName) && p.getDefinition().hasRepresentation(PropertyRepresentation.XMLATTR))
393        return p;
394    return null;
395  }
396
397  private Property getTextProp(List<Property> properties) {
398    for (Property p : properties)
399      if (p.getDefinition().hasRepresentation(PropertyRepresentation.XMLTEXT))
400        return p;
401    return null;
402  }
403
404  private String convertForDateFormatFromExternal(String fmt, String av) throws FHIRException {
405    if ("v3".equals(fmt)) {
406      DateTimeType d = DateTimeType.parseV3(av);
407      return d.asStringValue();
408    } else
409      throw new FHIRException("Unknown Data format '" + fmt + "'");
410  }
411
412  private String convertForDateFormatToExternal(String fmt, String av) throws FHIRException {
413    if ("v3".equals(fmt)) {
414      DateTimeType d = new DateTimeType(av);
415      return d.getAsV3();
416    } else
417      throw new FHIRException("Unknown Date format '" + fmt + "'");
418  }
419
420  private void parseResource(String string, org.w3c.dom.Element container, Element parent, Property elementProperty)
421      throws FHIRFormatError, DefinitionException, FHIRException, IOException {
422    org.w3c.dom.Element res = XMLUtil.getFirstChild(container);
423    String name = res.getLocalName();
424    StructureDefinition sd = context.fetchResource(StructureDefinition.class,
425        ProfileUtilities.sdNs(name, context.getOverrideVersionNs()));
426    if (sd == null)
427      throw new FHIRFormatError(
428          "Contained resource does not appear to be a FHIR resource (unknown name '" + res.getLocalName() + "')");
429    parent.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd),
430        SpecialElement.fromProperty(parent.getProperty()), elementProperty);
431    parent.setType(name);
432    parseChildren(res.getLocalName(), res, parent);
433  }
434
435  private void reapComments(org.w3c.dom.Element element, Element context) {
436    Node node = element.getPreviousSibling();
437    while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
438      if (node.getNodeType() == Node.COMMENT_NODE)
439        context.getComments().add(0, node.getTextContent());
440      node = node.getPreviousSibling();
441    }
442    node = element.getLastChild();
443    while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
444      node = node.getPreviousSibling();
445    }
446    while (node != null) {
447      if (node.getNodeType() == Node.COMMENT_NODE)
448        context.getComments().add(node.getTextContent());
449      node = node.getNextSibling();
450    }
451  }
452
453  private boolean isAttr(Property property) {
454    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
455      if (r.getValue() == PropertyRepresentation.XMLATTR) {
456        return true;
457      }
458    }
459    return false;
460  }
461
462  private boolean isCdaText(Property property) {
463    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
464      if (r.getValue() == PropertyRepresentation.CDATEXT) {
465        return true;
466      }
467    }
468    return false;
469  }
470
471  private boolean isTypeAttr(Property property) {
472    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
473      if (r.getValue() == PropertyRepresentation.TYPEATTR) {
474        return true;
475      }
476    }
477    return false;
478  }
479
480  private boolean isText(Property property) {
481    for (Enumeration<PropertyRepresentation> r : property.getDefinition().getRepresentation()) {
482      if (r.getValue() == PropertyRepresentation.XMLTEXT) {
483        return true;
484      }
485    }
486    return false;
487  }
488
489  @Override
490  public void compose(Element e, OutputStream stream, OutputStyle style, String base)
491      throws IOException, FHIRException {
492    XMLWriter xml = new XMLWriter(stream, "UTF-8");
493    xml.setSortAttributes(false);
494    xml.setPretty(style == OutputStyle.PRETTY);
495    xml.start();
496    xml.setDefaultNamespace(e.getProperty().getNamespace());
497    if (hasTypeAttr(e))
498      xml.namespace("http://www.w3.org/2001/XMLSchema-instance", "xsi");
499    composeElement(xml, e, e.getType(), true);
500    xml.end();
501
502  }
503
504  private boolean hasTypeAttr(Element e) {
505    if (isTypeAttr(e.getProperty()))
506      return true;
507    for (Element c : e.getChildren()) {
508      if (hasTypeAttr(c))
509        return true;
510    }
511    return false;
512  }
513
514  public void compose(Element e, IXMLWriter xml) throws Exception {
515    xml.start();
516    xml.setDefaultNamespace(e.getProperty().getNamespace());
517    composeElement(xml, e, e.getType(), true);
518    xml.end();
519  }
520
521  private void composeElement(IXMLWriter xml, Element element, String elementName, boolean root)
522      throws IOException, FHIRException {
523    if (showDecorations) {
524      @SuppressWarnings("unchecked")
525      List<ElementDecoration> decorations = (List<ElementDecoration>) element.getUserData("fhir.decorations");
526      if (decorations != null)
527        for (ElementDecoration d : decorations)
528          xml.decorate(d);
529    }
530    for (String s : element.getComments()) {
531      xml.comment(s, true);
532    }
533    if (isText(element.getProperty())) {
534      if (linkResolver != null)
535        xml.link(linkResolver.resolveProperty(element.getProperty()));
536      xml.enter(elementName);
537      xml.text(element.getValue());
538      xml.exit(elementName);
539    } else if (!element.hasChildren() && !element.hasValue()) {
540      if (element.getExplicitType() != null)
541        xml.attribute("xsi:type", element.getExplicitType());
542      xml.element(elementName);
543    } else if (element.isPrimitive() || (element.hasType() && isPrimitive(element.getType()))) {
544      if (element.getType().equals("xhtml")) {
545        String rawXhtml = element.getValue();
546        if (isCdaText(element.getProperty())) {
547          new CDANarrativeFormat().convert(xml, element.getXhtml());
548        } else
549          xml.escapedText(rawXhtml);
550      } else if (isText(element.getProperty())) {
551        if (linkResolver != null)
552          xml.link(linkResolver.resolveProperty(element.getProperty()));
553        xml.text(element.getValue());
554      } else {
555        if (isTypeAttr(element.getProperty()) && !Utilities.noString(element.getType())) {
556          xml.attribute("xsi:type", element.getType());
557        }
558        if (element.hasValue()) {
559          if (linkResolver != null)
560            xml.link(linkResolver.resolveType(element.getType()));
561          xml.attribute("value", element.getValue());
562        }
563        if (linkResolver != null)
564          xml.link(linkResolver.resolveProperty(element.getProperty()));
565        if (element.hasChildren()) {
566          xml.enter(elementName);
567          for (Element child : element.getChildren())
568            composeElement(xml, child, child.getName(), false);
569          xml.exit(elementName);
570        } else
571          xml.element(elementName);
572      }
573    } else {
574      if (isTypeAttr(element.getProperty()) && !Utilities.noString(element.getType())) {
575        xml.attribute("xsi:type", element.getType());
576      }
577      for (Element child : element.getChildren()) {
578        if (isAttr(child.getProperty())) {
579          if (linkResolver != null)
580            xml.link(linkResolver.resolveType(child.getType()));
581          String av = child.getValue();
582          if (ToolingExtensions.hasExtension(child.getProperty().getDefinition(),
583              "http://www.healthintersections.com.au/fhir/StructureDefinition/elementdefinition-dateformat"))
584            av = convertForDateFormatToExternal(
585                ToolingExtensions.readStringExtension(child.getProperty().getDefinition(),
586                    "http://www.healthintersections.com.au/fhir/StructureDefinition/elementdefinition-dateformat"),
587                av);
588          xml.attribute(child.getName(), av);
589        }
590      }
591      if (linkResolver != null)
592        xml.link(linkResolver.resolveProperty(element.getProperty()));
593      xml.enter(elementName);
594      if (!root && element.getSpecial() != null) {
595        if (linkResolver != null)
596          xml.link(linkResolver.resolveProperty(element.getProperty()));
597        xml.enter(element.getType());
598      }
599      for (Element child : element.getChildren()) {
600        if (isText(child.getProperty())) {
601          if (linkResolver != null)
602            xml.link(linkResolver.resolveProperty(element.getProperty()));
603          xml.text(child.getValue());
604        } else if (!isAttr(child.getProperty()))
605          composeElement(xml, child, child.getName(), false);
606      }
607      if (!root && element.getSpecial() != null)
608        xml.exit(element.getType());
609      xml.exit(elementName);
610    }
611  }
612
613}