001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 * http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.parser;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeDeclaredChildDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
025import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
026import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum;
027import ca.uhn.fhir.context.ConfigurationException;
028import ca.uhn.fhir.context.FhirContext;
029import ca.uhn.fhir.context.FhirVersionEnum;
030import ca.uhn.fhir.context.RuntimeChildChoiceDefinition;
031import ca.uhn.fhir.context.RuntimeChildContainedResources;
032import ca.uhn.fhir.context.RuntimeChildNarrativeDefinition;
033import ca.uhn.fhir.context.RuntimeResourceDefinition;
034import ca.uhn.fhir.i18n.Msg;
035import ca.uhn.fhir.model.api.IIdentifiableElement;
036import ca.uhn.fhir.model.api.IResource;
037import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions;
038import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
039import ca.uhn.fhir.model.api.Tag;
040import ca.uhn.fhir.model.api.TagList;
041import ca.uhn.fhir.parser.path.EncodeContextPath;
042import ca.uhn.fhir.rest.api.Constants;
043import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
044import ca.uhn.fhir.util.BundleUtil;
045import ca.uhn.fhir.util.FhirTerser;
046import ca.uhn.fhir.util.UrlUtil;
047import com.google.common.base.Charsets;
048import org.apache.commons.io.output.StringBuilderWriter;
049import org.apache.commons.lang3.StringUtils;
050import org.apache.commons.lang3.Validate;
051import org.hl7.fhir.instance.model.api.IBase;
052import org.hl7.fhir.instance.model.api.IBaseBundle;
053import org.hl7.fhir.instance.model.api.IBaseCoding;
054import org.hl7.fhir.instance.model.api.IBaseElement;
055import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
056import org.hl7.fhir.instance.model.api.IBaseHasModifierExtensions;
057import org.hl7.fhir.instance.model.api.IBaseMetaType;
058import org.hl7.fhir.instance.model.api.IBaseReference;
059import org.hl7.fhir.instance.model.api.IBaseResource;
060import org.hl7.fhir.instance.model.api.IIdType;
061import org.hl7.fhir.instance.model.api.IPrimitiveType;
062
063import javax.annotation.Nullable;
064import java.io.IOException;
065import java.io.InputStream;
066import java.io.InputStreamReader;
067import java.io.Reader;
068import java.io.StringReader;
069import java.io.Writer;
070import java.lang.reflect.Modifier;
071import java.util.ArrayList;
072import java.util.Arrays;
073import java.util.Collection;
074import java.util.Collections;
075import java.util.HashMap;
076import java.util.HashSet;
077import java.util.List;
078import java.util.Map;
079import java.util.Objects;
080import java.util.Set;
081import java.util.stream.Collectors;
082
083import static org.apache.commons.lang3.StringUtils.isBlank;
084import static org.apache.commons.lang3.StringUtils.isNotBlank;
085
086@SuppressWarnings("WeakerAccess")
087public abstract class BaseParser implements IParser {
088
089        /**
090         * Any resources that were created by the parser (i.e. by parsing a serialized resource) will have
091         * a {@link IBaseResource#getUserData(String) user data} property with this key.
092         *
093         * @since 5.0.0
094         */
095        public static final String RESOURCE_CREATED_BY_PARSER = BaseParser.class.getName() + "_" + "RESOURCE_CREATED_BY_PARSER";
096
097        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseParser.class);
098
099        private static final Set<String> notEncodeForContainedResource = new HashSet<>(Arrays.asList("security", "versionId", "lastUpdated"));
100
101        private FhirTerser.ContainedResources myContainedResources;
102        private boolean myEncodeElementsAppliesToChildResourcesOnly;
103        private FhirContext myContext;
104        private List<EncodeContextPath> myDontEncodeElements;
105        private List<EncodeContextPath> myEncodeElements;
106        private Set<String> myEncodeElementsAppliesToResourceTypes;
107        private IIdType myEncodeForceResourceId;
108        private IParserErrorHandler myErrorHandler;
109        private boolean myOmitResourceId;
110        private List<Class<? extends IBaseResource>> myPreferTypes;
111        private String myServerBaseUrl;
112        private Boolean myStripVersionsFromReferences;
113        private Boolean myOverrideResourceIdWithBundleEntryFullUrl;
114        private boolean mySummaryMode;
115        private boolean mySuppressNarratives;
116        private Set<String> myDontStripVersionsFromReferencesAtPaths;
117        /**
118         * Constructor
119         */
120        public BaseParser(FhirContext theContext, IParserErrorHandler theParserErrorHandler) {
121                myContext = theContext;
122                myErrorHandler = theParserErrorHandler;
123        }
124
125        protected FhirContext getContext() {
126                return myContext;
127        }
128
129        List<EncodeContextPath> getDontEncodeElements() {
130                return myDontEncodeElements;
131        }
132
133        @Override
134        public IParser setDontEncodeElements(Collection<String> theDontEncodeElements) {
135                if (theDontEncodeElements == null || theDontEncodeElements.isEmpty()) {
136                        myDontEncodeElements = null;
137                } else {
138                        myDontEncodeElements = theDontEncodeElements
139                                .stream()
140                                .map(EncodeContextPath::new)
141                                .collect(Collectors.toList());
142                }
143                return this;
144        }
145
146        List<EncodeContextPath> getEncodeElements() {
147                return myEncodeElements;
148        }
149
150        @Override
151        public IParser setEncodeElements(Set<String> theEncodeElements) {
152
153                if (theEncodeElements == null || theEncodeElements.isEmpty()) {
154                        myEncodeElements = null;
155                        myEncodeElementsAppliesToResourceTypes = null;
156                } else {
157                        myEncodeElements = theEncodeElements
158                                .stream()
159                                .map(EncodeContextPath::new)
160                                .collect(Collectors.toList());
161
162                        myEncodeElementsAppliesToResourceTypes = new HashSet<>();
163                        for (String next : myEncodeElements.stream().map(t -> t.getPath().get(0).getName()).collect(Collectors.toList())) {
164                                if (next.startsWith("*")) {
165                                        myEncodeElementsAppliesToResourceTypes = null;
166                                        break;
167                                }
168                                int dotIdx = next.indexOf('.');
169                                if (dotIdx == -1) {
170                                        myEncodeElementsAppliesToResourceTypes.add(next);
171                                } else {
172                                        myEncodeElementsAppliesToResourceTypes.add(next.substring(0, dotIdx));
173                                }
174                        }
175
176                }
177
178                return this;
179        }
180
181        protected Iterable<CompositeChildElement> compositeChildIterator(IBase theCompositeElement, final boolean theContainedResource, final CompositeChildElement theParent, EncodeContext theEncodeContext) {
182                BaseRuntimeElementCompositeDefinition<?> elementDef = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theCompositeElement.getClass());
183                return theEncodeContext.getCompositeChildrenCache().computeIfAbsent(new Key(elementDef, theContainedResource, theParent, theEncodeContext), (k) -> {
184
185                        final List<BaseRuntimeChildDefinition> children = elementDef.getChildrenAndExtension();
186                        final List<CompositeChildElement> result = new ArrayList<>(children.size());
187
188                        for (final BaseRuntimeChildDefinition child : children) {
189                                CompositeChildElement myNext = new CompositeChildElement(theParent, child, theEncodeContext);
190
191                                /*
192                                 * There are lots of reasons we might skip encoding a particular child
193                                 */
194                                if (myNext.getDef().getElementName().equals("id")) {
195                                        continue;
196                                } else if (!myNext.shouldBeEncoded(theContainedResource)) {
197                                        continue;
198                                } else if (myNext.getDef() instanceof RuntimeChildNarrativeDefinition) {
199                                        if (isSuppressNarratives() || isSummaryMode()) {
200                                                continue;
201                                        }
202                                } else if (myNext.getDef() instanceof RuntimeChildContainedResources) {
203                                        if (theContainedResource) {
204                                                continue;
205                                        }
206                                }
207                                result.add(myNext);
208                        }
209                        return result;
210                });
211        }
212
213
214        private String determineReferenceText(IBaseReference theRef, CompositeChildElement theCompositeChildElement) {
215                IIdType ref = theRef.getReferenceElement();
216                if (isBlank(ref.getIdPart())) {
217                        String reference = ref.getValue();
218                        if (theRef.getResource() != null) {
219                                IIdType containedId = getContainedResources().getResourceId(theRef.getResource());
220                                if (containedId != null && !containedId.isEmpty()) {
221                                        if (containedId.isLocal()) {
222                                                reference = containedId.getValue();
223                                        } else {
224                                                reference = "#" + containedId.getValue();
225                                        }
226                                } else {
227                                        IIdType refId = theRef.getResource().getIdElement();
228                                        if (refId != null) {
229                                                if (refId.hasIdPart()) {
230                                                        if (refId.getValue().startsWith("urn:")) {
231                                                                reference = refId.getValue();
232                                                        } else {
233                                                                if (!refId.hasResourceType()) {
234                                                                        refId = refId.withResourceType(myContext.getResourceDefinition(theRef.getResource()).getName());
235                                                                }
236                                                                if (isStripVersionsFromReferences(theCompositeChildElement)) {
237                                                                        reference = refId.toVersionless().getValue();
238                                                                } else {
239                                                                        reference = refId.getValue();
240                                                                }
241                                                        }
242                                                }
243                                        }
244                                }
245                        }
246                        return reference;
247                }
248                if (!ref.hasResourceType() && !ref.isLocal() && theRef.getResource() != null) {
249                        ref = ref.withResourceType(myContext.getResourceDefinition(theRef.getResource()).getName());
250                }
251                if (isNotBlank(myServerBaseUrl) && StringUtils.equals(myServerBaseUrl, ref.getBaseUrl())) {
252                        if (isStripVersionsFromReferences(theCompositeChildElement)) {
253                                return ref.toUnqualifiedVersionless().getValue();
254                        }
255                        return ref.toUnqualified().getValue();
256                }
257                if (isStripVersionsFromReferences(theCompositeChildElement)) {
258                        return ref.toVersionless().getValue();
259                }
260                return ref.getValue();
261        }
262
263        protected abstract void doEncodeResourceToWriter(IBaseResource theResource, Writer theWriter, EncodeContext theEncodeContext) throws IOException, DataFormatException;
264
265        protected abstract <T extends IBaseResource> T doParseResource(Class<T> theResourceType, Reader theReader) throws DataFormatException;
266
267        @Override
268        public String encodeResourceToString(IBaseResource theResource) throws DataFormatException {
269                Writer stringWriter = new StringBuilderWriter();
270                try {
271                        encodeResourceToWriter(theResource, stringWriter);
272                } catch (IOException e) {
273                        throw new Error(Msg.code(1828) + "Encountered IOException during write to string - This should not happen!");
274                }
275                return stringWriter.toString();
276        }
277
278        @Override
279        public final void encodeResourceToWriter(IBaseResource theResource, Writer theWriter) throws IOException, DataFormatException {
280                EncodeContext encodeContext = new EncodeContext();
281
282                encodeResourceToWriter(theResource, theWriter, encodeContext);
283        }
284
285        protected void encodeResourceToWriter(IBaseResource theResource, Writer theWriter, EncodeContext theEncodeContext) throws IOException {
286                Validate.notNull(theResource, "theResource can not be null");
287                Validate.notNull(theWriter, "theWriter can not be null");
288                Validate.notNull(theEncodeContext, "theEncodeContext can not be null");
289
290                if (myContext.getVersion().getVersion() == FhirVersionEnum.R4B && theResource.getStructureFhirVersionEnum() == FhirVersionEnum.R5) {
291                        // TODO: remove once we've bumped the core lib version
292                } else
293                if (theResource.getStructureFhirVersionEnum() != myContext.getVersion().getVersion()) {
294                        throw new IllegalArgumentException(Msg.code(1829) + "This parser is for FHIR version " + myContext.getVersion().getVersion() + " - Can not encode a structure for version " + theResource.getStructureFhirVersionEnum());
295                }
296
297                String resourceName = myContext.getResourceType(theResource);
298                theEncodeContext.pushPath(resourceName, true);
299
300                doEncodeResourceToWriter(theResource, theWriter, theEncodeContext);
301
302                theEncodeContext.popPath();
303        }
304
305        private void filterCodingsWithNoCodeOrSystem(List<? extends IBaseCoding> tagList) {
306                for (int i = 0; i < tagList.size(); i++) {
307                        if (isBlank(tagList.get(i).getCode()) && isBlank(tagList.get(i).getSystem())) {
308                                tagList.remove(i);
309                                i--;
310                        }
311                }
312        }
313
314        protected IIdType fixContainedResourceId(String theValue) {
315                IIdType retVal = (IIdType) myContext.getElementDefinition("id").newInstance();
316                if (StringUtils.isNotBlank(theValue) && theValue.charAt(0) == '#') {
317                        retVal.setValue(theValue.substring(1));
318                } else {
319                        retVal.setValue(theValue);
320                }
321                return retVal;
322        }
323
324        @SuppressWarnings("unchecked")
325        ChildNameAndDef getChildNameAndDef(BaseRuntimeChildDefinition theChild, IBase theValue) {
326                Class<? extends IBase> type = theValue.getClass();
327                String childName = theChild.getChildNameByDatatype(type);
328                BaseRuntimeElementDefinition<?> childDef = theChild.getChildElementDefinitionByDatatype(type);
329                if (childDef == null) {
330                        // if (theValue instanceof IBaseExtension) {
331                        // return null;
332                        // }
333
334                        /*
335                         * For RI structures Enumeration class, this replaces the child def
336                         * with the "code" one. This is messy, and presumably there is a better
337                         * way..
338                         */
339                        BaseRuntimeElementDefinition<?> elementDef = myContext.getElementDefinition(type);
340                        if (elementDef.getName().equals("code")) {
341                                Class<? extends IBase> type2 = myContext.getElementDefinition("code").getImplementingClass();
342                                childDef = theChild.getChildElementDefinitionByDatatype(type2);
343                                childName = theChild.getChildNameByDatatype(type2);
344                        }
345
346                        // See possibly the user has extended a built-in type without
347                        // declaring it anywhere, as in XmlParserDstu3Test#testEncodeUndeclaredBlock
348                        if (childDef == null) {
349                                Class<?> nextSuperType = theValue.getClass();
350                                while (IBase.class.isAssignableFrom(nextSuperType) && childDef == null) {
351                                        if (Modifier.isAbstract(nextSuperType.getModifiers()) == false) {
352                                                BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition((Class<? extends IBase>) nextSuperType);
353                                                Class<?> nextChildType = def.getImplementingClass();
354                                                childDef = theChild.getChildElementDefinitionByDatatype((Class<? extends IBase>) nextChildType);
355                                                childName = theChild.getChildNameByDatatype((Class<? extends IBase>) nextChildType);
356                                        }
357                                        nextSuperType = nextSuperType.getSuperclass();
358                                }
359                        }
360
361                        if (childDef == null) {
362                                throwExceptionForUnknownChildType(theChild, type);
363                        }
364                }
365
366                return new ChildNameAndDef(childName, childDef);
367        }
368
369        protected String getCompositeElementId(IBase theElement) {
370                String elementId = null;
371                if (!(theElement instanceof IBaseResource)) {
372                        if (theElement instanceof IBaseElement) {
373                                elementId = ((IBaseElement) theElement).getId();
374                        } else if (theElement instanceof IIdentifiableElement) {
375                                elementId = ((IIdentifiableElement) theElement).getElementSpecificId();
376                        }
377                }
378                return elementId;
379        }
380
381        FhirTerser.ContainedResources getContainedResources() {
382                return myContainedResources;
383        }
384
385        void setContainedResources(FhirTerser.ContainedResources theContainedResources) {
386                myContainedResources = theContainedResources;
387        }
388
389        @Override
390        public Set<String> getDontStripVersionsFromReferencesAtPaths() {
391                return myDontStripVersionsFromReferencesAtPaths;
392        }
393
394        @Override
395        public IIdType getEncodeForceResourceId() {
396                return myEncodeForceResourceId;
397        }
398
399        @Override
400        public BaseParser setEncodeForceResourceId(IIdType theEncodeForceResourceId) {
401                myEncodeForceResourceId = theEncodeForceResourceId;
402                return this;
403        }
404
405        protected IParserErrorHandler getErrorHandler() {
406                return myErrorHandler;
407        }
408
409        protected List<Map.Entry<ResourceMetadataKeyEnum<?>, Object>> getExtensionMetadataKeys(IResource resource) {
410                List<Map.Entry<ResourceMetadataKeyEnum<?>, Object>> extensionMetadataKeys = new ArrayList<>();
411                for (Map.Entry<ResourceMetadataKeyEnum<?>, Object> entry : resource.getResourceMetadata().entrySet()) {
412                        if (entry.getKey() instanceof ResourceMetadataKeyEnum.ExtensionResourceMetadataKey) {
413                                extensionMetadataKeys.add(entry);
414                        }
415                }
416
417                return extensionMetadataKeys;
418        }
419
420        protected String getExtensionUrl(final String extensionUrl) {
421                String url = extensionUrl;
422                if (StringUtils.isNotBlank(extensionUrl) && StringUtils.isNotBlank(myServerBaseUrl)) {
423                        url = !UrlUtil.isValid(extensionUrl) && extensionUrl.startsWith("/") ? myServerBaseUrl + extensionUrl : extensionUrl;
424                }
425                return url;
426        }
427
428        protected TagList getMetaTagsForEncoding(IResource theIResource, EncodeContext theEncodeContext) {
429                TagList tags = ResourceMetadataKeyEnum.TAG_LIST.get(theIResource);
430                if (shouldAddSubsettedTag(theEncodeContext)) {
431                        tags = new TagList(tags);
432                        tags.add(new Tag(getSubsettedCodeSystem(), Constants.TAG_SUBSETTED_CODE, subsetDescription()));
433                }
434
435                return tags;
436        }
437
438        @Override
439        public List<Class<? extends IBaseResource>> getPreferTypes() {
440                return myPreferTypes;
441        }
442
443        @Override
444        public void setPreferTypes(List<Class<? extends IBaseResource>> thePreferTypes) {
445                if (thePreferTypes != null) {
446                        ArrayList<Class<? extends IBaseResource>> types = new ArrayList<>();
447                        for (Class<? extends IBaseResource> next : thePreferTypes) {
448                                if (Modifier.isAbstract(next.getModifiers()) == false) {
449                                        types.add(next);
450                                }
451                        }
452                        myPreferTypes = Collections.unmodifiableList(types);
453                } else {
454                        myPreferTypes = thePreferTypes;
455                }
456        }
457
458        @SuppressWarnings("deprecation")
459        protected <T extends IPrimitiveType<String>> List<T> getProfileTagsForEncoding(IBaseResource theResource, List<T> theProfiles) {
460                switch (myContext.getAddProfileTagWhenEncoding()) {
461                        case NEVER:
462                                return theProfiles;
463                        case ONLY_FOR_CUSTOM:
464                                RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theResource);
465                                if (resDef.isStandardType()) {
466                                        return theProfiles;
467                                }
468                                break;
469                        case ALWAYS:
470                                break;
471                }
472
473                RuntimeResourceDefinition nextDef = myContext.getResourceDefinition(theResource);
474                String profile = nextDef.getResourceProfile(myServerBaseUrl);
475                if (isNotBlank(profile)) {
476                        for (T next : theProfiles) {
477                                if (profile.equals(next.getValue())) {
478                                        return theProfiles;
479                                }
480                        }
481
482                        List<T> newList = new ArrayList<>(theProfiles);
483
484                        BaseRuntimeElementDefinition<?> idElement = myContext.getElementDefinition("id");
485                        @SuppressWarnings("unchecked")
486                        T newId = (T) idElement.newInstance();
487                        newId.setValue(profile);
488
489                        newList.add(newId);
490                        return newList;
491                }
492
493                return theProfiles;
494        }
495
496        protected String getServerBaseUrl() {
497                return myServerBaseUrl;
498        }
499
500        @Override
501        public Boolean getStripVersionsFromReferences() {
502                return myStripVersionsFromReferences;
503        }
504
505        /**
506         * If set to <code>true</code> (default is <code>false</code>), narratives will not be included in the encoded
507         * values.
508         *
509         * @deprecated Use {@link #isSuppressNarratives()}
510         */
511        @Deprecated
512        public boolean getSuppressNarratives() {
513                return mySuppressNarratives;
514        }
515
516        protected boolean isChildContained(BaseRuntimeElementDefinition<?> childDef, boolean theIncludedResource) {
517                return (childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCES || childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST) && getContainedResources().isEmpty() == false
518                        && theIncludedResource == false;
519        }
520
521        @Override
522        public boolean isEncodeElementsAppliesToChildResourcesOnly() {
523                return myEncodeElementsAppliesToChildResourcesOnly;
524        }
525
526        @Override
527        public void setEncodeElementsAppliesToChildResourcesOnly(boolean theEncodeElementsAppliesToChildResourcesOnly) {
528                myEncodeElementsAppliesToChildResourcesOnly = theEncodeElementsAppliesToChildResourcesOnly;
529        }
530
531        @Override
532        public boolean isOmitResourceId() {
533                return myOmitResourceId;
534        }
535
536        private boolean isOverrideResourceIdWithBundleEntryFullUrl() {
537                Boolean overrideResourceIdWithBundleEntryFullUrl = myOverrideResourceIdWithBundleEntryFullUrl;
538                if (overrideResourceIdWithBundleEntryFullUrl != null) {
539                        return overrideResourceIdWithBundleEntryFullUrl;
540                }
541
542                return myContext.getParserOptions().isOverrideResourceIdWithBundleEntryFullUrl();
543        }
544
545        private boolean isStripVersionsFromReferences(CompositeChildElement theCompositeChildElement) {
546                Boolean stripVersionsFromReferences = myStripVersionsFromReferences;
547                if (stripVersionsFromReferences != null) {
548                        return stripVersionsFromReferences;
549                }
550
551                if (myContext.getParserOptions().isStripVersionsFromReferences() == false) {
552                        return false;
553                }
554
555                Set<String> dontStripVersionsFromReferencesAtPaths = myDontStripVersionsFromReferencesAtPaths;
556                if (dontStripVersionsFromReferencesAtPaths != null) {
557                        if (dontStripVersionsFromReferencesAtPaths.isEmpty() == false && theCompositeChildElement.anyPathMatches(dontStripVersionsFromReferencesAtPaths)) {
558                                return false;
559                        }
560                }
561
562                dontStripVersionsFromReferencesAtPaths = myContext.getParserOptions().getDontStripVersionsFromReferencesAtPaths();
563                return dontStripVersionsFromReferencesAtPaths.isEmpty() != false || !theCompositeChildElement.anyPathMatches(dontStripVersionsFromReferencesAtPaths);
564        }
565
566        @Override
567        public boolean isSummaryMode() {
568                return mySummaryMode;
569        }
570
571        /**
572         * If set to <code>true</code> (default is <code>false</code>), narratives will not be included in the encoded
573         * values.
574         *
575         * @since 1.2
576         */
577        public boolean isSuppressNarratives() {
578                return mySuppressNarratives;
579        }
580
581        @Override
582        public IBaseResource parseResource(InputStream theInputStream) throws DataFormatException {
583                return parseResource(new InputStreamReader(theInputStream, Charsets.UTF_8));
584        }
585
586        @Override
587        public <T extends IBaseResource> T parseResource(Class<T> theResourceType, InputStream theInputStream) throws DataFormatException {
588                return parseResource(theResourceType, new InputStreamReader(theInputStream, Constants.CHARSET_UTF8));
589        }
590
591        @Override
592        public <T extends IBaseResource> T parseResource(Class<T> theResourceType, Reader theReader) throws DataFormatException {
593
594                /*
595                 * We do this so that the context can verify that the structure is for
596                 * the correct FHIR version
597                 */
598                if (theResourceType != null) {
599                        myContext.getResourceDefinition(theResourceType);
600                }
601
602                // Actually do the parse
603                T retVal = doParseResource(theResourceType, theReader);
604
605                RuntimeResourceDefinition def = myContext.getResourceDefinition(retVal);
606                if ("Bundle".equals(def.getName())) {
607
608                        if (isOverrideResourceIdWithBundleEntryFullUrl()) {
609                                BundleUtil.processEntries(myContext, (IBaseBundle) retVal, t -> {
610                                        String fullUrl = t.getFullUrl();
611                                        if (fullUrl != null) {
612                                                IBaseResource resource = t.getResource();
613                                                if (resource != null) {
614                                                        IIdType resourceId = resource.getIdElement();
615                                                        if (isBlank(resourceId.getValue())) {
616                                                                resourceId.setValue(fullUrl);
617                                                        } else {
618                                                                if (fullUrl.startsWith("urn:") && fullUrl.length() > resourceId.getIdPart().length() && fullUrl.charAt(fullUrl.length() - resourceId.getIdPart().length() - 1) == ':' && fullUrl.endsWith(resourceId.getIdPart())) {
619                                                                        resourceId.setValue(fullUrl);
620                                                                } else {
621                                                                        IIdType fullUrlId = myContext.getVersion().newIdType();
622                                                                        fullUrlId.setValue(fullUrl);
623                                                                        if (myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
624                                                                                IIdType newId = fullUrlId;
625                                                                                if (!newId.hasVersionIdPart() && resourceId.hasVersionIdPart()) {
626                                                                                        newId = newId.withVersion(resourceId.getVersionIdPart());
627                                                                                }
628                                                                                resourceId.setValue(newId.getValue());
629                                                                        } else if (StringUtils.equals(fullUrlId.getIdPart(), resourceId.getIdPart())) {
630                                                                                if (fullUrlId.hasBaseUrl()) {
631                                                                                        IIdType newResourceId = resourceId.withServerBase(fullUrlId.getBaseUrl(), resourceId.getResourceType());
632                                                                                        resourceId.setValue(newResourceId.getValue());
633                                                                                }
634                                                                        }
635                                                                }
636                                                        }
637                                                }
638                                        }
639                                });
640                        }
641
642                }
643
644                return retVal;
645        }
646
647        @SuppressWarnings("cast")
648        @Override
649        public <T extends IBaseResource> T parseResource(Class<T> theResourceType, String theMessageString) {
650                StringReader reader = new StringReader(theMessageString);
651                return parseResource(theResourceType, reader);
652        }
653
654        @Override
655        public IBaseResource parseResource(Reader theReader) throws ConfigurationException, DataFormatException {
656                return parseResource(null, theReader);
657        }
658
659        @Override
660        public IBaseResource parseResource(String theMessageString) throws ConfigurationException, DataFormatException {
661                return parseResource(null, theMessageString);
662        }
663
664        protected List<? extends IBase> preProcessValues(BaseRuntimeChildDefinition theMetaChildUncast, IBaseResource theResource, List<? extends IBase> theValues,
665                                                                                                                                         CompositeChildElement theCompositeChildElement, EncodeContext theEncodeContext) {
666                if (myContext.getVersion().getVersion().isRi()) {
667
668                        /*
669                         * If we're encoding the meta tag, we do some massaging of the meta values before
670                         * encoding. But if there is no meta element at all, we create one since we're possibly going to be
671                         * adding things to it
672                         */
673                        if (theValues.isEmpty() && theMetaChildUncast.getElementName().equals("meta")) {
674                                BaseRuntimeElementDefinition<?> metaChild = theMetaChildUncast.getChildByName("meta");
675                                if (IBaseMetaType.class.isAssignableFrom(metaChild.getImplementingClass())) {
676                                        IBaseMetaType newType = (IBaseMetaType) metaChild.newInstance();
677                                        theValues = Collections.singletonList(newType);
678                                }
679                        }
680
681                        if (theValues.size() == 1 && theValues.get(0) instanceof IBaseMetaType) {
682
683                                IBaseMetaType metaValue = (IBaseMetaType) theValues.get(0);
684                                try {
685                                        metaValue = (IBaseMetaType) metaValue.getClass().getMethod("copy").invoke(metaValue);
686                                } catch (Exception e) {
687                                        throw new InternalErrorException(Msg.code(1830) + "Failed to duplicate meta", e);
688                                }
689
690                                if (isBlank(metaValue.getVersionId())) {
691                                        if (theResource.getIdElement().hasVersionIdPart()) {
692                                                metaValue.setVersionId(theResource.getIdElement().getVersionIdPart());
693                                        }
694                                }
695
696                                filterCodingsWithNoCodeOrSystem(metaValue.getTag());
697                                filterCodingsWithNoCodeOrSystem(metaValue.getSecurity());
698
699                                List<? extends IPrimitiveType<String>> newProfileList = getProfileTagsForEncoding(theResource, metaValue.getProfile());
700                                List<? extends IPrimitiveType<String>> oldProfileList = metaValue.getProfile();
701                                if (oldProfileList != newProfileList) {
702                                        oldProfileList.clear();
703                                        for (IPrimitiveType<String> next : newProfileList) {
704                                                if (isNotBlank(next.getValue())) {
705                                                        metaValue.addProfile(next.getValue());
706                                                }
707                                        }
708                                }
709
710                                if (shouldAddSubsettedTag(theEncodeContext)) {
711                                        IBaseCoding coding = metaValue.addTag();
712                                        coding.setCode(Constants.TAG_SUBSETTED_CODE);
713                                        coding.setSystem(getSubsettedCodeSystem());
714                                        coding.setDisplay(subsetDescription());
715                                }
716
717                                return Collections.singletonList(metaValue);
718                        }
719                }
720
721                @SuppressWarnings("unchecked")
722                List<IBase> retVal = (List<IBase>) theValues;
723
724                for (int i = 0; i < retVal.size(); i++) {
725                        IBase next = retVal.get(i);
726
727                        /*
728                         * If we have automatically contained any resources via
729                         * their references, this ensures that we output the new
730                         * local reference
731                         */
732                        if (next instanceof IBaseReference) {
733                                IBaseReference nextRef = (IBaseReference) next;
734                                String refText = determineReferenceText(nextRef, theCompositeChildElement);
735                                if (!StringUtils.equals(refText, nextRef.getReferenceElement().getValue())) {
736
737                                        if (retVal == theValues) {
738                                                retVal = new ArrayList<>(theValues);
739                                        }
740                                        IBaseReference newRef = (IBaseReference) myContext.getElementDefinition(nextRef.getClass()).newInstance();
741                                        myContext.newTerser().cloneInto(nextRef, newRef, true);
742                                        newRef.setReference(refText);
743                                        retVal.set(i, newRef);
744
745                                }
746                        }
747                }
748
749                return retVal;
750        }
751
752        private String getSubsettedCodeSystem() {
753                if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) {
754                        return Constants.TAG_SUBSETTED_SYSTEM_R4;
755                } else {
756                        return Constants.TAG_SUBSETTED_SYSTEM_DSTU3;
757                }
758        }
759
760        @Override
761        public IParser setDontStripVersionsFromReferencesAtPaths(String... thePaths) {
762                if (thePaths == null) {
763                        setDontStripVersionsFromReferencesAtPaths((List<String>) null);
764                } else {
765                        setDontStripVersionsFromReferencesAtPaths(Arrays.asList(thePaths));
766                }
767                return this;
768        }
769
770        @SuppressWarnings("unchecked")
771        @Override
772        public IParser setDontStripVersionsFromReferencesAtPaths(Collection<String> thePaths) {
773                if (thePaths == null) {
774                        myDontStripVersionsFromReferencesAtPaths = Collections.emptySet();
775                } else if (thePaths instanceof HashSet) {
776                        myDontStripVersionsFromReferencesAtPaths = (Set<String>) ((HashSet<String>) thePaths).clone();
777                } else {
778                        myDontStripVersionsFromReferencesAtPaths = new HashSet<>(thePaths);
779                }
780                return this;
781        }
782
783        @Override
784        public IParser setOmitResourceId(boolean theOmitResourceId) {
785                myOmitResourceId = theOmitResourceId;
786                return this;
787        }
788
789        @Override
790        public IParser setOverrideResourceIdWithBundleEntryFullUrl(Boolean theOverrideResourceIdWithBundleEntryFullUrl) {
791                myOverrideResourceIdWithBundleEntryFullUrl = theOverrideResourceIdWithBundleEntryFullUrl;
792                return this;
793        }
794
795        @Override
796        public IParser setParserErrorHandler(IParserErrorHandler theErrorHandler) {
797                Validate.notNull(theErrorHandler, "theErrorHandler must not be null");
798                myErrorHandler = theErrorHandler;
799                return this;
800        }
801
802        @Override
803        public IParser setServerBaseUrl(String theUrl) {
804                myServerBaseUrl = isNotBlank(theUrl) ? theUrl : null;
805                return this;
806        }
807
808        @Override
809        public IParser setStripVersionsFromReferences(Boolean theStripVersionsFromReferences) {
810                myStripVersionsFromReferences = theStripVersionsFromReferences;
811                return this;
812        }
813
814        @Override
815        public IParser setSummaryMode(boolean theSummaryMode) {
816                mySummaryMode = theSummaryMode;
817                return this;
818        }
819
820        @Override
821        public IParser setSuppressNarratives(boolean theSuppressNarratives) {
822                mySuppressNarratives = theSuppressNarratives;
823                return this;
824        }
825
826        protected boolean shouldAddSubsettedTag(EncodeContext theEncodeContext) {
827                if (isSummaryMode()) {
828                        return true;
829                }
830                if (isSuppressNarratives()) {
831                        return true;
832                }
833                if (myEncodeElements != null) {
834                        if (isEncodeElementsAppliesToChildResourcesOnly() && theEncodeContext.getResourcePath().size() < 2) {
835                                return false;
836                        }
837
838                        String currentResourceName = theEncodeContext.getResourcePath().get(theEncodeContext.getResourcePath().size() - 1).getName();
839                        return myEncodeElementsAppliesToResourceTypes == null || myEncodeElementsAppliesToResourceTypes.contains(currentResourceName);
840                }
841
842                return false;
843        }
844
845        protected boolean shouldEncodeResourceId(IBaseResource theResource, EncodeContext theEncodeContext) {
846                boolean retVal = true;
847                if (isOmitResourceId() && theEncodeContext.getPath().size() == 1) {
848                        retVal = false;
849                } else {
850                        if (myDontEncodeElements != null) {
851                                String resourceName = myContext.getResourceType(theResource);
852                                if (myDontEncodeElements.stream().anyMatch(t -> t.equalsPath(resourceName + ".id"))) {
853                                        retVal = false;
854                                } else if (myDontEncodeElements.stream().anyMatch(t -> t.equalsPath("*.id"))) {
855                                        retVal = false;
856                                } else if (theEncodeContext.getResourcePath().size() == 1 && myDontEncodeElements.stream().anyMatch(t -> t.equalsPath("id"))) {
857                                        retVal = false;
858                                }
859                        }
860                }
861                return retVal;
862        }
863
864        /**
865         * Used for DSTU2 only
866         */
867        protected boolean shouldEncodeResourceMeta(IResource theResource) {
868                return shouldEncodePath(theResource, "meta");
869        }
870
871        /**
872         * Used for DSTU2 only
873         */
874        protected boolean shouldEncodePath(IResource theResource, String thePath) {
875                if (myDontEncodeElements != null) {
876                        String resourceName = myContext.getResourceType(theResource);
877                        if (myDontEncodeElements.stream().anyMatch(t -> t.equalsPath(resourceName + "." + thePath))) {
878                                return false;
879                        } else return myDontEncodeElements.stream().noneMatch(t -> t.equalsPath("*." + thePath));
880                }
881                return true;
882        }
883
884        private String subsetDescription() {
885                return "Resource encoded in summary mode";
886        }
887
888        protected void throwExceptionForUnknownChildType(BaseRuntimeChildDefinition nextChild, Class<? extends IBase> theType) {
889                if (nextChild instanceof BaseRuntimeDeclaredChildDefinition) {
890                        StringBuilder b = new StringBuilder();
891                        b.append(nextChild.getElementName());
892                        b.append(" has type ");
893                        b.append(theType.getName());
894                        b.append(" but this is not a valid type for this element");
895                        if (nextChild instanceof RuntimeChildChoiceDefinition) {
896                                RuntimeChildChoiceDefinition choice = (RuntimeChildChoiceDefinition) nextChild;
897                                b.append(" - Expected one of: " + choice.getValidChildTypes());
898                        }
899                        throw new DataFormatException(Msg.code(1831) + b.toString());
900                }
901                throw new DataFormatException(Msg.code(1832) + nextChild + " has no child of type " + theType);
902        }
903
904        protected boolean shouldEncodeResource(String theName) {
905                if (myDontEncodeElements != null) {
906                        for (EncodeContextPath next : myDontEncodeElements) {
907                                if (next.equalsPath(theName)) {
908                                        return false;
909                                }
910                        }
911                }
912                return true;
913        }
914
915        protected boolean isFhirVersionLessThanOrEqualTo(FhirVersionEnum theFhirVersionEnum) {
916                final FhirVersionEnum apiFhirVersion = myContext.getVersion().getVersion();
917                return theFhirVersionEnum == apiFhirVersion || apiFhirVersion.isOlderThan(theFhirVersionEnum);
918        }
919
920        class ChildNameAndDef {
921
922                private final BaseRuntimeElementDefinition<?> myChildDef;
923                private final String myChildName;
924
925                public ChildNameAndDef(String theChildName, BaseRuntimeElementDefinition<?> theChildDef) {
926                        myChildName = theChildName;
927                        myChildDef = theChildDef;
928                }
929
930                public BaseRuntimeElementDefinition<?> getChildDef() {
931                        return myChildDef;
932                }
933
934                public String getChildName() {
935                        return myChildName;
936                }
937
938        }
939
940        /**
941         * EncodeContext is a shared state object that is passed around the
942         * encode process
943         */
944        public class EncodeContext extends EncodeContextPath {
945                private final Map<Key, List<BaseParser.CompositeChildElement>> myCompositeChildrenCache = new HashMap<>();
946
947                public Map<Key, List<BaseParser.CompositeChildElement>> getCompositeChildrenCache() {
948                        return myCompositeChildrenCache;
949                }
950
951        }
952
953
954        protected class CompositeChildElement {
955                private final BaseRuntimeChildDefinition myDef;
956                private final CompositeChildElement myParent;
957                private final RuntimeResourceDefinition myResDef;
958                private final EncodeContext myEncodeContext;
959
960                public CompositeChildElement(CompositeChildElement theParent, @Nullable BaseRuntimeChildDefinition theDef, EncodeContext theEncodeContext) {
961                        myDef = theDef;
962                        myParent = theParent;
963                        myResDef = null;
964                        myEncodeContext = theEncodeContext;
965
966                        if (ourLog.isTraceEnabled()) {
967                                if (theParent != null) {
968                                        StringBuilder path = theParent.buildPath();
969                                        if (path != null) {
970                                                path.append('.');
971                                                if (myDef != null) {
972                                                        path.append(myDef.getElementName());
973                                                }
974                                                ourLog.trace(" * Next path: {}", path.toString());
975                                        }
976                                }
977                        }
978
979                }
980
981                public CompositeChildElement(RuntimeResourceDefinition theResDef, EncodeContext theEncodeContext) {
982                        myResDef = theResDef;
983                        myDef = null;
984                        myParent = null;
985                        myEncodeContext = theEncodeContext;
986                }
987
988        @Override
989        public String toString() {
990            return myDef.getElementName();
991        }
992
993        private void addParent(CompositeChildElement theParent, StringBuilder theB) {
994                        if (theParent != null) {
995                                if (theParent.myResDef != null) {
996                                        theB.append(theParent.myResDef.getName());
997                                        return;
998                                }
999
1000                                if (theParent.myParent != null) {
1001                                        addParent(theParent.myParent, theB);
1002                                }
1003
1004                                if (theParent.myDef != null) {
1005                                        if (theB.length() > 0) {
1006                                                theB.append('.');
1007                                        }
1008                                        theB.append(theParent.myDef.getElementName());
1009                                }
1010                        }
1011                }
1012
1013                public boolean anyPathMatches(Set<String> thePaths) {
1014                        StringBuilder b = new StringBuilder();
1015                        addParent(this, b);
1016
1017                        String path = b.toString();
1018                        return thePaths.contains(path);
1019                }
1020
1021                private StringBuilder buildPath() {
1022                        if (myResDef != null) {
1023                                StringBuilder b = new StringBuilder();
1024                                b.append(myResDef.getName());
1025                                return b;
1026                        } else if (myParent != null) {
1027                                StringBuilder b = myParent.buildPath();
1028                                if (b != null && myDef != null) {
1029                                        b.append('.');
1030                                        b.append(myDef.getElementName());
1031                                }
1032                                return b;
1033                        } else {
1034                                return null;
1035                        }
1036                }
1037
1038                private boolean checkIfParentShouldBeEncodedAndBuildPath() {
1039                        List<EncodeContextPath> encodeElements = myEncodeElements;
1040
1041                        String currentResourceName = myEncodeContext.getResourcePath().get(myEncodeContext.getResourcePath().size() - 1).getName();
1042                        if (myEncodeElementsAppliesToResourceTypes != null && !myEncodeElementsAppliesToResourceTypes.contains(currentResourceName)) {
1043                                encodeElements = null;
1044                        }
1045
1046                        boolean retVal = checkIfPathMatchesForEncoding(encodeElements, true);
1047
1048                        /*
1049                         * We force the meta tag to be encoded even if it's not specified as an element in the
1050                         * elements filter, specifically because we'll need it in order to automatically add
1051                         * the SUBSETTED tag
1052                         */
1053                        if (!retVal) {
1054                                if ("meta".equals(myEncodeContext.getLeafResourcePathFirstField()) && shouldAddSubsettedTag(myEncodeContext)) {
1055                                        // The next element is a child of the <meta> element
1056                                        retVal = true;
1057                                } else if ("meta".equals(myDef.getElementName()) && shouldAddSubsettedTag(myEncodeContext)) {
1058                                        // The next element is the <meta> element
1059                                        retVal = true;
1060                                }
1061                        }
1062
1063                        return retVal;
1064                }
1065
1066                private boolean checkIfParentShouldNotBeEncodedAndBuildPath() {
1067                        return checkIfPathMatchesForEncoding(myDontEncodeElements, false);
1068                }
1069
1070                private boolean checkIfPathMatchesForEncoding(List<EncodeContextPath> theElements, boolean theCheckingForEncodeElements) {
1071
1072                        boolean retVal = false;
1073                        if (myDef != null) {
1074                                myEncodeContext.pushPath(myDef.getElementName(), false);
1075                        }
1076
1077                        if (theCheckingForEncodeElements && isEncodeElementsAppliesToChildResourcesOnly() && myEncodeContext.getResourcePath().size() < 2) {
1078                                retVal = true;
1079                        } else if (theElements == null) {
1080                                retVal = true;
1081                        } else {
1082                                EncodeContextPath currentResourcePath = myEncodeContext.getCurrentResourcePath();
1083                                ourLog.trace("Current resource path: {}", currentResourcePath);
1084                                for (EncodeContextPath next : theElements) {
1085
1086                                        if (next.startsWith(currentResourcePath, true)) {
1087                                                if (theCheckingForEncodeElements || next.getPath().size() == currentResourcePath.getPath().size()) {
1088                                                        retVal = true;
1089                                                        break;
1090                                                }
1091                                        }
1092
1093                                        if (next.getPath().get(next.getPath().size() - 1).getName().equals("(mandatory)")) {
1094                                                if (myDef.getMin() > 0) {
1095                                                        retVal = true;
1096                                                        break;
1097                                                }
1098                                                if (currentResourcePath.getPath().size() > next.getPath().size()) {
1099                                                        retVal = true;
1100                                                        break;
1101                                                }
1102                                        }
1103
1104                                }
1105                        }
1106
1107                        if (myDef != null) {
1108                                myEncodeContext.popPath();
1109                        }
1110
1111                        return retVal;
1112                }
1113
1114                public BaseRuntimeChildDefinition getDef() {
1115                        return myDef;
1116                }
1117
1118                public CompositeChildElement getParent() {
1119                        return myParent;
1120                }
1121
1122                public boolean shouldBeEncoded(boolean theContainedResource) {
1123                        boolean retVal = true;
1124                        if (myEncodeElements != null) {
1125                                retVal = checkIfParentShouldBeEncodedAndBuildPath();
1126                        }
1127                        if (retVal && myDontEncodeElements != null) {
1128                                retVal = !checkIfParentShouldNotBeEncodedAndBuildPath();
1129                        }
1130                        if (theContainedResource) {
1131                                retVal = !notEncodeForContainedResource.contains(myDef.getElementName());
1132                        }
1133                        if (retVal && isSummaryMode() && (getDef() == null || !getDef().isSummary())) {
1134                                String resourceName = myEncodeContext.getLeafResourceName();
1135                                // Technically the spec says we shouldn't include extensions in CapabilityStatement
1136                                // but we will do so because there are people who depend on this behaviour, at least
1137                                // as of 2019-07. See
1138                                // https://github.com/smart-on-fhir/Swift-FHIR/issues/26
1139                                // for example.
1140                                if (("Conformance".equals(resourceName) || "CapabilityStatement".equals(resourceName)) &&
1141                                        ("extension".equals(myDef.getElementName()) || "extension".equals(myEncodeContext.getLeafElementName())
1142                                        )) {
1143                                        // skip
1144                                } else {
1145                                        retVal = false;
1146                                }
1147                        }
1148
1149                        return retVal;
1150                }
1151
1152                @Override
1153                public int hashCode() {
1154                        final int prime = 31;
1155                        int result = 1;
1156                        result = prime * result + ((myDef == null) ? 0 : myDef.hashCode());
1157                        result = prime * result + ((myParent == null) ? 0 : myParent.hashCode());
1158                        result = prime * result + ((myResDef == null) ? 0 : myResDef.hashCode());
1159                        result = prime * result + ((myEncodeContext == null) ? 0 : myEncodeContext.hashCode());
1160                        return result;
1161                }
1162
1163                @Override
1164                public boolean equals(Object obj) {
1165                        if (this == obj)
1166                                return true;
1167
1168                        if (obj instanceof CompositeChildElement) {
1169                                final CompositeChildElement that = (CompositeChildElement) obj;
1170                                return Objects.equals(this.getEnclosingInstance(), that.getEnclosingInstance()) &&
1171                                        Objects.equals(this.myDef, that.myDef) &&
1172                                        Objects.equals(this.myParent, that.myParent) &&
1173                                        Objects.equals(this.myResDef, that.myResDef) &&
1174                                        Objects.equals(this.myEncodeContext, that.myEncodeContext);
1175                        }
1176                        return false;
1177                }
1178
1179                private BaseParser getEnclosingInstance() {
1180                        return BaseParser.this;
1181                }
1182        }
1183
1184        private static class Key {
1185                private final BaseRuntimeElementCompositeDefinition<?> resDef;
1186                private final boolean theContainedResource;
1187                private final BaseParser.CompositeChildElement theParent;
1188                private final BaseParser.EncodeContext theEncodeContext;
1189
1190                public Key(BaseRuntimeElementCompositeDefinition<?> resDef, final boolean theContainedResource, final BaseParser.CompositeChildElement theParent, BaseParser.EncodeContext theEncodeContext) {
1191                        this.resDef = resDef;
1192                        this.theContainedResource = theContainedResource;
1193                        this.theParent = theParent;
1194                        this.theEncodeContext = theEncodeContext;
1195                }
1196
1197                @Override
1198                public int hashCode() {
1199                        final int prime = 31;
1200                        int result = 1;
1201                        result = prime * result + ((resDef == null) ? 0 : resDef.hashCode());
1202                        result = prime * result + (theContainedResource ? 1231 : 1237);
1203                        result = prime * result + ((theParent == null) ? 0 : theParent.hashCode());
1204                        result = prime * result + ((theEncodeContext == null) ? 0 : theEncodeContext.hashCode());
1205                        return result;
1206                }
1207
1208                @Override
1209                public boolean equals(final Object obj) {
1210                        if (this == obj) {
1211                                return true;
1212                        }
1213                        if (obj instanceof Key) {
1214                                final Key that = (Key) obj;
1215                                return Objects.equals(this.resDef, that.resDef) &&
1216                                        this.theContainedResource == that.theContainedResource &&
1217                                        Objects.equals(this.theParent, that.theParent) &&
1218                                        Objects.equals(this.theEncodeContext, that.theEncodeContext);
1219                        }
1220                        return false;
1221                }
1222        }
1223
1224
1225        protected static <T> List<T> extractMetadataListNotNull(IResource resource, ResourceMetadataKeyEnum<List<T>> key) {
1226                List<? extends T> securityLabels = key.get(resource);
1227                if (securityLabels == null) {
1228                        securityLabels = Collections.emptyList();
1229                }
1230                return new ArrayList<>(securityLabels);
1231        }
1232
1233        static boolean hasNoExtensions(IBase theElement) {
1234                if (theElement instanceof ISupportsUndeclaredExtensions) {
1235                        ISupportsUndeclaredExtensions res = (ISupportsUndeclaredExtensions) theElement;
1236                        if (res.getUndeclaredExtensions().size() > 0 || res.getUndeclaredModifierExtensions().size() > 0) {
1237                                return false;
1238                        }
1239                }
1240                if (theElement instanceof IBaseHasExtensions) {
1241                        IBaseHasExtensions res = (IBaseHasExtensions) theElement;
1242                        if (res.hasExtension()) {
1243                                return false;
1244                        }
1245                }
1246                if (theElement instanceof IBaseHasModifierExtensions) {
1247                        IBaseHasModifierExtensions res = (IBaseHasModifierExtensions) theElement;
1248                        return !res.hasModifierExtension();
1249                }
1250                return true;
1251        }
1252
1253}