001package org.hl7.fhir.r5.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.r5.model.Base;
074import org.hl7.fhir.r5.model.DataType;
075import org.hl7.fhir.r5.model.DomainResource;
076import org.hl7.fhir.r5.model.Element;
077import org.hl7.fhir.r5.model.Resource;
078import org.hl7.fhir.r5.model.StringType;
079import org.hl7.fhir.r5.test.utils.ClassesLoadedFlags;
080import org.hl7.fhir.utilities.Utilities;
081import org.hl7.fhir.utilities.xhtml.NodeType;
082import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
083import org.hl7.fhir.utilities.xhtml.XhtmlNode;
084import org.hl7.fhir.utilities.xhtml.XhtmlParser;
085import org.hl7.fhir.utilities.xml.IXMLWriter;
086import org.hl7.fhir.utilities.xml.XMLWriter;
087import org.slf4j.LoggerFactory;
088import org.xmlpull.v1.XmlPullParser;
089import org.xmlpull.v1.XmlPullParserException;
090import org.xmlpull.v1.XmlPullParserFactory;
091
092/**
093 * General parser for XML content. You instantiate an XmlParser of these, but you 
094 * actually use parse or parseGeneral defined on this class
095 * 
096 * The two classes are separated to keep generated and manually maintained code apart.
097 */
098public abstract class XmlParserBase extends ParserBase implements IParser {
099
100  static {
101    LoggerFactory.getLogger("org.hl7.fhir.r5.formats.XmlParserBase").debug("XML Parser is being loaded");
102    ClassesLoadedFlags.ourXmlParserBaseLoaded = true;
103  }
104
105        @Override
106        public ParserType getType() {
107                return ParserType.XML;
108        }
109        
110        
111        // -- in descendent generated code --------------------------------------
112
113  abstract protected Resource parseResource(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError ;
114  abstract protected DataType parseType(XmlPullParser xml, String type) throws XmlPullParserException, IOException, FHIRFormatError ;
115  abstract protected DataType parseAnyType(XmlPullParser xml, String type) throws XmlPullParserException, IOException, FHIRFormatError ;
116        abstract protected void composeType(String prefix, DataType type) throws IOException ;
117
118        /* -- entry points --------------------------------------------------- */
119
120        /**
121         * Parse content that is known to be a resource
122         * @ 
123         */
124        @Override
125        public Resource parse(InputStream input) throws IOException, FHIRFormatError {
126                try {
127                        XmlPullParser xpp = loadXml(input);
128                        return parse(xpp);
129                } catch (XmlPullParserException e) {
130                        throw new FHIRFormatError(e.getMessage(), e);
131                }
132        }
133
134        /**
135         * parse xml that is known to be a resource, and that is already being read by an XML Pull Parser
136         * This is if a resource is in a bigger piece of XML.   
137         * @ 
138         */
139        public Resource parse(XmlPullParser xpp)  throws IOException, FHIRFormatError, XmlPullParserException {
140                if (xpp.getNamespace() == null)
141                        throw new FHIRFormatError("This does not appear to be a FHIR resource (no namespace '"+xpp.getNamespace()+"') (@ /) "+Integer.toString(xpp.getEventType()));
142                if (!xpp.getNamespace().equals(FHIR_NS))
143                        throw new FHIRFormatError("This does not appear to be a FHIR resource (wrong namespace '"+xpp.getNamespace()+"') (@ /)");
144                return parseResource(xpp);
145        }
146
147        @Override
148        public DataType parseType(InputStream input, String knownType) throws IOException, FHIRFormatError  {
149                try {
150                        XmlPullParser xml = loadXml(input);
151                        return parseType(xml, knownType);
152                } catch (XmlPullParserException e) {
153                        throw new FHIRFormatError(e.getMessage(), e);
154                }
155        }
156
157  @Override
158  public DataType parseAnyType(InputStream input, String knownType) throws IOException, FHIRFormatError  {
159    try {
160      XmlPullParser xml = loadXml(input);
161      return parseAnyType(xml, knownType);
162    } catch (XmlPullParserException e) {
163      throw new FHIRFormatError(e.getMessage(), e);
164    }
165  }
166
167        /**
168         * 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)
169         * @ 
170         */
171        @Override
172        public void compose(OutputStream stream, Resource resource)  throws IOException {
173                XMLWriter writer = new XMLWriter(stream, "UTF-8");
174                writer.setPretty(style == OutputStyle.PRETTY);
175                writer.start();
176                compose(writer, resource, writer.isPretty());
177                writer.end();
178        }
179
180        /**
181         * 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)
182         * @ 
183         */
184        public void compose(OutputStream stream, Resource resource, boolean htmlPretty)  throws IOException {
185                XMLWriter writer = new XMLWriter(stream, "UTF-8");
186                writer.setPretty(style == OutputStyle.PRETTY);
187                writer.start();
188                compose(writer, resource, htmlPretty);
189                writer.end();
190        }
191
192        /**
193         * Compose a type to a stream (used in the spec, for example, but not normally in production)
194         * @ 
195         */
196        public void compose(OutputStream stream, String rootName, DataType type)  throws IOException {
197                xml = new XMLWriter(stream, "UTF-8");
198                xml.setPretty(style == OutputStyle.PRETTY);
199                xml.start();
200                xml.setDefaultNamespace(FHIR_NS);
201                composeType(Utilities.noString(rootName) ? "value" : rootName, type);
202                xml.end();
203        }
204
205        @Override
206        public void compose(OutputStream stream, DataType type, String rootName)  throws IOException {
207                xml = new XMLWriter(stream, "UTF-8");
208                xml.setPretty(style == OutputStyle.PRETTY);
209                xml.start();
210                xml.setDefaultNamespace(FHIR_NS);
211                composeType(Utilities.noString(rootName) ? "value" : rootName, type);
212                xml.end();
213        }
214
215
216  protected boolean composeCustomResource(Resource resource) throws IOException {
217    if (customResourceHandlers.containsKey(resource.fhirType())) {
218      customResourceHandlers.get(resource.fhirType()).composerXml(xml).composeResource(resource);
219      return true;
220    } else {
221      return false;
222    }
223  }
224
225  protected Resource parseCustomResource(XmlPullParser xpp) throws FHIRFormatError, IOException, XmlPullParserException {
226    if (customResourceHandlers.containsKey(xpp.getName())) {
227    return customResourceHandlers.get(xpp.getName()).parserXml(allowUnknownContent).parse(xpp);
228  } else {
229    return null;
230  }
231  }
232
233  
234        /* -- xml routines --------------------------------------------------- */
235
236        protected XmlPullParser loadXml(String source) throws UnsupportedEncodingException, XmlPullParserException, IOException {
237                return loadXml(new ByteArrayInputStream(source.getBytes("UTF-8")));
238        }
239
240        protected XmlPullParser loadXml(InputStream stream) throws XmlPullParserException, IOException {
241                BufferedInputStream input = new BufferedInputStream(stream);
242                XmlPullParserFactory factory = XmlPullParserFactory.newInstance(System.getProperty(XmlPullParserFactory.PROPERTY_NAME), null);
243                factory.setNamespaceAware(true);
244                factory.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, false);
245                XmlPullParser xpp = factory.newPullParser();
246                xpp.setInput(input, "UTF-8");
247                next(xpp);
248                nextNoWhitespace(xpp);
249
250                return xpp;
251        }
252
253        protected int next(XmlPullParser xpp) throws XmlPullParserException, IOException {
254                if (handleComments)
255                        return xpp.nextToken();
256                else
257                        return xpp.next();    
258        }
259
260        protected List<String> comments = new ArrayList<String>();
261
262        protected int nextNoWhitespace(XmlPullParser xpp) throws XmlPullParserException, IOException {
263                int eventType = xpp.getEventType();
264                while ((eventType == XmlPullParser.TEXT && xpp.isWhitespace()) || (eventType == XmlPullParser.COMMENT) 
265                                || (eventType == XmlPullParser.CDSECT) || (eventType == XmlPullParser.IGNORABLE_WHITESPACE)
266                                || (eventType == XmlPullParser.PROCESSING_INSTRUCTION) || (eventType == XmlPullParser.DOCDECL)) {
267                        if (eventType == XmlPullParser.COMMENT) {
268                                comments.add(xpp.getText());
269                        } else if (eventType == XmlPullParser.DOCDECL) {
270              throw new XmlPullParserException("DTD declarations are not allowed"); 
271      }  
272                        eventType = next(xpp);
273                }
274                return eventType;
275        }
276
277
278        protected void skipElementWithContent(XmlPullParser xpp) throws XmlPullParserException, IOException  {
279                // when this is called, we are pointing an element that may have content
280                while (xpp.getEventType() != XmlPullParser.END_TAG) {
281                        next(xpp);
282                        if (xpp.getEventType() == XmlPullParser.START_TAG) 
283                                skipElementWithContent(xpp);
284                }
285                next(xpp);
286        }
287
288        protected void skipEmptyElement(XmlPullParser xpp) throws XmlPullParserException, IOException {
289                while (xpp.getEventType() != XmlPullParser.END_TAG) 
290                        next(xpp);
291                next(xpp);
292        }
293
294        protected IXMLWriter xml;
295        protected boolean htmlPretty;
296  private String schemaPath;
297  
298  public String getSchemaPath() {
299    return schemaPath;
300  }
301  public void setSchemaPath(String schemaPath) {
302    this.schemaPath = schemaPath;
303  }
304
305
306
307        /* -- worker routines --------------------------------------------------- */
308
309        protected void parseTypeAttributes(XmlPullParser xpp, DataType t) {
310                parseElementAttributes(xpp, t);
311        }
312
313        protected void parseElementAttributes(XmlPullParser xpp, Element e) {
314                if (xpp.getAttributeValue(null, "id") != null) {
315                        e.setId(xpp.getAttributeValue(null, "id"));
316                        idMap.put(e.getId(), e);
317                }
318                if (!comments.isEmpty()) {
319                        e.getFormatCommentsPre().addAll(comments);
320                        comments.clear();
321                }
322        }
323
324        protected void parseElementClose(Base e) {
325                if (!comments.isEmpty()) {
326                        e.getFormatCommentsPost().addAll(comments);
327                        comments.clear();
328                }
329        }
330
331        protected void parseBackboneAttributes(XmlPullParser xpp, Element e) {
332                parseElementAttributes(xpp, e);
333        }
334
335  protected void parseResourceAttributes(XmlPullParser xpp, Resource r) {
336  }
337
338        private String pathForLocation(XmlPullParser xpp) {
339                return xpp.getPositionDescription();
340        }
341
342
343        protected void unknownContent(XmlPullParser xpp) throws FHIRFormatError, XmlPullParserException, IOException {
344                if (!isAllowUnknownContent())
345                        throw new FHIRFormatError("Unknown Content "+xpp.getName()+" @ "+pathForLocation(xpp));
346                // otherwise, read over whatever element this is 
347                int count = 1;
348                do {
349            xpp.next();
350                  if (xpp.getEventType() == XmlPullParser.END_TAG)
351                    count--;
352      if (xpp.getEventType() == XmlPullParser.START_TAG)
353        count++;                  
354                } while (count > 0);
355    xpp.next();
356        }
357
358        protected XhtmlNode parseXhtml(XmlPullParser xpp) throws XmlPullParserException, IOException, FHIRFormatError {
359                XhtmlParser prsr = new XhtmlParser();
360                try {
361                        return prsr.parseHtmlNode(xpp);
362                } catch (org.hl7.fhir.exceptions.FHIRFormatError e) {
363                        throw new FHIRFormatError(e.getMessage(), e);
364                }
365        }
366
367        private String parseString(XmlPullParser xpp) throws XmlPullParserException, FHIRFormatError, IOException {
368                StringBuilder res = new StringBuilder();
369                next(xpp);
370                while (xpp.getEventType() == XmlPullParser.TEXT || xpp.getEventType() == XmlPullParser.IGNORABLE_WHITESPACE || xpp.getEventType() == XmlPullParser.ENTITY_REF) {
371                        res.append(xpp.getText());
372                        next(xpp);
373                }
374                if (xpp.getEventType() != XmlPullParser.END_TAG)
375                        throw new FHIRFormatError("Bad String Structure - parsed "+res.toString()+" now found "+Integer.toString(xpp.getEventType()));
376                next(xpp);
377                return res.length() == 0 ? null : res.toString();
378        }
379
380        private int parseInt(XmlPullParser xpp) throws FHIRFormatError, XmlPullParserException, IOException {
381                int res = -1;
382                String textNode = parseString(xpp);
383                res = java.lang.Integer.parseInt(textNode);
384                return res;
385        }
386
387        protected DomainResource parseDomainResourceContained(XmlPullParser xpp)  throws IOException, FHIRFormatError, XmlPullParserException {
388                next(xpp);
389                int eventType = nextNoWhitespace(xpp);
390                if (eventType == XmlPullParser.START_TAG) { 
391                        DomainResource dr = (DomainResource) parseResource(xpp);
392                        nextNoWhitespace(xpp);
393                        next(xpp);
394                        return dr;
395                } else {
396                        unknownContent(xpp);
397                        return null;
398                }
399        } 
400        protected Resource parseResourceContained(XmlPullParser xpp) throws IOException, FHIRFormatError, XmlPullParserException  {
401                next(xpp);
402                int eventType = nextNoWhitespace(xpp);
403                if (eventType == XmlPullParser.START_TAG) { 
404                        Resource r = (Resource) parseResource(xpp);
405                        nextNoWhitespace(xpp);
406                        next(xpp);
407                        return r;
408                } else {
409                        unknownContent(xpp);
410                        return null;
411                }
412        }
413
414        public void compose(IXMLWriter writer, Resource resource, boolean htmlPretty)  throws IOException   {
415                this.htmlPretty = htmlPretty;
416                xml = writer;
417                xml.setDefaultNamespace(FHIR_NS);
418                if (schemaPath != null) {
419                  xml.setSchemaLocation(FHIR_NS, Utilities.pathURL(schemaPath, resource.fhirType()+".xsd"));
420                }
421                composeResource(resource);
422        }
423
424        protected abstract void composeResource(Resource resource) throws IOException ;
425
426        protected void composeElementAttributes(Element element) throws IOException {
427                if (style != OutputStyle.CANONICAL)
428                        for (String comment : element.getFormatCommentsPre())
429                                xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
430                if (element.getId() != null) 
431                        xml.attribute("id", element.getId());
432        }
433
434        protected void composeElementClose(Base base) throws IOException {
435                if (style != OutputStyle.CANONICAL)
436                        for (String comment : base.getFormatCommentsPost())
437                                xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
438        }
439  protected void composeResourceAttributes(Resource element) throws IOException {
440    if (style != OutputStyle.CANONICAL)
441      for (String comment : element.getFormatCommentsPre())
442        xml.comment(comment, getOutputStyle() == OutputStyle.PRETTY);
443  }
444
445        protected void composeTypeAttributes(DataType type) throws IOException {
446                composeElementAttributes(type);
447        }
448
449        protected void composeXhtml(String name, XhtmlNode html) throws IOException {
450                if (!Utilities.noString(xhtmlMessage)) {
451                        xml.enter(XhtmlComposer.XHTML_NS, name);
452                        xml.comment(xhtmlMessage, false);
453                        xml.exit(XhtmlComposer.XHTML_NS, name);
454                } else {
455                        XhtmlComposer comp = new XhtmlComposer(XhtmlComposer.XML, htmlPretty);
456                        // name is also found in the html and should the same
457                        // ? check that
458                        boolean oldPretty = xml.isPretty();
459                        xml.setPretty(htmlPretty);
460                        if (html.getNodeType() != NodeType.Text && html.getNsDecl() == null)
461                                xml.namespace(XhtmlComposer.XHTML_NS, null);
462                        comp.compose(xml, html);
463                        xml.setPretty(oldPretty);
464                }
465        }
466
467  protected void composeBaseElements(Base element) throws IOException {
468    // nothing
469  }
470
471        abstract protected void composeString(String name, StringType value) throws IOException ;
472
473        protected void composeString(String name, IIdType value) throws IOException  {
474                composeString(name, new StringType(value.getValue()));
475        }    
476
477
478        protected void composeDomainResource(String name, DomainResource res) throws IOException  {
479                xml.enter(FHIR_NS, name);
480                composeResource(res.getResourceType().toString(), res);
481                xml.exit(FHIR_NS, name);
482        }
483
484        
485
486        protected abstract void composeResource(String name, Resource res) throws IOException ;
487
488}