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, encodeContext);
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 theEncodeContext,
349                        final Integer cardinalityIndex) {
350
351                String childGenericName = childDefinition.getElementName();
352
353                theEncodeContext.pushPath(childGenericName, false);
354                try {
355
356                        if (element == null || element.isEmpty()) {
357                                if (!isChildContained(childDef, includedResource, theEncodeContext)) {
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, theEncodeContext),
416                                                                                                theEncodeContext);
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, theEncodeContext);
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, theEncodeContext);
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                                                                theEncodeContext,
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, theEncodeContext)) {
478                                                break;
479                                        }
480                                        theEncodeContext.pushPath(resourceName, true);
481                                        IIdType resourceId = processResourceID(resource, theEncodeContext);
482                                        encodeResourceToRDFStreamWriter(
483                                                        resource, rdfModel, false, resourceId, theEncodeContext, false, null);
484                                        theEncodeContext.popPath();
485                                        break;
486                                }
487                                case PRIMITIVE_XHTML:
488                                case PRIMITIVE_XHTML_HL7ORG: {
489                                        IBaseXhtml xHtmlNode = (IBaseXhtml) element;
490                                        if (xHtmlNode != null) {
491                                                String value = xHtmlNode.getValueAsString();
492                                                String propertyName =
493                                                                constructPredicateName(resource, childDefinition, childName, parentElement);
494                                                rdfResource.addProperty(rdfModel.createProperty(propertyName), value);
495                                        }
496                                        break;
497                                }
498                                case EXTENSION_DECLARED:
499                                case UNDECL_EXT:
500                                default: {
501                                        throw new IllegalStateException(
502                                                        Msg.code(1847) + "Unexpected node - should not happen: " + childDef.getName());
503                                }
504                        }
505                } finally {
506                        theEncodeContext.popPath();
507                }
508
509                return rdfModel;
510        }
511
512        /**
513         * Maps hapi internal fhirType attribute to XSDDatatype enumeration
514         * @param fhirType hapi field type
515         * @return XSDDatatype value
516         */
517        private XSDDatatype getXSDDataTypeForFhirType(String fhirType, String value) {
518                switch (fhirType) {
519                        case "boolean":
520                                return XSDDatatype.XSDboolean;
521                        case "uri":
522                                return XSDDatatype.XSDanyURI;
523                        case "decimal":
524                                return XSDDatatype.XSDdecimal;
525                        case "date":
526                                return XSDDatatype.XSDdate;
527                        case "dateTime":
528                        case "instant":
529                                switch (value.length()) { // assumes valid lexical value
530                                        case 4:
531                                                return XSDDatatype.XSDgYear;
532                                        case 7:
533                                                return XSDDatatype.XSDgYearMonth;
534                                        case 10:
535                                                return XSDDatatype.XSDdate;
536                                        default:
537                                                return XSDDatatype.XSDdateTime;
538                                }
539                        case "code":
540                        case "string":
541                        default:
542                                return XSDDatatype.XSDstring;
543                }
544        }
545
546        private IIdType processResourceID(final IBaseResource resource, final EncodeContext encodeContext) {
547                IIdType resourceId = null;
548
549                if (StringUtils.isNotBlank(resource.getIdElement().getIdPart())) {
550                        resourceId = resource.getIdElement();
551                        if (resource.getIdElement().getValue().startsWith("urn:")) {
552                                resourceId = null;
553                        }
554                }
555
556                if (!super.shouldEncodeResourceId(resource, encodeContext)) {
557                        resourceId = null;
558                } else if (encodeContext.getResourcePath().size() == 1 && super.getEncodeForceResourceId() != null) {
559                        resourceId = super.getEncodeForceResourceId();
560                }
561
562                return resourceId;
563        }
564
565        private Model encodeExtension(
566                        final IBaseResource resource,
567                        Model rdfModel,
568                        Resource rdfResource,
569                        final boolean containedResource,
570                        final CompositeChildElement nextChildElem,
571                        final BaseRuntimeChildDefinition nextChild,
572                        final IBase nextValue,
573                        final String childName,
574                        final BaseRuntimeElementDefinition<?> childDef,
575                        final EncodeContext encodeContext,
576                        Integer cardinalityIndex) {
577                BaseRuntimeDeclaredChildDefinition extDef = (BaseRuntimeDeclaredChildDefinition) nextChild;
578
579                Resource childResource = rdfModel.createResource();
580                String extensionPredicateName = constructPredicateName(resource, extDef, extDef.getElementName(), null);
581                rdfResource.addProperty(rdfModel.createProperty(extensionPredicateName), childResource);
582                if (cardinalityIndex != null && cardinalityIndex > -1) {
583                        childResource.addProperty(
584                                        rdfModel.createProperty(FHIR_NS + FHIR_INDEX), cardinalityIndex.toString(), XSDDatatype.XSDinteger);
585                }
586
587                rdfModel = encodeChildElementToStreamWriter(
588                                resource,
589                                null,
590                                rdfModel,
591                                childResource,
592                                nextChild,
593                                nextValue,
594                                childName,
595                                childDef,
596                                containedResource,
597                                nextChildElem,
598                                encodeContext,
599                                cardinalityIndex);
600
601                return rdfModel;
602        }
603
604        private Model encodeCompositeElementToStreamWriter(
605                        final IBaseResource resource,
606                        final IBase element,
607                        Model rdfModel,
608                        Resource rdfResource,
609                        final boolean containedResource,
610                        final CompositeChildElement parent,
611                        final EncodeContext encodeContext) {
612
613                for (CompositeChildElement nextChildElem :
614                                super.compositeChildIterator(element, containedResource, parent, encodeContext)) {
615
616                        BaseRuntimeChildDefinition nextChild = nextChildElem.getDef();
617
618                        if (nextChild instanceof RuntimeChildNarrativeDefinition) {
619                                INarrativeGenerator gen = getContext().getNarrativeGenerator();
620                                if (gen != null) {
621                                        INarrative narrative;
622                                        if (resource instanceof IResource) {
623                                                narrative = ((IResource) resource).getText();
624                                        } else if (resource instanceof IDomainResource) {
625                                                narrative = ((IDomainResource) resource).getText();
626                                        } else {
627                                                narrative = null;
628                                        }
629                                        assert narrative != null;
630                                        if (narrative.isEmpty()) {
631                                                gen.populateResourceNarrative(getContext(), resource);
632                                        } else {
633                                                RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;
634
635                                                // This is where we populate the parent of the narrative
636                                                Resource childResource = rdfModel.createResource();
637
638                                                String propertyName = constructPredicateName(resource, child, child.getElementName(), element);
639                                                rdfResource.addProperty(rdfModel.createProperty(propertyName), childResource);
640
641                                                String childName = nextChild.getChildNameByDatatype(child.getDatatype());
642                                                BaseRuntimeElementDefinition<?> type = child.getChildByName(childName);
643                                                rdfModel = encodeChildElementToStreamWriter(
644                                                                resource,
645                                                                element,
646                                                                rdfModel,
647                                                                childResource,
648                                                                nextChild,
649                                                                narrative,
650                                                                childName,
651                                                                type,
652                                                                containedResource,
653                                                                nextChildElem,
654                                                                encodeContext,
655                                                                null);
656                                                continue;
657                                        }
658                                }
659                        }
660
661                        if (nextChild instanceof RuntimeChildDirectResource) {
662
663                                List<? extends IBase> values = nextChild.getAccessor().getValues(element);
664                                if (values == null || values.isEmpty()) {
665                                        continue;
666                                }
667
668                                IBaseResource directChildResource = (IBaseResource) values.get(0);
669                                // If it is a direct resource, we need to create a new subject for it.
670                                Resource childResource = encodeResourceToRDFStreamWriter(
671                                                directChildResource,
672                                                rdfModel,
673                                                false,
674                                                directChildResource.getIdElement(),
675                                                encodeContext,
676                                                false,
677                                                null);
678                                String propertyName = constructPredicateName(resource, nextChild, nextChild.getElementName(), element);
679                                rdfResource.addProperty(rdfModel.createProperty(propertyName), childResource);
680
681                                continue;
682                        }
683
684                        if (nextChild instanceof RuntimeChildContainedResources) {
685                                List<? extends IBase> values = nextChild.getAccessor().getValues(element);
686                                int i = 0;
687                                for (IBase containedResourceEntity : values) {
688                                        rdfModel = encodeChildElementToStreamWriter(
689                                                        resource,
690                                                        element,
691                                                        rdfModel,
692                                                        rdfResource,
693                                                        nextChild,
694                                                        containedResourceEntity,
695                                                        nextChild.getChildNameByDatatype(null),
696                                                        nextChild.getChildElementDefinitionByDatatype(null),
697                                                        containedResource,
698                                                        nextChildElem,
699                                                        encodeContext,
700                                                        i);
701                                        i++;
702                                }
703                        } else {
704
705                                List<? extends IBase> values = nextChild.getAccessor().getValues(element);
706                                values = super.preProcessValues(nextChild, resource, values, nextChildElem, encodeContext);
707
708                                if (values == null || values.isEmpty()) {
709                                        continue;
710                                }
711
712                                Integer cardinalityIndex = null;
713                                int indexCounter = 0;
714
715                                for (IBase nextValue : values) {
716                                        if (nextChild.getMax() != 1) {
717                                                cardinalityIndex = indexCounter;
718                                                indexCounter++;
719                                        }
720                                        if ((nextValue == null || nextValue.isEmpty())) {
721                                                continue;
722                                        }
723
724                                        ChildNameAndDef childNameAndDef = super.getChildNameAndDef(nextChild, nextValue);
725                                        if (childNameAndDef == null) {
726                                                continue;
727                                        }
728
729                                        String childName = childNameAndDef.getChildName();
730                                        BaseRuntimeElementDefinition<?> childDef = childNameAndDef.getChildDef();
731                                        String extensionUrl = getExtensionUrl(nextChild.getExtensionUrl());
732
733                                        if (extensionUrl != null && !childName.equals(EXTENSION)) {
734                                                rdfModel = encodeExtension(
735                                                                resource,
736                                                                rdfModel,
737                                                                rdfResource,
738                                                                containedResource,
739                                                                nextChildElem,
740                                                                nextChild,
741                                                                nextValue,
742                                                                childName,
743                                                                childDef,
744                                                                encodeContext,
745                                                                cardinalityIndex);
746                                        } else if (nextChild instanceof RuntimeChildExtension) {
747                                                IBaseExtension<?, ?> extension = (IBaseExtension<?, ?>) nextValue;
748                                                if ((extension.getValue() == null
749                                                                || extension.getValue().isEmpty())) {
750                                                        if (extension.getExtension().isEmpty()) {
751                                                                continue;
752                                                        }
753                                                }
754                                                rdfModel = encodeExtension(
755                                                                resource,
756                                                                rdfModel,
757                                                                rdfResource,
758                                                                containedResource,
759                                                                nextChildElem,
760                                                                nextChild,
761                                                                nextValue,
762                                                                childName,
763                                                                childDef,
764                                                                encodeContext,
765                                                                cardinalityIndex);
766                                        } else if (!(nextChild instanceof RuntimeChildNarrativeDefinition) || !containedResource) {
767
768                                                // If the child is not a value type, create a child object (blank node) for subordinate
769                                                // predicates to be attached to
770                                                if (childDef.getChildType() != PRIMITIVE_DATATYPE
771                                                                && childDef.getChildType() != PRIMITIVE_XHTML_HL7ORG
772                                                                && childDef.getChildType() != PRIMITIVE_XHTML
773                                                                && childDef.getChildType() != ID_DATATYPE) {
774                                                        Resource childResource = rdfModel.createResource();
775
776                                                        String propertyName = constructPredicateName(resource, nextChild, childName, nextValue);
777                                                        rdfResource.addProperty(rdfModel.createProperty(propertyName), childResource);
778                                                        if (cardinalityIndex != null && cardinalityIndex > -1) {
779                                                                childResource.addProperty(
780                                                                                rdfModel.createProperty(FHIR_NS + FHIR_INDEX),
781                                                                                cardinalityIndex.toString(),
782                                                                                XSDDatatype.XSDinteger);
783                                                        }
784                                                        rdfModel = encodeChildElementToStreamWriter(
785                                                                        resource,
786                                                                        element,
787                                                                        rdfModel,
788                                                                        childResource,
789                                                                        nextChild,
790                                                                        nextValue,
791                                                                        childName,
792                                                                        childDef,
793                                                                        containedResource,
794                                                                        nextChildElem,
795                                                                        encodeContext,
796                                                                        cardinalityIndex);
797                                                } else {
798                                                        rdfModel = encodeChildElementToStreamWriter(
799                                                                        resource,
800                                                                        element,
801                                                                        rdfModel,
802                                                                        rdfResource,
803                                                                        nextChild,
804                                                                        nextValue,
805                                                                        childName,
806                                                                        childDef,
807                                                                        containedResource,
808                                                                        nextChildElem,
809                                                                        encodeContext,
810                                                                        cardinalityIndex);
811                                                }
812                                        }
813                                }
814                        }
815                }
816                return rdfModel;
817        }
818
819        private <T extends IBaseResource> T parseResource(Class<T> resourceType, Model rdfModel) {
820                // jsonMode of true is passed in so that the xhtml parser state behaves as expected
821                // Push PreResourceState
822                ParserState<T> parserState =
823                                ParserState.getPreResourceInstance(this, resourceType, getContext(), true, getErrorHandler());
824                return parseRootResource(rdfModel, parserState, resourceType);
825        }
826
827        private <T> T parseRootResource(Model rdfModel, ParserState<T> parserState, Class<T> resourceType) {
828                logger.trace("Entering parseRootResource with state: {}", parserState);
829
830                StmtIterator rootStatementIterator = rdfModel.listStatements(
831                                null, rdfModel.getProperty(FHIR_NS + NODE_ROLE), rdfModel.getProperty(FHIR_NS + TREE_ROOT));
832
833                Resource rootResource;
834                String fhirResourceType, fhirTypeString;
835                while (rootStatementIterator.hasNext()) {
836                        Statement rootStatement = rootStatementIterator.next();
837                        rootResource = rootStatement.getSubject();
838
839                        // If a resourceType is not provided via the server framework, discern it based on the rdf:type Arc
840                        if (resourceType == null) {
841                                Statement resourceTypeStatement = rootResource.getProperty(RDF.type);
842                                fhirTypeString = resourceTypeStatement.getObject().toString();
843                                if (fhirTypeString.startsWith(FHIR_NS)) {
844                                        fhirTypeString = fhirTypeString.replace(FHIR_NS, "");
845                                }
846                        } else {
847                                fhirTypeString = resourceType.getSimpleName();
848                        }
849
850                        RuntimeResourceDefinition definition = getContext().getResourceDefinition(fhirTypeString);
851                        fhirResourceType = definition.getName();
852
853                        parseResource(parserState, fhirResourceType, rootResource);
854
855                        // Pop PreResourceState
856                        parserState.endingElement();
857                }
858                return parserState.getObject();
859        }
860
861        private <T> void parseResource(ParserState<T> parserState, String resourceType, RDFNode rootNode) {
862                // Push top-level entity
863                parserState.enteringNewElement(FHIR_NS, resourceType);
864
865                if (rootNode instanceof Resource) {
866                        Resource rootResource = rootNode.asResource();
867                        List<Statement> statements = rootResource.listProperties().toList();
868                        statements.sort(new FhirIndexStatementComparator());
869                        for (Statement statement : statements) {
870                                String predicateAttributeName = extractAttributeNameFromPredicate(statement);
871                                if (predicateAttributeName != null) {
872                                        if (predicateAttributeName.equals(MODIFIER_EXTENSION)) {
873                                                processExtension(parserState, statement.getObject(), true);
874                                        } else if (predicateAttributeName.equals(EXTENSION)) {
875                                                processExtension(parserState, statement.getObject(), false);
876                                        } else {
877                                                processStatementObject(parserState, predicateAttributeName, statement.getObject());
878                                        }
879                                }
880                        }
881                } else if (rootNode instanceof Literal) {
882                        parserState.attributeValue(VALUE, rootNode.asLiteral().getString());
883                }
884
885                // Pop top-level entity
886                parserState.endingElement();
887        }
888
889        private String extractAttributeNameFromPredicate(Statement statement) {
890                String predicateUri = statement.getPredicate().getURI();
891
892                // If the predicateURI is one we're ignoring, return null
893                // This minimizes 'Unknown Element' warnings in the parsing process
894                if (ignoredPredicates.contains(predicateUri)) {
895                        return null;
896                }
897
898                String predicateObjectAttribute = predicateUri.substring(predicateUri.lastIndexOf("/") + 1);
899                String predicateAttributeName;
900                if (predicateObjectAttribute.contains(".")) {
901                        predicateAttributeName = predicateObjectAttribute.substring(predicateObjectAttribute.lastIndexOf(".") + 1);
902                } else {
903                        predicateAttributeName = predicateObjectAttribute;
904                }
905                return predicateAttributeName;
906        }
907
908        private <T> void processStatementObject(
909                        ParserState<T> parserState, String predicateAttributeName, RDFNode statementObject) {
910                logger.trace(
911                                "Entering processStatementObject with state: {}, for attribute {}",
912                                parserState,
913                                predicateAttributeName);
914                // Push attribute element
915                parserState.enteringNewElement(FHIR_NS, predicateAttributeName);
916
917                if (statementObject != null) {
918                        if (statementObject.isLiteral()) {
919                                // If the object is a literal, apply the value directly
920                                parserState.attributeValue(VALUE, statementObject.asLiteral().getLexicalForm());
921                        } else if (statementObject.isAnon()) {
922                                // If the object is a blank node,
923                                Resource resourceObject = statementObject.asResource();
924
925                                boolean containedResource = false;
926                                if (predicateAttributeName.equals(CONTAINED)) {
927                                        containedResource = true;
928                                        parserState.enteringNewElement(
929                                                        FHIR_NS,
930                                                        resourceObject
931                                                                        .getProperty(resourceObject.getModel().createProperty(RDF.type.getURI()))
932                                                                        .getObject()
933                                                                        .toString()
934                                                                        .replace(FHIR_NS, ""));
935                                }
936
937                                List<Statement> objectStatements =
938                                                resourceObject.listProperties().toList();
939                                objectStatements.sort(new FhirIndexStatementComparator());
940                                for (Statement objectProperty : objectStatements) {
941                                        if (objectProperty.getPredicate().hasURI(FHIR_NS + VALUE)) {
942                                                predicateAttributeName = VALUE;
943                                                parserState.attributeValue(
944                                                                predicateAttributeName,
945                                                                objectProperty.getObject().asLiteral().getLexicalForm());
946                                        } else {
947                                                // Otherwise, process it as a net-new node
948                                                predicateAttributeName = extractAttributeNameFromPredicate(objectProperty);
949                                                if (predicateAttributeName != null) {
950                                                        if (predicateAttributeName.equals(EXTENSION)) {
951                                                                processExtension(parserState, objectProperty.getObject(), false);
952                                                        } else if (predicateAttributeName.equals(MODIFIER_EXTENSION)) {
953                                                                processExtension(parserState, objectProperty.getObject(), true);
954                                                        } else {
955                                                                processStatementObject(parserState, predicateAttributeName, objectProperty.getObject());
956                                                        }
957                                                }
958                                        }
959                                }
960
961                                if (containedResource) {
962                                        // Leave the contained resource element we created
963                                        parserState.endingElement();
964                                }
965                        } else if (statementObject.isResource()) {
966                                Resource innerResource = statementObject.asResource();
967                                Statement resourceTypeStatement = innerResource.getProperty(RDF.type);
968                                String fhirTypeString = resourceTypeStatement.getObject().toString();
969                                if (fhirTypeString.startsWith(FHIR_NS)) {
970                                        fhirTypeString = fhirTypeString.replace(FHIR_NS, "");
971                                }
972                                parseResource(parserState, fhirTypeString, innerResource);
973                        }
974                }
975
976                // Pop attribute element
977                parserState.endingElement();
978        }
979
980        private <T> void processExtension(ParserState<T> parserState, RDFNode statementObject, boolean isModifier) {
981                logger.trace("Entering processExtension with state: {}", parserState);
982                Resource resource = statementObject.asResource();
983                Statement urlProperty = resource.getProperty(resource.getModel().createProperty(FHIR_NS + EXTENSION_URL));
984                Resource urlPropertyResource = urlProperty.getObject().asResource();
985                String extensionUrl = urlPropertyResource
986                                .getProperty(resource.getModel().createProperty(FHIR_NS + VALUE))
987                                .getObject()
988                                .asLiteral()
989                                .getString();
990
991                List<Statement> extensionStatements = resource.listProperties().toList();
992                String extensionValueType = null;
993                RDFNode extensionValueResource = null;
994                for (Statement statement : extensionStatements) {
995                        String propertyUri = statement.getPredicate().getURI();
996                        if (propertyUri.contains("Extension.value")) {
997                                extensionValueType = propertyUri.replace(FHIR_NS + "Extension.", "");
998                                BaseRuntimeElementDefinition<?> target = getContext()
999                                                .getRuntimeChildUndeclaredExtensionDefinition()
1000                                                .getChildByName(extensionValueType);
1001                                if (target.getChildType().equals(ID_DATATYPE)
1002                                                || target.getChildType().equals(PRIMITIVE_DATATYPE)) {
1003                                        extensionValueResource = statement
1004                                                        .getObject()
1005                                                        .asResource()
1006                                                        .getProperty(resource.getModel().createProperty(FHIR_NS + VALUE))
1007                                                        .getObject()
1008                                                        .asLiteral();
1009                                } else {
1010                                        extensionValueResource = statement.getObject().asResource();
1011                                }
1012                                break;
1013                        }
1014                }
1015
1016                parserState.enteringNewElementExtension(null, extensionUrl, isModifier, null);
1017                // Some extensions don't have their own values - they then have more extensions inside of them
1018                if (extensionValueType != null) {
1019                        parseResource(parserState, extensionValueType, extensionValueResource);
1020                }
1021
1022                for (Statement statement : extensionStatements) {
1023                        String propertyUri = statement.getPredicate().getURI();
1024                        if (propertyUri.equals(FHIR_NS + ELEMENT_EXTENSION)) {
1025                                processExtension(parserState, statement.getObject(), false);
1026                        }
1027                }
1028
1029                parserState.endingElement();
1030        }
1031
1032        static class FhirIndexStatementComparator implements Comparator<Statement> {
1033
1034                @Override
1035                public int compare(Statement arg0, Statement arg1) {
1036                        int result =
1037                                        arg0.getPredicate().getURI().compareTo(arg1.getPredicate().getURI());
1038                        if (result == 0) {
1039                                if (arg0.getObject().isResource() && arg1.getObject().isResource()) {
1040                                        Resource resource0 = arg0.getObject().asResource();
1041                                        Resource resource1 = arg1.getObject().asResource();
1042
1043                                        result = Integer.compare(getFhirIndex(resource0), getFhirIndex(resource1));
1044                                }
1045                        }
1046                        return result;
1047                }
1048
1049                private int getFhirIndex(Resource resource) {
1050                        if (resource.hasProperty(resource.getModel().createProperty(FHIR_NS + FHIR_INDEX))) {
1051                                return resource.getProperty(resource.getModel().createProperty(FHIR_NS + FHIR_INDEX))
1052                                                .getInt();
1053                        }
1054                        return -1;
1055                }
1056        }
1057}