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}