001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2025 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.util;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
025import ca.uhn.fhir.context.FhirContext;
026import ca.uhn.fhir.context.RuntimeResourceDefinition;
027import ca.uhn.fhir.i18n.Msg;
028import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
029import ca.uhn.fhir.model.primitive.IdDt;
030import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
031import ca.uhn.fhir.model.valueset.BundleTypeEnum;
032import ca.uhn.fhir.rest.api.PatchTypeEnum;
033import ca.uhn.fhir.rest.api.RequestTypeEnum;
034import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
035import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
036import ca.uhn.fhir.util.bundle.BundleEntryMutator;
037import ca.uhn.fhir.util.bundle.BundleEntryParts;
038import ca.uhn.fhir.util.bundle.EntryListAccumulator;
039import ca.uhn.fhir.util.bundle.ModifiableBundleEntry;
040import ca.uhn.fhir.util.bundle.ModifiableBundleEntryParts;
041import ca.uhn.fhir.util.bundle.PartsConverter;
042import ca.uhn.fhir.util.bundle.SearchBundleEntryParts;
043import com.google.common.collect.Sets;
044import jakarta.annotation.Nonnull;
045import jakarta.annotation.Nullable;
046import org.apache.commons.lang3.Validate;
047import org.apache.commons.lang3.tuple.Pair;
048import org.hl7.fhir.instance.model.api.IBase;
049import org.hl7.fhir.instance.model.api.IBaseBackboneElement;
050import org.hl7.fhir.instance.model.api.IBaseBinary;
051import org.hl7.fhir.instance.model.api.IBaseBundle;
052import org.hl7.fhir.instance.model.api.IBaseReference;
053import org.hl7.fhir.instance.model.api.IBaseResource;
054import org.hl7.fhir.instance.model.api.IPrimitiveType;
055import org.slf4j.Logger;
056import org.slf4j.LoggerFactory;
057
058import java.math.BigDecimal;
059import java.util.ArrayList;
060import java.util.HashMap;
061import java.util.LinkedHashSet;
062import java.util.List;
063import java.util.Map;
064import java.util.Objects;
065import java.util.Set;
066import java.util.function.Consumer;
067import java.util.stream.Collectors;
068
069import static org.apache.commons.lang3.StringUtils.defaultString;
070import static org.apache.commons.lang3.StringUtils.isBlank;
071import static org.apache.commons.lang3.StringUtils.isNotBlank;
072import static org.hl7.fhir.instance.model.api.IBaseBundle.LINK_PREV;
073
074/**
075 * Fetch resources from a bundle
076 */
077public class BundleUtil {
078
079        public static final String DIFFERENT_LINK_ERROR_MSG =
080                        "Mismatching 'previous' and 'prev' links exist. 'previous' " + "is: '$PREVIOUS' and 'prev' is: '$PREV'.";
081        public static final String BUNDLE_TYPE_TRANSACTION_RESPONSE = "transaction-response";
082        private static final Logger ourLog = LoggerFactory.getLogger(BundleUtil.class);
083
084        private static final String PREVIOUS = LINK_PREV;
085        private static final String PREV = "prev";
086        private static final Set<String> previousOrPrev = Sets.newHashSet(PREVIOUS, PREV);
087        static int WHITE = 1;
088        static int GRAY = 2;
089        static int BLACK = 3;
090
091        /**
092         * Non instantiable
093         */
094        private BundleUtil() {
095                // nothing
096        }
097
098        /**
099         * @return Returns <code>null</code> if the link isn't found or has no value
100         */
101        public static String getLinkUrlOfType(FhirContext theContext, IBaseBundle theBundle, String theLinkRelation) {
102                return getLinkUrlOfType(theContext, theBundle, theLinkRelation, true);
103        }
104
105        private static String getLinkUrlOfType(
106                        FhirContext theContext, IBaseBundle theBundle, String theLinkRelation, boolean isPreviousCheck) {
107                RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
108                BaseRuntimeChildDefinition entryChild = def.getChildByName("link");
109                List<IBase> links = entryChild.getAccessor().getValues(theBundle);
110                for (IBase nextLink : links) {
111
112                        boolean isRightRel = false;
113                        BaseRuntimeElementCompositeDefinition<?> relDef =
114                                        (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(nextLink.getClass());
115                        BaseRuntimeChildDefinition relChild = relDef.getChildByName("relation");
116                        List<IBase> relValues = relChild.getAccessor().getValues(nextLink);
117                        for (IBase next : relValues) {
118                                IPrimitiveType<?> nextValue = (IPrimitiveType<?>) next;
119                                if (isRelationMatch(
120                                                theContext, theBundle, theLinkRelation, nextValue.getValueAsString(), isPreviousCheck)) {
121                                        isRightRel = true;
122                                }
123                        }
124
125                        if (!isRightRel) {
126                                continue;
127                        }
128
129                        BaseRuntimeElementCompositeDefinition<?> linkDef =
130                                        (BaseRuntimeElementCompositeDefinition<?>) theContext.getElementDefinition(nextLink.getClass());
131                        BaseRuntimeChildDefinition urlChild = linkDef.getChildByName("url");
132                        List<IBase> values = urlChild.getAccessor().getValues(nextLink);
133                        for (IBase nextUrl : values) {
134                                IPrimitiveType<?> nextValue = (IPrimitiveType<?>) nextUrl;
135                                if (isNotBlank(nextValue.getValueAsString())) {
136                                        return nextValue.getValueAsString();
137                                }
138                        }
139                }
140
141                return null;
142        }
143
144        private static boolean isRelationMatch(
145                        FhirContext theContext, IBaseBundle theBundle, String value, String matching, boolean theIsPreviousCheck) {
146                if (!theIsPreviousCheck) {
147                        return value.equals(matching);
148                }
149
150                if (previousOrPrev.contains(value)) {
151                        validateUniqueOrMatchingPreviousValues(theContext, theBundle);
152                        if (previousOrPrev.contains(matching)) {
153                                return true;
154                        }
155                }
156                return (value.equals(matching));
157        }
158
159        private static void validateUniqueOrMatchingPreviousValues(FhirContext theContext, IBaseBundle theBundle) {
160                String previousLink = getLinkNoCheck(theContext, theBundle, PREVIOUS);
161                String prevLink = getLinkNoCheck(theContext, theBundle, PREV);
162                if (prevLink != null && previousLink != null) {
163                        if (!previousLink.equals(prevLink)) {
164                                String msg = DIFFERENT_LINK_ERROR_MSG
165                                                .replace("$PREVIOUS", previousLink)
166                                                .replace("$PREV", prevLink);
167                                throw new InternalErrorException(Msg.code(2368) + msg);
168                        }
169                }
170        }
171
172        private static String getLinkNoCheck(FhirContext theContext, IBaseBundle theBundle, String theLinkRelation) {
173                return getLinkUrlOfType(theContext, theBundle, theLinkRelation, false);
174        }
175
176        /**
177         * Returns a collection of Pairs, one for each entry in the bundle. Each pair will contain
178         * the values of Bundle.entry.fullUrl, and Bundle.entry.resource respectively. Nulls
179         * are possible in either or both values in the Pair.
180         *
181         * @since 7.0.0
182         */
183        @SuppressWarnings("unchecked")
184        public static List<Pair<String, IBaseResource>> getBundleEntryFullUrlsAndResources(
185                        FhirContext theContext, IBaseBundle theBundle) {
186                RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
187                BaseRuntimeChildDefinition entryChild = def.getChildByName("entry");
188                List<IBase> entries = entryChild.getAccessor().getValues(theBundle);
189
190                BaseRuntimeElementCompositeDefinition<?> entryChildElem =
191                                (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry");
192                BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource");
193
194                BaseRuntimeChildDefinition urlChild = entryChildElem.getChildByName("fullUrl");
195
196                List<Pair<String, IBaseResource>> retVal = new ArrayList<>(entries.size());
197                for (IBase nextEntry : entries) {
198
199                        String fullUrl = urlChild.getAccessor()
200                                        .getFirstValueOrNull(nextEntry)
201                                        .map(t -> (((IPrimitiveType<?>) t).getValueAsString()))
202                                        .orElse(null);
203                        IBaseResource resource = (IBaseResource)
204                                        resourceChild.getAccessor().getFirstValueOrNull(nextEntry).orElse(null);
205
206                        retVal.add(Pair.of(fullUrl, resource));
207                }
208
209                return retVal;
210        }
211
212        /**
213         * Returns true if the resource provided is *not* a BundleEntrySearchModeEnum OUTCOME or INCLUDE
214         * (ie, if MATCH or null, this will return true)
215         * @param theResource
216         * @return
217         */
218        public static boolean isMatchResource(IBaseResource theResource) {
219                BundleEntrySearchModeEnum matchType = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(theResource);
220                if (matchType == null || matchType == BundleEntrySearchModeEnum.MATCH) {
221                        return true;
222                }
223                return false;
224        }
225
226        public static List<Pair<String, IBaseResource>> getBundleEntryUrlsAndResources(
227                        FhirContext theContext, IBaseBundle theBundle) {
228                RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
229                BaseRuntimeChildDefinition entryChild = def.getChildByName("entry");
230                List<IBase> entries = entryChild.getAccessor().getValues(theBundle);
231
232                BaseRuntimeElementCompositeDefinition<?> entryChildElem =
233                                (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry");
234                BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource");
235
236                BaseRuntimeChildDefinition requestChild = entryChildElem.getChildByName("request");
237                BaseRuntimeElementCompositeDefinition<?> requestDef =
238                                (BaseRuntimeElementCompositeDefinition<?>) requestChild.getChildByName("request");
239
240                BaseRuntimeChildDefinition urlChild = requestDef.getChildByName("url");
241
242                List<Pair<String, IBaseResource>> retVal = new ArrayList<>(entries.size());
243                for (IBase nextEntry : entries) {
244
245                        String url = requestChild
246                                        .getAccessor()
247                                        .getFirstValueOrNull(nextEntry)
248                                        .flatMap(e -> urlChild.getAccessor().getFirstValueOrNull(e))
249                                        .map(t -> ((IPrimitiveType<?>) t).getValueAsString())
250                                        .orElse(null);
251
252                        IBaseResource resource = (IBaseResource)
253                                        resourceChild.getAccessor().getFirstValueOrNull(nextEntry).orElse(null);
254
255                        retVal.add(Pair.of(url, resource));
256                }
257
258                return retVal;
259        }
260
261        public static String getBundleType(FhirContext theContext, IBaseBundle theBundle) {
262                RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
263                BaseRuntimeChildDefinition entryChild = def.getChildByName("type");
264                List<IBase> entries = entryChild.getAccessor().getValues(theBundle);
265                if (entries.size() > 0) {
266                        IPrimitiveType<?> typeElement = (IPrimitiveType<?>) entries.get(0);
267                        return typeElement.getValueAsString();
268                }
269                return null;
270        }
271
272        public static BundleTypeEnum getBundleTypeEnum(FhirContext theContext, IBaseBundle theBundle) {
273                String bundleTypeCode = BundleUtil.getBundleType(theContext, theBundle);
274                if (isBlank(bundleTypeCode)) {
275                        return null;
276                }
277                return BundleTypeEnum.forCode(bundleTypeCode);
278        }
279
280        public static void setBundleType(FhirContext theContext, IBaseBundle theBundle, String theType) {
281                RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
282                BaseRuntimeChildDefinition entryChild = def.getChildByName("type");
283                BaseRuntimeElementDefinition<?> element = entryChild.getChildByName("type");
284                IPrimitiveType<?> typeInstance =
285                                (IPrimitiveType<?>) element.newInstance(entryChild.getInstanceConstructorArguments());
286                typeInstance.setValueAsString(theType);
287
288                entryChild.getMutator().setValue(theBundle, typeInstance);
289        }
290
291        public static Integer getTotal(FhirContext theContext, IBaseBundle theBundle) {
292                RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
293                BaseRuntimeChildDefinition entryChild = def.getChildByName("total");
294                List<IBase> entries = entryChild.getAccessor().getValues(theBundle);
295                if (entries.size() > 0) {
296                        @SuppressWarnings("unchecked")
297                        IPrimitiveType<Number> typeElement = (IPrimitiveType<Number>) entries.get(0);
298                        if (typeElement != null && typeElement.getValue() != null) {
299                                return typeElement.getValue().intValue();
300                        }
301                }
302                return null;
303        }
304
305        public static void setTotal(FhirContext theContext, IBaseBundle theBundle, Integer theTotal) {
306                RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
307                BaseRuntimeChildDefinition entryChild = def.getChildByName("total");
308                @SuppressWarnings("unchecked")
309                IPrimitiveType<Integer> value =
310                                (IPrimitiveType<Integer>) entryChild.getChildByName("total").newInstance();
311                value.setValue(theTotal);
312                entryChild.getMutator().setValue(theBundle, value);
313        }
314
315        /**
316         * Extract all of the resources from a given bundle
317         */
318        public static List<BundleEntryParts> toListOfEntries(FhirContext theContext, IBaseBundle theBundle) {
319                EntryListAccumulator entryListAccumulator = new EntryListAccumulator();
320                processEntries(theContext, theBundle, entryListAccumulator);
321                return entryListAccumulator.getList();
322        }
323
324        public static <T> List<T> toListOfEntries(
325                        FhirContext theContext, IBaseBundle theBundle, PartsConverter<T> partsConverter) {
326                RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle);
327                BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry");
328                List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle);
329                return entries.stream().map(partsConverter::fromElement).toList();
330        }
331
332        /**
333         * Returns a list of entries in the Bundle with a modifiable type that can be used
334         * to manipulate the entries.
335         *
336         * @param theContext The FhirContext associated with the version of FHIR
337         * @param theBundle The Bundle
338         * @return A list of modifiable entries
339         * @since 8.6.0
340         */
341        public static List<ModifiableBundleEntryParts> toListOfEntriesModifiable(
342                        FhirContext theContext, IBaseBundle theBundle) {
343                RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle);
344                BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry");
345                List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle);
346
347                BaseRuntimeElementCompositeDefinition<?> entryChildContentsDef =
348                                (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry");
349                BaseRuntimeChildDefinition fullUrlChildDef = entryChildContentsDef.getChildByName("fullUrl");
350                BaseRuntimeChildDefinition resourceChildDef = entryChildContentsDef.getChildByName("resource");
351                BaseRuntimeChildDefinition requestChildDef = entryChildContentsDef.getChildByName("request");
352                BaseRuntimeElementCompositeDefinition<?> requestChildContentsDef =
353                                (BaseRuntimeElementCompositeDefinition<?>) requestChildDef.getChildByName("request");
354                BaseRuntimeChildDefinition requestUrlChildDef = requestChildContentsDef.getChildByName("url");
355                BaseRuntimeChildDefinition requestIfNoneExistChildDef = requestChildContentsDef.getChildByName("ifNoneExist");
356                BaseRuntimeChildDefinition methodChildDef = requestChildContentsDef.getChildByName("method");
357
358                List<ModifiableBundleEntryParts> retVal = new ArrayList<>(entries.size());
359                for (IBase nextEntry : entries) {
360                        BundleEntryParts parts = getBundleEntryParts(
361                                        fullUrlChildDef,
362                                        resourceChildDef,
363                                        requestChildDef,
364                                        requestUrlChildDef,
365                                        requestIfNoneExistChildDef,
366                                        methodChildDef,
367                                        nextEntry);
368                        /*
369                         * All 3 might be null - That's ok because we still want to know the
370                         * order in the original bundle.
371                         */
372                        BundleEntryMutator mutator = new BundleEntryMutator(
373                                        theContext,
374                                        nextEntry,
375                                        requestChildDef,
376                                        requestChildContentsDef,
377                                        entryChildContentsDef,
378                                        methodChildDef);
379                        ModifiableBundleEntryParts entry = new ModifiableBundleEntryParts(theContext, parts, mutator);
380                        retVal.add(entry);
381                }
382
383                return retVal;
384        }
385
386        /**
387         * Invokes a consumer for each entry in the Bundle, passing a {@link ModifiableBundleEntryParts}
388         * which contains a version-independent means of accessing and modifiing each entry
389         *
390         * @param theContext The FhirContext for the FHIR version associated with the Bundle
391         * @param theBundle The Bundle to process
392         * @param theEntryConsumer A consumer to process each entry
393         * @since 8.6.0
394         */
395        public static void processAllEntries(
396                        FhirContext theContext, IBaseBundle theBundle, Consumer<ModifiableBundleEntryParts> theEntryConsumer) {
397                toListOfEntriesModifiable(theContext, theBundle).forEach(theEntryConsumer);
398        }
399
400        /**
401         * Function which will do an in-place sort of a bundles' entries, to the correct processing order, which is:
402         * 1. Deletes
403         * 2. Creates
404         * 3. Updates
405         * <p>
406         * Furthermore, within these operation types, the entries will be sorted based on the order in which they should be processed
407         * e.g. if you have 2 CREATEs, one for a Patient, and one for an Observation which has this Patient as its Subject,
408         * the patient will come first, then the observation.
409         * <p>
410         * In cases of there being a cyclic dependency (e.g. Organization/1 is partOf Organization/2 and Organization/2 is partOf Organization/1)
411         * this function will throw an IllegalStateException.
412         *
413         * @param theContext The FhirContext.
414         * @param theBundle  The {@link IBaseBundle} which contains the entries you would like sorted into processing order.
415         */
416        public static void sortEntriesIntoProcessingOrder(FhirContext theContext, IBaseBundle theBundle)
417                        throws IllegalStateException {
418                Map<BundleEntryParts, IBase> partsToIBaseMap = getPartsToIBaseMap(theContext, theBundle);
419
420                // Get all deletions.
421                LinkedHashSet<IBase> deleteParts =
422                                sortEntriesOfTypeIntoProcessingOrder(theContext, RequestTypeEnum.DELETE, partsToIBaseMap);
423                validatePartsNotNull(deleteParts);
424                LinkedHashSet<IBase> retVal = new LinkedHashSet<>(deleteParts);
425
426                // Get all Creations
427                LinkedHashSet<IBase> createParts =
428                                sortEntriesOfTypeIntoProcessingOrder(theContext, RequestTypeEnum.POST, partsToIBaseMap);
429                validatePartsNotNull(createParts);
430                retVal.addAll(createParts);
431
432                // Get all Updates
433                LinkedHashSet<IBase> updateParts =
434                                sortEntriesOfTypeIntoProcessingOrder(theContext, RequestTypeEnum.PUT, partsToIBaseMap);
435                validatePartsNotNull(updateParts);
436                retVal.addAll(updateParts);
437
438                // Once we are done adding all DELETE, POST, PUT operations, add everything else.
439                // Since this is a set, it will just fail to add already-added operations.
440                retVal.addAll(partsToIBaseMap.values());
441
442                // Blow away the entries and reset them in the right order.
443                TerserUtil.clearField(theContext, theBundle, "entry");
444                TerserUtil.setField(theContext, "entry", theBundle, retVal.toArray(new IBase[0]));
445        }
446
447        /**
448         * Converts a Bundle containing resources into a FHIR transaction which
449         * creates/updates the resources. This method does not modify the original
450         * bundle, but returns a new copy.
451         * <p>
452         * This method is mostly intended for test scenarios where you have a Bundle
453         * containing search results or other sourced resources, and want to upload
454         * these resources to a server using a single FHIR transaction.
455         * </p>
456         * <p>
457         * The Bundle is converted using the following logic:
458         * <ul>
459         *     <li>Bundle.type is changed to <code>transaction</code></li>
460         *     <li>Bundle.request.method is changed to <code>PUT</code></li>
461         *     <li>Bundle.request.url is changed to <code>[resourceType]/[id]</code></li>
462         *     <li>Bundle.fullUrl is changed to <code>[resourceType]/[id]</code></li>
463         * </ul>
464         * </p>
465         *
466         * @param theContext         The FhirContext to use with the bundle
467         * @param theBundle          The Bundle to modify. All resources in the Bundle should have an ID.
468         * @param thePrefixIdsOrNull If not <code>null</code>, all resource IDs and all references in the Bundle will be
469         *                           modified to such that their IDs contain the given prefix. For example, for a value
470         *                           of "A", the resource "Patient/123" will be changed to be "Patient/A123". If set to
471         *                           <code>null</code>, resource IDs are unchanged.
472         * @since 7.4.0
473         */
474        public static <T extends IBaseBundle> T convertBundleIntoTransaction(
475                        @Nonnull FhirContext theContext, @Nonnull T theBundle, @Nullable String thePrefixIdsOrNull) {
476                String prefix = defaultString(thePrefixIdsOrNull);
477
478                BundleBuilder bb = new BundleBuilder(theContext);
479
480                FhirTerser terser = theContext.newTerser();
481                List<IBase> entries = terser.getValues(theBundle, "Bundle.entry");
482                for (var entry : entries) {
483                        IBaseResource resource = terser.getSingleValueOrNull(entry, "resource", IBaseResource.class);
484                        if (resource != null) {
485                                Validate.isTrue(resource.getIdElement().hasIdPart(), "Resource in bundle has no ID");
486                                String newId = theContext.getResourceType(resource) + "/" + prefix
487                                                + resource.getIdElement().getIdPart();
488
489                                IBaseResource resourceClone = terser.clone(resource);
490                                resourceClone.setId(newId);
491
492                                if (isNotBlank(prefix)) {
493                                        for (var ref : terser.getAllResourceReferences(resourceClone)) {
494                                                var refElement = ref.getResourceReference().getReferenceElement();
495                                                ref.getResourceReference()
496                                                                .setReference(refElement.getResourceType() + "/" + prefix + refElement.getIdPart());
497                                        }
498                                }
499
500                                bb.addTransactionUpdateEntry(resourceClone);
501                        }
502                }
503
504                return bb.getBundleTyped();
505        }
506
507        private static void validatePartsNotNull(LinkedHashSet<IBase> theDeleteParts) {
508                if (theDeleteParts == null) {
509                        throw new IllegalStateException(
510                                        Msg.code(1745) + "This transaction contains a cycle, so it cannot be sorted.");
511                }
512        }
513
514        private static LinkedHashSet<IBase> sortEntriesOfTypeIntoProcessingOrder(
515                        FhirContext theContext,
516                        RequestTypeEnum theRequestTypeEnum,
517                        Map<BundleEntryParts, IBase> thePartsToIBaseMap) {
518                SortLegality legality = new SortLegality();
519                HashMap<String, Integer> color = new HashMap<>();
520                HashMap<String, List<String>> adjList = new HashMap<>();
521                List<String> topologicalOrder = new ArrayList<>();
522                Set<BundleEntryParts> bundleEntryParts = thePartsToIBaseMap.keySet().stream()
523                                .filter(part -> part.getRequestType().equals(theRequestTypeEnum))
524                                .collect(Collectors.toSet());
525                HashMap<String, BundleEntryParts> resourceIdToBundleEntryMap = new HashMap<>();
526
527                for (BundleEntryParts bundleEntryPart : bundleEntryParts) {
528                        IBaseResource resource = bundleEntryPart.getResource();
529                        if (resource != null) {
530                                String resourceId = resource.getIdElement().toVersionless().toString();
531                                resourceIdToBundleEntryMap.put(resourceId, bundleEntryPart);
532                                if (resourceId == null) {
533                                        if (bundleEntryPart.getFullUrl() != null) {
534                                                resourceId = bundleEntryPart.getFullUrl();
535                                        }
536                                }
537
538                                color.put(resourceId, WHITE);
539                        }
540                }
541
542                for (BundleEntryParts bundleEntryPart : bundleEntryParts) {
543                        IBaseResource resource = bundleEntryPart.getResource();
544                        if (resource != null) {
545                                String resourceId = resource.getIdElement().toVersionless().toString();
546                                resourceIdToBundleEntryMap.put(resourceId, bundleEntryPart);
547                                if (resourceId == null) {
548                                        if (bundleEntryPart.getFullUrl() != null) {
549                                                resourceId = bundleEntryPart.getFullUrl();
550                                        }
551                                }
552                                List<ResourceReferenceInfo> allResourceReferences =
553                                                theContext.newTerser().getAllResourceReferences(resource);
554                                String finalResourceId = resourceId;
555                                allResourceReferences.forEach(refInfo -> {
556                                        String referencedResourceId = refInfo.getResourceReference()
557                                                        .getReferenceElement()
558                                                        .toVersionless()
559                                                        .getValue();
560                                        if (color.containsKey(referencedResourceId)) {
561                                                if (!adjList.containsKey(finalResourceId)) {
562                                                        adjList.put(finalResourceId, new ArrayList<>());
563                                                }
564                                                adjList.get(finalResourceId).add(referencedResourceId);
565                                        }
566                                });
567                        }
568                }
569
570                for (Map.Entry<String, Integer> entry : color.entrySet()) {
571                        if (entry.getValue() == WHITE) {
572                                depthFirstSearch(entry.getKey(), color, adjList, topologicalOrder, legality);
573                        }
574                }
575
576                if (legality.isLegal()) {
577                        if (ourLog.isDebugEnabled()) {
578                                ourLog.debug("Topological order is: {}", String.join(",", topologicalOrder));
579                        }
580
581                        LinkedHashSet<IBase> orderedEntries = new LinkedHashSet<>();
582                        for (int i = 0; i < topologicalOrder.size(); i++) {
583                                BundleEntryParts bep;
584                                if (theRequestTypeEnum.equals(RequestTypeEnum.DELETE)) {
585                                        int index = topologicalOrder.size() - i - 1;
586                                        bep = resourceIdToBundleEntryMap.get(topologicalOrder.get(index));
587                                } else {
588                                        bep = resourceIdToBundleEntryMap.get(topologicalOrder.get(i));
589                                }
590                                IBase base = thePartsToIBaseMap.get(bep);
591                                orderedEntries.add(base);
592                        }
593
594                        return orderedEntries;
595
596                } else {
597                        return null;
598                }
599        }
600
601        private static void depthFirstSearch(
602                        String theResourceId,
603                        HashMap<String, Integer> theResourceIdToColor,
604                        HashMap<String, List<String>> theAdjList,
605                        List<String> theTopologicalOrder,
606                        SortLegality theLegality) {
607
608                if (!theLegality.isLegal()) {
609                        ourLog.debug("Found a cycle while trying to sort bundle entries. This bundle is not sortable.");
610                        return;
611                }
612
613                // We are currently recursing over this node (gray)
614                theResourceIdToColor.put(theResourceId, GRAY);
615
616                for (String neighbourResourceId : theAdjList.getOrDefault(theResourceId, new ArrayList<>())) {
617                        if (theResourceIdToColor.get(neighbourResourceId) == WHITE) {
618                                depthFirstSearch(
619                                                neighbourResourceId, theResourceIdToColor, theAdjList, theTopologicalOrder, theLegality);
620                        } else if (theResourceIdToColor.get(neighbourResourceId) == GRAY) {
621                                theLegality.setLegal(false);
622                                return;
623                        }
624                }
625                // Mark the node as black
626                theResourceIdToColor.put(theResourceId, BLACK);
627                theTopologicalOrder.add(theResourceId);
628        }
629
630        private static Map<BundleEntryParts, IBase> getPartsToIBaseMap(FhirContext theContext, IBaseBundle theBundle) {
631                RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle);
632                BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry");
633                List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle);
634
635                BaseRuntimeElementCompositeDefinition<?> entryChildContentsDef =
636                                (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry");
637                BaseRuntimeChildDefinition fullUrlChildDef = entryChildContentsDef.getChildByName("fullUrl");
638                BaseRuntimeChildDefinition resourceChildDef = entryChildContentsDef.getChildByName("resource");
639                BaseRuntimeChildDefinition requestChildDef = entryChildContentsDef.getChildByName("request");
640                BaseRuntimeElementCompositeDefinition<?> requestChildContentsDef =
641                                (BaseRuntimeElementCompositeDefinition<?>) requestChildDef.getChildByName("request");
642                BaseRuntimeChildDefinition requestUrlChildDef = requestChildContentsDef.getChildByName("url");
643                BaseRuntimeChildDefinition requestIfNoneExistChildDef = requestChildContentsDef.getChildByName("ifNoneExist");
644                BaseRuntimeChildDefinition methodChildDef = requestChildContentsDef.getChildByName("method");
645                Map<BundleEntryParts, IBase> map = new HashMap<>();
646                for (IBase nextEntry : entries) {
647                        BundleEntryParts parts = getBundleEntryParts(
648                                        fullUrlChildDef,
649                                        resourceChildDef,
650                                        requestChildDef,
651                                        requestUrlChildDef,
652                                        requestIfNoneExistChildDef,
653                                        methodChildDef,
654                                        nextEntry);
655                        /*
656                         * All 3 might be null - That's ok because we still want to know the
657                         * order in the original bundle.
658                         */
659                        map.put(parts, nextEntry);
660                }
661                return map;
662        }
663
664        public static List<SearchBundleEntryParts> getSearchBundleEntryParts(
665                        FhirContext theContext, IBaseBundle theBundle) {
666                RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle);
667                BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry");
668                List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle);
669
670                BaseRuntimeElementCompositeDefinition<?> entryChildContentsDef =
671                                (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry");
672                BaseRuntimeChildDefinition fullUrlChildDef = entryChildContentsDef.getChildByName("fullUrl");
673                BaseRuntimeChildDefinition resourceChildDef = entryChildContentsDef.getChildByName("resource");
674                BaseRuntimeChildDefinition searchChildDef = entryChildContentsDef.getChildByName("search");
675                BaseRuntimeElementCompositeDefinition<?> searchChildContentsDef =
676                                (BaseRuntimeElementCompositeDefinition<?>) searchChildDef.getChildByName("search");
677                BaseRuntimeChildDefinition searchModeChildDef = searchChildContentsDef.getChildByName("mode");
678                BaseRuntimeChildDefinition searchScoreChildDef = searchChildContentsDef.getChildByName("score");
679
680                List<SearchBundleEntryParts> retVal = new ArrayList<>();
681                for (IBase nextEntry : entries) {
682                        SearchBundleEntryParts parts = getSearchBundleEntryParts(
683                                        fullUrlChildDef,
684                                        resourceChildDef,
685                                        searchChildDef,
686                                        searchModeChildDef,
687                                        searchScoreChildDef,
688                                        nextEntry);
689                        retVal.add(parts);
690                }
691                return retVal;
692        }
693
694        private static SearchBundleEntryParts getSearchBundleEntryParts(
695                        BaseRuntimeChildDefinition theFullUrlChildDef,
696                        BaseRuntimeChildDefinition theResourceChildDef,
697                        BaseRuntimeChildDefinition theSearchChildDef,
698                        BaseRuntimeChildDefinition theSearchModeChildDef,
699                        BaseRuntimeChildDefinition theSearchScoreChildDef,
700                        IBase entry) {
701                IBaseResource resource = null;
702                String matchMode = null;
703                BigDecimal searchScore = null;
704
705                String fullUrl = theFullUrlChildDef
706                                .getAccessor()
707                                .getFirstValueOrNull(entry)
708                                .map(t -> ((IPrimitiveType<?>) t).getValueAsString())
709                                .orElse(null);
710
711                for (IBase nextResource : theResourceChildDef.getAccessor().getValues(entry)) {
712                        resource = (IBaseResource) nextResource;
713                }
714
715                for (IBase nextSearch : theSearchChildDef.getAccessor().getValues(entry)) {
716                        for (IBase nextUrl : theSearchModeChildDef.getAccessor().getValues(nextSearch)) {
717                                matchMode = ((IPrimitiveType<?>) nextUrl).getValueAsString();
718                        }
719                        for (IBase nextUrl : theSearchScoreChildDef.getAccessor().getValues(nextSearch)) {
720                                searchScore = (BigDecimal) ((IPrimitiveType<?>) nextUrl).getValue();
721                        }
722                }
723
724                return new SearchBundleEntryParts(fullUrl, resource, matchMode, searchScore);
725        }
726
727        /**
728         * Given a bundle, and a consumer, apply the consumer to each entry in the bundle.
729         *
730         * @param theContext   The FHIR Context
731         * @param theBundle    The bundle to have its entries processed.
732         * @param theProcessor a {@link Consumer} which will operate on all the entries of a bundle.
733         * @deprecated Use {@link #processAllEntries(FhirContext, IBaseBundle, Consumer)} instead
734         */
735        @Deprecated(since = "8.6.0", forRemoval = true)
736        public static void processEntries(
737                        FhirContext theContext, IBaseBundle theBundle, Consumer<ModifiableBundleEntry> theProcessor) {
738                RuntimeResourceDefinition bundleDef = theContext.getResourceDefinition(theBundle);
739                BaseRuntimeChildDefinition entryChildDef = bundleDef.getChildByName("entry");
740                List<IBase> entries = entryChildDef.getAccessor().getValues(theBundle);
741
742                BaseRuntimeElementCompositeDefinition<?> entryChildContentsDef =
743                                (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry");
744                BaseRuntimeChildDefinition fullUrlChildDef = entryChildContentsDef.getChildByName("fullUrl");
745                BaseRuntimeChildDefinition resourceChildDef = entryChildContentsDef.getChildByName("resource");
746                BaseRuntimeChildDefinition requestChildDef = entryChildContentsDef.getChildByName("request");
747                BaseRuntimeElementCompositeDefinition<?> requestChildContentsDef =
748                                (BaseRuntimeElementCompositeDefinition<?>) requestChildDef.getChildByName("request");
749                BaseRuntimeChildDefinition requestUrlChildDef = requestChildContentsDef.getChildByName("url");
750                BaseRuntimeChildDefinition requestIfNoneExistChildDef = requestChildContentsDef.getChildByName("ifNoneExist");
751                BaseRuntimeChildDefinition methodChildDef = requestChildContentsDef.getChildByName("method");
752
753                for (IBase nextEntry : entries) {
754                        BundleEntryParts parts = getBundleEntryParts(
755                                        fullUrlChildDef,
756                                        resourceChildDef,
757                                        requestChildDef,
758                                        requestUrlChildDef,
759                                        requestIfNoneExistChildDef,
760                                        methodChildDef,
761                                        nextEntry);
762                        /*
763                         * All 3 might be null - That's ok because we still want to know the
764                         * order in the original bundle.
765                         */
766                        BundleEntryMutator mutator = new BundleEntryMutator(
767                                        theContext,
768                                        nextEntry,
769                                        requestChildDef,
770                                        requestChildContentsDef,
771                                        entryChildContentsDef,
772                                        methodChildDef);
773                        ModifiableBundleEntry entry = new ModifiableBundleEntry(parts, mutator);
774                        theProcessor.accept(entry);
775                }
776        }
777
778        private static BundleEntryParts getBundleEntryParts(
779                        BaseRuntimeChildDefinition fullUrlChildDef,
780                        BaseRuntimeChildDefinition resourceChildDef,
781                        BaseRuntimeChildDefinition requestChildDef,
782                        BaseRuntimeChildDefinition requestUrlChildDef,
783                        BaseRuntimeChildDefinition requestIfNoneExistChildDef,
784                        BaseRuntimeChildDefinition methodChildDef,
785                        IBase nextEntry) {
786                IBaseResource resource = null;
787                String url = null;
788                RequestTypeEnum requestType = null;
789                String conditionalUrl = null;
790                String fullUrl = fullUrlChildDef
791                                .getAccessor()
792                                .getFirstValueOrNull(nextEntry)
793                                .map(t -> ((IPrimitiveType<?>) t).getValueAsString())
794                                .orElse(null);
795
796                for (IBase nextResource : resourceChildDef.getAccessor().getValues(nextEntry)) {
797                        resource = (IBaseResource) nextResource;
798                }
799                for (IBase nextRequest : requestChildDef.getAccessor().getValues(nextEntry)) {
800                        for (IBase nextUrl : requestUrlChildDef.getAccessor().getValues(nextRequest)) {
801                                url = ((IPrimitiveType<?>) nextUrl).getValueAsString();
802                        }
803                        for (IBase nextMethod : methodChildDef.getAccessor().getValues(nextRequest)) {
804                                String methodString = ((IPrimitiveType<?>) nextMethod).getValueAsString();
805                                if (isNotBlank(methodString)) {
806                                        requestType = RequestTypeEnum.valueOf(methodString);
807                                }
808                        }
809
810                        if (requestType != null) {
811                                //noinspection EnumSwitchStatementWhichMissesCases
812                                switch (requestType) {
813                                        case PUT, DELETE, PATCH -> conditionalUrl = url != null && url.contains("?") ? url : null;
814                                        case POST -> {
815                                                List<IBase> ifNoneExistReps =
816                                                                requestIfNoneExistChildDef.getAccessor().getValues(nextRequest);
817                                                if (!ifNoneExistReps.isEmpty()) {
818                                                        IPrimitiveType<?> ifNoneExist = (IPrimitiveType<?>) ifNoneExistReps.get(0);
819                                                        conditionalUrl = ifNoneExist.getValueAsString();
820                                                }
821                                        }
822                                }
823                        }
824                }
825                return new BundleEntryParts(fullUrl, requestType, url, resource, conditionalUrl, requestType);
826        }
827
828        /**
829         * Extract all of the resources from a given bundle
830         */
831        public static List<IBaseResource> toListOfResources(FhirContext theContext, IBaseBundle theBundle) {
832                return toListOfResourcesOfType(theContext, theBundle, IBaseResource.class);
833        }
834
835        /**
836         * Extract all of ids of all the resources from a given bundle
837         */
838        public static List<String> toListOfResourceIds(FhirContext theContext, IBaseBundle theBundle) {
839                return toListOfResourcesOfType(theContext, theBundle, IBaseResource.class).stream()
840                                .map(resource -> resource.getIdElement().getIdPart())
841                                .collect(Collectors.toList());
842        }
843
844        /**
845         * Extract all of the resources of a given type from a given bundle
846         */
847        @SuppressWarnings("unchecked")
848        public static <T extends IBaseResource> List<T> toListOfResourcesOfType(
849                        FhirContext theContext, IBaseBundle theBundle, Class<T> theTypeToInclude) {
850                Objects.requireNonNull(theTypeToInclude, "ResourceType must not be null");
851                List<T> retVal = new ArrayList<>();
852
853                RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
854                BaseRuntimeChildDefinition entryChild = def.getChildByName("entry");
855                List<IBase> entries = entryChild.getAccessor().getValues(theBundle);
856
857                BaseRuntimeElementCompositeDefinition<?> entryChildElem =
858                                (BaseRuntimeElementCompositeDefinition<?>) entryChild.getChildByName("entry");
859                BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource");
860                for (IBase nextEntry : entries) {
861                        for (IBase next : resourceChild.getAccessor().getValues(nextEntry)) {
862                                if (theTypeToInclude.isAssignableFrom(next.getClass())) {
863                                        retVal.add((T) next);
864                                }
865                        }
866                }
867                return retVal;
868        }
869
870        @Nonnull
871        public static List<CanonicalBundleEntry> toListOfCanonicalBundleEntries(
872                        FhirContext theContext, IBaseBundle theBundle) {
873                List<CanonicalBundleEntry> retVal = new ArrayList<>();
874
875                RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle);
876                BaseRuntimeChildDefinition entryChild = def.getChildByName("entry");
877                List<IBase> entries = entryChild.getAccessor().getValues(theBundle);
878
879                for (IBase nextEntry : entries) {
880                        CanonicalBundleEntry canonicalEntry =
881                                        CanonicalBundleEntry.fromBundleEntry(theContext, (IBaseBackboneElement) nextEntry);
882                        retVal.add(canonicalEntry);
883                }
884
885                return retVal;
886        }
887
888        public static IBase getReferenceInBundle(
889                        @Nonnull FhirContext theFhirContext, @Nonnull String theUrl, @Nullable Object theAppContext) {
890                if (!(theAppContext instanceof IBaseBundle) || isBlank(theUrl) || theUrl.startsWith("#")) {
891                        return null;
892                }
893
894                /*
895                 * If this is a reference that is a UUID, we must be looking for local references within a Bundle
896                 */
897                IBaseBundle bundle = (IBaseBundle) theAppContext;
898
899                final boolean isPlaceholderReference = theUrl.startsWith("urn:");
900                final String unqualifiedVersionlessReference =
901                                new IdDt(theUrl).toUnqualifiedVersionless().getValue();
902
903                for (BundleEntryParts next : BundleUtil.toListOfEntries(theFhirContext, bundle)) {
904                        IBaseResource nextResource = next.getResource();
905                        if (nextResource == null) {
906                                continue;
907                        }
908                        if (isPlaceholderReference) {
909                                if (theUrl.equals(next.getFullUrl())
910                                                || theUrl.equals(nextResource.getIdElement().getValue())) {
911                                        return nextResource;
912                                }
913                        } else {
914                                if (unqualifiedVersionlessReference.equals(
915                                                nextResource.getIdElement().toUnqualifiedVersionless().getValue())) {
916                                        return nextResource;
917                                }
918                        }
919                }
920                return null;
921        }
922
923        /**
924         * DSTU3 did not allow the PATCH verb for transaction bundles- so instead we infer that a bundle
925         * is a patch if the payload is a binary resource containing a patch. This method
926         * tests whether a resource (which should have come from
927         * <code>Bundle.entry.resource</code> is a Binary resource with a patch
928         * payload type.
929         */
930        public static boolean isDstu3TransactionPatch(FhirContext theContext, IBaseResource thePayloadResource) {
931                boolean isPatch = false;
932                if (thePayloadResource instanceof IBaseBinary) {
933                        String contentType = ((IBaseBinary) thePayloadResource).getContentType();
934                        try {
935                                PatchTypeEnum.forContentTypeOrThrowInvalidRequestException(theContext, contentType);
936                                isPatch = true;
937                        } catch (InvalidRequestException e) {
938                                // ignore
939                        }
940                }
941                return isPatch;
942        }
943
944        /**
945         * create a new bundle entry and set a value for a single field
946         *
947         * @param theContext   Context holding resource definition
948         * @param theFieldName Child field name of the bundle entry to set
949         * @param theValues    The values to set on the bundle entry child field name
950         * @return the new bundle entry
951         */
952        public static IBase createNewBundleEntryWithSingleField(
953                        FhirContext theContext, String theFieldName, IBase... theValues) {
954                IBaseBundle newBundle = TerserUtil.newResource(theContext, "Bundle");
955                BaseRuntimeChildDefinition entryChildDef =
956                                theContext.getResourceDefinition(newBundle).getChildByName("entry");
957
958                BaseRuntimeElementCompositeDefinition<?> entryChildElem =
959                                (BaseRuntimeElementCompositeDefinition<?>) entryChildDef.getChildByName("entry");
960                BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName(theFieldName);
961                IBase bundleEntry = entryChildElem.newInstance();
962                for (IBase value : theValues) {
963                        try {
964                                resourceChild.getMutator().addValue(bundleEntry, value);
965                        } catch (UnsupportedOperationException e) {
966                                ourLog.warn(
967                                                "Resource {} does not support multiple values, but an attempt to set {} was made. Setting the first item only",
968                                                bundleEntry,
969                                                theValues);
970                                resourceChild.getMutator().setValue(bundleEntry, value);
971                                break;
972                        }
973                }
974                return bundleEntry;
975        }
976
977        /**
978         * Get resource from bundle by resource type and reference
979         *
980         * @param theContext   FhirContext
981         * @param theBundle    IBaseBundle
982         * @param theReference IBaseReference
983         * @return IBaseResource if found and null if not found.
984         */
985        @Nonnull
986        public static IBaseResource getResourceByReferenceAndResourceType(
987                        @Nonnull FhirContext theContext, @Nonnull IBaseBundle theBundle, @Nonnull IBaseReference theReference) {
988                return toListOfResources(theContext, theBundle).stream()
989                                .filter(theResource -> theReference
990                                                .getReferenceElement()
991                                                .getIdPart()
992                                                .equals(theResource.getIdElement().getIdPart()))
993                                .findFirst()
994                                .orElse(null);
995        }
996
997        private static class SortLegality {
998                private boolean myIsLegal;
999
1000                SortLegality() {
1001                        this.myIsLegal = true;
1002                }
1003
1004                public boolean isLegal() {
1005                        return myIsLegal;
1006                }
1007
1008                private void setLegal(boolean theLegal) {
1009                        myIsLegal = theLegal;
1010                }
1011        }
1012}