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}