001/*-
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.server.method;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.rest.api.BundleLinks;
025import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
026import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
027import ca.uhn.fhir.rest.api.server.IBundleProvider;
028import ca.uhn.fhir.rest.api.server.IRestfulServer;
029import ca.uhn.fhir.rest.api.server.RequestDetails;
030import ca.uhn.fhir.rest.server.IPagingProvider;
031import ca.uhn.fhir.rest.server.RestfulServerUtils;
032import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
033import org.apache.commons.lang3.StringUtils;
034import org.hl7.fhir.instance.model.api.IBaseBundle;
035import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
036import org.hl7.fhir.instance.model.api.IBaseResource;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.List;
043import java.util.Objects;
044
045/**
046 * Service to build a FHIR Bundle from a request and a Bundle Provider
047 */
048public class ResponseBundleBuilder {
049        private static final Logger ourLog = LoggerFactory.getLogger(ResponseBundleBuilder.class);
050
051        private final boolean myIsOffsetModeHistory;
052
053        public ResponseBundleBuilder(boolean theIsOffsetModeHistory) {
054                myIsOffsetModeHistory = theIsOffsetModeHistory;
055        }
056
057        IBaseBundle buildResponseBundle(ResponseBundleRequest theResponseBundleRequest) {
058                final ResponsePage responsePage = buildResponsePage(theResponseBundleRequest);
059
060                removeNulls(responsePage.getResourceList());
061                validateIds(responsePage.getResourceList());
062
063                BundleLinks links = buildLinks(theResponseBundleRequest, responsePage);
064
065                return buildBundle(theResponseBundleRequest, responsePage, links);
066        }
067
068        private static IBaseBundle buildBundle(
069                        ResponseBundleRequest theResponseBundleRequest, ResponsePage pageResponse, BundleLinks links) {
070                final IRestfulServer<?> server = theResponseBundleRequest.server;
071                FhirContext ctx = server.getFhirContext();
072                final IVersionSpecificBundleFactory bundleFactory = ctx.newBundleFactory();
073                final IBundleProvider bundleProvider = theResponseBundleRequest.bundleProvider;
074
075                bundleFactory.addRootPropertiesToBundle(
076                                bundleProvider.getUuid(), links, bundleProvider.size(), bundleProvider.getPublished());
077                bundleFactory.addResourcesToBundle(
078                                new ArrayList<>(pageResponse.getResourceList()),
079                                theResponseBundleRequest.bundleType,
080                                links.serverBase,
081                                server.getBundleInclusionRule(),
082                                theResponseBundleRequest.includes);
083
084                IBaseBundle baseBundle = (IBaseBundle) bundleFactory.getResourceBundle();
085
086                return baseBundle;
087        }
088
089        private ResponsePage buildResponsePage(ResponseBundleRequest theResponseBundleRequest) {
090                final IRestfulServer<?> server = theResponseBundleRequest.server;
091                final IBundleProvider bundleProvider = theResponseBundleRequest.bundleProvider;
092                final RequestedPage requestedPage = theResponseBundleRequest.requestedPage;
093                final List<IBaseResource> resourceList;
094                final int pageSize;
095
096                ResponsePage.ResponsePageBuilder responsePageBuilder = new ResponsePage.ResponsePageBuilder();
097
098                int numToReturn;
099                String searchId = null;
100
101                Integer bundleSize = bundleProvider.containsAllResources()
102                                ? (Integer) bundleProvider.getResourceListComplete().size()
103                                : bundleProvider.size();
104
105                if (requestedPage.offset != null || !server.canStoreSearchResults()) {
106                        pageSize = offsetCalculatePageSize(server, requestedPage, bundleSize);
107                        numToReturn = pageSize;
108
109                        resourceList = offsetBuildResourceList(bundleProvider, requestedPage, numToReturn, responsePageBuilder);
110                        RestfulServerUtils.validateResourceListNotNull(resourceList);
111                } else {
112                        pageSize = pagingCalculatePageSize(requestedPage, server.getPagingProvider());
113
114                        if (bundleSize == null) {
115                                numToReturn = pageSize;
116                        } else {
117                                numToReturn = Math.min(pageSize, bundleSize.intValue() - theResponseBundleRequest.offset);
118                        }
119
120                        // numToReturn is the number of matched resources to return; there may be included or op-outcomes
121                        // to include as well
122
123                        resourceList =
124                                        pagingBuildResourceList(theResponseBundleRequest, bundleProvider, numToReturn, responsePageBuilder);
125                        RestfulServerUtils.validateResourceListNotNull(resourceList);
126
127                        searchId = pagingBuildSearchId(theResponseBundleRequest, numToReturn, bundleProvider.size());
128                }
129
130                // We should leave the IBundleProvider to populate these values (specifically resourceList).
131                // But since we haven't updated all such providers, we will
132                // build it here (this is at best 'duplicating' work).
133                responsePageBuilder
134                                .setSearchId(searchId)
135                                .setPageSize(pageSize)
136                                .setNumToReturn(numToReturn)
137                                .setBundleProvider(bundleProvider)
138                                .setResources(resourceList);
139
140                return responsePageBuilder.build();
141        }
142
143        private static String pagingBuildSearchId(
144                        ResponseBundleRequest theResponseBundleRequest, int theNumToReturn, Integer theNumTotalResults) {
145                final IPagingProvider pagingProvider = theResponseBundleRequest.server.getPagingProvider();
146                String retval = null;
147
148                if (theResponseBundleRequest.searchId != null) {
149                        retval = theResponseBundleRequest.searchId;
150                } else {
151                        if (theNumTotalResults == null || theNumTotalResults > theNumToReturn) {
152                                retval = pagingProvider.storeResultList(
153                                                theResponseBundleRequest.requestDetails, theResponseBundleRequest.bundleProvider);
154                                if (StringUtils.isBlank(retval)) {
155                                        ourLog.info(
156                                                        "Found {} results but paging provider did not provide an ID to use for paging",
157                                                        theNumTotalResults);
158                                        retval = null;
159                                }
160                        }
161                }
162                return retval;
163        }
164
165        private static List<IBaseResource> pagingBuildResourceList(
166                        ResponseBundleRequest theResponseBundleRequest,
167                        IBundleProvider theBundleProvider,
168                        int theNumToReturn,
169                        ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
170                final List<IBaseResource> retval;
171                if (theNumToReturn > 0 || theBundleProvider.getCurrentPageId() != null) {
172                        retval = theBundleProvider.getResources(
173                                        theResponseBundleRequest.offset,
174                                        theNumToReturn + theResponseBundleRequest.offset,
175                                        theResponsePageBuilder);
176                } else {
177                        retval = Collections.emptyList();
178                }
179                return retval;
180        }
181
182        private static int pagingCalculatePageSize(RequestedPage theRequestedPage, IPagingProvider thePagingProvider) {
183                if (theRequestedPage.limit == null || theRequestedPage.limit.equals(0)) {
184                        return thePagingProvider.getDefaultPageSize();
185                } else {
186                        return Math.min(thePagingProvider.getMaximumPageSize(), theRequestedPage.limit);
187                }
188        }
189
190        private List<IBaseResource> offsetBuildResourceList(
191                        IBundleProvider theBundleProvider,
192                        RequestedPage theRequestedPage,
193                        int theNumToReturn,
194                        ResponsePage.ResponsePageBuilder theResponsePageBuilder) {
195                final List<IBaseResource> retval;
196                if ((theRequestedPage.offset != null && !myIsOffsetModeHistory)
197                                || theBundleProvider.getCurrentPageOffset() != null) {
198                        // When offset query is done theResult already contains correct amount (+ their includes etc.) so return
199                        // everything
200                        retval = theBundleProvider.getResources(0, Integer.MAX_VALUE, theResponsePageBuilder);
201                } else if (theNumToReturn > 0) {
202                        retval = theBundleProvider.getResources(0, theNumToReturn, theResponsePageBuilder);
203                } else {
204                        retval = Collections.emptyList();
205                }
206                return retval;
207        }
208
209        private static int offsetCalculatePageSize(
210                        IRestfulServer<?> server, RequestedPage theRequestedPage, Integer theNumTotalResults) {
211                final int retval;
212                if (theRequestedPage.limit != null) {
213                        retval = theRequestedPage.limit;
214                } else {
215                        if (server.getDefaultPageSize() != null) {
216                                retval = server.getDefaultPageSize();
217                        } else {
218                                retval = theNumTotalResults != null ? theNumTotalResults : Integer.MAX_VALUE;
219                        }
220                }
221                return retval;
222        }
223
224        private static void validateIds(List<IBaseResource> theResourceList) {
225                /*
226                 * Make sure all returned resources have an ID (if not, this is a bug
227                 * in the user server code)
228                 */
229                for (IBaseResource next : theResourceList) {
230                        if (next.getIdElement() == null || next.getIdElement().isEmpty()) {
231                                if (!(next instanceof IBaseOperationOutcome)) {
232                                        throw new InternalErrorException(Msg.code(435) + "Server method returned resource of type["
233                                                        + next.getClass().getSimpleName()
234                                                        + "] with no ID specified (IResource#setId(IdDt) must be called)");
235                                }
236                        }
237                }
238        }
239
240        private static void removeNulls(List<IBaseResource> resourceList) {
241                /*
242                 * Remove any null entries in the list - This generally shouldn't happen but can if
243                 * data has been manually purged from the JPA database
244                 */
245                boolean hasNull = false;
246                for (IBaseResource next : resourceList) {
247                        if (next == null) {
248                                hasNull = true;
249                                break;
250                        }
251                }
252                if (hasNull) {
253                        resourceList.removeIf(Objects::isNull);
254                }
255        }
256
257        private BundleLinks buildLinks(ResponseBundleRequest theResponseBundleRequest, ResponsePage theResponsePage) {
258                final IRestfulServer<?> server = theResponseBundleRequest.server;
259                final RequestedPage pageRequest = theResponseBundleRequest.requestedPage;
260
261                BundleLinks retval = new BundleLinks(
262                                theResponseBundleRequest.requestDetails.getFhirServerBase(),
263                                theResponseBundleRequest.includes,
264                                RestfulServerUtils.prettyPrintResponse(server, theResponseBundleRequest.requestDetails),
265                                theResponseBundleRequest.bundleType);
266
267                // set self link
268                retval.setSelf(theResponseBundleRequest.linkSelf);
269
270                // determine if we are using offset / uncached pages
271                theResponsePage.setUseOffsetPaging(pageRequest.offset != null
272                                || (!server.canStoreSearchResults() && !isEverythingOperation(theResponseBundleRequest.requestDetails))
273                                || myIsOffsetModeHistory);
274                theResponsePage.setResponseBundleRequest(theResponseBundleRequest);
275                theResponsePage.setRequestedPage(pageRequest);
276
277                // generate our links
278                theResponsePage.setNextPageIfNecessary(retval);
279                theResponsePage.setPreviousPageIfNecessary(retval);
280
281                return retval;
282        }
283
284        private boolean isEverythingOperation(RequestDetails theRequest) {
285                return (theRequest.getRestOperationType() == RestOperationTypeEnum.EXTENDED_OPERATION_TYPE
286                                                || theRequest.getRestOperationType() == RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE)
287                                && theRequest.getOperation() != null
288                                && theRequest.getOperation().equals("$everything");
289        }
290}