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