001package org.hl7.fhir.r4.formats;
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/*
033Copyright (c) 2011+, HL7, Inc
034All rights reserved.
035
036Redistribution and use in source and binary forms, with or without modification, 
037are permitted provided that the following conditions are met:
038
039 * Redistributions of source code must retain the above copyright notice, this 
040   list of conditions and the following disclaimer.
041 * Redistributions in binary form must reproduce the above copyright notice, 
042   this list of conditions and the following disclaimer in the documentation 
043   and/or other materials provided with the distribution.
044 * Neither the name of HL7 nor the names of its contributors may be used to 
045   endorse or promote products derived from this software without specific 
046   prior written permission.
047
048THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
049ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
050WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
051IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
052INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
053NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
054PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
055WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
056ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
057POSSIBILITY OF SUCH DAMAGE.
058
059 */
060
061import java.io.BufferedInputStream;
062import java.io.ByteArrayInputStream;
063import java.io.IOException;
064import java.io.InputStream;
065import java.io.OutputStream;
066import java.io.UnsupportedEncodingException;
067import java.util.ArrayList;
068import java.util.List;
069
070import org.hl7.fhir.exceptions.FHIRFormatError;
071import org.hl7.fhir.instance.model.api.IIdType;
072import org.hl7.fhir.r4.model.Base;
073import org.hl7.fhir.r4.model.DomainResource;
074import org.hl7.fhir.r4.model.Element;
075import org.hl7.fhir.r4.model.Resource;
076import org.hl7.fhir.r4.model.StringType;
077import org.hl7.fhir.r4.model.Type;
078import org.hl7.fhir.utilities.Utilities;
079import org.hl7.fhir.utilities.xhtml.NodeType;
080import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
081import org.hl7.fhir.utilities.xhtml.XhtmlNode;
082import org.hl7.fhir.utilities.xhtml.XhtmlParser;
083import org.hl7.fhir.utilities.xml.IXMLWriter;
084import org.hl7.fhir.utilities.xml.XMLWriter;
085import org.xmlpull.v1.XmlPullParser;
086import org.xmlpull.v1.XmlPullParserException;
087import org.xmlpull.v1.XmlPullParserFactory;
088
089/**
090 * General parser for XML content. You instantiate an XmlParser of these, but
091 * you actually use parse or parseGeneral defined on this class
092 * 
093 * The two classes are separated to keep generated and manually maintained code
094 * apart.
095 */
096public abstract class XmlParserBase extends ParserBase implements IParser {
097
098  public enum XmlVersion {
099    V1_0, V1_1
100  }
101
102  private XmlVersion version;
103
104  public XmlParserBase(XmlVersion ver) {
105    super();
106    version = ver;
107  }
108
109  public XmlParserBase() {
110    super();
111    version = XmlVersion.V1_0;
112  }
113
114  @Override
115  public ParserType getType() {
116    return ParserType.XML;
117  }
118
119  // -- in descendent generated code --------------------------------------
120
121  abstract protected Resource parseResource(XmlPullParser xpp)
122      throws XmlPullParserException, IOException, FHIRFormatError;
123
124  abstract protected Type parseType(XmlPullParser xml, String type)
125      throws XmlPullParserException, IOException, FHIRFormatError;
126
127  abstract protected Type parseAnyType(XmlPullParser xml, String type)
128      throws XmlPullParserException, IOException, FHIRFormatError;
129
130  abstract protected void composeType(String prefix, Type type) throws IOException;
131
132  /* -- entry points --------------------------------------------------- */
133
134  /**
135   * Parse content that is known to be a resource @
136   */
137  @Override
138  public Resource parse(InputStream input) throws IOException, FHIRFormatError {
139    try {
140      XmlPullParser xpp = loadXml(input);
141      return parse(xpp);
142    } catch (XmlPullParserException e) {
143      throw new FHIRFormatError(e.getMessage(), e);
144    }
145  }
146
147  /**
148   * parse xml that is known to be a resource, and that is already being read by
149   * an XML Pull Parser This is if a resource is in a bigger piece of XML. @
150   */
151  public Resource parse(XmlPullParser xpp) throws IOException, FHIRFormatError, XmlPullParserException {
152    if (xpp.getNamespace() == null)
153      throw new FHIRFormatError("This does not appear to be a FHIR resource (no namespace '" + xpp.getNamespace()
154          + "') (@ /) " + Integer.toString(xpp.getEventType()));
155    if (!xpp.getNamespace().equals(FHIR_NS))
156      throw new FHIRFormatError(
157          "This does not appear to be a FHIR resource (wrong namespace '" + xpp.getNamespace() + "') (@ /)");
158    return parseResource(xpp);
159  }
160
161  @Override
162  public Type parseType(InputStream input, String knownType) throws IOException, FHIRFormatError {
163    try {
164      XmlPullParser xml = loadXml(input);
165      return parseType(xml, knownType);
166    } catch (XmlPullParserException e) {
167      throw new FHIRFormatError(e.getMessage(), e);
168    }
169  }
170
171  @Override
172  public Type parseAnyType(InputStream input, String knownType) throws IOException, FHIRFormatError {
173    try {
174      XmlPullParser xml = loadXml(input);
175      return parseAnyType(xml, knownType);
176    } catch (XmlPullParserException e) {
177      throw new FHIRFormatError(e.getMessage(), e);
178    }
179  }
180
181  /**
182   * Compose a resource to a stream, possibly using pretty presentation for a
183   * human reader (used in the spec, for example, but not normally in
184   * production) @
185   */
186  @Override
187  public void compose(OutputStream stream, Resource resource) throws IOException {
188    XMLWriter writer = new XMLWriter(stream, "UTF-8", version == XmlVersion.V1_1);
189    writer.setPretty(style == OutputStyle.PRETTY);
190    writer.start();
191    compose(writer, resource, writer.isPretty());
192    writer.end();
193  }
194
195  /**
196   * Compose a resource to a stream, possibly using pretty presentation for a
197   * human reader, and maybe a different choice in the xhtml narrative (used in
198   * the spec in one place, but should not be used in production) @
199   */
200  public void compose(OutputStream stream, Resource resource, boolean htmlPretty) throws IOException {
201    XMLWriter writer = new XMLWriter(stream, "UTF-8", version == XmlVersion.V1_1);
202    writer.setPretty(style == OutputStyle.PRETTY);
203    writer.start();
204    compose(writer, resource, htmlPretty);
205    writer.end();
206  }
207
208  /**
209   * Compose a type to a stream (used in the spec, for example, but not normally
210   * in production) @
211   */
212  public void compose(OutputStream stream, String rootName, Type type) throws IOException {
213    xml = new XMLWriter(stream, "UTF-8", version == XmlVersion.V1_1);
214    xml.setPretty(style == OutputStyle.PRETTY);
215    xml.start();
216    xml.setDefaultNamespace(FHIR_NS);
217    composeType(Utilities.noString(rootName) ? "value" : rootName, type);
218    xml.end();
219  }
220
221  @Override
222  public void compose(OutputStream stream, Type type, String rootName) throws IOException {
223    xml = new XMLWriter(stream, "UTF-8", version == XmlVersion.V1_1);
224    xml.setPretty(style == OutputStyle.PRETTY);
225    xml.start();
226    xml.setDefaultNamespace(FHIR_NS);
227    composeType(Utilities.noString(rootName) ? "value" : rootName, type);
228    xml.end();
229  }
230
231  /* -- xml routines --------------------------------------------------- */
232
233  protected XmlPullParser loadXml(String source)
234      throws UnsupportedEncodingException, XmlPullParserException, IOException {
235    return loadXml(new ByteArrayInputStream(source.getBytes("UTF-8")));
236  }
237
238  protected XmlPullParser loadXml(InputStream stream) throws XmlPullParserException, IOException {
239    BufferedInputStream input = new BufferedInputStream(stream);
240    XmlPullParserFactory factory = XmlPullParserFactory
241        .newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null);
242    factory.setNamespaceAware(true);
243    factory.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, false);
244    XmlPullParser xpp = factory.newPullParser();
245    xpp.setInput(input, "UTF-8");
246    next(xpp);
247    nextNoWhitespace(xpp);
248
249    return xpp;
250  }
251
252  protected int next(XmlPullParser xpp) throws XmlPullParserException, IOException {
253    if (handleComments)
254      return xpp.nextToken();
255    else
256      return xpp.next();
257  }
258
259  protected List<String> comments = new ArrayList<String>();
260
261  protected int nextNoWhitespace(XmlPullParser xpp) throws XmlPullParserException, IOException {
262    int eventType = xpp.getEventType();
263    while ((eventType == XmlPullParser.TEXT && xpp.isWhitespace()) || (eventType == XmlPullParser.COMMENT)
264        || (eventType == XmlPullParser.CDSECT) || (eventType == XmlPullParser.IGNORABLE_WHITESPACE)
265        || (eventType == XmlPullParser.PROCESSING_INSTRUCTION) || (eventType == XmlPullParser.DOCDECL)) {
266      if (eventType == XmlPullParser.COMMENT) {
267        comments.add(xpp.getText());
268      } else if (eventType == XmlPullParser.DOCDECL) {
269        throw new XmlPullParserException("DTD declarations are not allowed");
270      }
271      eventType = next(xpp);
272    }
273    return eventType;
274  }
275
276  protected void skipElementWithContent(XmlPullParser xpp) throws XmlPullParserException, IOException {
277    // when this is called, we are pointing an element that may have content
278    while (xpp.getEventType() != XmlPullParser.END_TAG) {
279      next(xpp);
280      if (xpp.getEventType() == XmlPullParser.START_TAG)
281        skipElementWithContent(xpp);
282    }
283    next(xpp);
284  }
285
286  protected void skipEmptyElement(XmlPullParser xpp) throws XmlPullParserException, IOException {
287    while (xpp.getEventType() != XmlPullParser.END_TAG)
288      next(xpp);
289    next(xpp);
290  }
291
292  protected IXMLWriter xml;
293  protected boolean htmlPretty;
294
295  /* -- worker routines --------------------------------------------------- */
296
297  protected void parseTypeAttributes(XmlPullParser xpp, Type t) {
298    parseElementAttributes(xpp, t);
299  }
300
301  protected void parseElementAttributes(XmlPullParser xpp, Element e) {
302    if (xpp.getAttributeValue(null, "id") != null) {
303      e.setId(xpp.getAttributeValue(null, "id"));
304      idMap.put(e.getId(), e);
305    }
306    if (!comments.isEmpty()) {
307      e.getFormatCommentsPre().addAll(comments);
308      comments.clear();
309    }
310  }
311
312  protected void parseElementClose(Base e) {
313    if (!comments.isEmpty()) {
314      e.getFormatCommentsPost().addAll(comments);
315      comments.clear();
316    }
317  }
318
319  protected void parseBackboneAttributes(XmlPullParser xpp, Element e) {
320    parseElementAttributes(xpp, e);
321  }
322
323  private String pathForLocation(XmlPullParser xpp) {
324    return xpp.getPositionDescription();
325  }
326
327  protected void unknownContent(XmlPullParser xpp) throws FHIRFormatError, XmlPullParserException, IOException {
328    if (!isAllowUnknownContent()) {
329      throw new FHIRFormatError("Unknown Content " + xpp.getName() + " @ " + pathForLocation(xpp));
330    }
331    // otherwise, read over whatever element this is
332    int count = 1;
333    do {
334      xpp.next();
335      if (xpp.getEventType() == XmlPullParser.END_TAG)
336        count--;
337      if (xpp.getEventType() == XmlPullParser.START_TAG)
338        count++;
339    } while (count > 0);
340    xpp.next();
341  }
342
343  protected XhtmlNode parseXhtml(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError {
344    XhtmlParser prsr = new XhtmlParser();
345    try {
346      return prsr.parseHtmlNode(xpp);
347    } catch (org.hl7.fhir.exceptions.FHIRFormatError e) {
348      throw new FHIRFormatError(e.getMessage(), e);
349    }
350  }
351
352  private String parseString(XmlPullParser xpp) throws XmlPullParserException, FHIRFormatError, IOException {
353    StringBuilder res = new StringBuilder();
354    next(xpp);
355    while (xpp.getEventType() == XmlPullParser.TEXT || xpp.getEventType() == XmlPullParser.IGNORABLE_WHITESPACE
356        || xpp.getEventType() == XmlPullParser.ENTITY_REF) {
357      res.append(xpp.getText());
358      next(xpp);
359    }
360    if (xpp.getEventType() != XmlPullParser.END_TAG)
361      throw new FHIRFormatError(
362          "Bad String Structure - parsed " + res.toString() + " now found " + Integer.toString(xpp.getEventType()));
363    next(xpp);
364    return res.length() == 0 ? null : res.toString();
365  }
366
367  private int parseInt(XmlPullParser xpp) throws FHIRFormatError, XmlPullParserException, IOException {
368    int res = -1;
369    String textNode = parseString(xpp);
370    res = java.lang.Integer.parseInt(textNode);
371    return res;
372  }
373
374  protected DomainResource parseDomainResourceContained(XmlPullParser xpp)
375      throws IOException, FHIRFormatError, XmlPullParserException {
376    next(xpp);
377    int eventType = nextNoWhitespace(xpp);
378    if (eventType == XmlPullParser.START_TAG) {
379      DomainResource dr = (DomainResource) parseResource(xpp);
380      nextNoWhitespace(xpp);
381      next(xpp);
382      return dr;
383    } else {
384      unknownContent(xpp);
385      return null;
386    }
387  }
388
389  protected Resource parseResourceContained(XmlPullParser xpp)
390      throws IOException, FHIRFormatError, XmlPullParserException {
391    next(xpp);
392    int eventType = nextNoWhitespace(xpp);
393    if (eventType == XmlPullParser.START_TAG) {
394      Resource r = (Resource) parseResource(xpp);
395      nextNoWhitespace(xpp);
396      next(xpp);
397      return r;
398    } else {
399      unknownContent(xpp);
400      return null;
401    }
402  }
403
404  public void compose(IXMLWriter writer, Resource resource, boolean htmlPretty) throws IOException {
405    this.htmlPretty = htmlPretty;
406    xml = writer;
407    xml.setDefaultNamespace(FHIR_NS);
408    composeResource(resource);
409  }
410
411  protected abstract void composeResource(Resource resource) throws IOException;
412
413  protected void composeElementAttributes(Element element) throws IOException {
414    if (style != OutputStyle.CANONICAL)
415      for (String comment : element.getFormatCommentsPre())
416        xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
417    if (element.getId() != null)
418      xml.attribute("id", element.getId());
419  }
420
421  protected void composeElementClose(Base base) throws IOException {
422    if (style != OutputStyle.CANONICAL)
423      for (String comment : base.getFormatCommentsPost())
424        xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
425  }
426
427  protected void composeTypeAttributes(Type type) throws IOException {
428    composeElementAttributes(type);
429  }
430
431  protected void composeXhtml(String name, XhtmlNode html) throws IOException {
432    if (!Utilities.noString(xhtmlMessage)) {
433      xml.enter(XhtmlComposer.XHTML_NS, name);
434      xml.comment(xhtmlMessage, false);
435      xml.exit(XhtmlComposer.XHTML_NS, name);
436    } else {
437      XhtmlComposer comp = new XhtmlComposer(XhtmlComposer.XML, htmlPretty);
438      // name is also found in the html and should the same
439      // ? check that
440      boolean oldPretty = xml.isPretty();
441      xml.setPretty(htmlPretty);
442      if (html.getNodeType() != NodeType.Text && html.getNsDecl() == null)
443        xml.namespace(XhtmlComposer.XHTML_NS, null);
444      comp.compose(xml, html);
445      xml.setPretty(oldPretty);
446    }
447  }
448
449  abstract protected void composeString(String name, StringType value) throws IOException;
450
451  protected void composeString(String name, IIdType value) throws IOException {
452    composeString(name, new StringType(value.getValue()));
453  }
454
455  protected void composeDomainResource(String name, DomainResource res) throws IOException {
456    xml.enter(FHIR_NS, name);
457    composeResource(res.getResourceType().toString(), res);
458    xml.exit(FHIR_NS, name);
459  }
460
461  protected abstract void composeResource(String name, Resource res) throws IOException;
462
463}