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