001package ca.uhn.fhir.parser; 002 003/*- 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2019 University Health Network 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 023import ca.uhn.fhir.context.*; 024import ca.uhn.fhir.model.api.IResource; 025import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions; 026import ca.uhn.fhir.narrative.INarrativeGenerator; 027import ca.uhn.fhir.rest.api.EncodingEnum; 028import ca.uhn.fhir.util.ElementUtil; 029import ca.uhn.fhir.util.rdf.RDFUtil; 030import org.apache.commons.lang3.StringUtils; 031import org.apache.jena.riot.Lang; 032import org.apache.jena.riot.system.StreamRDF; 033import org.hl7.fhir.instance.model.api.*; 034 035import java.io.*; 036import java.util.ArrayList; 037import java.util.List; 038 039import static org.apache.commons.lang3.StringUtils.isNotBlank; 040 041/** 042 * This class is the FHIR RDF parser/encoder. Users should not interact with this class directly, but should use 043 * {@link FhirContext#newRDFParser()} to get an instance. 044 */ 045public class RDFParser extends BaseParser { 046 047 private static final String FHIR_NS = "http://hl7.org/fhir"; 048 private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(RDFParser.class); 049 050 private FhirContext context; 051 private Lang lang; 052 053 /** 054 * Do not use this constructor, the recommended way to obtain a new instance of the RDF parser is to invoke 055 * {@link FhirContext#newRDFParser()}. 056 * 057 * @param parserErrorHandler the Parser Error Handler 058 */ 059 public RDFParser(final FhirContext context, final IParserErrorHandler parserErrorHandler, final Lang lang) { 060 super(context, parserErrorHandler); 061 this.context = context; 062 this.lang = lang; 063 } 064 065 @Override 066 protected void doEncodeResourceToWriter(final IBaseResource resource, 067 final Writer writer, 068 final EncodeContext encodeContext) { 069 StreamRDF eventWriter = RDFUtil.createRDFWriter(writer, this.lang); 070 eventWriter.base(FHIR_NS); 071 encodeResourceToRDFStreamWriter(resource, eventWriter, encodeContext); 072 } 073 074 @Override 075 protected <T extends IBaseResource> T doParseResource(final Class<T> resourceType, 076 final Reader reader) throws DataFormatException { 077 078 StreamRDF streamReader = RDFUtil.createRDFReader(reader, this.lang); 079 streamReader.base(FHIR_NS); 080 return parseResource(resourceType, streamReader); 081 } 082 083 @Override 084 public EncodingEnum getEncoding() { 085 return EncodingEnum.RDF; 086 } 087 088 @Override 089 public IParser setPrettyPrint(final boolean prettyPrint) { 090 return this; 091 } 092 093 private void encodeResourceToRDFStreamWriter(final IBaseResource resource, 094 final StreamRDF streamWriter, 095 final boolean containedResource, 096 final IIdType resourceId, 097 final EncodeContext encodeContext) { 098 RuntimeResourceDefinition resDef = this.context.getResourceDefinition(resource); 099 if (resDef == null) { 100 throw new ConfigurationException("Unknown resource type: " + resource.getClass()); 101 } 102 103 if (!containedResource) { 104 super.containResourcesForEncoding(resource); 105 } 106 107 if (resource instanceof IAnyResource) { 108 // HL7.org Structures 109 if (resourceId != null) { 110 writeCommentsPre(streamWriter, resourceId); 111 streamWriter.start(); 112 streamWriter.triple(RDFUtil.triple("<value> " + resourceId.getIdPart() + " </value>")); 113 streamWriter.finish(); 114 writeCommentsPost(streamWriter, resourceId); 115 } 116 117 encodeCompositeElementToStreamWriter(resource, resource, streamWriter, containedResource, new CompositeChildElement(resDef, encodeContext), encodeContext); 118 119 } else { 120 121 // DSTU2+ 122 if (resourceId != null) { 123 streamWriter.start(); 124 streamWriter.triple(RDFUtil.triple("<value> " + resourceId.getIdPart() + " </value>")); 125 encodeExtensionsIfPresent(resource, streamWriter, resourceId, false, encodeContext); 126 streamWriter.finish(); 127 writeCommentsPost(streamWriter, resourceId); 128 } 129 /* 130 InstantDt updated = (InstantDt) resource.getResourceMetadata().get(ResourceMetadataKeyEnum.UPDATED); 131 IdDt idDt = resource.getId(); 132 String versionIdPart = idDt.getVersionIdPart(); 133 if (isBlank(versionIdPart)) { 134 versionIdPart = ResourceMetadataKeyEnum.VERSION.get(resource); 135 } 136 List<BaseCodingDt> securityLabels = extractMetadataListNotNull(resource, ResourceMetadataKeyEnum.SECURITY_LABELS); 137 List<? extends IIdType> profiles = extractMetadataListNotNull(resource, ResourceMetadataKeyEnum.PROFILES); 138 profiles = super.getProfileTagsForEncoding(resource, profiles); 139 140 TagList tags = getMetaTagsForEncoding((resource), encodeContext); 141 142 if (!ElementUtil.isEmpty(versionIdPart, updated, securityLabels, tags, profiles)) { 143 streamWriter.start(); 144 145 for (IIdType profile : profiles) { 146 streamWriter.start(); 147 streamWriter.triple(RDFUtil.triple("<value> " + profile.getValue() + " </value>")); 148 streamWriter.finish(); 149 } 150 for (BaseCodingDt securityLabel : securityLabels) { 151 streamWriter.start(); 152 encodeCompositeElementToStreamWriter(resource, securityLabel, streamWriter, containedResource, null, encodeContext); 153 streamWriter.finish(); 154 } 155 if (tags != null) { 156 for (Tag tag : tags) { 157 if (tag.isEmpty()) { 158 continue; 159 } 160 streamWriter.start(); 161 streamWriter.triple(RDFUtil.triple("<system> " + tag.getScheme() + " </system>")); 162 streamWriter.triple(RDFUtil.triple("<code> " + tag.getTerm() + " </code>")); 163 streamWriter.triple(RDFUtil.triple("<display> " + tag.getLabel() + " </display>")); 164 streamWriter.finish(); 165 } 166 } 167 streamWriter.finish(); 168 } 169 */ 170 if (resource instanceof IBaseBinary) { 171 IBaseBinary bin = (IBaseBinary) resource; 172 streamWriter.triple(RDFUtil.triple("<contentType> " + bin.getContentType() + " </contentType>")); 173 streamWriter.triple(RDFUtil.triple("<content> " + bin.getContentAsBase64() + " </content>")); 174 } else { 175 encodeCompositeElementToStreamWriter(resource, resource, streamWriter, containedResource, new CompositeChildElement(resDef, encodeContext), encodeContext); 176 } 177 178 } 179 180 streamWriter.finish(); 181 } 182 183 private void writeCommentsPre(final StreamRDF eventWriter, final IBase element) { 184 if (element != null && element.hasFormatComment()) { 185 for (String next : element.getFormatCommentsPre()) { 186 if (isNotBlank(next)) { 187 eventWriter.base(next); 188 } 189 } 190 } 191 } 192 193 private void writeCommentsPost(final StreamRDF eventWriter, final IBase element) { 194 if (element != null && element.hasFormatComment()) { 195 for (String next : element.getFormatCommentsPost()) { 196 if (isNotBlank(next)) { 197 eventWriter.base(next); 198 } 199 } 200 } 201 } 202 203 private void encodeChildElementToStreamWriter(final IBaseResource resource, 204 final StreamRDF eventWriter, 205 final BaseRuntimeChildDefinition childDefinition, 206 final IBase element, 207 final String childName, 208 final BaseRuntimeElementDefinition<?> childDef, 209 final String extensionUrl, 210 final boolean includedResource, 211 final CompositeChildElement parent, 212 final EncodeContext encodeContext) { 213 214 String childGenericName = childDefinition.getElementName(); 215 216 encodeContext.pushPath(childGenericName, false); 217 try { 218 219 if (element == null || element.isEmpty()) { 220 if (!isChildContained(childDef, includedResource)) { 221 return; 222 } 223 } 224 225 writeCommentsPre(eventWriter, element); 226 227 switch (childDef.getChildType()) { 228 case ID_DATATYPE: { 229 IIdType value = (IIdType) element; 230 assert value != null; 231 String encodedValue = "id".equals(childName) ? value.getIdPart() : value.getValue(); 232 if (StringUtils.isNotBlank(encodedValue) || !hasNoExtensions(value)) { 233 eventWriter.start(); 234 if (StringUtils.isNotBlank(encodedValue)) { 235 eventWriter.triple(RDFUtil.triple("<value> " + encodedValue + " </value>")); 236 } 237 encodeExtensionsIfPresent(resource, eventWriter, element, includedResource, encodeContext); 238 eventWriter.finish(); 239 } 240 break; 241 } 242 case PRIMITIVE_DATATYPE: { 243 IPrimitiveType<?> pd = (IPrimitiveType) element; 244 assert pd != null; 245 String value = pd.getValueAsString(); 246 if (value != null || !hasNoExtensions(pd)) { 247 eventWriter.start(); 248 String elementId = getCompositeElementId(element); 249 if (isNotBlank(elementId)) { 250 eventWriter.triple(RDFUtil.triple("<id> " + elementId + " </id>")); 251 } 252 if (value != null) { 253 eventWriter.triple(RDFUtil.triple("<value> " + value + " </value>")); 254 } 255 encodeExtensionsIfPresent(resource, eventWriter, element, includedResource, encodeContext); 256 eventWriter.finish(); 257 } 258 break; 259 } 260 case RESOURCE_BLOCK: 261 case COMPOSITE_DATATYPE: { 262 eventWriter.start(); 263 String elementId = getCompositeElementId(element); 264 if (isNotBlank(elementId)) { 265 eventWriter.triple(RDFUtil.triple("<id> " + elementId + " </id>")); 266 } 267 if (isNotBlank(extensionUrl)) { 268 eventWriter.triple(RDFUtil.triple("<url> " + extensionUrl + " </url>")); 269 } 270 encodeCompositeElementToStreamWriter(resource, element, eventWriter, includedResource, parent, encodeContext); 271 eventWriter.finish(); 272 break; 273 } 274 case CONTAINED_RESOURCE_LIST: 275 case CONTAINED_RESOURCES: { 276 /* 277 * Disable per #103 for (IResource next : value.getContainedResources()) { if (getContainedResources().getResourceId(next) != null) { continue; } 278 * theEventWriter.writeStartElement("contained"); encodeResourceToRDFStreamWriter(next, theEventWriter, true, fixContainedResourceId(next.getId().getValue())); 279 * theEventWriter.writeEndElement(); } 280 */ 281 for (IBaseResource next : getContainedResources().getContainedResources()) { 282 IIdType resourceId = getContainedResources().getResourceId(next); 283 eventWriter.start(); 284 encodeResourceToRDFStreamWriter(next, eventWriter, true, fixContainedResourceId(resourceId.getValue()), encodeContext); 285 eventWriter.finish(); 286 } 287 break; 288 } 289 case RESOURCE: { 290 IBaseResource baseResource = (IBaseResource) element; 291 String resourceName = this.context.getResourceDefinition(baseResource).getName(); 292 if (!super.shouldEncodeResource(resourceName)) { 293 break; 294 } 295 eventWriter.start(); 296 encodeContext.pushPath(resourceName, true); 297 encodeResourceToRDFStreamWriter(resource, eventWriter, encodeContext); 298 encodeContext.popPath(); 299 eventWriter.finish(); 300 break; 301 } 302 case EXTENSION_DECLARED: 303 case UNDECL_EXT: { 304 throw new IllegalStateException("state should not happen: " + childDef.getName()); 305 } 306 } 307 308 writeCommentsPost(eventWriter, element); 309 310 } finally { 311 encodeContext.popPath(); 312 } 313 314 } 315 316 private void encodeResourceToRDFStreamWriter(final IBaseResource resource, 317 final StreamRDF eventWriter, 318 final EncodeContext encodeContext) { 319 IIdType resourceId = null; 320 321 if (StringUtils.isNotBlank(resource.getIdElement().getIdPart())) { 322 resourceId = resource.getIdElement(); 323 if (resource.getIdElement().getValue().startsWith("urn:")) { 324 resourceId = null; 325 } 326 } 327 328 if (!super.shouldEncodeResourceId(resource, encodeContext)) { 329 resourceId = null; 330 } else if (encodeContext.getResourcePath().size() == 1 && getEncodeForceResourceId() != null) { 331 resourceId = getEncodeForceResourceId(); 332 } 333 334 encodeResourceToRDFStreamWriter(resource, eventWriter, false, resourceId, encodeContext); 335 } 336 337 private void encodeUndeclaredExtensions(final IBaseResource resource, 338 final StreamRDF eventWriter, 339 final List<? extends IBaseExtension<?, ?>> extensions, 340 final boolean includedResource, 341 final EncodeContext encodeContext) { 342 for (IBaseExtension<?, ?> next : extensions) { 343 if (next == null || (ElementUtil.isEmpty(next.getValue()) && next.getExtension().isEmpty())) { 344 continue; 345 } 346 347 writeCommentsPre(eventWriter, next); 348 349 eventWriter.start(); 350 351 String elementId = getCompositeElementId(next); 352 if (isNotBlank(elementId)) { 353 eventWriter.triple(RDFUtil.triple("<id> " + elementId + " </id>")); 354 } 355 356 String url = getExtensionUrl(next.getUrl()); 357 eventWriter.triple(RDFUtil.triple("<url> " + url + " </url>")); 358 359 if (next.getValue() != null) { 360 IBaseDatatype value = next.getValue(); 361 RuntimeChildUndeclaredExtensionDefinition extDef = this.context.getRuntimeChildUndeclaredExtensionDefinition(); 362 String childName = extDef.getChildNameByDatatype(value.getClass()); 363 BaseRuntimeElementDefinition<?> childDef; 364 if (childName == null) { 365 childDef = this.context.getElementDefinition(value.getClass()); 366 if (childDef == null) { 367 throw new ConfigurationException("Unable to encode extension, unrecognized child element type: " + value.getClass().getCanonicalName()); 368 } 369 childName = RuntimeChildUndeclaredExtensionDefinition.createExtensionChildName(childDef); 370 } else { 371 childDef = extDef.getChildElementDefinitionByDatatype(value.getClass()); 372 if (childDef == null) { 373 throw new ConfigurationException("Unable to encode extension, unrecognized child element type: " + value.getClass().getCanonicalName()); 374 } 375 } 376 encodeChildElementToStreamWriter(resource, eventWriter, extDef, value, childName, 377 childDef, null, includedResource, null, encodeContext); 378 } 379 380 // child extensions 381 encodeExtensionsIfPresent(resource, eventWriter, next, includedResource, encodeContext); 382 383 eventWriter.finish(); 384 385 writeCommentsPost(eventWriter, next); 386 387 } 388 } 389 390 private void encodeExtensionsIfPresent(final IBaseResource resource, 391 final StreamRDF writer, 392 final IBase element, 393 final boolean includedResource, 394 final EncodeContext encodeContext) { 395 if (element instanceof ISupportsUndeclaredExtensions) { 396 ISupportsUndeclaredExtensions res = (ISupportsUndeclaredExtensions) element; 397 encodeUndeclaredExtensions(resource, writer, toBaseExtensionList(res.getUndeclaredExtensions()), includedResource, encodeContext); 398 encodeUndeclaredExtensions(resource, writer, toBaseExtensionList(res.getUndeclaredModifierExtensions()), includedResource, encodeContext); 399 } 400 if (element instanceof IBaseHasExtensions) { 401 IBaseHasExtensions res = (IBaseHasExtensions) element; 402 encodeUndeclaredExtensions(resource, writer, res.getExtension(), includedResource, encodeContext); 403 } 404 if (element instanceof IBaseHasModifierExtensions) { 405 IBaseHasModifierExtensions res = (IBaseHasModifierExtensions) element; 406 encodeUndeclaredExtensions(resource, writer, res.getModifierExtension(), includedResource, encodeContext); 407 } 408 } 409 410 private void encodeExtension(final IBaseResource resource, 411 final StreamRDF eventWriter, 412 final boolean containedResource, 413 final CompositeChildElement nextChildElem, 414 final BaseRuntimeChildDefinition nextChild, 415 final IBase nextValue, 416 final String childName, 417 final String extensionUrl, 418 final BaseRuntimeElementDefinition<?> childDef, 419 final EncodeContext encodeContext) { 420 BaseRuntimeDeclaredChildDefinition extDef = (BaseRuntimeDeclaredChildDefinition) nextChild; 421 eventWriter.start(); 422 423 String elementId = getCompositeElementId(nextValue); 424 if (isNotBlank(elementId)) { 425 eventWriter.triple(RDFUtil.triple("<id> " + elementId + " </id>")); 426 } 427 eventWriter.triple(RDFUtil.triple("<url> " + extensionUrl + " </url>")); 428 encodeChildElementToStreamWriter(resource, eventWriter, nextChild, nextValue, childName, 429 childDef, null, containedResource, nextChildElem, encodeContext); 430 eventWriter.finish(); 431 } 432 433 private void encodeCompositeElementToStreamWriter(final IBaseResource resource, 434 final IBase element, 435 final StreamRDF streamRDF, 436 final boolean containedResource, 437 final CompositeChildElement parent, 438 final EncodeContext encodeContext) { 439 440 for (CompositeChildElement nextChildElem : super.compositeChildIterator(element, containedResource, parent, encodeContext)) { 441 442 BaseRuntimeChildDefinition nextChild = nextChildElem.getDef(); 443 444 if (nextChild.getElementName().equals("url") && element instanceof IBaseExtension) { 445 /* 446 * RDF encoding is a one-off for extensions. The URL element goes in an attribute 447 * instead of being encoded as a normal element, only for RDF encoding 448 */ 449 continue; 450 } 451 452 if (nextChild instanceof RuntimeChildNarrativeDefinition) { 453 INarrativeGenerator gen = this.context.getNarrativeGenerator(); 454 INarrative narr; 455 if (resource instanceof IResource) { 456 narr = ((IResource) resource).getText(); 457 } else if (resource instanceof IDomainResource) { 458 narr = ((IDomainResource) resource).getText(); 459 } else { 460 narr = null; 461 } 462 assert narr != null; 463 if (gen != null && narr.isEmpty()) { 464 gen.populateResourceNarrative(this.context, resource); 465 } 466 if (!narr.isEmpty()) { 467 RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild; 468 String childName = nextChild.getChildNameByDatatype(child.getDatatype()); 469 BaseRuntimeElementDefinition<?> type = child.getChildByName(childName); 470 encodeChildElementToStreamWriter(resource, 471 streamRDF, nextChild, narr, childName, type, null, 472 containedResource, nextChildElem, encodeContext); 473 continue; 474 } 475 } 476 477 if (nextChild instanceof RuntimeChildContainedResources) { 478 encodeChildElementToStreamWriter(resource, streamRDF, nextChild, null, 479 nextChild.getChildNameByDatatype(null), 480 nextChild.getChildElementDefinitionByDatatype(null), null, 481 containedResource, nextChildElem, encodeContext); 482 } else { 483 484 List<? extends IBase> values = nextChild.getAccessor().getValues(element); 485 values = super.preProcessValues(nextChild, resource, values, nextChildElem, encodeContext); 486 487 if (values == null || values.isEmpty()) { 488 continue; 489 } 490 for (IBase nextValue : values) { 491 if ((nextValue == null || nextValue.isEmpty())) { 492 continue; 493 } 494 495 ChildNameAndDef childNameAndDef = super.getChildNameAndDef(nextChild, nextValue); 496 if (childNameAndDef == null) { 497 continue; 498 } 499 500 String childName = childNameAndDef.getChildName(); 501 BaseRuntimeElementDefinition<?> childDef = childNameAndDef.getChildDef(); 502 String extensionUrl = getExtensionUrl(nextChild.getExtensionUrl()); 503 504 if (extensionUrl != null && !childName.equals("extension")) { 505 encodeExtension(resource, streamRDF, containedResource, nextChildElem, nextChild, 506 nextValue, childName, extensionUrl, childDef, encodeContext); 507 } else if (nextChild instanceof RuntimeChildExtension) { 508 IBaseExtension<?, ?> extension = (IBaseExtension<?, ?>) nextValue; 509 if ((extension.getValue() == null || extension.getValue().isEmpty())) { 510 if (extension.getExtension().isEmpty()) { 511 continue; 512 } 513 } 514 encodeChildElementToStreamWriter(resource, streamRDF, nextChild, nextValue, 515 childName, childDef, getExtensionUrl(extension.getUrl()), 516 containedResource, nextChildElem, encodeContext); 517 } else if (!(nextChild instanceof RuntimeChildNarrativeDefinition) || !containedResource) { 518 encodeChildElementToStreamWriter(resource, streamRDF, nextChild, nextValue, 519 childName, childDef, extensionUrl, containedResource, nextChildElem, encodeContext); 520 } 521 } 522 } 523 } 524 } 525 526 private <Q extends IBaseExtension<?, ?>> List<IBaseExtension<?, ?>> toBaseExtensionList(final List<Q> theList) { 527 List<IBaseExtension<?, ?>> retVal = new ArrayList<>(theList.size()); 528 retVal.addAll(theList); 529 return retVal; 530 } 531 532 private <T extends IBaseResource> T parseResource(Class<T> resourceType, StreamRDF streamReader) { 533 ParserState<T> parserState = ParserState.getPreResourceInstance(this, resourceType, context, false, getErrorHandler()); 534 return doRDFLoop(streamReader, parserState); 535 } 536 537 538 private <T> T doRDFLoop(StreamRDF streamReader, ParserState<T> parserState) { 539 logger.trace("Entering RDF parsing loop with state: {}", parserState); 540 return parserState.getObject(); 541 } 542}