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