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