001package org.hl7.fhir.dstu2.formats;
002
003/*-
004 * #%L
005 * org.hl7.fhir.dstu2
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.dstu2.model.Base;
062import org.hl7.fhir.dstu2.model.DomainResource;
063import org.hl7.fhir.dstu2.model.Element;
064import org.hl7.fhir.dstu2.model.Resource;
065import org.hl7.fhir.dstu2.model.StringType;
066import org.hl7.fhir.dstu2.model.Type;
067import org.hl7.fhir.instance.model.api.IIdType;
068import org.hl7.fhir.exceptions.FHIRFormatError;
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 void composeType(String prefix, Type type) throws IOException ;
098
099        /* -- entry points --------------------------------------------------- */
100
101        /**
102         * Parse content that is known to be a resource
103         * @ 
104         */
105        @Override
106        public Resource parse(InputStream input) throws IOException, FHIRFormatError {
107                try {
108                        XmlPullParser xpp = loadXml(input);
109                        return parse(xpp);
110                } catch (XmlPullParserException e) {
111                        throw new FHIRFormatError(e.getMessage(), e);
112                }
113        }
114
115        /**
116         * parse xml that is known to be a resource, and that is already being read by an XML Pull Parser
117         * This is if a resource is in a bigger piece of XML.   
118         * @ 
119         */
120        public Resource parse(XmlPullParser xpp)  throws IOException, FHIRFormatError, XmlPullParserException {
121                if (xpp.getNamespace() == null)
122                        throw new FHIRFormatError("This does not appear to be a FHIR resource (no namespace '"+xpp.getNamespace()+"') (@ /) "+Integer.toString(xpp.getEventType()));
123                if (!xpp.getNamespace().equals(FHIR_NS))
124                        throw new FHIRFormatError("This does not appear to be a FHIR resource (wrong namespace '"+xpp.getNamespace()+"') (@ /)");
125                return parseResource(xpp);
126        }
127
128        @Override
129        public Type parseType(InputStream input, String knownType) throws IOException, FHIRFormatError  {
130                try {
131                        XmlPullParser xml = loadXml(input);
132                        return parseType(xml, knownType);
133                } catch (XmlPullParserException e) {
134                        throw new FHIRFormatError(e.getMessage(), e);
135                }
136        }
137
138
139        /**
140         * 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)
141         * @ 
142         */
143        @Override
144        public void compose(OutputStream stream, Resource resource)  throws IOException {
145                XMLWriter writer = new XMLWriter(stream, "UTF-8");
146                writer.setPretty(style == OutputStyle.PRETTY);
147                writer.start();
148                compose(writer, resource, writer.isPretty());
149                writer.end();
150        }
151
152        /**
153         * 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)
154         * @ 
155         */
156        public void compose(OutputStream stream, Resource resource, boolean htmlPretty)  throws IOException {
157                XMLWriter writer = new XMLWriter(stream, "UTF-8");
158                writer.setPretty(style == OutputStyle.PRETTY);
159                writer.start();
160                compose(writer, resource, htmlPretty);
161                writer.end();
162        }
163
164
165        /**
166         * Compose a type to a stream (used in the spec, for example, but not normally in production)
167         * @ 
168         */
169        public void compose(OutputStream stream, String rootName, Type type)  throws IOException {
170                xml = new XMLWriter(stream, "UTF-8");
171                xml.setPretty(style == OutputStyle.PRETTY);
172                xml.start();
173                xml.setDefaultNamespace(FHIR_NS);
174                composeType(Utilities.noString(rootName) ? "value" : rootName, type);
175                xml.end();
176        }
177
178        @Override
179        public void compose(OutputStream stream, Type type, String rootName)  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
189
190        /* -- xml routines --------------------------------------------------- */
191
192        protected XmlPullParser loadXml(String source) throws UnsupportedEncodingException, XmlPullParserException, IOException {
193                return loadXml(new ByteArrayInputStream(source.getBytes("UTF-8")));
194        }
195
196        protected XmlPullParser loadXml(InputStream stream) throws XmlPullParserException, IOException {
197                BufferedInputStream input = new BufferedInputStream(stream);
198                XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null);
199                factory.setNamespaceAware(true);
200                XmlPullParser xpp = factory.newPullParser();
201                xpp.setInput(input, "UTF-8");
202                next(xpp);
203                nextNoWhitespace(xpp);
204
205                return xpp;
206        }
207
208        protected int next(XmlPullParser xpp) throws XmlPullParserException, IOException {
209                if (handleComments)
210                        return xpp.nextToken();
211                else
212                        return xpp.next();    
213        }
214
215        protected List<String> comments = new ArrayList<String>();
216
217        protected int nextNoWhitespace(XmlPullParser xpp) throws XmlPullParserException, IOException {
218                int eventType = xpp.getEventType();
219                while ((eventType == XmlPullParser.TEXT && xpp.isWhitespace()) || (eventType == XmlPullParser.COMMENT) 
220                                || (eventType == XmlPullParser.CDSECT) || (eventType == XmlPullParser.IGNORABLE_WHITESPACE)
221                                || (eventType == XmlPullParser.PROCESSING_INSTRUCTION) || (eventType == XmlPullParser.DOCDECL)) {
222                        if (eventType == XmlPullParser.COMMENT) {
223                                comments.add(xpp.getText());
224                        }
225                        eventType = next(xpp);
226                }
227                return eventType;
228        }
229
230
231        protected void skipElementWithContent(XmlPullParser xpp) throws XmlPullParserException, IOException  {
232                // when this is called, we are pointing an element that may have content
233                while (xpp.getEventType() != XmlPullParser.END_TAG) {
234                        next(xpp);
235                        if (xpp.getEventType() == XmlPullParser.START_TAG) 
236                                skipElementWithContent(xpp);
237                }
238                next(xpp);
239        }
240
241        protected void skipEmptyElement(XmlPullParser xpp) throws XmlPullParserException, IOException {
242                while (xpp.getEventType() != XmlPullParser.END_TAG) 
243                        next(xpp);
244                next(xpp);
245        }
246
247        protected IXMLWriter xml;
248        protected boolean htmlPretty;
249
250
251
252        /* -- worker routines --------------------------------------------------- */
253
254        protected void parseTypeAttributes(XmlPullParser xpp, Type t) {
255                parseElementAttributes(xpp, t);
256        }
257
258        protected void parseElementAttributes(XmlPullParser xpp, Element e) {
259                if (xpp.getAttributeValue(null, "id") != null) {
260                        e.setId(xpp.getAttributeValue(null, "id"));
261                        idMap.put(e.getId(), e);
262                }
263                if (!comments.isEmpty()) {
264                        e.getFormatCommentsPre().addAll(comments);
265                        comments.clear();
266                }
267        }
268
269        protected void parseElementClose(Base e) {
270                if (!comments.isEmpty()) {
271                        e.getFormatCommentsPost().addAll(comments);
272                        comments.clear();
273                }
274        }
275
276        protected void parseBackboneAttributes(XmlPullParser xpp, Element e) {
277                parseElementAttributes(xpp, e);
278        }
279
280        private String pathForLocation(XmlPullParser xpp) {
281                return xpp.getPositionDescription();
282        }
283
284
285        protected void unknownContent(XmlPullParser xpp) throws FHIRFormatError {
286                if (!isAllowUnknownContent())
287                        throw new FHIRFormatError("Unknown Content "+xpp.getName()+" @ "+pathForLocation(xpp));
288        }
289
290        protected XhtmlNode parseXhtml(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError {
291                XhtmlParser prsr = new XhtmlParser();
292                return prsr.parseHtmlNode(xpp);
293        }
294
295        private String parseString(XmlPullParser xpp) throws XmlPullParserException, FHIRFormatError, IOException {
296                StringBuilder res = new StringBuilder();
297                next(xpp);
298                while (xpp.getEventType() == XmlPullParser.TEXT || xpp.getEventType() == XmlPullParser.IGNORABLE_WHITESPACE || xpp.getEventType() == XmlPullParser.ENTITY_REF) {
299                        res.append(xpp.getText());
300                        next(xpp);
301                }
302                if (xpp.getEventType() != XmlPullParser.END_TAG)
303                        throw new FHIRFormatError("Bad String Structure - parsed "+res.toString()+" now found "+Integer.toString(xpp.getEventType()));
304                next(xpp);
305                return res.length() == 0 ? null : res.toString();
306        }
307
308        private int parseInt(XmlPullParser xpp) throws FHIRFormatError, XmlPullParserException, IOException {
309                int res = -1;
310                String textNode = parseString(xpp);
311                res = java.lang.Integer.parseInt(textNode);
312                return res;
313        }
314
315        protected DomainResource parseDomainResourceContained(XmlPullParser xpp)  throws IOException, FHIRFormatError, XmlPullParserException {
316                next(xpp);
317                int eventType = nextNoWhitespace(xpp);
318                if (eventType == XmlPullParser.START_TAG) { 
319                        DomainResource dr = (DomainResource) parseResource(xpp);
320                        nextNoWhitespace(xpp);
321                        next(xpp);
322                        return dr;
323                } else {
324                        unknownContent(xpp);
325                        return null;
326                }
327        } 
328        protected Resource parseResourceContained(XmlPullParser xpp) throws IOException, FHIRFormatError, XmlPullParserException  {
329                next(xpp);
330                int eventType = nextNoWhitespace(xpp);
331                if (eventType == XmlPullParser.START_TAG) { 
332                        Resource r = (Resource) parseResource(xpp);
333                        nextNoWhitespace(xpp);
334                        next(xpp);
335                        return r;
336                } else {
337                        unknownContent(xpp);
338                        return null;
339                }
340        }
341
342        public void compose(IXMLWriter writer, Resource resource, boolean htmlPretty)  throws IOException   {
343                this.htmlPretty = htmlPretty;
344                xml = writer;
345                xml.setDefaultNamespace(FHIR_NS);
346                composeResource(resource);
347        }
348
349        protected abstract void composeResource(Resource resource) throws IOException ;
350
351        protected void composeElementAttributes(Element element) throws IOException {
352                if (style != OutputStyle.CANONICAL)
353                        for (String comment : element.getFormatCommentsPre())
354                                xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
355                if (element.getId() != null) 
356                        xml.attribute("id", element.getId());
357        }
358
359        protected void composeElementClose(Base base) throws IOException {
360                if (style != OutputStyle.CANONICAL)
361                        for (String comment : base.getFormatCommentsPost())
362                                xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
363        }
364        protected void composeTypeAttributes(Type type) throws IOException {
365                composeElementAttributes(type);
366        }
367
368        protected void composeXhtml(String name, XhtmlNode html) throws IOException {
369                if (!Utilities.noString(xhtmlMessage)) {
370                        xml.enter(XhtmlComposer.XHTML_NS, name);
371                        xml.comment(xhtmlMessage, false);
372                        xml.exit(XhtmlComposer.XHTML_NS, name);
373                } else {
374                        XhtmlComposer comp = new XhtmlComposer(true, htmlPretty);
375                        // name is also found in the html and should the same
376                        // ? check that
377                        boolean oldPretty = xml.isPretty();
378                        if (html.getNodeType() != NodeType.Text)
379                                xml.namespace(XhtmlComposer.XHTML_NS, null);
380                        comp.compose(xml, html);
381                        xml.setPretty(oldPretty);
382                }
383        }
384
385
386        abstract protected void composeString(String name, StringType value) throws IOException ;
387
388        protected void composeString(String name, IIdType value) throws IOException  {
389                composeString(name, new StringType(value.getValue()));
390        }    
391
392
393        protected void composeDomainResource(String name, DomainResource res) throws IOException  {
394                xml.enter(FHIR_NS, name);
395                composeResource(res.getResourceType().toString(), res);
396                xml.exit(FHIR_NS, name);
397        }
398
399        protected abstract void composeResource(String name, Resource res) throws IOException ;
400
401}