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