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.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.HookParams; 025import ca.uhn.fhir.interceptor.api.Pointcut; 026import ca.uhn.fhir.model.api.Include; 027import ca.uhn.fhir.model.valueset.BundleTypeEnum; 028import ca.uhn.fhir.rest.api.Constants; 029import ca.uhn.fhir.rest.api.RequestTypeEnum; 030import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 031import ca.uhn.fhir.rest.api.server.IBundleProvider; 032import ca.uhn.fhir.rest.api.server.IRestfulServer; 033import ca.uhn.fhir.rest.api.server.RequestDetails; 034import ca.uhn.fhir.rest.server.IPagingProvider; 035import ca.uhn.fhir.rest.server.RestfulServerUtils; 036import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 037import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 038import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; 039import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 040import ca.uhn.fhir.util.ReflectionUtil; 041import jakarta.annotation.Nonnull; 042import org.hl7.fhir.instance.model.api.IBaseResource; 043 044import java.lang.reflect.Method; 045import java.util.HashSet; 046import java.util.Set; 047 048import static org.apache.commons.lang3.StringUtils.isBlank; 049import static org.apache.commons.lang3.StringUtils.isNotBlank; 050 051public class PageMethodBinding extends BaseResourceReturningMethodBinding { 052 053 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PageMethodBinding.class); 054 055 public PageMethodBinding(FhirContext theContext, Method theMethod) { 056 super(null, theMethod, theContext, null); 057 } 058 059 public IBaseResource provider() { 060 return null; 061 } 062 063 @Override 064 protected BundleTypeEnum getResponseBundleType() { 065 return null; 066 } 067 068 @Override 069 public ReturnTypeEnum getReturnType() { 070 return ReturnTypeEnum.BUNDLE; 071 } 072 073 @Override 074 public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) 075 throws InvalidRequestException, InternalErrorException { 076 return handlePagingRequest( 077 theServer, theRequest, theRequest.getParameters().get(Constants.PARAM_PAGINGACTION)[0]); 078 } 079 080 @Override 081 public IBaseResource doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) { 082 return handlePagingRequest( 083 theServer, theRequest, theRequest.getParameters().get(Constants.PARAM_PAGINGACTION)[0]); 084 } 085 086 private IBaseResource handlePagingRequest( 087 IRestfulServer<?> theServer, RequestDetails theRequest, String thePagingAction) { 088 IPagingProvider pagingProvider = theServer.getPagingProvider(); 089 if (pagingProvider == null) { 090 throw new InvalidRequestException(Msg.code(416) + "This server does not support paging"); 091 } 092 093 // Interceptor invoke: SERVER_INCOMING_REQUEST_PRE_HANDLED 094 populateRequestDetailsForInterceptor(theRequest, ReflectionUtil.EMPTY_OBJECT_ARRAY); 095 callPreHandledHooks(theRequest); 096 097 ResponseBundleRequest responseBundleRequest = 098 buildResponseBundleRequest(theServer, theRequest, thePagingAction, pagingProvider); 099 return myResponseBundleBuilder.buildResponseBundle(responseBundleRequest); 100 } 101 102 private ResponseBundleRequest buildResponseBundleRequest( 103 IRestfulServer<?> theServer, 104 RequestDetails theRequest, 105 String thePagingAction, 106 IPagingProvider thePagingProvider) { 107 int offset = 0; 108 IBundleProvider bundleProvider; 109 110 String pageId = null; 111 String[] pageIdParams = theRequest.getParameters().get(Constants.PARAM_PAGEID); 112 if (pageIdParams != null) { 113 if (pageIdParams.length > 0) { 114 if (isNotBlank(pageIdParams[0])) { 115 pageId = pageIdParams[0]; 116 } 117 } 118 } 119 120 if (pageId != null) { 121 // This is a page request by Search ID and Page ID 122 123 bundleProvider = thePagingProvider.retrieveResultList(theRequest, thePagingAction, pageId); 124 validateHaveBundleProvider(thePagingAction, bundleProvider); 125 126 } else { 127 // This is a page request by Search ID and Offset 128 129 bundleProvider = thePagingProvider.retrieveResultList(theRequest, thePagingAction); 130 validateHaveBundleProvider(thePagingAction, bundleProvider); 131 132 offset = OffsetCalculator.calculateOffset(theRequest, bundleProvider); 133 } 134 135 /** 136 * TODO KHS can this be consolidated with PageMethodBinding.getRequestIncludesFromParams ? 137 */ 138 Set<Include> includes = new HashSet<>(); 139 String[] reqIncludes = theRequest.getParameters().get(Constants.PARAM_INCLUDE); 140 if (reqIncludes != null) { 141 for (String nextInclude : reqIncludes) { 142 includes.add(new Include(nextInclude)); 143 } 144 } 145 146 String linkSelfBase = theRequest.getFhirServerBase(); 147 String completeUrl = theRequest.getCompleteUrl(); 148 String linkSelf = 149 linkSelfBase + completeUrl.substring(theRequest.getCompleteUrl().indexOf('?')); 150 151 BundleTypeEnum bundleType = null; 152 String[] bundleTypeValues = theRequest.getParameters().get(Constants.PARAM_BUNDLETYPE); 153 if (bundleTypeValues != null) { 154 bundleType = BundleTypeEnum.VALUESET_BINDER.fromCodeString(bundleTypeValues[0]); 155 } 156 157 Integer count = RestfulServerUtils.extractCountParameter(theRequest); 158 if (count == null) { 159 count = thePagingProvider.getDefaultPageSize(); 160 } else if (count > thePagingProvider.getMaximumPageSize()) { 161 count = thePagingProvider.getMaximumPageSize(); 162 } 163 164 ResponseBundleRequest responseBundleRequest = new ResponseBundleRequest( 165 theServer, bundleProvider, theRequest, offset, count, linkSelf, includes, bundleType, thePagingAction); 166 return responseBundleRequest; 167 } 168 169 static void callPreHandledHooks(RequestDetails theRequest) { 170 HookParams preHandledParams = new HookParams(); 171 preHandledParams.add(RestOperationTypeEnum.class, theRequest.getRestOperationType()); 172 preHandledParams.add(RequestDetails.class, theRequest); 173 preHandledParams.addIfMatchesType(ServletRequestDetails.class, theRequest); 174 if (theRequest.getInterceptorBroadcaster() != null) { 175 theRequest 176 .getInterceptorBroadcaster() 177 .callHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED, preHandledParams); 178 } 179 } 180 181 private void validateHaveBundleProvider(String thePagingAction, IBundleProvider theBundleProvider) { 182 // Return an HTTP 410 if the search is not known 183 if (theBundleProvider == null) { 184 ourLog.info("Client requested unknown paging ID[{}]", thePagingAction); 185 String msg = 186 getContext().getLocalizer().getMessage(PageMethodBinding.class, "unknownSearchId", thePagingAction); 187 throw new ResourceGoneException(Msg.code(417) + msg); 188 } 189 } 190 191 @Nonnull 192 @Override 193 public RestOperationTypeEnum getRestOperationType() { 194 return RestOperationTypeEnum.GET_PAGE; 195 } 196 197 @Override 198 public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) { 199 String[] pageId = theRequest.getParameters().get(Constants.PARAM_PAGINGACTION); 200 if (pageId == null || pageId.length == 0 || isBlank(pageId[0])) { 201 return MethodMatchEnum.NONE; 202 } 203 204 if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) { 205 return MethodMatchEnum.NONE; 206 } 207 208 return MethodMatchEnum.EXACT; 209 } 210}