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