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