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