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}