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