001package ca.uhn.fhir.rest.server.method; 002 003import ca.uhn.fhir.context.ConfigurationException; 004import ca.uhn.fhir.context.FhirContext; 005import ca.uhn.fhir.interceptor.api.HookParams; 006import ca.uhn.fhir.interceptor.api.Pointcut; 007import ca.uhn.fhir.model.api.IResource; 008import ca.uhn.fhir.model.api.Include; 009import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; 010import ca.uhn.fhir.model.valueset.BundleTypeEnum; 011import ca.uhn.fhir.rest.api.*; 012import ca.uhn.fhir.rest.api.server.IBundleProvider; 013import ca.uhn.fhir.rest.api.server.IRestfulServer; 014import ca.uhn.fhir.rest.api.server.RequestDetails; 015import ca.uhn.fhir.rest.api.server.ResponseDetails; 016import ca.uhn.fhir.rest.server.IPagingProvider; 017import ca.uhn.fhir.rest.server.RestfulServerUtils; 018import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding; 019import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 020import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 021import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 022import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 023import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 024import ca.uhn.fhir.util.ReflectionUtil; 025import ca.uhn.fhir.util.UrlUtil; 026import org.hl7.fhir.instance.model.api.IBaseResource; 027import org.hl7.fhir.instance.model.api.IPrimitiveType; 028 029import javax.servlet.http.HttpServletRequest; 030import javax.servlet.http.HttpServletResponse; 031import java.io.IOException; 032import java.lang.reflect.Method; 033import java.lang.reflect.Modifier; 034import java.util.*; 035 036import static org.apache.commons.lang3.StringUtils.isBlank; 037import static org.apache.commons.lang3.StringUtils.isNotBlank; 038 039/* 040 * #%L 041 * HAPI FHIR - Server Framework 042 * %% 043 * Copyright (C) 2014 - 2019 University Health Network 044 * %% 045 * Licensed under the Apache License, Version 2.0 (the "License"); 046 * you may not use this file except in compliance with the License. 047 * You may obtain a copy of the License at 048 * 049 * http://www.apache.org/licenses/LICENSE-2.0 050 * 051 * Unless required by applicable law or agreed to in writing, software 052 * distributed under the License is distributed on an "AS IS" BASIS, 053 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 054 * See the License for the specific language governing permissions and 055 * limitations under the License. 056 * #L% 057 */ 058 059public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Object> { 060 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class); 061 062 private MethodReturnTypeEnum myMethodReturnType; 063 private String myResourceName; 064 065 @SuppressWarnings("unchecked") 066 public BaseResourceReturningMethodBinding(Class<?> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { 067 super(theMethod, theContext, theProvider); 068 069 Class<?> methodReturnType = theMethod.getReturnType(); 070 if (Collection.class.isAssignableFrom(methodReturnType)) { 071 072 myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES; 073 Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod); 074 if (collectionType != null) { 075 if (!Object.class.equals(collectionType) && !IBaseResource.class.isAssignableFrom(collectionType)) { 076 throw new ConfigurationException( 077 "Method " + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() + " returns an invalid collection generic type: " + collectionType); 078 } 079 } 080 081 } else if (IBaseResource.class.isAssignableFrom(methodReturnType)) { 082 if (Modifier.isAbstract(methodReturnType.getModifiers()) == false && theContext.getResourceDefinition((Class<? extends IBaseResource>) methodReturnType).isBundle()) { 083 myMethodReturnType = MethodReturnTypeEnum.BUNDLE_RESOURCE; 084 } else { 085 myMethodReturnType = MethodReturnTypeEnum.RESOURCE; 086 } 087 } else if (IBundleProvider.class.isAssignableFrom(methodReturnType)) { 088 myMethodReturnType = MethodReturnTypeEnum.BUNDLE_PROVIDER; 089 } else if (MethodOutcome.class.isAssignableFrom(methodReturnType)) { 090 myMethodReturnType = MethodReturnTypeEnum.METHOD_OUTCOME; 091 } else { 092 throw new ConfigurationException( 093 "Invalid return type '" + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName()); 094 } 095 096 if (theReturnResourceType != null) { 097 if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) { 098 099 // If we're returning an abstract type, that's ok, but if we know the resource 100 // type let's grab it 101 if (!Modifier.isAbstract(theReturnResourceType.getModifiers()) && !Modifier.isInterface(theReturnResourceType.getModifiers())) { 102 Class<? extends IBaseResource> resourceType = (Class<? extends IResource>) theReturnResourceType; 103 myResourceName = theContext.getResourceDefinition(resourceType).getName(); 104 } 105 } 106 } 107 108 } 109 110 IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes, 111 IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) { 112 IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory(); 113 114 int numToReturn; 115 String searchId = null; 116 List<IBaseResource> resourceList; 117 Integer numTotalResults = theResult.size(); 118 if (theServer.getPagingProvider() == null) { 119 numToReturn = numTotalResults; 120 if (numToReturn > 0) { 121 resourceList = theResult.getResources(0, numToReturn); 122 } else { 123 resourceList = Collections.emptyList(); 124 } 125 RestfulServerUtils.validateResourceListNotNull(resourceList); 126 127 } else { 128 IPagingProvider pagingProvider = theServer.getPagingProvider(); 129 if (theLimit == null || theLimit.equals(Integer.valueOf(0))) { 130 numToReturn = pagingProvider.getDefaultPageSize(); 131 } else { 132 numToReturn = Math.min(pagingProvider.getMaximumPageSize(), theLimit); 133 } 134 135 if (numTotalResults != null) { 136 numToReturn = Math.min(numToReturn, numTotalResults - theOffset); 137 } 138 139 if (numToReturn > 0 || theResult.getCurrentPageId() != null) { 140 resourceList = theResult.getResources(theOffset, numToReturn + theOffset); 141 } else { 142 resourceList = Collections.emptyList(); 143 } 144 RestfulServerUtils.validateResourceListNotNull(resourceList); 145 146 if (numTotalResults == null) { 147 numTotalResults = theResult.size(); 148 } 149 150 if (theSearchId != null) { 151 searchId = theSearchId; 152 } else { 153 if (numTotalResults == null || numTotalResults > numToReturn) { 154 searchId = pagingProvider.storeResultList(theResult); 155 if (isBlank(searchId)) { 156 ourLog.info("Found {} results but paging provider did not provide an ID to use for paging", numTotalResults); 157 searchId = null; 158 } 159 } 160 } 161 } 162 163 /* 164 * Remove any null entries in the list - This generally shouldn't happen but can if 165 * data has been manually purged from the JPA database 166 */ 167 boolean hasNull = false; 168 for (IBaseResource next : resourceList) { 169 if (next == null) { 170 hasNull = true; 171 break; 172 } 173 } 174 if (hasNull) { 175 resourceList.removeIf(Objects::isNull); 176 } 177 178 /* 179 * Make sure all returned resources have an ID (if not, this is a bug 180 * in the user server code) 181 */ 182 for (IBaseResource next : resourceList) { 183 if (next.getIdElement() == null || next.getIdElement().isEmpty()) { 184 if (!(next instanceof BaseOperationOutcome)) { 185 throw new InternalErrorException("Server method returned resource of type[" + next.getClass().getSimpleName() + "] with no ID specified (IResource#setId(IdDt) must be called)"); 186 } 187 } 188 } 189 190 String serverBase = theRequest.getFhirServerBase(); 191 boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest); 192 193 String linkPrev = null; 194 String linkNext = null; 195 196 if (isNotBlank(theResult.getCurrentPageId())) { 197 // We're doing named pages 198 searchId = theResult.getUuid(); 199 if (isNotBlank(theResult.getNextPageId())) { 200 linkNext = RestfulServerUtils.createPagingLink(theIncludes, theRequest, searchId, theResult.getNextPageId(), theRequest.getParameters(), prettyPrint, theBundleType); 201 } 202 if (isNotBlank(theResult.getPreviousPageId())) { 203 linkPrev = RestfulServerUtils.createPagingLink(theIncludes, theRequest, searchId, theResult.getPreviousPageId(), theRequest.getParameters(), prettyPrint, theBundleType); 204 } 205 } else if (searchId != null) { 206 /* 207 * We're doing offset pages - Note that we only return paging links if we actually 208 * included some results in the response. We do this to avoid situations where 209 * people have faked the offset number to some huge number to avoid them getting 210 * back paging links that don't make sense. 211 */ 212 if (resourceList.size() > 0) { 213 if (numTotalResults == null || theOffset + numToReturn < numTotalResults) { 214 linkNext = (RestfulServerUtils.createPagingLink(theIncludes, theRequest, searchId, theOffset + numToReturn, numToReturn, theRequest.getParameters(), prettyPrint, theBundleType)); 215 } 216 if (theOffset > 0) { 217 int start = Math.max(0, theOffset - theLimit); 218 linkPrev = RestfulServerUtils.createPagingLink(theIncludes, theRequest, searchId, start, theLimit, theRequest.getParameters(), prettyPrint, theBundleType); 219 } 220 } 221 } 222 223 bundleFactory.addRootPropertiesToBundle(theResult.getUuid(), serverBase, theLinkSelf, linkPrev, linkNext, theResult.size(), theBundleType, theResult.getPublished()); 224 bundleFactory.addResourcesToBundle(new ArrayList<>(resourceList), theBundleType, serverBase, theServer.getBundleInclusionRule(), theIncludes); 225 226 if (theServer.getPagingProvider() != null) { 227 int limit; 228 limit = theLimit != null ? theLimit : theServer.getPagingProvider().getDefaultPageSize(); 229 limit = Math.min(limit, theServer.getPagingProvider().getMaximumPageSize()); 230 231 } 232 233 return bundleFactory.getResourceBundle(); 234 235 } 236 237 public IBaseResource doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) { 238 Object[] params = createMethodParams(theRequest); 239 240 Object resultObj = invokeServer(theServer, theRequest, params); 241 242 Integer count = RestfulServerUtils.extractCountParameter(theRequest); 243 244 final IBaseResource responseObject; 245 246 switch (getReturnType()) { 247 case BUNDLE: { 248 249 /* 250 * Figure out the self-link for this request 251 */ 252 String serverBase = theRequest.getServerBaseForRequest(); 253 String linkSelf; 254 StringBuilder b = new StringBuilder(); 255 b.append(serverBase); 256 257 if (isNotBlank(theRequest.getRequestPath())) { 258 b.append('/'); 259 if (isNotBlank(theRequest.getTenantId()) && theRequest.getRequestPath().startsWith(theRequest.getTenantId() + "/")) { 260 b.append(theRequest.getRequestPath().substring(theRequest.getTenantId().length() + 1)); 261 } else { 262 b.append(theRequest.getRequestPath()); 263 } 264 } 265 // For POST the URL parameters get jumbled with the post body parameters so don't include them, they might be huge 266 if (theRequest.getRequestType() == RequestTypeEnum.GET) { 267 boolean first = true; 268 Map<String, String[]> parameters = theRequest.getParameters(); 269 for (String nextParamName : new TreeSet<>(parameters.keySet())) { 270 for (String nextParamValue : parameters.get(nextParamName)) { 271 if (first) { 272 b.append('?'); 273 first = false; 274 } else { 275 b.append('&'); 276 } 277 b.append(UrlUtil.escapeUrlParam(nextParamName)); 278 b.append('='); 279 b.append(UrlUtil.escapeUrlParam(nextParamValue)); 280 } 281 } 282 } 283 linkSelf = b.toString(); 284 285 if (getMethodReturnType() == MethodReturnTypeEnum.BUNDLE_RESOURCE) { 286 IBaseResource resource; 287 IPrimitiveType<Date> lastUpdated; 288 if (resultObj instanceof IBundleProvider) { 289 IBundleProvider result = (IBundleProvider) resultObj; 290 resource = result.getResources(0, 1).get(0); 291 lastUpdated = result.getPublished(); 292 } else { 293 resource = (IBaseResource) resultObj; 294 lastUpdated = theServer.getFhirContext().getVersion().getLastUpdated(resource); 295 } 296 297 /* 298 * We assume that the bundle we got back from the handling method may not have everything populated (e.g. self links, bundle type, etc) so we do that here. 299 */ 300 IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory(); 301 bundleFactory.initializeWithBundleResource(resource); 302 bundleFactory.addRootPropertiesToBundle(null, theRequest.getFhirServerBase(), linkSelf, null, null, count, getResponseBundleType(), lastUpdated); 303 304 responseObject = resource; 305 } else { 306 Set<Include> includes = getRequestIncludesFromParams(params); 307 308 IBundleProvider result = (IBundleProvider) resultObj; 309 if (count == null) { 310 count = result.preferredPageSize(); 311 } 312 313 Integer offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_PAGINGOFFSET); 314 if (offsetI == null || offsetI < 0) { 315 offsetI = 0; 316 } 317 318 Integer resultSize = result.size(); 319 int start; 320 if (resultSize != null) { 321 start = Math.max(0, Math.min(offsetI, resultSize - 1)); 322 } else { 323 start = offsetI; 324 } 325 326 ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest, theServer.getDefaultResponseEncoding()); 327 EncodingEnum linkEncoding = theRequest.getParameters().containsKey(Constants.PARAM_FORMAT) && responseEncoding != null ? responseEncoding.getEncoding() : null; 328 329 responseObject = createBundleFromBundleProvider(theServer, theRequest, count, linkSelf, includes, result, start, getResponseBundleType(), linkEncoding, null); 330 } 331 break; 332 } 333 case RESOURCE: { 334 IBundleProvider result = (IBundleProvider) resultObj; 335 if (result.size() == 0) { 336 throw new ResourceNotFoundException(theRequest.getId()); 337 } else if (result.size() > 1) { 338 throw new InternalErrorException("Method returned multiple resources"); 339 } 340 341 IBaseResource resource = result.getResources(0, 1).get(0); 342 responseObject = resource; 343 break; 344 } 345 default: 346 throw new IllegalStateException(); // should not happen 347 } 348 return responseObject; 349 } 350 351 public MethodReturnTypeEnum getMethodReturnType() { 352 return myMethodReturnType; 353 } 354 355 @Override 356 public String getResourceName() { 357 return myResourceName; 358 } 359 360 protected void setResourceName(String theResourceName) { 361 myResourceName = theResourceName; 362 } 363 364 /** 365 * If the response is a bundle, this type will be placed in the root of the bundle (can be null) 366 */ 367 protected abstract BundleTypeEnum getResponseBundleType(); 368 369 public abstract ReturnTypeEnum getReturnType(); 370 371 @Override 372 public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { 373 374 IBaseResource response = doInvokeServer(theServer, theRequest); 375 376 Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequest); 377 378 ResponseDetails responseDetails = new ResponseDetails(); 379 responseDetails.setResponseResource(response); 380 responseDetails.setResponseCode(Constants.STATUS_HTTP_200_OK); 381 382 if (!callOutgoingResponseHook(theRequest, responseDetails)) { 383 return null; 384 } 385 386 boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest); 387 388 return theRequest.getResponse().streamResponseAsResource(responseDetails.getResponseResource(), prettyPrint, summaryMode, responseDetails.getResponseCode(), null, theRequest.isRespondGzip(), isAddContentLocationHeader()); 389 390 } 391 392 public abstract Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException; 393 394 /** 395 * Should the response include a Content-Location header. Search method bunding (and any others?) may override this to disable the content-location, since it doesn't make sense 396 */ 397 protected boolean isAddContentLocationHeader() { 398 return true; 399 } 400 401 public enum MethodReturnTypeEnum { 402 BUNDLE, 403 BUNDLE_PROVIDER, 404 BUNDLE_RESOURCE, 405 LIST_OF_RESOURCES, 406 METHOD_OUTCOME, 407 RESOURCE 408 } 409 410 public enum ReturnTypeEnum { 411 BUNDLE, 412 RESOURCE 413 } 414 415 static boolean callOutgoingResponseHook(RequestDetails theRequest, ResponseDetails theResponseDetails) { 416 HttpServletRequest servletRequest = null; 417 HttpServletResponse servletResponse = null; 418 if (theRequest instanceof ServletRequestDetails) { 419 servletRequest = ((ServletRequestDetails) theRequest).getServletRequest(); 420 servletResponse = ((ServletRequestDetails) theRequest).getServletResponse(); 421 } 422 423 HookParams responseParams = new HookParams(); 424 responseParams.add(RequestDetails.class, theRequest); 425 responseParams.addIfMatchesType(ServletRequestDetails.class, theRequest); 426 responseParams.add(IBaseResource.class, theResponseDetails.getResponseResource()); 427 responseParams.add(ResponseDetails.class, theResponseDetails); 428 responseParams.add(HttpServletRequest.class, servletRequest); 429 responseParams.add(HttpServletResponse.class, servletResponse); 430 if (theRequest.getInterceptorBroadcaster() != null) { 431 if (!theRequest.getInterceptorBroadcaster().callHooks(Pointcut.SERVER_OUTGOING_RESPONSE, responseParams)) { 432 return false; 433 } 434 } 435 return true; 436 } 437 438}