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                }
326                // otherwise, read over whatever element this is 
327                int count = 1;
328                do {
329            xpp.next();
330                  if (xpp.getEventType() == XmlPullParser.END_TAG)
331                    count--;
332      if (xpp.getEventType() == XmlPullParser.START_TAG)
333        count++;                  
334                } while (count > 0);
335    xpp.next();
336        }
337
338        protected XhtmlNode parseXhtml(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError {
339                XhtmlParser prsr = new XhtmlParser();
340                try {
341                        return prsr.parseHtmlNode(xpp);
342                } catch (org.hl7.fhir.exceptions.FHIRFormatError e) {
343                        throw new FHIRFormatError(e.getMessage(), e);
344                }
345        }
346
347        private String parseString(XmlPullParser xpp) throws XmlPullParserException, FHIRFormatError, IOException {
348                StringBuilder res = new StringBuilder();
349                next(xpp);
350                while (xpp.getEventType() == XmlPullParser.TEXT || xpp.getEventType() == XmlPullParser.IGNORABLE_WHITESPACE || xpp.getEventType() == XmlPullParser.ENTITY_REF) {
351                        res.append(xpp.getText());
352                        next(xpp);
353                }
354                if (xpp.getEventType() != XmlPullParser.END_TAG)
355                        throw new FHIRFormatError("Bad String Structure - parsed "+res.toString()+" now found "+Integer.toString(xpp.getEventType()));
356                next(xpp);
357                return res.length() == 0 ? null : res.toString();
358        }
359
360        private int parseInt(XmlPullParser xpp) throws FHIRFormatError, XmlPullParserException, IOException {
361                int res = -1;
362                String textNode = parseString(xpp);
363                res = java.lang.Integer.parseInt(textNode);
364                return res;
365        }
366
367        protected DomainResource parseDomainResourceContained(XmlPullParser xpp)  throws IOException, FHIRFormatError, XmlPullParserException {
368                next(xpp);
369                int eventType = nextNoWhitespace(xpp);
370                if (eventType == XmlPullParser.START_TAG) { 
371                        DomainResource dr = (DomainResource) parseResource(xpp);
372                        nextNoWhitespace(xpp);
373                        next(xpp);
374                        return dr;
375                } else {
376                        unknownContent(xpp);
377                        return null;
378                }
379        } 
380        protected Resource parseResourceContained(XmlPullParser xpp) throws IOException, FHIRFormatError, XmlPullParserException  {
381                next(xpp);
382                int eventType = nextNoWhitespace(xpp);
383                if (eventType == XmlPullParser.START_TAG) { 
384                        Resource r = (Resource) parseResource(xpp);
385                        nextNoWhitespace(xpp);
386                        next(xpp);
387                        return r;
388                } else {
389                        unknownContent(xpp);
390                        return null;
391                }
392        }
393
394        public void compose(IXMLWriter writer, Resource resource, boolean htmlPretty)  throws IOException   {
395                this.htmlPretty = htmlPretty;
396                xml = writer;
397                xml.setDefaultNamespace(FHIR_NS);
398                composeResource(resource);
399        }
400
401        protected abstract void composeResource(Resource resource) throws IOException ;
402
403        protected void composeElementAttributes(Element element) throws IOException {
404                if (style != OutputStyle.CANONICAL)
405                        for (String comment : element.getFormatCommentsPre())
406                                xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
407                if (element.getId() != null) 
408                        xml.attribute("id", element.getId());
409        }
410
411        protected void composeElementClose(Base base) throws IOException {
412                if (style != OutputStyle.CANONICAL)
413                        for (String comment : base.getFormatCommentsPost())
414                                xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
415        }
416        protected void composeTypeAttributes(Type type) throws IOException {
417                composeElementAttributes(type);
418        }
419
420        protected void composeXhtml(String name, XhtmlNode html) throws IOException {
421                if (!Utilities.noString(xhtmlMessage)) {
422                        xml.enter(XhtmlComposer.XHTML_NS, name);
423                        xml.comment(xhtmlMessage, false);
424                        xml.exit(XhtmlComposer.XHTML_NS, name);
425                } else {
426                        XhtmlComposer comp = new XhtmlComposer(XhtmlComposer.XML, htmlPretty);
427                        // name is also found in the html and should the same
428                        // ? check that
429                        boolean oldPretty = xml.isPretty();
430                        xml.setPretty(htmlPretty);
431                        if (html.getNodeType() != NodeType.Text && html.getNsDecl() == null)
432                                xml.namespace(XhtmlComposer.XHTML_NS, null);
433                        comp.compose(xml, html);
434                        xml.setPretty(oldPretty);
435                }
436        }
437
438
439        abstract protected void composeString(String name, StringType value) throws IOException ;
440
441        protected void composeString(String name, IIdType value) throws IOException  {
442                composeString(name, new StringType(value.getValue()));
443        }    
444
445
446        protected void composeDomainResource(String name, DomainResource res) throws IOException  {
447                xml.enter(FHIR_NS, name);
448                composeResource(res.getResourceType().toString(), res);
449                xml.exit(FHIR_NS, name);
450        }
451
452        
453
454        protected abstract void composeResource(String name, Resource res) throws IOException ;
455
456}