001package org.hl7.fhir.dstu3.formats;
002
003/*-
004 * #%L
005 * org.hl7.fhir.dstu3
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.dstu3.model.Base;
062import org.hl7.fhir.dstu3.model.DomainResource;
063import org.hl7.fhir.dstu3.model.Element;
064import org.hl7.fhir.dstu3.model.Resource;
065import org.hl7.fhir.dstu3.model.StringType;
066import org.hl7.fhir.dstu3.model.Type;
067import org.hl7.fhir.exceptions.FHIRFormatError;
068import org.hl7.fhir.instance.model.api.IIdType;
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                factory.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, false);
201                XmlPullParser xpp = factory.newPullParser();
202                xpp.setInput(input, "UTF-8");
203                next(xpp);
204                nextNoWhitespace(xpp);
205
206                return xpp;
207        }
208
209        protected int next(XmlPullParser xpp) throws XmlPullParserException, IOException {
210                if (handleComments)
211                        return xpp.nextToken();
212                else
213                        return xpp.next();    
214        }
215
216        protected List<String> comments = new ArrayList<String>();
217
218        protected int nextNoWhitespace(XmlPullParser xpp) throws XmlPullParserException, IOException {
219                int eventType = xpp.getEventType();
220                while ((eventType == XmlPullParser.TEXT && xpp.isWhitespace()) || (eventType == XmlPullParser.COMMENT) 
221                                || (eventType == XmlPullParser.CDSECT) || (eventType == XmlPullParser.IGNORABLE_WHITESPACE)
222                                || (eventType == XmlPullParser.PROCESSING_INSTRUCTION) || (eventType == XmlPullParser.DOCDECL)) {
223                        if (eventType == XmlPullParser.COMMENT) {
224                                comments.add(xpp.getText());
225                        } else if (eventType == XmlPullParser.DOCDECL) {
226              throw new XmlPullParserException("DTD declarations are not allowed"); 
227      }  
228                        eventType = next(xpp);
229                }
230                return eventType;
231        }
232
233
234        protected void skipElementWithContent(XmlPullParser xpp) throws XmlPullParserException, IOException  {
235                // when this is called, we are pointing an element that may have content
236                while (xpp.getEventType() != XmlPullParser.END_TAG) {
237                        next(xpp);
238                        if (xpp.getEventType() == XmlPullParser.START_TAG) 
239                                skipElementWithContent(xpp);
240                }
241                next(xpp);
242        }
243
244        protected void skipEmptyElement(XmlPullParser xpp) throws XmlPullParserException, IOException {
245                while (xpp.getEventType() != XmlPullParser.END_TAG) 
246                        next(xpp);
247                next(xpp);
248        }
249
250        protected IXMLWriter xml;
251        protected boolean htmlPretty;
252
253
254
255        /* -- worker routines --------------------------------------------------- */
256
257        protected void parseTypeAttributes(XmlPullParser xpp, Type t) {
258                parseElementAttributes(xpp, t);
259        }
260
261        protected void parseElementAttributes(XmlPullParser xpp, Element e) {
262                if (xpp.getAttributeValue(null, "id") != null) {
263                        e.setId(xpp.getAttributeValue(null, "id"));
264                        idMap.put(e.getId(), e);
265                }
266                if (!comments.isEmpty()) {
267                        e.getFormatCommentsPre().addAll(comments);
268                        comments.clear();
269                }
270        }
271
272        protected void parseElementClose(Base e) {
273                if (!comments.isEmpty()) {
274                        e.getFormatCommentsPost().addAll(comments);
275                        comments.clear();
276                }
277        }
278
279        protected void parseBackboneAttributes(XmlPullParser xpp, Element e) {
280                parseElementAttributes(xpp, e);
281        }
282
283        private String pathForLocation(XmlPullParser xpp) {
284                return xpp.getPositionDescription();
285        }
286
287
288        protected void unknownContent(XmlPullParser xpp) throws FHIRFormatError {
289                if (!isAllowUnknownContent())
290                        throw new FHIRFormatError("Unknown Content "+xpp.getName()+" @ "+pathForLocation(xpp));
291        }
292
293        protected XhtmlNode parseXhtml(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError {
294                XhtmlParser prsr = new XhtmlParser();
295                try {
296                        return prsr.parseHtmlNode(xpp);
297                } catch (org.hl7.fhir.exceptions.FHIRFormatError e) {
298                        throw new FHIRFormatError(e.getMessage(), e);
299                }
300        }
301
302        private String parseString(XmlPullParser xpp) throws XmlPullParserException, FHIRFormatError, IOException {
303                StringBuilder res = new StringBuilder();
304                next(xpp);
305                while (xpp.getEventType() == XmlPullParser.TEXT || xpp.getEventType() == XmlPullParser.IGNORABLE_WHITESPACE || xpp.getEventType() == XmlPullParser.ENTITY_REF) {
306                        res.append(xpp.getText());
307                        next(xpp);
308                }
309                if (xpp.getEventType() != XmlPullParser.END_TAG)
310                        throw new FHIRFormatError("Bad String Structure - parsed "+res.toString()+" now found "+Integer.toString(xpp.getEventType()));
311                next(xpp);
312                return res.length() == 0 ? null : res.toString();
313        }
314
315        private int parseInt(XmlPullParser xpp) throws FHIRFormatError, XmlPullParserException, IOException {
316                int res = -1;
317                String textNode = parseString(xpp);
318                res = java.lang.Integer.parseInt(textNode);
319                return res;
320        }
321
322        protected DomainResource parseDomainResourceContained(XmlPullParser xpp)  throws IOException, FHIRFormatError, XmlPullParserException {
323                next(xpp);
324                int eventType = nextNoWhitespace(xpp);
325                if (eventType == XmlPullParser.START_TAG) { 
326                        DomainResource dr = (DomainResource) parseResource(xpp);
327                        nextNoWhitespace(xpp);
328                        next(xpp);
329                        return dr;
330                } else {
331                        unknownContent(xpp);
332                        return null;
333                }
334        } 
335        protected Resource parseResourceContained(XmlPullParser xpp) throws IOException, FHIRFormatError, XmlPullParserException  {
336                next(xpp);
337                int eventType = nextNoWhitespace(xpp);
338                if (eventType == XmlPullParser.START_TAG) { 
339                        Resource r = (Resource) parseResource(xpp);
340                        nextNoWhitespace(xpp);
341                        next(xpp);
342                        return r;
343                } else {
344                        unknownContent(xpp);
345                        return null;
346                }
347        }
348
349        public void compose(IXMLWriter writer, Resource resource, boolean htmlPretty)  throws IOException   {
350                this.htmlPretty = htmlPretty;
351                xml = writer;
352                xml.setDefaultNamespace(FHIR_NS);
353                composeResource(resource);
354        }
355
356        protected abstract void composeResource(Resource resource) throws IOException ;
357
358        protected void composeElementAttributes(Element element) throws IOException {
359                if (style != OutputStyle.CANONICAL)
360                        for (String comment : element.getFormatCommentsPre())
361                                xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
362                if (element.getId() != null) 
363                        xml.attribute("id", element.getId());
364        }
365
366        protected void composeElementClose(Base base) throws IOException {
367                if (style != OutputStyle.CANONICAL)
368                        for (String comment : base.getFormatCommentsPost())
369                                xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
370        }
371        protected void composeTypeAttributes(Type type) throws IOException {
372                composeElementAttributes(type);
373        }
374
375        protected void composeXhtml(String name, XhtmlNode html) throws IOException {
376                if (!Utilities.noString(xhtmlMessage)) {
377                        xml.enter(XhtmlComposer.XHTML_NS, name);
378                        xml.comment(xhtmlMessage, false);
379                        xml.exit(XhtmlComposer.XHTML_NS, name);
380                } else {
381                        XhtmlComposer comp = new XhtmlComposer(XhtmlComposer.XML, htmlPretty);
382                        // name is also found in the html and should the same
383                        // ? check that
384                        boolean oldPretty = xml.isPretty();
385                        xml.setPretty(htmlPretty);
386                        if (html.getNodeType() != NodeType.Text && html.getNsDecl() == null)
387                                xml.namespace(XhtmlComposer.XHTML_NS, null);
388                        comp.compose(xml, html);
389                        xml.setPretty(oldPretty);
390                }
391        }
392
393
394        abstract protected void composeString(String name, StringType value) throws IOException ;
395
396        protected void composeString(String name, IIdType value) throws IOException  {
397                composeString(name, new StringType(value.getValue()));
398        }    
399
400
401        protected void composeDomainResource(String name, DomainResource res) throws IOException  {
402                xml.enter(FHIR_NS, name);
403                composeResource(res.getResourceType().toString(), res);
404                xml.exit(FHIR_NS, name);
405        }
406
407        
408
409        protected abstract void composeResource(String name, Resource res) throws IOException ;
410
411}