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