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