
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}