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