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