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