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