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
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.r4.model.Base;
074import org.hl7.fhir.r4.model.DomainResource;
075import org.hl7.fhir.r4.model.Element;
076import org.hl7.fhir.r4.model.Resource;
077import org.hl7.fhir.r4.model.StringType;
078import org.hl7.fhir.r4.model.Type;
079import org.hl7.fhir.utilities.Utilities;
080import org.hl7.fhir.utilities.xhtml.NodeType;
081import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
082import org.hl7.fhir.utilities.xhtml.XhtmlNode;
083import org.hl7.fhir.utilities.xhtml.XhtmlParser;
084import org.hl7.fhir.utilities.xml.IXMLWriter;
085import org.hl7.fhir.utilities.xml.XMLWriter;
086import org.xmlpull.v1.XmlPullParser;
087import org.xmlpull.v1.XmlPullParserException;
088import org.xmlpull.v1.XmlPullParserFactory;
089
090/**
091 * General parser for XML content. You instantiate an XmlParser of these, but you 
092 * actually use parse or parseGeneral defined on this class
093 * 
094 * The two classes are separated to keep generated and manually maintained code apart.
095 */
096public abstract class XmlParserBase extends ParserBase implements IParser {
097  
098  public enum XmlVersion { V1_0, V1_1  }
099  private XmlVersion version;
100
101  public XmlParserBase(XmlVersion ver) {
102    super();
103    version = ver;
104  }
105
106
107  public XmlParserBase() {
108    super();
109    version = XmlVersion.V1_0;
110  }
111  
112        @Override
113        public ParserType getType() {
114                return ParserType.XML;
115        }
116
117        // -- in descendent generated code --------------------------------------
118
119        abstract protected Resource parseResource(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError ;
120  abstract protected Type parseType(XmlPullParser xml, String type) throws XmlPullParserException, IOException, FHIRFormatError ;
121  abstract protected Type parseAnyType(XmlPullParser xml, String type) throws XmlPullParserException, IOException, FHIRFormatError ;
122        abstract protected void composeType(String prefix, Type type) throws IOException ;
123
124        /* -- entry points --------------------------------------------------- */
125
126        /**
127         * Parse content that is known to be a resource
128         * @ 
129         */
130        @Override
131        public Resource parse(InputStream input) throws IOException, FHIRFormatError {
132                try {
133                        XmlPullParser xpp = loadXml(input);
134                        return parse(xpp);
135                } catch (XmlPullParserException e) {
136                        throw new FHIRFormatError(e.getMessage(), e);
137                }
138        }
139
140        /**
141         * parse xml that is known to be a resource, and that is already being read by an XML Pull Parser
142         * This is if a resource is in a bigger piece of XML.   
143         * @ 
144         */
145        public Resource parse(XmlPullParser xpp)  throws IOException, FHIRFormatError, XmlPullParserException {
146                if (xpp.getNamespace() == null)
147                        throw new FHIRFormatError("This does not appear to be a FHIR resource (no namespace '"+xpp.getNamespace()+"') (@ /) "+Integer.toString(xpp.getEventType()));
148                if (!xpp.getNamespace().equals(FHIR_NS))
149                        throw new FHIRFormatError("This does not appear to be a FHIR resource (wrong namespace '"+xpp.getNamespace()+"') (@ /)");
150                return parseResource(xpp);
151        }
152
153        @Override
154        public Type parseType(InputStream input, String knownType) throws IOException, FHIRFormatError  {
155                try {
156                        XmlPullParser xml = loadXml(input);
157                        return parseType(xml, knownType);
158                } catch (XmlPullParserException e) {
159                        throw new FHIRFormatError(e.getMessage(), e);
160                }
161        }
162
163  @Override
164  public Type parseAnyType(InputStream input, String knownType) throws IOException, FHIRFormatError  {
165    try {
166      XmlPullParser xml = loadXml(input);
167      return parseAnyType(xml, knownType);
168    } catch (XmlPullParserException e) {
169      throw new FHIRFormatError(e.getMessage(), e);
170    }
171  }
172
173        /**
174         * 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)
175         * @ 
176         */
177        @Override
178        public void compose(OutputStream stream, Resource resource)  throws IOException {
179                XMLWriter writer = new XMLWriter(stream, "UTF-8", version == XmlVersion.V1_1);
180                writer.setPretty(style == OutputStyle.PRETTY);
181                writer.start();
182                compose(writer, resource, writer.isPretty());
183                writer.end();
184        }
185
186        /**
187         * 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)
188         * @ 
189         */
190        public void compose(OutputStream stream, Resource resource, boolean htmlPretty)  throws IOException {
191                XMLWriter writer = new XMLWriter(stream, "UTF-8", version == XmlVersion.V1_1);
192                writer.setPretty(style == OutputStyle.PRETTY);
193                writer.start();
194                compose(writer, resource, htmlPretty);
195                writer.end();
196        }
197
198
199        /**
200         * Compose a type to a stream (used in the spec, for example, but not normally in production)
201         * @ 
202         */
203        public void compose(OutputStream stream, String rootName, Type type)  throws IOException {
204                xml = new XMLWriter(stream, "UTF-8", version == XmlVersion.V1_1);
205                xml.setPretty(style == OutputStyle.PRETTY);
206                xml.start();
207                xml.setDefaultNamespace(FHIR_NS);
208                composeType(Utilities.noString(rootName) ? "value" : rootName, type);
209                xml.end();
210        }
211
212        @Override
213        public void compose(OutputStream stream, Type type, String rootName)  throws IOException {
214                xml = new XMLWriter(stream, "UTF-8", version == XmlVersion.V1_1);
215                xml.setPretty(style == OutputStyle.PRETTY);
216                xml.start();
217                xml.setDefaultNamespace(FHIR_NS);
218                composeType(Utilities.noString(rootName) ? "value" : rootName, type);
219                xml.end();
220        }
221
222
223
224        /* -- xml routines --------------------------------------------------- */
225
226        protected XmlPullParser loadXml(String source) throws UnsupportedEncodingException, XmlPullParserException, IOException {
227                return loadXml(new ByteArrayInputStream(source.getBytes("UTF-8")));
228        }
229
230        protected XmlPullParser loadXml(InputStream stream) throws XmlPullParserException, IOException {
231                BufferedInputStream input = new BufferedInputStream(stream);
232                XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null);
233                factory.setNamespaceAware(true);
234                factory.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, false);
235                XmlPullParser xpp = factory.newPullParser();
236                xpp.setInput(input, "UTF-8");
237                next(xpp);
238                nextNoWhitespace(xpp);
239
240                return xpp;
241        }
242
243        protected int next(XmlPullParser xpp) throws XmlPullParserException, IOException {
244                if (handleComments)
245                        return xpp.nextToken();
246                else
247                        return xpp.next();    
248        }
249
250        protected List<String> comments = new ArrayList<String>();
251
252        protected int nextNoWhitespace(XmlPullParser xpp) throws XmlPullParserException, IOException {
253                int eventType = xpp.getEventType();
254                while ((eventType == XmlPullParser.TEXT && xpp.isWhitespace()) || (eventType == XmlPullParser.COMMENT) 
255                                || (eventType == XmlPullParser.CDSECT) || (eventType == XmlPullParser.IGNORABLE_WHITESPACE)
256                                || (eventType == XmlPullParser.PROCESSING_INSTRUCTION) || (eventType == XmlPullParser.DOCDECL)) {
257                        if (eventType == XmlPullParser.COMMENT) {
258                                comments.add(xpp.getText());
259                        } else if (eventType == XmlPullParser.DOCDECL) {
260              throw new XmlPullParserException("DTD declarations are not allowed"); 
261      }  
262                        eventType = next(xpp);
263                }
264                return eventType;
265        }
266
267
268        protected void skipElementWithContent(XmlPullParser xpp) throws XmlPullParserException, IOException  {
269                // when this is called, we are pointing an element that may have content
270                while (xpp.getEventType() != XmlPullParser.END_TAG) {
271                        next(xpp);
272                        if (xpp.getEventType() == XmlPullParser.START_TAG) 
273                                skipElementWithContent(xpp);
274                }
275                next(xpp);
276        }
277
278        protected void skipEmptyElement(XmlPullParser xpp) throws XmlPullParserException, IOException {
279                while (xpp.getEventType() != XmlPullParser.END_TAG) 
280                        next(xpp);
281                next(xpp);
282        }
283
284        protected IXMLWriter xml;
285        protected boolean htmlPretty;
286
287
288
289        /* -- worker routines --------------------------------------------------- */
290
291        protected void parseTypeAttributes(XmlPullParser xpp, Type t) {
292                parseElementAttributes(xpp, t);
293        }
294
295        protected void parseElementAttributes(XmlPullParser xpp, Element e) {
296                if (xpp.getAttributeValue(null, "id") != null) {
297                        e.setId(xpp.getAttributeValue(null, "id"));
298                        idMap.put(e.getId(), e);
299                }
300                if (!comments.isEmpty()) {
301                        e.getFormatCommentsPre().addAll(comments);
302                        comments.clear();
303                }
304        }
305
306        protected void parseElementClose(Base e) {
307                if (!comments.isEmpty()) {
308                        e.getFormatCommentsPost().addAll(comments);
309                        comments.clear();
310                }
311        }
312
313        protected void parseBackboneAttributes(XmlPullParser xpp, Element e) {
314                parseElementAttributes(xpp, e);
315        }
316
317        private String pathForLocation(XmlPullParser xpp) {
318                return xpp.getPositionDescription();
319        }
320
321
322        protected void unknownContent(XmlPullParser xpp) throws FHIRFormatError, XmlPullParserException, IOException {
323                if (!isAllowUnknownContent())
324                        throw new FHIRFormatError("Unknown Content "+xpp.getName()+" @ "+pathForLocation(xpp));
325                // otherwise, read over whatever element this is 
326                int count = 1;
327                do {
328            xpp.next();
329                  if (xpp.getEventType() == XmlPullParser.END_TAG)
330                    count--;
331      if (xpp.getEventType() == XmlPullParser.START_TAG)
332        count++;                  
333                } while (count > 0);
334    xpp.next();
335        }
336
337        protected XhtmlNode parseXhtml(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError {
338                XhtmlParser prsr = new XhtmlParser();
339                try {
340                        return prsr.parseHtmlNode(xpp);
341                } catch (org.hl7.fhir.exceptions.FHIRFormatError e) {
342                        throw new FHIRFormatError(e.getMessage(), e);
343                }
344        }
345
346        private String parseString(XmlPullParser xpp) throws XmlPullParserException, FHIRFormatError, IOException {
347                StringBuilder res = new StringBuilder();
348                next(xpp);
349                while (xpp.getEventType() == XmlPullParser.TEXT || xpp.getEventType() == XmlPullParser.IGNORABLE_WHITESPACE || xpp.getEventType() == XmlPullParser.ENTITY_REF) {
350                        res.append(xpp.getText());
351                        next(xpp);
352                }
353                if (xpp.getEventType() != XmlPullParser.END_TAG)
354                        throw new FHIRFormatError("Bad String Structure - parsed "+res.toString()+" now found "+Integer.toString(xpp.getEventType()));
355                next(xpp);
356                return res.length() == 0 ? null : res.toString();
357        }
358
359        private int parseInt(XmlPullParser xpp) throws FHIRFormatError, XmlPullParserException, IOException {
360                int res = -1;
361                String textNode = parseString(xpp);
362                res = java.lang.Integer.parseInt(textNode);
363                return res;
364        }
365
366        protected DomainResource parseDomainResourceContained(XmlPullParser xpp)  throws IOException, FHIRFormatError, XmlPullParserException {
367                next(xpp);
368                int eventType = nextNoWhitespace(xpp);
369                if (eventType == XmlPullParser.START_TAG) { 
370                        DomainResource dr = (DomainResource) parseResource(xpp);
371                        nextNoWhitespace(xpp);
372                        next(xpp);
373                        return dr;
374                } else {
375                        unknownContent(xpp);
376                        return null;
377                }
378        } 
379        protected Resource parseResourceContained(XmlPullParser xpp) throws IOException, FHIRFormatError, XmlPullParserException  {
380                next(xpp);
381                int eventType = nextNoWhitespace(xpp);
382                if (eventType == XmlPullParser.START_TAG) { 
383                        Resource r = (Resource) parseResource(xpp);
384                        nextNoWhitespace(xpp);
385                        next(xpp);
386                        return r;
387                } else {
388                        unknownContent(xpp);
389                        return null;
390                }
391        }
392
393        public void compose(IXMLWriter writer, Resource resource, boolean htmlPretty)  throws IOException   {
394                this.htmlPretty = htmlPretty;
395                xml = writer;
396                xml.setDefaultNamespace(FHIR_NS);
397                composeResource(resource);
398        }
399
400        protected abstract void composeResource(Resource resource) throws IOException ;
401
402        protected void composeElementAttributes(Element element) throws IOException {
403                if (style != OutputStyle.CANONICAL)
404                        for (String comment : element.getFormatCommentsPre())
405                                xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
406                if (element.getId() != null) 
407                        xml.attribute("id", element.getId());
408        }
409
410        protected void composeElementClose(Base base) throws IOException {
411                if (style != OutputStyle.CANONICAL)
412                        for (String comment : base.getFormatCommentsPost())
413                                xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
414        }
415        protected void composeTypeAttributes(Type type) throws IOException {
416                composeElementAttributes(type);
417        }
418
419        protected void composeXhtml(String name, XhtmlNode html) throws IOException {
420                if (!Utilities.noString(xhtmlMessage)) {
421                        xml.enter(XhtmlComposer.XHTML_NS, name);
422                        xml.comment(xhtmlMessage, false);
423                        xml.exit(XhtmlComposer.XHTML_NS, name);
424                } else {
425                        XhtmlComposer comp = new XhtmlComposer(XhtmlComposer.XML, htmlPretty);
426                        // name is also found in the html and should the same
427                        // ? check that
428                        boolean oldPretty = xml.isPretty();
429                        xml.setPretty(htmlPretty);
430                        if (html.getNodeType() != NodeType.Text && html.getNsDecl() == null)
431                                xml.namespace(XhtmlComposer.XHTML_NS, null);
432                        comp.compose(xml, html);
433                        xml.setPretty(oldPretty);
434                }
435        }
436
437
438        abstract protected void composeString(String name, StringType value) throws IOException ;
439
440        protected void composeString(String name, IIdType value) throws IOException  {
441                composeString(name, new StringType(value.getValue()));
442        }    
443
444
445        protected void composeDomainResource(String name, DomainResource res) throws IOException  {
446                xml.enter(FHIR_NS, name);
447                composeResource(res.getResourceType().toString(), res);
448                xml.exit(FHIR_NS, name);
449        }
450
451        
452
453        protected abstract void composeResource(String name, Resource res) throws IOException ;
454
455}