001/*-
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.parser;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeDeclaredChildDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
025import ca.uhn.fhir.context.ConfigurationException;
026import ca.uhn.fhir.context.FhirContext;
027import ca.uhn.fhir.context.RuntimeChildContainedResources;
028import ca.uhn.fhir.context.RuntimeChildDirectResource;
029import ca.uhn.fhir.context.RuntimeChildExtension;
030import ca.uhn.fhir.context.RuntimeChildNarrativeDefinition;
031import ca.uhn.fhir.context.RuntimeResourceDefinition;
032import ca.uhn.fhir.i18n.Msg;
033import ca.uhn.fhir.model.api.IResource;
034import ca.uhn.fhir.narrative.INarrativeGenerator;
035import ca.uhn.fhir.rest.api.EncodingEnum;
036import ca.uhn.fhir.util.rdf.RDFUtil;
037import org.apache.commons.lang3.StringUtils;
038import org.apache.jena.datatypes.xsd.XSDDatatype;
039import org.apache.jena.irix.IRIs;
040import org.apache.jena.rdf.model.Literal;
041import org.apache.jena.rdf.model.Model;
042import org.apache.jena.rdf.model.RDFNode;
043import org.apache.jena.rdf.model.Resource;
044import org.apache.jena.rdf.model.Statement;
045import org.apache.jena.rdf.model.StmtIterator;
046import org.apache.jena.riot.Lang;
047import org.apache.jena.vocabulary.RDF;
048import org.hl7.fhir.instance.model.api.IAnyResource;
049import org.hl7.fhir.instance.model.api.IBase;
050import org.hl7.fhir.instance.model.api.IBaseBackboneElement;
051import org.hl7.fhir.instance.model.api.IBaseDatatypeElement;
052import org.hl7.fhir.instance.model.api.IBaseElement;
053import org.hl7.fhir.instance.model.api.IBaseExtension;
054import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
055import org.hl7.fhir.instance.model.api.IBaseResource;
056import org.hl7.fhir.instance.model.api.IBaseXhtml;
057import org.hl7.fhir.instance.model.api.IDomainResource;
058import org.hl7.fhir.instance.model.api.IIdType;
059import org.hl7.fhir.instance.model.api.INarrative;
060import org.hl7.fhir.instance.model.api.IPrimitiveType;
061
062import java.io.Reader;
063import java.io.Writer;
064import java.util.Arrays;
065import java.util.Comparator;
066import java.util.HashMap;
067import java.util.List;
068import java.util.Map;
069
070import static ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum.ID_DATATYPE;
071import static ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum.PRIMITIVE_DATATYPE;
072import static ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum.PRIMITIVE_XHTML;
073import static ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum.PRIMITIVE_XHTML_HL7ORG;
074
075/**
076 * This class is the FHIR RDF parser/encoder. Users should not interact with this class directly, but should use
077 * {@link FhirContext#newRDFParser()} to get an instance.
078 */
079public class RDFParser extends BaseParser {
080
081        private static final String VALUE = "value";
082        private static final String FHIR_INDEX = "index";
083        private static final String FHIR_PREFIX = "fhir";
084        private static final String FHIR_NS = "http://hl7.org/fhir/";
085        private static final String RDF_PREFIX = "rdf";
086        private static final String RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
087        private static final String RDFS_PREFIX = "rdfs";
088        private static final String RDFS_NS = "http://www.w3.org/2000/01/rdf-schema#";
089        private static final String XSD_PREFIX = "xsd";
090        private static final String XSD_NS = "http://www.w3.org/2001/XMLSchema#";
091        private static final String SCT_PREFIX = "sct";
092        private static final String SCT_NS = "http://snomed.info/id#";
093        private static final String EXTENSION_URL = "Extension.url";
094        private static final String ELEMENT_EXTENSION = "Element.extension";
095
096        private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(RDFParser.class);
097
098        public static final String NODE_ROLE = "nodeRole";
099        private static final List<String> ignoredPredicates =
100                        Arrays.asList(RDF.type.getURI(), FHIR_NS + FHIR_INDEX, FHIR_NS + NODE_ROLE);
101        public static final String TREE_ROOT = "treeRoot";
102        public static final String RESOURCE_ID = "Resource.id";
103        public static final String ID = "id";
104        public static final String ELEMENT_ID = "Element.id";
105        public static final String DOMAIN_RESOURCE_CONTAINED = "DomainResource.contained";
106        public static final String EXTENSION = "extension";
107        public static final String CONTAINED = "contained";
108        public static final String MODIFIER_EXTENSION = "modifierExtension";
109        private final Map<Class, String> classToFhirTypeMap = new HashMap<>();
110
111        private final Lang lang;
112
113        /**
114         * Do not use this constructor, the recommended way to obtain a new instance of the RDF parser is to invoke
115         * {@link FhirContext#newRDFParser()}.
116         *
117         * @param parserErrorHandler the Parser Error Handler
118         */
119        public RDFParser(final FhirContext context, final IParserErrorHandler parserErrorHandler, final Lang lang) {
120                super(context, parserErrorHandler);
121                this.lang = lang;
122        }
123
124        @Override
125        public EncodingEnum getEncoding() {
126                return EncodingEnum.RDF;
127        }
128
129        @Override
130        public IParser setPrettyPrint(final boolean prettyPrint) {
131                return this;
132        }
133
134        /**
135         * Writes the provided resource to the writer.  This should only be called for the top-level resource being encoded.
136         * @param resource FHIR resource for writing
137         * @param writer The writer to write to -- Note: Jena prefers streams over writers
138         * @param encodeContext encoding content from parent
139         */
140        @Override
141        protected void doEncodeResourceToWriter(
142                        final IBaseResource resource, final Writer writer, final EncodeContext encodeContext) {
143                Model rdfModel = RDFUtil.initializeRDFModel();
144
145                // Establish the namespaces and prefixes needed
146                HashMap<String, String> prefixes = new HashMap<>();
147                prefixes.put(RDF_PREFIX, RDF_NS);
148                prefixes.put(RDFS_PREFIX, RDFS_NS);
149                prefixes.put(XSD_PREFIX, XSD_NS);
150                prefixes.put(FHIR_PREFIX, FHIR_NS);
151                prefixes.put(SCT_PREFIX, SCT_NS);
152
153                for (String key : prefixes.keySet()) {
154                        rdfModel.setNsPrefix(key, prefixes.get(key));
155                }
156
157                IIdType resourceId = processResourceID(resource, encodeContext);
158
159                encodeResourceToRDFStreamWriter(resource, rdfModel, false, resourceId, encodeContext, true, null);
160
161                RDFUtil.writeRDFModel(writer, rdfModel, lang);
162        }
163
164        /**
165         * Parses RDF content to a FHIR resource using Apache Jena
166         * @param resourceType Class of FHIR resource being deserialized
167         * @param reader Reader containing RDF (turtle) content
168         * @param <T> Type parameter denoting which resource is being parsed
169         * @return Populated FHIR resource
170         * @throws DataFormatException Exception that can be thrown from parser
171         */
172        @Override
173        protected <T extends IBaseResource> T doParseResource(final Class<T> resourceType, final Reader reader)
174                        throws DataFormatException {
175                Model model = RDFUtil.readRDFToModel(reader, this.lang);
176                return parseResource(resourceType, model);
177        }
178
179        private Resource encodeResourceToRDFStreamWriter(
180                        final IBaseResource resource,
181                        final Model rdfModel,
182                        final boolean containedResource,
183                        final IIdType resourceId,
184                        final EncodeContext encodeContext,
185                        final boolean rootResource,
186                        Resource parentResource) {
187
188                RuntimeResourceDefinition resDef = getContext().getResourceDefinition(resource);
189                if (resDef == null) {
190                        throw new ConfigurationException(Msg.code(1845) + "Unknown resource type: " + resource.getClass());
191                }
192
193                if (!containedResource) {
194                        containResourcesInReferences(resource);
195                }
196
197                if (!(resource instanceof IAnyResource)) {
198                        throw new IllegalStateException(Msg.code(1846) + "Unsupported resource found: "
199                                        + resource.getClass().getName());
200                }
201
202                // Create absolute IRI for the resource
203                String uriBase = resource.getIdElement().getBaseUrl();
204                if (uriBase == null) {
205                        uriBase = getServerBaseUrl();
206                }
207                if (uriBase == null) {
208                        uriBase = FHIR_NS;
209                }
210                if (!uriBase.endsWith("/")) {
211                        uriBase = uriBase + "/";
212                }
213
214                if (parentResource == null) {
215                        if (!resource.getIdElement().toUnqualified().hasIdPart()) {
216                                parentResource = rdfModel.getResource(null);
217                        } else {
218
219                                String resourceUri = IRIs.resolve(
220                                                                uriBase, resource.getIdElement().toUnqualified().toString())
221                                                .toString();
222                                parentResource = rdfModel.getResource(resourceUri);
223                        }
224                        // If the resource already exists and has statements, return that existing resource.
225                        if (parentResource != null
226                                        && parentResource.listProperties().toList().size() > 0) {
227                                return parentResource;
228                        } else if (parentResource == null) {
229                                return null;
230                        }
231                }
232
233                parentResource.addProperty(RDF.type, rdfModel.createProperty(FHIR_NS + resDef.getName()));
234
235                // Only the top-level resource should have the nodeRole set to treeRoot
236                if (rootResource) {
237                        parentResource.addProperty(
238                                        rdfModel.createProperty(FHIR_NS + NODE_ROLE), rdfModel.createProperty(FHIR_NS + TREE_ROOT));
239                }
240
241                if (resourceId != null
242                                && resourceId.getIdPart() != null
243                                && !resourceId.getValue().startsWith("urn:")) {
244                        parentResource.addProperty(
245                                        rdfModel.createProperty(FHIR_NS + RESOURCE_ID),
246                                        createFhirValueBlankNode(rdfModel, resourceId.getIdPart()));
247                }
248
249                encodeCompositeElementToStreamWriter(
250                                resource,
251                                resource,
252                                rdfModel,
253                                parentResource,
254                                containedResource,
255                                new CompositeChildElement(resDef, encodeContext),
256                                encodeContext);
257
258                return parentResource;
259        }
260
261        /**
262         * Utility method to create a blank node with a fhir:value predicate
263         * @param rdfModel Model to create node within
264         * @param value value object - assumed to be xsd:string
265         * @return Blank node resource containing fhir:value
266         */
267        private Resource createFhirValueBlankNode(Model rdfModel, String value) {
268                return createFhirValueBlankNode(rdfModel, value, XSDDatatype.XSDstring, null);
269        }
270        /**
271         * Utility method to create a blank node with a fhir:value predicate accepting a specific data type and index
272         * @param rdfModel Model to create node within
273         * @param value value object
274         * @param xsdDataType data type for value
275         * @param cardinalityIndex if a collection, this value is written as a fhir:index predicate
276         * @return Blank node resource containing fhir:value (and possibly fhir:index)
277         */
278        private Resource createFhirValueBlankNode(
279                        Model rdfModel, String value, XSDDatatype xsdDataType, Integer cardinalityIndex) {
280                Resource fhirValueBlankNodeResource = rdfModel.createResource()
281                                .addProperty(rdfModel.createProperty(FHIR_NS + VALUE), rdfModel.createTypedLiteral(value, xsdDataType));
282
283                if (cardinalityIndex != null && cardinalityIndex > -1) {
284                        fhirValueBlankNodeResource.addProperty(
285                                        rdfModel.createProperty(FHIR_NS + FHIR_INDEX),
286                                        rdfModel.createTypedLiteral(cardinalityIndex, XSDDatatype.XSDinteger));
287                }
288                return fhirValueBlankNodeResource;
289        }
290
291        /**
292         * Builds the predicate name based on field definition
293         * @param resource Resource being interrogated
294         * @param definition field definition
295         * @param childName childName which been massaged for different data types
296         * @return String of predicate name
297         */
298        private String constructPredicateName(
299                        IBaseResource resource, BaseRuntimeChildDefinition definition, String childName, IBase parentElement) {
300                String basePropertyName = FHIR_NS + resource.fhirType() + "." + childName;
301                String classBasedPropertyName;
302
303                if (definition instanceof BaseRuntimeDeclaredChildDefinition) {
304                        BaseRuntimeDeclaredChildDefinition declaredDef = (BaseRuntimeDeclaredChildDefinition) definition;
305                        Class declaringClass = declaredDef.getField().getDeclaringClass();
306                        if (declaringClass != resource.getClass()) {
307                                String property = null;
308                                if (IBaseBackboneElement.class.isAssignableFrom(declaringClass)
309                                                || IBaseDatatypeElement.class.isAssignableFrom(declaringClass)) {
310                                        if (classToFhirTypeMap.containsKey(declaringClass)) {
311                                                property = classToFhirTypeMap.get(declaringClass);
312                                        } else {
313                                                try {
314                                                        IBase elem = (IBase)
315                                                                        declaringClass.getDeclaredConstructor().newInstance();
316                                                        property = elem.fhirType();
317                                                        classToFhirTypeMap.put(declaringClass, property);
318                                                } catch (Exception ex) {
319                                                        logger.debug("Error instantiating an " + declaringClass.getSimpleName()
320                                                                        + " to retrieve its FhirType");
321                                                }
322                                        }
323                                } else {
324                                        if ("MetadataResource".equals(declaringClass.getSimpleName())) {
325                                                property = resource.getClass().getSimpleName();
326                                        } else {
327                                                property = declaredDef.getField().getDeclaringClass().getSimpleName();
328                                        }
329                                }
330                                classBasedPropertyName = FHIR_NS + property + "." + childName;
331                                return classBasedPropertyName;
332                        }
333                }
334                return basePropertyName;
335        }
336
337        private Model encodeChildElementToStreamWriter(
338                        final IBaseResource resource,
339                        IBase parentElement,
340                        Model rdfModel,
341                        Resource rdfResource,
342                        final BaseRuntimeChildDefinition childDefinition,
343                        final IBase element,
344                        final String childName,
345                        final BaseRuntimeElementDefinition<?> childDef,
346                        final boolean includedResource,
347                        final CompositeChildElement parent,
348                        final EncodeContext encodeContext,
349                        final Integer cardinalityIndex) {
350
351                String childGenericName = childDefinition.getElementName();
352
353                encodeContext.pushPath(childGenericName, false);
354                try {
355
356                        if (element == null || element.isEmpty()) {
357                                if (!isChildContained(childDef, includedResource)) {
358                                        return rdfModel;
359                                }
360                        }
361
362                        switch (childDef.getChildType()) {
363                                case ID_DATATYPE: {
364                                        IIdType value = (IIdType) element;
365                                        assert value != null;
366                                        String encodedValue = ID.equals(childName) ? value.getIdPart() : value.getValue();
367                                        if (StringUtils.isNotBlank(encodedValue) || !hasNoExtensions(value)) {
368                                                if (StringUtils.isNotBlank(encodedValue)) {
369
370                                                        String propertyName =
371                                                                        constructPredicateName(resource, childDefinition, childName, parentElement);
372                                                        if (element != null) {
373                                                                XSDDatatype dataType = getXSDDataTypeForFhirType(element.fhirType(), encodedValue);
374                                                                rdfResource.addProperty(
375                                                                                rdfModel.createProperty(propertyName),
376                                                                                this.createFhirValueBlankNode(
377                                                                                                rdfModel, encodedValue, dataType, cardinalityIndex));
378                                                        }
379                                                }
380                                        }
381                                        break;
382                                }
383                                case PRIMITIVE_DATATYPE: {
384                                        IPrimitiveType<?> pd = (IPrimitiveType<?>) element;
385                                        assert pd != null;
386                                        String value = pd.getValueAsString();
387                                        if (value != null || !hasNoExtensions(pd)) {
388                                                if (value != null) {
389                                                        String propertyName =
390                                                                        constructPredicateName(resource, childDefinition, childName, parentElement);
391                                                        XSDDatatype dataType = getXSDDataTypeForFhirType(pd.fhirType(), value);
392                                                        Resource valueResource =
393                                                                        this.createFhirValueBlankNode(rdfModel, value, dataType, cardinalityIndex);
394                                                        if (!hasNoExtensions(pd)) {
395                                                                IBaseHasExtensions hasExtension = (IBaseHasExtensions) pd;
396                                                                if (hasExtension.getExtension() != null
397                                                                                && hasExtension.getExtension().size() > 0) {
398                                                                        int i = 0;
399                                                                        for (IBaseExtension extension : hasExtension.getExtension()) {
400                                                                                RuntimeResourceDefinition resDef =
401                                                                                                getContext().getResourceDefinition(resource);
402                                                                                Resource extensionResource = rdfModel.createResource();
403                                                                                extensionResource.addProperty(
404                                                                                                rdfModel.createProperty(FHIR_NS + FHIR_INDEX),
405                                                                                                rdfModel.createTypedLiteral(i, XSDDatatype.XSDinteger));
406                                                                                valueResource.addProperty(
407                                                                                                rdfModel.createProperty(FHIR_NS + ELEMENT_EXTENSION),
408                                                                                                extensionResource);
409                                                                                encodeCompositeElementToStreamWriter(
410                                                                                                resource,
411                                                                                                extension,
412                                                                                                rdfModel,
413                                                                                                extensionResource,
414                                                                                                false,
415                                                                                                new CompositeChildElement(resDef, encodeContext),
416                                                                                                encodeContext);
417                                                                        }
418                                                                }
419                                                        }
420
421                                                        rdfResource.addProperty(rdfModel.createProperty(propertyName), valueResource);
422                                                }
423                                        }
424                                        break;
425                                }
426                                case RESOURCE_BLOCK:
427                                case COMPOSITE_DATATYPE: {
428                                        String idString = null;
429                                        String idPredicate = null;
430                                        if (element instanceof IBaseResource) {
431                                                idPredicate = FHIR_NS + RESOURCE_ID;
432                                                IIdType resourceId = processResourceID((IBaseResource) element, encodeContext);
433                                                if (resourceId != null) {
434                                                        idString = resourceId.getIdPart();
435                                                }
436                                        } else if (element instanceof IBaseElement) {
437                                                idPredicate = FHIR_NS + ELEMENT_ID;
438                                                if (((IBaseElement) element).getId() != null) {
439                                                        idString = ((IBaseElement) element).getId();
440                                                }
441                                        }
442                                        if (idString != null) {
443                                                rdfResource.addProperty(
444                                                                rdfModel.createProperty(idPredicate), createFhirValueBlankNode(rdfModel, idString));
445                                        }
446                                        rdfModel = encodeCompositeElementToStreamWriter(
447                                                        resource, element, rdfModel, rdfResource, includedResource, parent, encodeContext);
448                                        break;
449                                }
450                                case CONTAINED_RESOURCE_LIST:
451                                case CONTAINED_RESOURCES: {
452                                        if (element != null) {
453                                                IIdType resourceId = ((IBaseResource) element).getIdElement();
454                                                Resource containedResource = rdfModel.createResource();
455                                                rdfResource.addProperty(
456                                                                rdfModel.createProperty(FHIR_NS + DOMAIN_RESOURCE_CONTAINED), containedResource);
457                                                if (cardinalityIndex != null) {
458                                                        containedResource.addProperty(
459                                                                        rdfModel.createProperty(FHIR_NS + FHIR_INDEX),
460                                                                        cardinalityIndex.toString(),
461                                                                        XSDDatatype.XSDinteger);
462                                                }
463                                                encodeResourceToRDFStreamWriter(
464                                                                (IBaseResource) element,
465                                                                rdfModel,
466                                                                true,
467                                                                super.fixContainedResourceId(resourceId.getValue()),
468                                                                encodeContext,
469                                                                false,
470                                                                containedResource);
471                                        }
472                                        break;
473                                }
474                                case RESOURCE: {
475                                        IBaseResource baseResource = (IBaseResource) element;
476                                        String resourceName = getContext().getResourceType(baseResource);
477                                        if (!super.shouldEncodeResource(resourceName)) {
478                                                break;
479                                        }
480                                        encodeContext.pushPath(resourceName, true);
481                                        IIdType resourceId = processResourceID(resource, encodeContext);
482                                        encodeResourceToRDFStreamWriter(resource, rdfModel, false, resourceId, encodeContext, false, null);
483                                        encodeContext.popPath();
484                                        break;
485                                }
486                                case PRIMITIVE_XHTML:
487                                case PRIMITIVE_XHTML_HL7ORG: {
488                                        IBaseXhtml xHtmlNode = (IBaseXhtml) element;
489                                        if (xHtmlNode != null) {
490                                                String value = xHtmlNode.getValueAsString();
491                                                String propertyName =
492                                                                constructPredicateName(resource, childDefinition, childName, parentElement);
493                                                rdfResource.addProperty(rdfModel.createProperty(propertyName), value);
494                                        }
495                                        break;
496                                }
497                                case EXTENSION_DECLARED:
498                                case UNDECL_EXT:
499                                default: {
500                                        throw new IllegalStateException(
501                                                        Msg.code(1847) + "Unexpected node - should not happen: " + childDef.getName());
502                                }
503                        }
504                } finally {
505                        encodeContext.popPath();
506                }
507
508                return rdfModel;
509        }
510
511        /**
512         * Maps hapi internal fhirType attribute to XSDDatatype enumeration
513         * @param fhirType hapi field type
514         * @return XSDDatatype value
515         */
516        private XSDDatatype getXSDDataTypeForFhirType(String fhirType, String value) {
517                switch (fhirType) {
518                        case "boolean":
519                                return XSDDatatype.XSDboolean;
520                        case "uri":
521                                return XSDDatatype.XSDanyURI;
522                        case "decimal":
523                                return XSDDatatype.XSDdecimal;
524                        case "date":
525                                return XSDDatatype.XSDdate;
526                        case "dateTime":
527                        case "instant":
528                                switch (value.length()) { // assumes valid lexical value
529                                        case 4:
530                                                return XSDDatatype.XSDgYear;
531                                        case 7:
532                                                return XSDDatatype.XSDgYearMonth;
533                                        case 10:
534                                                return XSDDatatype.XSDdate;
535                                        default:
536                                                return XSDDatatype.XSDdateTime;
537                                }
538                        case "code":
539                        case "string":
540                        default:
541                                return XSDDatatype.XSDstring;
542                }
543        }
544
545        private IIdType processResourceID(final IBaseResource resource, final EncodeContext encodeContext) {
546                IIdType resourceId = null;
547
548                if (StringUtils.isNotBlank(resource.getIdElement().getIdPart())) {
549                        resourceId = resource.getIdElement();
550                        if (resource.getIdElement().getValue().startsWith("urn:")) {
551                                resourceId = null;
552                        }
553                }
554
555                if (!super.shouldEncodeResourceId(resource, encodeContext)) {
556                        resourceId = null;
557                } else if (encodeContext.getResourcePath().size() == 1 && super.getEncodeForceResourceId() != null) {
558                        resourceId = super.getEncodeForceResourceId();
559                }
560
561                return resourceId;
562        }
563
564        private Model encodeExtension(
565                        final IBaseResource resource,
566                        Model rdfModel,
567                        Resource rdfResource,
568                        final boolean containedResource,
569                        final CompositeChildElement nextChildElem,
570                        final BaseRuntimeChildDefinition nextChild,
571                        final IBase nextValue,
572                        final String childName,
573                        final BaseRuntimeElementDefinition<?> childDef,
574                        final EncodeContext encodeContext,
575                        Integer cardinalityIndex) {
576                BaseRuntimeDeclaredChildDefinition extDef = (BaseRuntimeDeclaredChildDefinition) nextChild;
577
578                Resource childResource = rdfModel.createResource();
579                String extensionPredicateName = constructPredicateName(resource, extDef, extDef.getElementName(), null);
580                rdfResource.addProperty(rdfModel.createProperty(extensionPredicateName), childResource);
581                if (cardinalityIndex != null && cardinalityIndex > -1) {
582                        childResource.addProperty(
583                                        rdfModel.createProperty(FHIR_NS + FHIR_INDEX), cardinalityIndex.toString(), XSDDatatype.XSDinteger);
584                }
585
586                rdfModel = encodeChildElementToStreamWriter(
587                                resource,
588                                null,
589                                rdfModel,
590                                childResource,
591                                nextChild,
592                                nextValue,
593                                childName,
594                                childDef,
595                                containedResource,
596                                nextChildElem,
597                                encodeContext,
598                                cardinalityIndex);
599
600                return rdfModel;
601        }
602
603        private Model encodeCompositeElementToStreamWriter(
604                        final IBaseResource resource,
605                        final IBase element,
606                        Model rdfModel,
607                        Resource rdfResource,
608                        final boolean containedResource,
609                        final CompositeChildElement parent,
610                        final EncodeContext encodeContext) {
611
612                for (CompositeChildElement nextChildElem :
613                                super.compositeChildIterator(element, containedResource, parent, encodeContext)) {
614
615                        BaseRuntimeChildDefinition nextChild = nextChildElem.getDef();
616
617                        if (nextChild instanceof RuntimeChildNarrativeDefinition) {
618                                INarrativeGenerator gen = getContext().getNarrativeGenerator();
619                                if (gen != null) {
620                                        INarrative narrative;
621                                        if (resource instanceof IResource) {
622                                                narrative = ((IResource) resource).getText();
623                                        } else if (resource instanceof IDomainResource) {
624                                                narrative = ((IDomainResource) resource).getText();
625                                        } else {
626                                                narrative = null;
627                                        }
628                                        assert narrative != null;
629                                        if (narrative.isEmpty()) {
630                                                gen.populateResourceNarrative(getContext(), resource);
631                                        } else {
632                                                RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;
633
634                                                // This is where we populate the parent of the narrative
635                                                Resource childResource = rdfModel.createResource();
636
637                                                String propertyName = constructPredicateName(resource, child, child.getElementName(), element);
638                                                rdfResource.addProperty(rdfModel.createProperty(propertyName), childResource);
639
640                                                String childName = nextChild.getChildNameByDatatype(child.getDatatype());
641                                                BaseRuntimeElementDefinition<?> type = child.getChildByName(childName);
642                                                rdfModel = encodeChildElementToStreamWriter(
643                                                                resource,
644                                                                element,
645                                                                rdfModel,
646                                                                childResource,
647                                                                nextChild,
648                                                                narrative,
649                                                                childName,
650                                                                type,
651                                                                containedResource,
652                                                                nextChildElem,
653                                                                encodeContext,
654                                                                null);
655                                                continue;
656                                        }
657                                }
658                        }
659
660                        if (nextChild instanceof RuntimeChildDirectResource) {
661
662                                List<? extends IBase> values = nextChild.getAccessor().getValues(element);
663                                if (values == null || values.isEmpty()) {
664                                        continue;
665                                }
666
667                                IBaseResource directChildResource = (IBaseResource) values.get(0);
668                                // If it is a direct resource, we need to create a new subject for it.
669                                Resource childResource = encodeResourceToRDFStreamWriter(
670                                                directChildResource,
671                                                rdfModel,
672                                                false,
673                                                directChildResource.getIdElement(),
674                                                encodeContext,
675                                                false,
676                                                null);
677                                String propertyName = constructPredicateName(resource, nextChild, nextChild.getElementName(), element);
678                                rdfResource.addProperty(rdfModel.createProperty(propertyName), childResource);
679
680                                continue;
681                        }
682
683                        if (nextChild instanceof RuntimeChildContainedResources) {
684                                List<? extends IBase> values = nextChild.getAccessor().getValues(element);
685                                int i = 0;
686                                for (IBase containedResourceEntity : values) {
687                                        rdfModel = encodeChildElementToStreamWriter(
688                                                        resource,
689                                                        element,
690                                                        rdfModel,
691                                                        rdfResource,
692                                                        nextChild,
693                                                        containedResourceEntity,
694                                                        nextChild.getChildNameByDatatype(null),
695                                                        nextChild.getChildElementDefinitionByDatatype(null),
696                                                        containedResource,
697                                                        nextChildElem,
698                                                        encodeContext,
699                                                        i);
700                                        i++;
701                                }
702                        } else {
703
704                                List<? extends IBase> values = nextChild.getAccessor().getValues(element);
705                                values = super.preProcessValues(nextChild, resource, values, nextChildElem, encodeContext);
706
707                                if (values == null || values.isEmpty()) {
708                                        continue;
709                                }
710
711                                Integer cardinalityIndex = null;
712                                int indexCounter = 0;
713
714                                for (IBase nextValue : values) {
715                                        if (nextChild.getMax() != 1) {
716                                                cardinalityIndex = indexCounter;
717                                                indexCounter++;
718                                        }
719                                        if ((nextValue == null || nextValue.isEmpty())) {
720                                                continue;
721                                        }
722
723                                        ChildNameAndDef childNameAndDef = super.getChildNameAndDef(nextChild, nextValue);
724                                        if (childNameAndDef == null) {
725                                                continue;
726                                        }
727
728                                        String childName = childNameAndDef.getChildName();
729                                        BaseRuntimeElementDefinition<?> childDef = childNameAndDef.getChildDef();
730                                        String extensionUrl = getExtensionUrl(nextChild.getExtensionUrl());
731
732                                        if (extensionUrl != null && !childName.equals(EXTENSION)) {
733                                                rdfModel = encodeExtension(
734                                                                resource,
735                                                                rdfModel,
736                                                                rdfResource,
737                                                                containedResource,
738                                                                nextChildElem,
739                                                                nextChild,
740                                                                nextValue,
741                                                                childName,
742                                                                childDef,
743                                                                encodeContext,
744                                                                cardinalityIndex);
745                                        } else if (nextChild instanceof RuntimeChildExtension) {
746                                                IBaseExtension<?, ?> extension = (IBaseExtension<?, ?>) nextValue;
747                                                if ((extension.getValue() == null
748                                                                || extension.getValue().isEmpty())) {
749                                                        if (extension.getExtension().isEmpty()) {
750                                                                continue;
751                                                        }
752                                                }
753                                                rdfModel = encodeExtension(
754                                                                resource,
755                                                                rdfModel,
756                                                                rdfResource,
757                                                                containedResource,
758                                                                nextChildElem,
759                                                                nextChild,
760                                                                nextValue,
761                                                                childName,
762                                                                childDef,
763                                                                encodeContext,
764                                                                cardinalityIndex);
765                                        } else if (!(nextChild instanceof RuntimeChildNarrativeDefinition) || !containedResource) {
766
767                                                // If the child is not a value type, create a child object (blank node) for subordinate
768                                                // predicates to be attached to
769                                                if (childDef.getChildType() != PRIMITIVE_DATATYPE
770                                                                && childDef.getChildType() != PRIMITIVE_XHTML_HL7ORG
771                                                                && childDef.getChildType() != PRIMITIVE_XHTML
772                                                                && childDef.getChildType() != ID_DATATYPE) {
773                                                        Resource childResource = rdfModel.createResource();
774
775                                                        String propertyName = constructPredicateName(resource, nextChild, childName, nextValue);
776                                                        rdfResource.addProperty(rdfModel.createProperty(propertyName), childResource);
777                                                        if (cardinalityIndex != null && cardinalityIndex > -1) {
778                                                                childResource.addProperty(
779                                                                                rdfModel.createProperty(FHIR_NS + FHIR_INDEX),
780                                                                                cardinalityIndex.toString(),
781                                                                                XSDDatatype.XSDinteger);
782                                                        }
783                                                        rdfModel = encodeChildElementToStreamWriter(
784                                                                        resource,
785                                                                        element,
786                                                                        rdfModel,
787                                                                        childResource,
788                                                                        nextChild,
789                                                                        nextValue,
790                                                                        childName,
791                                                                        childDef,
792                                                                        containedResource,
793                                                                        nextChildElem,
794                                                                        encodeContext,
795                                                                        cardinalityIndex);
796                                                } else {
797                                                        rdfModel = encodeChildElementToStreamWriter(
798                                                                        resource,
799                                                                        element,
800                                                                        rdfModel,
801                                                                        rdfResource,
802                                                                        nextChild,
803                                                                        nextValue,
804                                                                        childName,
805                                                                        childDef,
806                                                                        containedResource,
807                                                                        nextChildElem,
808                                                                        encodeContext,
809                                                                        cardinalityIndex);
810                                                }
811                                        }
812                                }
813                        }
814                }
815                return rdfModel;
816        }
817
818        private <T extends IBaseResource> T parseResource(Class<T> resourceType, Model rdfModel) {
819                // jsonMode of true is passed in so that the xhtml parser state behaves as expected
820                // Push PreResourceState
821                ParserState<T> parserState =
822                                ParserState.getPreResourceInstance(this, resourceType, getContext(), true, getErrorHandler());
823                return parseRootResource(rdfModel, parserState, resourceType);
824        }
825
826        private <T> T parseRootResource(Model rdfModel, ParserState<T> parserState, Class<T> resourceType) {
827                logger.trace("Entering parseRootResource with state: {}", parserState);
828
829                StmtIterator rootStatementIterator = rdfModel.listStatements(
830                                null, rdfModel.getProperty(FHIR_NS + NODE_ROLE), rdfModel.getProperty(FHIR_NS + TREE_ROOT));
831
832                Resource rootResource;
833                String fhirResourceType, fhirTypeString;
834                while (rootStatementIterator.hasNext()) {
835                        Statement rootStatement = rootStatementIterator.next();
836                        rootResource = rootStatement.getSubject();
837
838                        // If a resourceType is not provided via the server framework, discern it based on the rdf:type Arc
839                        if (resourceType == null) {
840                                Statement resourceTypeStatement = rootResource.getProperty(RDF.type);
841                                fhirTypeString = resourceTypeStatement.getObject().toString();
842                                if (fhirTypeString.startsWith(FHIR_NS)) {
843                                        fhirTypeString = fhirTypeString.replace(FHIR_NS, "");
844                                }
845                        } else {
846                                fhirTypeString = resourceType.getSimpleName();
847                        }
848
849                        RuntimeResourceDefinition definition = getContext().getResourceDefinition(fhirTypeString);
850                        fhirResourceType = definition.getName();
851
852                        parseResource(parserState, fhirResourceType, rootResource);
853
854                        // Pop PreResourceState
855                        parserState.endingElement();
856                }
857                return parserState.getObject();
858        }
859
860        private <T> void parseResource(ParserState<T> parserState, String resourceType, RDFNode rootNode) {
861                // Push top-level entity
862                parserState.enteringNewElement(FHIR_NS, resourceType);
863
864                if (rootNode instanceof Resource) {
865                        Resource rootResource = rootNode.asResource();
866                        List<Statement> statements = rootResource.listProperties().toList();
867                        statements.sort(new FhirIndexStatementComparator());
868                        for (Statement statement : statements) {
869                                String predicateAttributeName = extractAttributeNameFromPredicate(statement);
870                                if (predicateAttributeName != null) {
871                                        if (predicateAttributeName.equals(MODIFIER_EXTENSION)) {
872                                                processExtension(parserState, statement.getObject(), true);
873                                        } else if (predicateAttributeName.equals(EXTENSION)) {
874                                                processExtension(parserState, statement.getObject(), false);
875                                        } else {
876                                                processStatementObject(parserState, predicateAttributeName, statement.getObject());
877                                        }
878                                }
879                        }
880                } else if (rootNode instanceof Literal) {
881                        parserState.attributeValue(VALUE, rootNode.asLiteral().getString());
882                }
883
884                // Pop top-level entity
885                parserState.endingElement();
886        }
887
888        private String extractAttributeNameFromPredicate(Statement statement) {
889                String predicateUri = statement.getPredicate().getURI();
890
891                // If the predicateURI is one we're ignoring, return null
892                // This minimizes 'Unknown Element' warnings in the parsing process
893                if (ignoredPredicates.contains(predicateUri)) {
894                        return null;
895                }
896
897                String predicateObjectAttribute = predicateUri.substring(predicateUri.lastIndexOf("/") + 1);
898                String predicateAttributeName;
899                if (predicateObjectAttribute.contains(".")) {
900                        predicateAttributeName = predicateObjectAttribute.substring(predicateObjectAttribute.lastIndexOf(".") + 1);
901                } else {
902                        predicateAttributeName = predicateObjectAttribute;
903                }
904                return predicateAttributeName;
905        }
906
907        private <T> void processStatementObject(
908                        ParserState<T> parserState, String predicateAttributeName, RDFNode statementObject) {
909                logger.trace(
910                                "Entering processStatementObject with state: {}, for attribute {}",
911                                parserState,
912                                predicateAttributeName);
913                // Push attribute element
914                parserState.enteringNewElement(FHIR_NS, predicateAttributeName);
915
916                if (statementObject != null) {
917                        if (statementObject.isLiteral()) {
918                                // If the object is a literal, apply the value directly
919                                parserState.attributeValue(VALUE, statementObject.asLiteral().getLexicalForm());
920                        } else if (statementObject.isAnon()) {
921                                // If the object is a blank node,
922                                Resource resourceObject = statementObject.asResource();
923
924                                boolean containedResource = false;
925                                if (predicateAttributeName.equals(CONTAINED)) {
926                                        containedResource = true;
927                                        parserState.enteringNewElement(
928                                                        FHIR_NS,
929                                                        resourceObject
930                                                                        .getProperty(resourceObject.getModel().createProperty(RDF.type.getURI()))
931                                                                        .getObject()
932                                                                        .toString()
933                                                                        .replace(FHIR_NS, ""));
934                                }
935
936                                List<Statement> objectStatements =
937                                                resourceObject.listProperties().toList();
938                                objectStatements.sort(new FhirIndexStatementComparator());
939                                for (Statement objectProperty : objectStatements) {
940                                        if (objectProperty.getPredicate().hasURI(FHIR_NS + VALUE)) {
941                                                predicateAttributeName = VALUE;
942                                                parserState.attributeValue(
943                                                                predicateAttributeName,
944                                                                objectProperty.getObject().asLiteral().getLexicalForm());
945                                        } else {
946                                                // Otherwise, process it as a net-new node
947                                                predicateAttributeName = extractAttributeNameFromPredicate(objectProperty);
948                                                if (predicateAttributeName != null) {
949                                                        if (predicateAttributeName.equals(EXTENSION)) {
950                                                                processExtension(parserState, objectProperty.getObject(), false);
951                                                        } else if (predicateAttributeName.equals(MODIFIER_EXTENSION)) {
952                                                                processExtension(parserState, objectProperty.getObject(), true);
953                                                        } else {
954                                                                processStatementObject(parserState, predicateAttributeName, objectProperty.getObject());
955                                                        }
956                                                }
957                                        }
958                                }
959
960                                if (containedResource) {
961                                        // Leave the contained resource element we created
962                                        parserState.endingElement();
963                                }
964                        } else if (statementObject.isResource()) {
965                                Resource innerResource = statementObject.asResource();
966                                Statement resourceTypeStatement = innerResource.getProperty(RDF.type);
967                                String fhirTypeString = resourceTypeStatement.getObject().toString();
968                                if (fhirTypeString.startsWith(FHIR_NS)) {
969                                        fhirTypeString = fhirTypeString.replace(FHIR_NS, "");
970                                }
971                                parseResource(parserState, fhirTypeString, innerResource);
972                        }
973                }
974
975                // Pop attribute element
976                parserState.endingElement();
977        }
978
979        private <T> void processExtension(ParserState<T> parserState, RDFNode statementObject, boolean isModifier) {
980                logger.trace("Entering processExtension with state: {}", parserState);
981                Resource resource = statementObject.asResource();
982                Statement urlProperty = resource.getProperty(resource.getModel().createProperty(FHIR_NS + EXTENSION_URL));
983                Resource urlPropertyResource = urlProperty.getObject().asResource();
984                String extensionUrl = urlPropertyResource
985                                .getProperty(resource.getModel().createProperty(FHIR_NS + VALUE))
986                                .getObject()
987                                .asLiteral()
988                                .getString();
989
990                List<Statement> extensionStatements = resource.listProperties().toList();
991                String extensionValueType = null;
992                RDFNode extensionValueResource = null;
993                for (Statement statement : extensionStatements) {
994                        String propertyUri = statement.getPredicate().getURI();
995                        if (propertyUri.contains("Extension.value")) {
996                                extensionValueType = propertyUri.replace(FHIR_NS + "Extension.", "");
997                                BaseRuntimeElementDefinition<?> target = getContext()
998                                                .getRuntimeChildUndeclaredExtensionDefinition()
999                                                .getChildByName(extensionValueType);
1000                                if (target.getChildType().equals(ID_DATATYPE)
1001                                                || target.getChildType().equals(PRIMITIVE_DATATYPE)) {
1002                                        extensionValueResource = statement
1003                                                        .getObject()
1004                                                        .asResource()
1005                                                        .getProperty(resource.getModel().createProperty(FHIR_NS + VALUE))
1006                                                        .getObject()
1007                                                        .asLiteral();
1008                                } else {
1009                                        extensionValueResource = statement.getObject().asResource();
1010                                }
1011                                break;
1012                        }
1013                }
1014
1015                parserState.enteringNewElementExtension(null, extensionUrl, isModifier, null);
1016                // Some extensions don't have their own values - they then have more extensions inside of them
1017                if (extensionValueType != null) {
1018                        parseResource(parserState, extensionValueType, extensionValueResource);
1019                }
1020
1021                for (Statement statement : extensionStatements) {
1022                        String propertyUri = statement.getPredicate().getURI();
1023                        if (propertyUri.equals(FHIR_NS + ELEMENT_EXTENSION)) {
1024                                processExtension(parserState, statement.getObject(), false);
1025                        }
1026                }
1027
1028                parserState.endingElement();
1029        }
1030
1031        static class FhirIndexStatementComparator implements Comparator<Statement> {
1032
1033                @Override
1034                public int compare(Statement arg0, Statement arg1) {
1035                        int result =
1036                                        arg0.getPredicate().getURI().compareTo(arg1.getPredicate().getURI());
1037                        if (result == 0) {
1038                                if (arg0.getObject().isResource() && arg1.getObject().isResource()) {
1039                                        Resource resource0 = arg0.getObject().asResource();
1040                                        Resource resource1 = arg1.getObject().asResource();
1041
1042                                        result = Integer.compare(getFhirIndex(resource0), getFhirIndex(resource1));
1043                                }
1044                        }
1045                        return result;
1046                }
1047
1048                private int getFhirIndex(Resource resource) {
1049                        if (resource.hasProperty(resource.getModel().createProperty(FHIR_NS + FHIR_INDEX))) {
1050                                return resource.getProperty(resource.getModel().createProperty(FHIR_NS + FHIR_INDEX))
1051                                                .getInt();
1052                        }
1053                        return -1;
1054                }
1055        }
1056}