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