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