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