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