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