001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 * http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.parser;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeDeclaredChildDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
025import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
026import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum;
027import ca.uhn.fhir.context.ConfigurationException;
028import ca.uhn.fhir.context.FhirContext;
029import ca.uhn.fhir.context.FhirVersionEnum;
030import ca.uhn.fhir.context.ParserOptions;
031import ca.uhn.fhir.context.RuntimeChildChoiceDefinition;
032import ca.uhn.fhir.context.RuntimeChildContainedResources;
033import ca.uhn.fhir.context.RuntimeChildNarrativeDefinition;
034import ca.uhn.fhir.context.RuntimeResourceDefinition;
035import ca.uhn.fhir.i18n.Msg;
036import ca.uhn.fhir.model.api.IIdentifiableElement;
037import ca.uhn.fhir.model.api.IResource;
038import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions;
039import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
040import ca.uhn.fhir.model.api.Tag;
041import ca.uhn.fhir.model.api.TagList;
042import ca.uhn.fhir.parser.path.EncodeContextPath;
043import ca.uhn.fhir.rest.api.Constants;
044import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
045import ca.uhn.fhir.util.BundleUtil;
046import ca.uhn.fhir.util.CollectionUtil;
047import ca.uhn.fhir.util.FhirTerser;
048import ca.uhn.fhir.util.MetaUtil;
049import ca.uhn.fhir.util.ResourceUtil;
050import ca.uhn.fhir.util.UrlUtil;
051import com.google.common.base.Charsets;
052import jakarta.annotation.Nullable;
053import org.apache.commons.io.IOUtils;
054import org.apache.commons.io.output.StringBuilderWriter;
055import org.apache.commons.lang3.StringUtils;
056import org.apache.commons.lang3.Validate;
057import org.apache.commons.lang3.tuple.Pair;
058import org.hl7.fhir.instance.model.api.IBase;
059import org.hl7.fhir.instance.model.api.IBaseBundle;
060import org.hl7.fhir.instance.model.api.IBaseCoding;
061import org.hl7.fhir.instance.model.api.IBaseElement;
062import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
063import org.hl7.fhir.instance.model.api.IBaseHasModifierExtensions;
064import org.hl7.fhir.instance.model.api.IBaseMetaType;
065import org.hl7.fhir.instance.model.api.IBaseReference;
066import org.hl7.fhir.instance.model.api.IBaseResource;
067import org.hl7.fhir.instance.model.api.IIdType;
068import org.hl7.fhir.instance.model.api.IPrimitiveType;
069
070import java.io.IOException;
071import java.io.InputStream;
072import java.io.InputStreamReader;
073import java.io.Reader;
074import java.io.StringReader;
075import java.io.Writer;
076import java.lang.reflect.Modifier;
077import java.util.ArrayList;
078import java.util.Arrays;
079import java.util.Collection;
080import java.util.Collections;
081import java.util.HashMap;
082import java.util.HashSet;
083import java.util.List;
084import java.util.Map;
085import java.util.Objects;
086import java.util.Set;
087import java.util.stream.Collectors;
088
089import static org.apache.commons.lang3.StringUtils.isBlank;
090import static org.apache.commons.lang3.StringUtils.isNotBlank;
091import static org.apache.commons.lang3.StringUtils.startsWith;
092
093@SuppressWarnings("WeakerAccess")
094public abstract class BaseParser implements IParser {
095
096        /**
097         * Any resources that were created by the parser (i.e. by parsing a serialized resource) will have
098         * a {@link IBaseResource#getUserData(String) user data} property with this key.
099         *
100         * @since 5.0.0
101         */
102        public static final String RESOURCE_CREATED_BY_PARSER =
103                        BaseParser.class.getName() + "_" + "RESOURCE_CREATED_BY_PARSER";
104
105        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseParser.class);
106
107        private static final Set<String> notEncodeForContainedResource =
108                        new HashSet<>(Arrays.asList("security", "versionId", "lastUpdated"));
109
110        private boolean myEncodeElementsAppliesToChildResourcesOnly;
111        private final FhirContext myContext;
112        private Collection<String> myDontEncodeElements;
113        private Collection<String> myEncodeElements;
114        private IIdType myEncodeForceResourceId;
115        private IParserErrorHandler myErrorHandler;
116        private boolean myOmitResourceId;
117        private List<Class<? extends IBaseResource>> myPreferTypes;
118        private String myServerBaseUrl;
119        private Boolean myStripVersionsFromReferences;
120        private Boolean myOverrideResourceIdWithBundleEntryFullUrl;
121        private boolean mySummaryMode;
122        private boolean mySuppressNarratives;
123        private Set<String> myDontStripVersionsFromReferencesAtPaths;
124        /**
125         * Constructor
126         */
127        public BaseParser(FhirContext theContext, IParserErrorHandler theParserErrorHandler) {
128                myContext = theContext;
129                myErrorHandler = theParserErrorHandler;
130        }
131
132        protected FhirContext getContext() {
133                return myContext;
134        }
135
136        @Override
137        public IParser setDontEncodeElements(Collection<String> theDontEncodeElements) {
138                myDontEncodeElements = theDontEncodeElements;
139                return this;
140        }
141
142        @Override
143        public IParser setEncodeElements(Set<String> theEncodeElements) {
144                myEncodeElements = theEncodeElements;
145                return this;
146        }
147
148        protected Iterable<CompositeChildElement> compositeChildIterator(
149                        IBase theCompositeElement,
150                        final boolean theContainedResource,
151                        final CompositeChildElement theParent,
152                        EncodeContext theEncodeContext) {
153                BaseRuntimeElementCompositeDefinition<?> elementDef = (BaseRuntimeElementCompositeDefinition<?>)
154                                myContext.getElementDefinition(theCompositeElement.getClass());
155                return theEncodeContext
156                                .getCompositeChildrenCache()
157                                .computeIfAbsent(new Key(elementDef, theContainedResource, theParent, theEncodeContext), (k) -> {
158                                        final List<BaseRuntimeChildDefinition> children = elementDef.getChildrenAndExtension();
159                                        final List<CompositeChildElement> result = new ArrayList<>(children.size());
160
161                                        for (final BaseRuntimeChildDefinition child : children) {
162                                                CompositeChildElement myNext = new CompositeChildElement(theParent, child, theEncodeContext);
163
164                                                /*
165                                                 * There are lots of reasons we might skip encoding a particular child
166                                                 */
167                                                if (myNext.getDef().getElementName().equals("id")) {
168                                                        continue;
169                                                } else if (!myNext.shouldBeEncoded(theContainedResource)) {
170                                                        continue;
171                                                } else if (myNext.getDef() instanceof RuntimeChildNarrativeDefinition) {
172                                                        if (isSuppressNarratives() || isSummaryMode()) {
173                                                                continue;
174                                                        }
175                                                } else if (myNext.getDef() instanceof RuntimeChildContainedResources) {
176                                                        if (theContainedResource) {
177                                                                continue;
178                                                        }
179                                                }
180                                                result.add(myNext);
181                                        }
182                                        return result;
183                                });
184        }
185
186        private String determineReferenceText(
187                        IBaseReference theRef,
188                        CompositeChildElement theCompositeChildElement,
189                        IBaseResource theResource,
190                        EncodeContext theContext) {
191                IIdType ref = theRef.getReferenceElement();
192                if (isBlank(ref.getIdPart())) {
193                        String reference = ref.getValue();
194                        if (theRef.getResource() != null) {
195                                IIdType containedId = theContext.getContainedResources().getResourceId(theRef.getResource());
196                                IIdType previouslyContainedId =
197                                                theContext.getContainedResources().getPreviouslyContainedResourceId(theRef.getResource());
198                                if (containedId != null && !containedId.isEmpty()) {
199                                        if (containedId.isLocal()) {
200                                                reference = containedId.getValue();
201                                        } else {
202                                                reference = "#" + containedId.getValue();
203                                        }
204                                } else if (previouslyContainedId != null) {
205                                        reference = "#" + previouslyContainedId.getValue();
206                                        theContext.getContainedResources().addContained(previouslyContainedId, theRef.getResource());
207                                } else {
208                                        IIdType refId = theRef.getResource().getIdElement();
209                                        if (refId != null) {
210                                                if (refId.hasIdPart()) {
211                                                        if (refId.getValue().startsWith("urn:")) {
212                                                                reference = refId.getValue();
213                                                        } else {
214                                                                if (!refId.hasResourceType()) {
215                                                                        refId = refId.withResourceType(myContext
216                                                                                        .getResourceDefinition(theRef.getResource())
217                                                                                        .getName());
218                                                                }
219                                                                if (isStripVersionsFromReferences(theCompositeChildElement, theResource)) {
220                                                                        reference = refId.toVersionless().getValue();
221                                                                } else {
222                                                                        reference = refId.getValue();
223                                                                }
224                                                        }
225                                                }
226                                        }
227                                }
228                        }
229                        return reference;
230                }
231                if (!ref.hasResourceType() && !ref.isLocal() && theRef.getResource() != null) {
232                        ref = ref.withResourceType(
233                                        myContext.getResourceDefinition(theRef.getResource()).getName());
234                }
235                if (isNotBlank(myServerBaseUrl) && StringUtils.equals(myServerBaseUrl, ref.getBaseUrl())) {
236                        if (isStripVersionsFromReferences(theCompositeChildElement, theResource)) {
237                                return ref.toUnqualifiedVersionless().getValue();
238                        }
239                        return ref.toUnqualified().getValue();
240                }
241                if (isStripVersionsFromReferences(theCompositeChildElement, theResource)) {
242                        return ref.toVersionless().getValue();
243                }
244                return ref.getValue();
245        }
246
247        protected abstract void doEncodeResourceToWriter(
248                        IBaseResource theResource, Writer theWriter, EncodeContext theEncodeContext)
249                        throws IOException, DataFormatException;
250
251        protected void doEncodeToWriter(IBase theElement, Writer theWriter, EncodeContext theEncodeContext)
252                        throws IOException, DataFormatException {
253                throw new InternalErrorException(Msg.code(2363) + "This parser does not support encoding non-resource values");
254        }
255
256        protected abstract <T extends IBaseResource> T doParseResource(Class<T> theResourceType, Reader theReader)
257                        throws DataFormatException;
258
259        @Override
260        public String encodeResourceToString(IBaseResource theResource) throws DataFormatException {
261                Writer stringWriter = new StringBuilderWriter();
262                try {
263                        encodeResourceToWriter(theResource, stringWriter);
264                } catch (IOException e) {
265                        throw new Error(
266                                        Msg.code(1828) + "Encountered IOException during write to string - This should not happen!", e);
267                }
268                return stringWriter.toString();
269        }
270
271        @Override
272        public final void encodeResourceToWriter(IBaseResource theResource, Writer theWriter)
273                        throws IOException, DataFormatException {
274                EncodeContext encodeContext =
275                                new EncodeContext(this, myContext.getParserOptions(), new FhirTerser.ContainedResources());
276                encodeResourceToWriter(theResource, theWriter, encodeContext);
277        }
278
279        @Override
280        public String encodeToString(IBase theElement) throws DataFormatException {
281                Writer stringWriter = new StringBuilderWriter();
282                try {
283                        encodeToWriter(theElement, stringWriter);
284                } catch (IOException e) {
285                        throw new Error(
286                                        Msg.code(2364) + "Encountered IOException during write to string - This should not happen!", e);
287                }
288                return stringWriter.toString();
289        }
290
291        @Override
292        public void encodeToWriter(IBase theElement, Writer theWriter) throws DataFormatException, IOException {
293                if (theElement instanceof IBaseResource) {
294                        encodeResourceToWriter((IBaseResource) theElement, theWriter);
295                } else if (theElement instanceof IPrimitiveType) {
296                        theWriter.write(((IPrimitiveType<?>) theElement).getValueAsString());
297                } else {
298                        EncodeContext encodeContext =
299                                        new EncodeContext(this, myContext.getParserOptions(), new FhirTerser.ContainedResources());
300                        encodeToWriter(theElement, theWriter, encodeContext);
301                }
302        }
303
304        @Override
305        public void parseInto(Reader theSource, IBase theTarget) throws IOException {
306                if (theTarget instanceof IPrimitiveType) {
307                        ((IPrimitiveType<?>) theTarget).setValueAsString(IOUtils.toString(theSource));
308                } else {
309                        doParseIntoComplexStructure(theSource, theTarget);
310                }
311        }
312
313        protected void doParseIntoComplexStructure(Reader theSource, IBase theTarget) {
314                throw new InternalErrorException(
315                                Msg.code(2633) + "This " + getEncoding() + " parser does not support parsing non-resource values");
316        }
317
318        protected void encodeResourceToWriter(IBaseResource theResource, Writer theWriter, EncodeContext theEncodeContext)
319                        throws IOException {
320                Validate.notNull(theResource, "theResource can not be null");
321                Validate.notNull(theWriter, "theWriter can not be null");
322                Validate.notNull(theEncodeContext, "theEncodeContext can not be null");
323
324                if (theResource.getStructureFhirVersionEnum() != myContext.getVersion().getVersion()) {
325                        throw new IllegalArgumentException(Msg.code(1829) + "This parser is for FHIR version "
326                                        + myContext.getVersion().getVersion() + " - Can not encode a structure for version "
327                                        + theResource.getStructureFhirVersionEnum());
328                }
329
330                String resourceName =
331                                myContext.getElementDefinition(theResource.getClass()).getName();
332                theEncodeContext.pushPath(resourceName, true);
333
334                doEncodeResourceToWriter(theResource, theWriter, theEncodeContext);
335
336                theEncodeContext.popPath();
337        }
338
339        protected void encodeToWriter(IBase theElement, Writer theWriter, EncodeContext theEncodeContext)
340                        throws IOException {
341                Validate.notNull(theElement, "theElement can not be null");
342                Validate.notNull(theWriter, "theWriter can not be null");
343                Validate.notNull(theEncodeContext, "theEncodeContext can not be null");
344
345                BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass());
346                String elementName = def.getName();
347                theEncodeContext.pushPath(elementName, true);
348
349                doEncodeToWriter(theElement, theWriter, theEncodeContext);
350
351                theEncodeContext.popPath();
352        }
353
354        private void filterCodingsWithNoCodeOrSystem(List<? extends IBaseCoding> tagList) {
355                for (int i = 0; i < tagList.size(); i++) {
356                        if (isBlank(tagList.get(i).getCode()) && isBlank(tagList.get(i).getSystem())) {
357                                tagList.remove(i);
358                                i--;
359                        }
360                }
361        }
362
363        protected IIdType fixContainedResourceId(String theValue) {
364                IIdType retVal = (IIdType) myContext.getElementDefinition("id").newInstance();
365                if (StringUtils.isNotBlank(theValue) && theValue.charAt(0) == '#') {
366                        retVal.setValue(theValue.substring(1));
367                } else {
368                        retVal.setValue(theValue);
369                }
370                return retVal;
371        }
372
373        @SuppressWarnings("unchecked")
374        ChildNameAndDef getChildNameAndDef(BaseRuntimeChildDefinition theChild, IBase theValue) {
375                Class<? extends IBase> type = theValue.getClass();
376                String childName = theChild.getChildNameByDatatype(type);
377                BaseRuntimeElementDefinition<?> childDef = theChild.getChildElementDefinitionByDatatype(type);
378                if (childDef == null) {
379                        // if (theValue instanceof IBaseExtension) {
380                        // return null;
381                        // }
382
383                        /*
384                         * For RI structures Enumeration class, this replaces the child def
385                         * with the "code" one. This is messy, and presumably there is a better
386                         * way..
387                         */
388                        BaseRuntimeElementDefinition<?> elementDef = myContext.getElementDefinition(type);
389                        if (elementDef.getName().equals("code")) {
390                                Class<? extends IBase> type2 =
391                                                myContext.getElementDefinition("code").getImplementingClass();
392                                childDef = theChild.getChildElementDefinitionByDatatype(type2);
393                                childName = theChild.getChildNameByDatatype(type2);
394                        }
395
396                        // See possibly the user has extended a built-in type without
397                        // declaring it anywhere, as in XmlParserDstu3Test#testEncodeUndeclaredBlock
398                        if (childDef == null) {
399                                Class<?> nextSuperType = theValue.getClass();
400                                while (IBase.class.isAssignableFrom(nextSuperType) && childDef == null) {
401                                        if (Modifier.isAbstract(nextSuperType.getModifiers()) == false) {
402                                                BaseRuntimeElementDefinition<?> def =
403                                                                myContext.getElementDefinition((Class<? extends IBase>) nextSuperType);
404                                                Class<?> nextChildType = def.getImplementingClass();
405                                                childDef = theChild.getChildElementDefinitionByDatatype((Class<? extends IBase>) nextChildType);
406                                                childName = theChild.getChildNameByDatatype((Class<? extends IBase>) nextChildType);
407                                        }
408                                        nextSuperType = nextSuperType.getSuperclass();
409                                }
410                        }
411
412                        if (childDef == null) {
413                                throwExceptionForUnknownChildType(theChild, type);
414                        }
415                }
416
417                return new ChildNameAndDef(childName, childDef);
418        }
419
420        protected String getCompositeElementId(IBase theElement) {
421                String elementId = null;
422                if (!(theElement instanceof IBaseResource)) {
423                        if (theElement instanceof IBaseElement) {
424                                elementId = ((IBaseElement) theElement).getId();
425                        } else if (theElement instanceof IIdentifiableElement) {
426                                elementId = ((IIdentifiableElement) theElement).getElementSpecificId();
427                        }
428                }
429                return elementId;
430        }
431
432        @Override
433        public Set<String> getDontStripVersionsFromReferencesAtPaths() {
434                return myDontStripVersionsFromReferencesAtPaths;
435        }
436
437        @Override
438        public IIdType getEncodeForceResourceId() {
439                return myEncodeForceResourceId;
440        }
441
442        @Override
443        public BaseParser setEncodeForceResourceId(IIdType theEncodeForceResourceId) {
444                myEncodeForceResourceId = theEncodeForceResourceId;
445                return this;
446        }
447
448        protected IParserErrorHandler getErrorHandler() {
449                return myErrorHandler;
450        }
451
452        protected List<Map.Entry<ResourceMetadataKeyEnum<?>, Object>> getExtensionMetadataKeys(IResource resource) {
453                List<Map.Entry<ResourceMetadataKeyEnum<?>, Object>> extensionMetadataKeys = new ArrayList<>();
454                for (Map.Entry<ResourceMetadataKeyEnum<?>, Object> entry :
455                                resource.getResourceMetadata().entrySet()) {
456                        if (entry.getKey() instanceof ResourceMetadataKeyEnum.ExtensionResourceMetadataKey) {
457                                extensionMetadataKeys.add(entry);
458                        }
459                }
460
461                return extensionMetadataKeys;
462        }
463
464        protected String getExtensionUrl(final String extensionUrl) {
465                String url = extensionUrl;
466                if (StringUtils.isNotBlank(extensionUrl) && StringUtils.isNotBlank(myServerBaseUrl)) {
467                        url = !UrlUtil.isValid(extensionUrl) && extensionUrl.startsWith("/")
468                                        ? myServerBaseUrl + extensionUrl
469                                        : extensionUrl;
470                }
471                return url;
472        }
473
474        protected TagList getMetaTagsForEncoding(IResource theIResource, EncodeContext theEncodeContext) {
475                TagList tags = ResourceMetadataKeyEnum.TAG_LIST.get(theIResource);
476                if (shouldAddSubsettedTag(theEncodeContext)) {
477                        tags = new TagList(tags);
478                        tags.add(new Tag(getSubsettedCodeSystem(), Constants.TAG_SUBSETTED_CODE, subsetDescription()));
479                }
480
481                return tags;
482        }
483
484        @Override
485        public List<Class<? extends IBaseResource>> getPreferTypes() {
486                return myPreferTypes;
487        }
488
489        @Override
490        public void setPreferTypes(List<Class<? extends IBaseResource>> thePreferTypes) {
491                if (thePreferTypes != null) {
492                        ArrayList<Class<? extends IBaseResource>> types = new ArrayList<>();
493                        for (Class<? extends IBaseResource> next : thePreferTypes) {
494                                if (Modifier.isAbstract(next.getModifiers()) == false) {
495                                        types.add(next);
496                                }
497                        }
498                        myPreferTypes = Collections.unmodifiableList(types);
499                } else {
500                        myPreferTypes = thePreferTypes;
501                }
502        }
503
504        @SuppressWarnings("deprecation")
505        protected <T extends IPrimitiveType<String>> List<T> getProfileTagsForEncoding(
506                        IBaseResource theResource, List<T> theProfiles) {
507                switch (myContext.getAddProfileTagWhenEncoding()) {
508                        case NEVER:
509                                return theProfiles;
510                        case ONLY_FOR_CUSTOM:
511                                RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theResource);
512                                if (resDef.isStandardType()) {
513                                        return theProfiles;
514                                }
515                                break;
516                        case ALWAYS:
517                                break;
518                }
519
520                RuntimeResourceDefinition nextDef = myContext.getResourceDefinition(theResource);
521                String profile = nextDef.getResourceProfile(myServerBaseUrl);
522                if (isNotBlank(profile)) {
523                        for (T next : theProfiles) {
524                                if (profile.equals(next.getValue())) {
525                                        return theProfiles;
526                                }
527                        }
528
529                        List<T> newList = new ArrayList<>(theProfiles);
530
531                        BaseRuntimeElementDefinition<?> idElement = myContext.getElementDefinition("id");
532                        @SuppressWarnings("unchecked")
533                        T newId = (T) idElement.newInstance();
534                        newId.setValue(profile);
535
536                        newList.add(newId);
537                        return newList;
538                }
539
540                return theProfiles;
541        }
542
543        protected String getServerBaseUrl() {
544                return myServerBaseUrl;
545        }
546
547        @Override
548        public Boolean getStripVersionsFromReferences() {
549                return myStripVersionsFromReferences;
550        }
551
552        /**
553         * If set to <code>true</code> (default is <code>false</code>), narratives will not be included in the encoded
554         * values.
555         *
556         * @deprecated Use {@link #isSuppressNarratives()}
557         */
558        @Deprecated
559        public boolean getSuppressNarratives() {
560                return mySuppressNarratives;
561        }
562
563        protected boolean isChildContained(
564                        BaseRuntimeElementDefinition<?> childDef, boolean theIncludedResource, EncodeContext theContext) {
565                return (childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCES
566                                                || childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST)
567                                && theContext.getContainedResources().isEmpty() == false
568                                && theIncludedResource == false;
569        }
570
571        @Override
572        public boolean isEncodeElementsAppliesToChildResourcesOnly() {
573                return myEncodeElementsAppliesToChildResourcesOnly;
574        }
575
576        @Override
577        public void setEncodeElementsAppliesToChildResourcesOnly(boolean theEncodeElementsAppliesToChildResourcesOnly) {
578                myEncodeElementsAppliesToChildResourcesOnly = theEncodeElementsAppliesToChildResourcesOnly;
579        }
580
581        @Override
582        public boolean isOmitResourceId() {
583                return myOmitResourceId;
584        }
585
586        private boolean isOverrideResourceIdWithBundleEntryFullUrl() {
587                Boolean overrideResourceIdWithBundleEntryFullUrl = myOverrideResourceIdWithBundleEntryFullUrl;
588                if (overrideResourceIdWithBundleEntryFullUrl != null) {
589                        return overrideResourceIdWithBundleEntryFullUrl;
590                }
591
592                return myContext.getParserOptions().isOverrideResourceIdWithBundleEntryFullUrl();
593        }
594
595        private boolean isStripVersionsFromReferences(
596                        CompositeChildElement theCompositeChildElement, IBaseResource theResource) {
597
598                if (theResource != null) {
599                        Set<String> autoVersionReferencesAtPathExtensions = MetaUtil.getAutoVersionReferencesAtPath(
600                                        theResource.getMeta(), myContext.getResourceType(theResource));
601
602                        if (!autoVersionReferencesAtPathExtensions.isEmpty()
603                                        && theCompositeChildElement.anyPathMatches(autoVersionReferencesAtPathExtensions)) {
604                                return false;
605                        }
606                }
607
608                Boolean stripVersionsFromReferences = myStripVersionsFromReferences;
609                if (stripVersionsFromReferences != null) {
610                        return stripVersionsFromReferences;
611                }
612
613                if (!myContext.getParserOptions().isStripVersionsFromReferences()) {
614                        return false;
615                }
616
617                Set<String> dontStripVersionsFromReferencesAtPaths = getDontStripVersionsFromReferencesAtPaths();
618                if (dontStripVersionsFromReferencesAtPaths != null
619                                && !dontStripVersionsFromReferencesAtPaths.isEmpty()
620                                && theCompositeChildElement.anyPathMatches(dontStripVersionsFromReferencesAtPaths)) {
621                        return false;
622                }
623
624                dontStripVersionsFromReferencesAtPaths =
625                                myContext.getParserOptions().getDontStripVersionsFromReferencesAtPaths();
626                return dontStripVersionsFromReferencesAtPaths.isEmpty()
627                                || !theCompositeChildElement.anyPathMatches(dontStripVersionsFromReferencesAtPaths);
628        }
629
630        @Override
631        public boolean isSummaryMode() {
632                return mySummaryMode;
633        }
634
635        /**
636         * If set to <code>true</code> (default is <code>false</code>), narratives will not be included in the encoded
637         * values.
638         *
639         * @since 1.2
640         */
641        public boolean isSuppressNarratives() {
642                return mySuppressNarratives;
643        }
644
645        @Override
646        public IBaseResource parseResource(InputStream theInputStream) throws DataFormatException {
647                return parseResource(new InputStreamReader(theInputStream, Charsets.UTF_8));
648        }
649
650        @Override
651        public <T extends IBaseResource> T parseResource(Class<T> theResourceType, InputStream theInputStream)
652                        throws DataFormatException {
653                return parseResource(theResourceType, new InputStreamReader(theInputStream, Constants.CHARSET_UTF8));
654        }
655
656        @Override
657        public <T extends IBaseResource> T parseResource(Class<T> theResourceType, Reader theReader)
658                        throws DataFormatException {
659
660                /*
661                 * We do this so that the context can verify that the structure is for
662                 * the correct FHIR version
663                 */
664                Reader readerToUse = theReader;
665                if (theResourceType != null) {
666                        myContext.getResourceDefinition(theResourceType);
667                        if (myContext.isStoreResourceJson()) {
668                                readerToUse = new PreserveStringReader(theReader);
669                        }
670                }
671
672                // Actually do the parse
673                T retVal = doParseResource(theResourceType, readerToUse);
674
675                if (theResourceType != null && myContext.isStoreResourceJson()) {
676                        PreserveStringReader psr = (PreserveStringReader) readerToUse;
677                        if (psr.hasString()) {
678                                try {
679                                        ResourceUtil.addRawDataToResource(retVal, getEncoding(), psr.toString());
680                                        psr.close();
681                                } catch (IOException ex) {
682                                        ourLog.warn(
683                                                        "Unable to store raw JSON on resource. This won't affect functionality, but validation will use the resource itself, which may result in different validation results than a $validation operation.",
684                                                        ex);
685                                }
686                        }
687                }
688
689                RuntimeResourceDefinition def = myContext.getResourceDefinition(retVal);
690                if ("Bundle".equals(def.getName())) {
691
692                        if (isOverrideResourceIdWithBundleEntryFullUrl()) {
693                                BundleUtil.processEntries(myContext, (IBaseBundle) retVal, t -> {
694                                        String fullUrl = t.getFullUrl();
695                                        if (fullUrl != null) {
696                                                IBaseResource resource = t.getResource();
697                                                if (resource != null) {
698                                                        IIdType resourceId = resource.getIdElement();
699                                                        if (isBlank(resourceId.getValue())) {
700                                                                resourceId.setValue(fullUrl);
701                                                        } else {
702                                                                if (fullUrl.startsWith("urn:")
703                                                                                && fullUrl.length()
704                                                                                                > resourceId.getIdPart().length()
705                                                                                && fullUrl.charAt(fullUrl.length()
706                                                                                                                - resourceId.getIdPart().length()
707                                                                                                                - 1)
708                                                                                                == ':'
709                                                                                && fullUrl.endsWith(resourceId.getIdPart())) {
710                                                                        resourceId.setValue(fullUrl);
711                                                                } else {
712                                                                        IIdType fullUrlId = myContext.getVersion().newIdType();
713                                                                        fullUrlId.setValue(fullUrl);
714                                                                        if (myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
715                                                                                IIdType newId = fullUrlId;
716                                                                                if (!newId.hasVersionIdPart() && resourceId.hasVersionIdPart()) {
717                                                                                        newId = newId.withVersion(resourceId.getVersionIdPart());
718                                                                                }
719                                                                                resourceId.setValue(newId.getValue());
720                                                                        } else if (StringUtils.equals(fullUrlId.getIdPart(), resourceId.getIdPart())) {
721                                                                                if (fullUrlId.hasBaseUrl()) {
722                                                                                        IIdType newResourceId = resourceId.withServerBase(
723                                                                                                        fullUrlId.getBaseUrl(), resourceId.getResourceType());
724                                                                                        resourceId.setValue(newResourceId.getValue());
725                                                                                }
726                                                                        }
727                                                                }
728                                                        }
729                                                }
730                                        }
731                                });
732                        }
733                }
734
735                return retVal;
736        }
737
738        @SuppressWarnings("cast")
739        @Override
740        public <T extends IBaseResource> T parseResource(Class<T> theResourceType, String theMessageString) {
741                StringReader reader = new StringReader(theMessageString);
742                return parseResource(theResourceType, reader);
743        }
744
745        @Override
746        public IBaseResource parseResource(Reader theReader) throws ConfigurationException, DataFormatException {
747                return parseResource(null, theReader);
748        }
749
750        @Override
751        public IBaseResource parseResource(String theMessageString) throws ConfigurationException, DataFormatException {
752                return parseResource(null, theMessageString);
753        }
754
755        protected List<? extends IBase> preProcessValues(
756                        BaseRuntimeChildDefinition theMetaChildUncast,
757                        IBaseResource theResource,
758                        List<? extends IBase> theValues,
759                        CompositeChildElement theCompositeChildElement,
760                        EncodeContext theEncodeContext) {
761                if (myContext.getVersion().getVersion().isRi()) {
762
763                        /*
764                         * If we're encoding the meta tag, we do some massaging of the meta values before
765                         * encoding. But if there is no meta element at all, we create one since we're possibly going to be
766                         * adding things to it
767                         */
768                        if (theValues.isEmpty() && theMetaChildUncast.getElementName().equals("meta")) {
769                                BaseRuntimeElementDefinition<?> metaChild = theMetaChildUncast.getChildByName("meta");
770                                if (IBaseMetaType.class.isAssignableFrom(metaChild.getImplementingClass())) {
771                                        IBaseMetaType newType = (IBaseMetaType) metaChild.newInstance();
772                                        theValues = Collections.singletonList(newType);
773                                }
774                        }
775
776                        if (theValues.size() == 1 && theValues.get(0) instanceof IBaseMetaType) {
777
778                                IBaseMetaType metaValue = (IBaseMetaType) theValues.get(0);
779                                try {
780                                        metaValue = (IBaseMetaType)
781                                                        metaValue.getClass().getMethod("copy").invoke(metaValue);
782                                } catch (Exception e) {
783                                        throw new InternalErrorException(Msg.code(1830) + "Failed to duplicate meta", e);
784                                }
785
786                                if (isBlank(metaValue.getVersionId())) {
787                                        if (theResource.getIdElement().hasVersionIdPart()) {
788                                                metaValue.setVersionId(theResource.getIdElement().getVersionIdPart());
789                                        }
790                                }
791
792                                filterCodingsWithNoCodeOrSystem(metaValue.getTag());
793                                filterCodingsWithNoCodeOrSystem(metaValue.getSecurity());
794
795                                List<? extends IPrimitiveType<String>> newProfileList =
796                                                getProfileTagsForEncoding(theResource, metaValue.getProfile());
797                                List<? extends IPrimitiveType<String>> oldProfileList = metaValue.getProfile();
798                                if (oldProfileList != newProfileList) {
799                                        oldProfileList.clear();
800                                        for (IPrimitiveType<String> next : newProfileList) {
801                                                if (isNotBlank(next.getValue())) {
802                                                        metaValue.addProfile(next.getValue());
803                                                }
804                                        }
805                                }
806
807                                if (shouldAddSubsettedTag(theEncodeContext)) {
808                                        IBaseCoding coding = metaValue.addTag();
809                                        coding.setCode(Constants.TAG_SUBSETTED_CODE);
810                                        coding.setSystem(getSubsettedCodeSystem());
811                                        coding.setDisplay(subsetDescription());
812                                }
813
814                                return Collections.singletonList(metaValue);
815                        }
816                }
817
818                @SuppressWarnings("unchecked")
819                List<IBase> retVal = (List<IBase>) theValues;
820
821                for (int i = 0; i < retVal.size(); i++) {
822                        IBase next = retVal.get(i);
823
824                        /*
825                         * If we have automatically contained any resources via
826                         * their references, this ensures that we output the new
827                         * local reference
828                         */
829                        if (next instanceof IBaseReference) {
830                                IBaseReference nextRef = (IBaseReference) next;
831                                String refText =
832                                                determineReferenceText(nextRef, theCompositeChildElement, theResource, theEncodeContext);
833                                if (!StringUtils.equals(refText, nextRef.getReferenceElement().getValue())) {
834
835                                        if (retVal == theValues) {
836                                                retVal = new ArrayList<>(theValues);
837                                        }
838                                        IBaseReference newRef = (IBaseReference)
839                                                        myContext.getElementDefinition(nextRef.getClass()).newInstance();
840                                        myContext.newTerser().cloneInto(nextRef, newRef, true);
841                                        newRef.setReference(refText);
842                                        retVal.set(i, newRef);
843                                }
844                        }
845                }
846
847                return retVal;
848        }
849
850        private String getSubsettedCodeSystem() {
851                if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) {
852                        return Constants.TAG_SUBSETTED_SYSTEM_R4;
853                } else {
854                        return Constants.TAG_SUBSETTED_SYSTEM_DSTU3;
855                }
856        }
857
858        @Override
859        public IParser setDontStripVersionsFromReferencesAtPaths(String... thePaths) {
860                if (thePaths == null) {
861                        setDontStripVersionsFromReferencesAtPaths((List<String>) null);
862                } else {
863                        setDontStripVersionsFromReferencesAtPaths(Arrays.asList(thePaths));
864                }
865                return this;
866        }
867
868        @SuppressWarnings("unchecked")
869        @Override
870        public IParser setDontStripVersionsFromReferencesAtPaths(Collection<String> thePaths) {
871                if (thePaths == null) {
872                        myDontStripVersionsFromReferencesAtPaths = Collections.emptySet();
873                } else if (thePaths instanceof HashSet) {
874                        myDontStripVersionsFromReferencesAtPaths = (Set<String>) ((HashSet<String>) thePaths).clone();
875                } else {
876                        myDontStripVersionsFromReferencesAtPaths = new HashSet<>(thePaths);
877                }
878                return this;
879        }
880
881        @Override
882        public IParser setOmitResourceId(boolean theOmitResourceId) {
883                myOmitResourceId = theOmitResourceId;
884                return this;
885        }
886
887        @Override
888        public IParser setOverrideResourceIdWithBundleEntryFullUrl(Boolean theOverrideResourceIdWithBundleEntryFullUrl) {
889                myOverrideResourceIdWithBundleEntryFullUrl = theOverrideResourceIdWithBundleEntryFullUrl;
890                return this;
891        }
892
893        @Override
894        public IParser setParserErrorHandler(IParserErrorHandler theErrorHandler) {
895                Validate.notNull(theErrorHandler, "theErrorHandler must not be null");
896                myErrorHandler = theErrorHandler;
897                return this;
898        }
899
900        @Override
901        public IParser setServerBaseUrl(String theUrl) {
902                myServerBaseUrl = isNotBlank(theUrl) ? theUrl : null;
903                return this;
904        }
905
906        @Override
907        public IParser setStripVersionsFromReferences(Boolean theStripVersionsFromReferences) {
908                myStripVersionsFromReferences = theStripVersionsFromReferences;
909                return this;
910        }
911
912        @Override
913        public IParser setSummaryMode(boolean theSummaryMode) {
914                mySummaryMode = theSummaryMode;
915                return this;
916        }
917
918        @Override
919        public IParser setSuppressNarratives(boolean theSuppressNarratives) {
920                mySuppressNarratives = theSuppressNarratives;
921                return this;
922        }
923
924        protected boolean shouldAddSubsettedTag(EncodeContext theEncodeContext) {
925                if (isSummaryMode()) {
926                        return true;
927                }
928                if (isSuppressNarratives()) {
929                        return true;
930                }
931                if (theEncodeContext.myEncodeElementPaths != null) {
932                        if (isEncodeElementsAppliesToChildResourcesOnly()
933                                        && theEncodeContext.getResourcePath().size() < 2) {
934                                return false;
935                        }
936
937                        String currentResourceName = theEncodeContext
938                                        .getResourcePath()
939                                        .get(theEncodeContext.getResourcePath().size() - 1)
940                                        .getName();
941                        return theEncodeContext.myEncodeElementsAppliesToResourceTypes == null
942                                        || theEncodeContext.myEncodeElementsAppliesToResourceTypes.contains(currentResourceName);
943                }
944
945                return false;
946        }
947
948        protected boolean shouldEncodeResourceId(IBaseResource theResource, EncodeContext theEncodeContext) {
949                boolean retVal = true;
950                if (isOmitResourceId() && theEncodeContext.getPath().size() == 1) {
951                        retVal = false;
952                } else {
953                        if (theEncodeContext.myDontEncodeElementPaths != null) {
954                                String resourceName = myContext.getResourceType(theResource);
955                                if (theEncodeContext.myDontEncodeElementPaths.stream()
956                                                .anyMatch(t -> t.equalsPath(resourceName + ".id"))) {
957                                        retVal = false;
958                                } else if (theEncodeContext.myDontEncodeElementPaths.stream().anyMatch(t -> t.equalsPath("*.id"))) {
959                                        retVal = false;
960                                } else if (theEncodeContext.getResourcePath().size() == 1
961                                                && theEncodeContext.myDontEncodeElementPaths.stream().anyMatch(t -> t.equalsPath("id"))) {
962                                        retVal = false;
963                                }
964                        }
965                }
966                return retVal;
967        }
968
969        /**
970         * Used for DSTU2 only
971         */
972        protected boolean shouldEncodeResourceMeta(IResource theResource, EncodeContext theEncodeContext) {
973                return shouldEncodePath(theResource, "meta", theEncodeContext);
974        }
975
976        /**
977         * Used for DSTU2 only
978         */
979        protected boolean shouldEncodePath(IResource theResource, String thePath, EncodeContext theEncodeContext) {
980                if (theEncodeContext.myDontEncodeElementPaths != null) {
981                        String resourceName = myContext.getResourceType(theResource);
982                        if (theEncodeContext.myDontEncodeElementPaths.stream()
983                                        .anyMatch(t -> t.equalsPath(resourceName + "." + thePath))) {
984                                return false;
985                        } else {
986                                return theEncodeContext.myDontEncodeElementPaths.stream().noneMatch(t -> t.equalsPath("*." + thePath));
987                        }
988                }
989                return true;
990        }
991
992        private String subsetDescription() {
993                return "Resource encoded in summary mode";
994        }
995
996        protected void throwExceptionForUnknownChildType(
997                        BaseRuntimeChildDefinition nextChild, Class<? extends IBase> theType) {
998                if (nextChild instanceof BaseRuntimeDeclaredChildDefinition) {
999                        StringBuilder b = new StringBuilder();
1000                        b.append(nextChild.getElementName());
1001                        b.append(" has type ");
1002                        b.append(theType.getName());
1003                        b.append(" but this is not a valid type for this element");
1004                        if (nextChild instanceof RuntimeChildChoiceDefinition) {
1005                                RuntimeChildChoiceDefinition choice = (RuntimeChildChoiceDefinition) nextChild;
1006                                b.append(" - Expected one of: ").append(choice.getValidChildTypes());
1007                        }
1008                        throw new DataFormatException(Msg.code(1831) + b);
1009                }
1010                throw new DataFormatException(Msg.code(1832) + nextChild + " has no child of type " + theType);
1011        }
1012
1013        protected boolean shouldEncodeResource(String theName, EncodeContext theEncodeContext) {
1014                if (theEncodeContext.myDontEncodeElementPaths != null) {
1015                        for (EncodeContextPath next : theEncodeContext.myDontEncodeElementPaths) {
1016                                if (next.equalsPath(theName)) {
1017                                        return false;
1018                                }
1019                        }
1020                }
1021                return true;
1022        }
1023
1024        protected void containResourcesInReferences(IBaseResource theResource, EncodeContext theContext) {
1025
1026                /*
1027                 * If a UUID is present in Bundle.entry.fullUrl but no value is present
1028                 * in Bundle.entry.resource.id, the resource has a discrete identity which
1029                 * should be copied into the resource ID. It will not be serialized in
1030                 * Resource.id because it's a placeholder/UUID value, but its presence there
1031                 * informs the serializer that we don't need to contain this resource.
1032                 */
1033                if (theResource instanceof IBaseBundle) {
1034                        List<Pair<String, IBaseResource>> entries =
1035                                        BundleUtil.getBundleEntryFullUrlsAndResources(getContext(), (IBaseBundle) theResource);
1036                        for (Pair<String, IBaseResource> nextEntry : entries) {
1037                                String fullUrl = nextEntry.getKey();
1038                                IBaseResource resource = nextEntry.getValue();
1039                                if (startsWith(fullUrl, "urn:")) {
1040                                        if (resource != null && resource.getIdElement().getValue() == null) {
1041                                                resource.getIdElement().setValue(fullUrl);
1042                                        }
1043                                }
1044                        }
1045                }
1046
1047                FhirTerser.ContainedResources containedResources =
1048                                getContext().newTerser().containResources(theResource, theContext.getContainedResources(), false);
1049                theContext.setContainedResources(containedResources);
1050        }
1051
1052        static class ChildNameAndDef {
1053
1054                private final BaseRuntimeElementDefinition<?> myChildDef;
1055                private final String myChildName;
1056
1057                public ChildNameAndDef(String theChildName, BaseRuntimeElementDefinition<?> theChildDef) {
1058                        myChildName = theChildName;
1059                        myChildDef = theChildDef;
1060                }
1061
1062                public BaseRuntimeElementDefinition<?> getChildDef() {
1063                        return myChildDef;
1064                }
1065
1066                public String getChildName() {
1067                        return myChildName;
1068                }
1069        }
1070
1071        /**
1072         * EncodeContext is a shared state object that is passed around the
1073         * encode process
1074         */
1075        class EncodeContext extends EncodeContextPath {
1076                private final Map<Key, List<BaseParser.CompositeChildElement>> myCompositeChildrenCache = new HashMap<>();
1077                private final List<EncodeContextPath> myEncodeElementPaths;
1078                private final Set<String> myEncodeElementsAppliesToResourceTypes;
1079                private final List<EncodeContextPath> myDontEncodeElementPaths;
1080                private FhirTerser.ContainedResources myContainedResources;
1081
1082                public EncodeContext(
1083                                BaseParser theParser,
1084                                ParserOptions theParserOptions,
1085                                FhirTerser.ContainedResources theContainedResources) {
1086                        Collection<String> encodeElements = theParser.myEncodeElements;
1087                        Collection<String> dontEncodeElements = theParser.myDontEncodeElements;
1088                        if (isSummaryMode()) {
1089                                encodeElements = CollectionUtil.nullSafeUnion(
1090                                                encodeElements, theParserOptions.getEncodeElementsForSummaryMode());
1091                                dontEncodeElements = CollectionUtil.nullSafeUnion(
1092                                                dontEncodeElements, theParserOptions.getDontEncodeElementsForSummaryMode());
1093                        }
1094
1095                        if (encodeElements == null || encodeElements.isEmpty()) {
1096                                myEncodeElementPaths = null;
1097                        } else {
1098                                myEncodeElementPaths =
1099                                                encodeElements.stream().map(EncodeContextPath::new).collect(Collectors.toList());
1100                        }
1101                        if (dontEncodeElements == null || dontEncodeElements.isEmpty()) {
1102                                myDontEncodeElementPaths = null;
1103                        } else {
1104                                myDontEncodeElementPaths =
1105                                                dontEncodeElements.stream().map(EncodeContextPath::new).collect(Collectors.toList());
1106                        }
1107
1108                        myContainedResources = theContainedResources;
1109
1110                        myEncodeElementsAppliesToResourceTypes =
1111                                        ParserUtil.determineApplicableResourceTypesForTerserPaths(myEncodeElementPaths);
1112                }
1113
1114                private Map<Key, List<BaseParser.CompositeChildElement>> getCompositeChildrenCache() {
1115                        return myCompositeChildrenCache;
1116                }
1117
1118                public FhirTerser.ContainedResources getContainedResources() {
1119                        return myContainedResources;
1120                }
1121
1122                public void setContainedResources(FhirTerser.ContainedResources theContainedResources) {
1123                        myContainedResources = theContainedResources;
1124                }
1125        }
1126
1127        protected class CompositeChildElement {
1128                private final BaseRuntimeChildDefinition myDef;
1129                private final CompositeChildElement myParent;
1130                private final RuntimeResourceDefinition myResDef;
1131                private final EncodeContext myEncodeContext;
1132
1133                public CompositeChildElement(
1134                                CompositeChildElement theParent,
1135                                @Nullable BaseRuntimeChildDefinition theDef,
1136                                EncodeContext theEncodeContext) {
1137                        myDef = theDef;
1138                        myParent = theParent;
1139                        myResDef = null;
1140                        myEncodeContext = theEncodeContext;
1141
1142                        if (ourLog.isTraceEnabled()) {
1143                                if (theParent != null) {
1144                                        StringBuilder path = theParent.buildPath();
1145                                        if (path != null) {
1146                                                path.append('.');
1147                                                if (myDef != null) {
1148                                                        path.append(myDef.getElementName());
1149                                                }
1150                                                ourLog.trace(" * Next path: {}", path);
1151                                        }
1152                                }
1153                        }
1154                }
1155
1156                public CompositeChildElement(RuntimeResourceDefinition theResDef, EncodeContext theEncodeContext) {
1157                        myResDef = theResDef;
1158                        myDef = null;
1159                        myParent = null;
1160                        myEncodeContext = theEncodeContext;
1161                }
1162
1163                @Override
1164                public String toString() {
1165                        return myDef.getElementName();
1166                }
1167
1168                private void addParent(CompositeChildElement theParent, StringBuilder theB) {
1169                        if (theParent != null) {
1170                                if (theParent.myResDef != null) {
1171                                        theB.append(theParent.myResDef.getName());
1172                                        return;
1173                                }
1174
1175                                if (theParent.myParent != null) {
1176                                        addParent(theParent.myParent, theB);
1177                                }
1178
1179                                if (theParent.myDef != null) {
1180                                        if (theB.length() > 0) {
1181                                                theB.append('.');
1182                                        }
1183                                        theB.append(theParent.myDef.getElementName());
1184                                }
1185                        }
1186                }
1187
1188                public boolean anyPathMatches(Set<String> thePaths) {
1189                        StringBuilder b = new StringBuilder();
1190                        addParent(this, b);
1191
1192                        String path = b.toString();
1193                        return thePaths.contains(path);
1194                }
1195
1196                private StringBuilder buildPath() {
1197                        if (myResDef != null) {
1198                                StringBuilder b = new StringBuilder();
1199                                b.append(myResDef.getName());
1200                                return b;
1201                        } else if (myParent != null) {
1202                                StringBuilder b = myParent.buildPath();
1203                                if (b != null && myDef != null) {
1204                                        b.append('.');
1205                                        b.append(myDef.getElementName());
1206                                }
1207                                return b;
1208                        } else {
1209                                return null;
1210                        }
1211                }
1212
1213                private boolean checkIfParentShouldBeEncodedAndBuildPath() {
1214                        List<EncodeContextPath> encodeElements = myEncodeContext.myEncodeElementPaths;
1215
1216                        String currentResourceName = myEncodeContext
1217                                        .getResourcePath()
1218                                        .get(myEncodeContext.getResourcePath().size() - 1)
1219                                        .getName();
1220                        if (myEncodeContext.myEncodeElementsAppliesToResourceTypes != null
1221                                        && !myEncodeContext.myEncodeElementsAppliesToResourceTypes.contains(currentResourceName)) {
1222                                encodeElements = null;
1223                        }
1224
1225                        boolean retVal = checkIfPathMatchesForEncoding(encodeElements, true);
1226
1227                        /*
1228                         * We force the meta tag to be encoded even if it's not specified as an element in the
1229                         * elements filter, specifically because we'll need it in order to automatically add
1230                         * the SUBSETTED tag
1231                         */
1232                        if (!retVal) {
1233                                if ("meta".equals(myEncodeContext.getLeafResourcePathFirstField())
1234                                                && shouldAddSubsettedTag(myEncodeContext)) {
1235                                        // The next element is a child of the <meta> element
1236                                        retVal = true;
1237                                } else if ("meta".equals(myDef.getElementName()) && shouldAddSubsettedTag(myEncodeContext)) {
1238                                        // The next element is the <meta> element
1239                                        retVal = true;
1240                                }
1241                        }
1242
1243                        return retVal;
1244                }
1245
1246                private boolean checkIfParentShouldNotBeEncodedAndBuildPath() {
1247                        return checkIfPathMatchesForEncoding(myEncodeContext.myDontEncodeElementPaths, false);
1248                }
1249
1250                private boolean checkIfPathMatchesForEncoding(
1251                                List<EncodeContextPath> theElements, boolean theCheckingForEncodeElements) {
1252
1253                        boolean retVal = false;
1254                        if (myDef != null) {
1255                                myEncodeContext.pushPath(myDef.getElementName(), false);
1256                        }
1257
1258                        if (theCheckingForEncodeElements
1259                                        && isEncodeElementsAppliesToChildResourcesOnly()
1260                                        && myEncodeContext.getResourcePath().size() < 2) {
1261                                retVal = true;
1262                        } else if (theElements == null) {
1263                                retVal = true;
1264                        } else {
1265                                EncodeContextPath currentResourcePath = myEncodeContext.getCurrentResourcePath();
1266                                ourLog.trace("Current resource path: {}", currentResourcePath);
1267                                for (EncodeContextPath next : theElements) {
1268
1269                                        if (next.startsWith(currentResourcePath, true)) {
1270                                                if (theCheckingForEncodeElements
1271                                                                || next.getPath().size()
1272                                                                                == currentResourcePath.getPath().size()) {
1273                                                        retVal = true;
1274                                                        break;
1275                                                }
1276                                        }
1277
1278                                        if (next.getPath().get(next.getPath().size() - 1).getName().equals("(mandatory)")) {
1279                                                if (myDef.getMin() > 0) {
1280                                                        retVal = true;
1281                                                        break;
1282                                                }
1283                                                if (currentResourcePath.getPath().size()
1284                                                                > next.getPath().size()) {
1285                                                        retVal = true;
1286                                                        break;
1287                                                }
1288                                        }
1289                                }
1290                        }
1291
1292                        if (myDef != null) {
1293                                myEncodeContext.popPath();
1294                        }
1295
1296                        return retVal;
1297                }
1298
1299                public BaseRuntimeChildDefinition getDef() {
1300                        return myDef;
1301                }
1302
1303                public CompositeChildElement getParent() {
1304                        return myParent;
1305                }
1306
1307                public boolean shouldBeEncoded(boolean theContainedResource) {
1308                        boolean retVal = true;
1309                        if (isSummaryMode() && (getDef() == null || !getDef().isSummary())) {
1310                                String resourceName = myEncodeContext.getLeafResourceName();
1311                                // Technically the spec says we shouldn't include extensions in CapabilityStatement
1312                                // but we will do so because there are people who depend on this behaviour, at least
1313                                // as of 2019-07. See
1314                                // https://github.com/smart-on-fhir/Swift-FHIR/issues/26
1315                                // for example.
1316                                if (("Conformance".equals(resourceName) || "CapabilityStatement".equals(resourceName))
1317                                                && ("extension".equals(myDef.getElementName())
1318                                                                || "extension".equals(myEncodeContext.getLeafElementName()))) {
1319                                        // skip
1320                                } else {
1321                                        retVal = false;
1322                                }
1323                        }
1324                        if (myEncodeContext.myEncodeElementPaths != null) {
1325                                retVal = checkIfParentShouldBeEncodedAndBuildPath();
1326                        }
1327                        if (retVal && myEncodeContext.myDontEncodeElementPaths != null) {
1328                                retVal = !checkIfParentShouldNotBeEncodedAndBuildPath();
1329                        }
1330                        if (theContainedResource) {
1331                                retVal = !notEncodeForContainedResource.contains(myDef.getElementName());
1332                        }
1333
1334                        return retVal;
1335                }
1336
1337                @Override
1338                public int hashCode() {
1339                        final int prime = 31;
1340                        int result = 1;
1341                        result = prime * result + ((myDef == null) ? 0 : myDef.hashCode());
1342                        result = prime * result + ((myParent == null) ? 0 : myParent.hashCode());
1343                        result = prime * result + ((myResDef == null) ? 0 : myResDef.hashCode());
1344                        result = prime * result + ((myEncodeContext == null) ? 0 : myEncodeContext.hashCode());
1345                        return result;
1346                }
1347
1348                @Override
1349                public boolean equals(Object obj) {
1350                        if (this == obj) return true;
1351
1352                        if (obj instanceof CompositeChildElement) {
1353                                final CompositeChildElement that = (CompositeChildElement) obj;
1354                                return Objects.equals(this.getEnclosingInstance(), that.getEnclosingInstance())
1355                                                && Objects.equals(this.myDef, that.myDef)
1356                                                && Objects.equals(this.myParent, that.myParent)
1357                                                && Objects.equals(this.myResDef, that.myResDef)
1358                                                && Objects.equals(this.myEncodeContext, that.myEncodeContext);
1359                        }
1360                        return false;
1361                }
1362
1363                private BaseParser getEnclosingInstance() {
1364                        return BaseParser.this;
1365                }
1366        }
1367
1368        private static class Key {
1369                private final BaseRuntimeElementCompositeDefinition<?> resDef;
1370                private final boolean theContainedResource;
1371                private final BaseParser.CompositeChildElement theParent;
1372                private final BaseParser.EncodeContext theEncodeContext;
1373
1374                public Key(
1375                                BaseRuntimeElementCompositeDefinition<?> resDef,
1376                                final boolean theContainedResource,
1377                                final BaseParser.CompositeChildElement theParent,
1378                                BaseParser.EncodeContext theEncodeContext) {
1379                        this.resDef = resDef;
1380                        this.theContainedResource = theContainedResource;
1381                        this.theParent = theParent;
1382                        this.theEncodeContext = theEncodeContext;
1383                }
1384
1385                @Override
1386                public int hashCode() {
1387                        final int prime = 31;
1388                        int result = 1;
1389                        result = prime * result + ((resDef == null) ? 0 : resDef.hashCode());
1390                        result = prime * result + (theContainedResource ? 1231 : 1237);
1391                        result = prime * result + ((theParent == null) ? 0 : theParent.hashCode());
1392                        result = prime * result + ((theEncodeContext == null) ? 0 : theEncodeContext.hashCode());
1393                        return result;
1394                }
1395
1396                @Override
1397                public boolean equals(final Object obj) {
1398                        if (this == obj) {
1399                                return true;
1400                        }
1401                        if (obj instanceof Key) {
1402                                final Key that = (Key) obj;
1403                                return Objects.equals(this.resDef, that.resDef)
1404                                                && this.theContainedResource == that.theContainedResource
1405                                                && Objects.equals(this.theParent, that.theParent)
1406                                                && Objects.equals(this.theEncodeContext, that.theEncodeContext);
1407                        }
1408                        return false;
1409                }
1410        }
1411
1412        protected static <T> List<T> extractMetadataListNotNull(IResource resource, ResourceMetadataKeyEnum<List<T>> key) {
1413                List<? extends T> securityLabels = key.get(resource);
1414                if (securityLabels == null) {
1415                        securityLabels = Collections.emptyList();
1416                }
1417                return new ArrayList<>(securityLabels);
1418        }
1419
1420        static boolean hasNoExtensions(IBase theElement) {
1421                if (theElement instanceof ISupportsUndeclaredExtensions) {
1422                        ISupportsUndeclaredExtensions res = (ISupportsUndeclaredExtensions) theElement;
1423                        if (res.getUndeclaredExtensions().size() > 0
1424                                        || res.getUndeclaredModifierExtensions().size() > 0) {
1425                                return false;
1426                        }
1427                }
1428                if (theElement instanceof IBaseHasExtensions) {
1429                        IBaseHasExtensions res = (IBaseHasExtensions) theElement;
1430                        if (res.hasExtension()) {
1431                                return false;
1432                        }
1433                }
1434                if (theElement instanceof IBaseHasModifierExtensions) {
1435                        IBaseHasModifierExtensions res = (IBaseHasModifierExtensions) theElement;
1436                        return !res.hasModifierExtension();
1437                }
1438                return true;
1439        }
1440}