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