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