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