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