001package ca.uhn.fhir.rest.method; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2017 University Health Network 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022import static org.apache.commons.lang3.StringUtils.isNotBlank; 023 024import java.io.IOException; 025import java.io.Reader; 026import java.lang.reflect.Method; 027import java.lang.reflect.Modifier; 028import java.util.*; 029 030import org.hl7.fhir.instance.model.api.IBaseBundle; 031import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 032import org.hl7.fhir.instance.model.api.IBaseResource; 033import org.hl7.fhir.instance.model.api.IPrimitiveType; 034 035import ca.uhn.fhir.context.ConfigurationException; 036import ca.uhn.fhir.context.FhirContext; 037import ca.uhn.fhir.context.FhirVersionEnum; 038import ca.uhn.fhir.model.api.Bundle; 039import ca.uhn.fhir.model.api.IResource; 040import ca.uhn.fhir.model.api.Include; 041import ca.uhn.fhir.model.valueset.BundleTypeEnum; 042import ca.uhn.fhir.parser.IParser; 043import ca.uhn.fhir.rest.api.MethodOutcome; 044import ca.uhn.fhir.rest.api.RequestTypeEnum; 045import ca.uhn.fhir.rest.api.SummaryEnum; 046import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException; 047import ca.uhn.fhir.rest.server.Constants; 048import ca.uhn.fhir.rest.server.EncodingEnum; 049import ca.uhn.fhir.rest.server.IBundleProvider; 050import ca.uhn.fhir.rest.server.IRestfulServer; 051import ca.uhn.fhir.rest.server.IVersionSpecificBundleFactory; 052import ca.uhn.fhir.rest.server.RestfulServerUtils; 053import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding; 054import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 055import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 056import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 057import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 058import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; 059import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; 060import ca.uhn.fhir.util.BundleUtil; 061import ca.uhn.fhir.util.ReflectionUtil; 062import ca.uhn.fhir.util.UrlUtil; 063 064public abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Object> { 065 protected static final Set<String> ALLOWED_PARAMS; 066 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class); 067 068 static { 069 HashSet<String> set = new HashSet<String>(); 070 set.add(Constants.PARAM_FORMAT); 071 set.add(Constants.PARAM_NARRATIVE); 072 set.add(Constants.PARAM_PRETTY); 073 set.add(Constants.PARAM_SORT); 074 set.add(Constants.PARAM_SORT_ASC); 075 set.add(Constants.PARAM_SORT_DESC); 076 set.add(Constants.PARAM_COUNT); 077 set.add(Constants.PARAM_SUMMARY); 078 set.add(Constants.PARAM_ELEMENTS); 079 set.add(ResponseHighlighterInterceptor.PARAM_RAW); 080 ALLOWED_PARAMS = Collections.unmodifiableSet(set); 081 } 082 083 private MethodReturnTypeEnum myMethodReturnType; 084 private Class<?> myResourceListCollectionType; 085 private String myResourceName; 086 private Class<? extends IBaseResource> myResourceType; 087 private List<Class<? extends IBaseResource>> myPreferTypesList; 088 089 @SuppressWarnings("unchecked") 090 public BaseResourceReturningMethodBinding(Class<?> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { 091 super(theMethod, theContext, theProvider); 092 093 Class<?> methodReturnType = theMethod.getReturnType(); 094 if (Collection.class.isAssignableFrom(methodReturnType)) { 095 096 myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES; 097 Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod); 098 if (collectionType != null) { 099 if (!Object.class.equals(collectionType) && !IBaseResource.class.isAssignableFrom(collectionType)) { 100 throw new ConfigurationException( 101 "Method " + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() + " returns an invalid collection generic type: " + collectionType); 102 } 103 } 104 myResourceListCollectionType = collectionType; 105 106 } else if (IBaseResource.class.isAssignableFrom(methodReturnType)) { 107 if (Modifier.isAbstract(methodReturnType.getModifiers()) == false && theContext.getResourceDefinition((Class<? extends IBaseResource>) methodReturnType).isBundle()) { 108 myMethodReturnType = MethodReturnTypeEnum.BUNDLE_RESOURCE; 109 } else { 110 myMethodReturnType = MethodReturnTypeEnum.RESOURCE; 111 } 112 } else if (Bundle.class.isAssignableFrom(methodReturnType)) { 113 myMethodReturnType = MethodReturnTypeEnum.BUNDLE; 114 } else if (IBundleProvider.class.isAssignableFrom(methodReturnType)) { 115 myMethodReturnType = MethodReturnTypeEnum.BUNDLE_PROVIDER; 116 } else if (MethodOutcome.class.isAssignableFrom(methodReturnType)) { 117 myMethodReturnType = MethodReturnTypeEnum.METHOD_OUTCOME; 118 } else { 119 throw new ConfigurationException( 120 "Invalid return type '" + methodReturnType.getCanonicalName() + "' on method '" + theMethod.getName() + "' on type: " + theMethod.getDeclaringClass().getCanonicalName()); 121 } 122 123 if (theReturnResourceType != null) { 124 if (IBaseResource.class.isAssignableFrom(theReturnResourceType)) { 125 if (Modifier.isAbstract(theReturnResourceType.getModifiers()) || Modifier.isInterface(theReturnResourceType.getModifiers())) { 126 // If we're returning an abstract type, that's ok 127 } else { 128 myResourceType = (Class<? extends IResource>) theReturnResourceType; 129 myResourceName = theContext.getResourceDefinition(myResourceType).getName(); 130 } 131 } 132 } 133 134 myPreferTypesList = createPreferTypesList(); 135 } 136 137 public MethodReturnTypeEnum getMethodReturnType() { 138 return myMethodReturnType; 139 } 140 141 @Override 142 public String getResourceName() { 143 return myResourceName; 144 } 145 146 /** 147 * If the response is a bundle, this type will be placed in the root of the bundle (can be null) 148 */ 149 protected abstract BundleTypeEnum getResponseBundleType(); 150 151 public abstract ReturnTypeEnum getReturnType(); 152 153 @Override 154 public Object invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) { 155 IParser parser = createAppropriateParserForParsingResponse(theResponseMimeType, theResponseReader, theResponseStatusCode, myPreferTypesList); 156 157 switch (getReturnType()) { 158 case BUNDLE: { 159 160 Bundle dstu1bundle = null; 161 IBaseBundle dstu2bundle = null; 162 List<? extends IBaseResource> listOfResources = null; 163 if (getMethodReturnType() == MethodReturnTypeEnum.BUNDLE || getContext().getVersion().getVersion() == FhirVersionEnum.DSTU1) { 164 if (myResourceType != null) { 165 dstu1bundle = parser.parseBundle(myResourceType, theResponseReader); 166 } else { 167 dstu1bundle = parser.parseBundle(theResponseReader); 168 } 169 listOfResources = dstu1bundle.toListOfResources(); 170 } else { 171 Class<? extends IBaseResource> type = getContext().getResourceDefinition("Bundle").getImplementingClass(); 172 dstu2bundle = (IBaseBundle) parser.parseResource(type, theResponseReader); 173 listOfResources = BundleUtil.toListOfResources(getContext(), dstu2bundle); 174 } 175 176 switch (getMethodReturnType()) { 177 case BUNDLE: 178 return dstu1bundle; 179 case BUNDLE_RESOURCE: 180 return dstu2bundle; 181 case LIST_OF_RESOURCES: 182 if (myResourceListCollectionType != null) { 183 for (Iterator<? extends IBaseResource> iter = listOfResources.iterator(); iter.hasNext();) { 184 IBaseResource next = iter.next(); 185 if (!myResourceListCollectionType.isAssignableFrom(next.getClass())) { 186 ourLog.debug("Not returning resource of type {} because it is not a subclass or instance of {}", next.getClass(), myResourceListCollectionType); 187 iter.remove(); 188 } 189 } 190 } 191 return listOfResources; 192 case RESOURCE: 193 //FIXME null access on dstu1bundle 194 List<IResource> list = dstu1bundle.toListOfResources(); 195 if (list.size() == 0) { 196 return null; 197 } else if (list.size() == 1) { 198 return list.get(0); 199 } else { 200 throw new InvalidResponseException(theResponseStatusCode, "FHIR server call returned a bundle with multiple resources, but this method is only able to returns one."); 201 } 202 case BUNDLE_PROVIDER: 203 throw new IllegalStateException("Return type of " + IBundleProvider.class.getSimpleName() + " is not supported in clients"); 204 default: 205 break; 206 } 207 break; 208 } 209 case RESOURCE: { 210 IBaseResource resource; 211 if (myResourceType != null) { 212 resource = parser.parseResource(myResourceType, theResponseReader); 213 } else { 214 resource = parser.parseResource(theResponseReader); 215 } 216 217 MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, resource); 218 219 switch (getMethodReturnType()) { 220 case BUNDLE: 221 return Bundle.withSingleResource((IResource) resource); 222 case LIST_OF_RESOURCES: 223 return Collections.singletonList(resource); 224 case RESOURCE: 225 return resource; 226 case BUNDLE_PROVIDER: 227 throw new IllegalStateException("Return type of " + IBundleProvider.class.getSimpleName() + " is not supported in clients"); 228 case BUNDLE_RESOURCE: 229 return resource; 230 case METHOD_OUTCOME: 231 MethodOutcome retVal = new MethodOutcome(); 232 retVal.setOperationOutcome((IBaseOperationOutcome) resource); 233 return retVal; 234 } 235 break; 236 } 237 } 238 239 throw new IllegalStateException("Should not get here!"); 240 } 241 242 @SuppressWarnings("unchecked") 243 private List<Class<? extends IBaseResource>> createPreferTypesList() { 244 List<Class<? extends IBaseResource>> preferTypes = null; 245 if (myResourceListCollectionType != null && IBaseResource.class.isAssignableFrom(myResourceListCollectionType)) { 246 preferTypes = new ArrayList<Class<? extends IBaseResource>>(1); 247 preferTypes.add((Class<? extends IBaseResource>) myResourceListCollectionType); 248// } else if (myResourceType != null) { 249// preferTypes = new ArrayList<Class<? extends IBaseResource>>(1); 250// preferTypes.add((Class<? extends IBaseResource>) myResourceListCollectionType); 251 } 252 return preferTypes; 253 } 254 255 @Override 256 public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { 257 258 final ResourceOrDstu1Bundle responseObject = doInvokeServer(theServer, theRequest); 259 260 Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequest); 261 if (responseObject.getResource() != null) { 262 263 for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) { 264 IServerInterceptor next = theServer.getInterceptors().get(i); 265 boolean continueProcessing = next.outgoingResponse(theRequest, responseObject.getResource()); 266 if (!continueProcessing) { 267 return null; 268 } 269 } 270 271 boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest); 272 273 return theRequest.getResponse().streamResponseAsResource(responseObject.getResource(), prettyPrint, summaryMode, Constants.STATUS_HTTP_200_OK, null, theRequest.isRespondGzip(), 274 isAddContentLocationHeader()); 275 276 } 277 // Is this request coming from a browser 278 String uaHeader = theRequest.getHeader("user-agent"); 279 boolean requestIsBrowser = false; 280 if (uaHeader != null && uaHeader.contains("Mozilla")) { 281 requestIsBrowser = true; 282 } 283 284 for (int i = theServer.getInterceptors().size() - 1; i >= 0; i--) { 285 IServerInterceptor next = theServer.getInterceptors().get(i); 286 boolean continueProcessing = next.outgoingResponse(theRequest, responseObject.getDstu1Bundle()); 287 if (!continueProcessing) { 288 ourLog.debug("Interceptor {} returned false, not continuing processing"); 289 return null; 290 } 291 } 292 293 return theRequest.getResponse().streamResponseAsBundle(responseObject.getDstu1Bundle(), summaryMode, theRequest.isRespondGzip(), requestIsBrowser); 294 } 295 296 public ResourceOrDstu1Bundle doInvokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) { 297 // Method params 298 Object[] params = new Object[getParameters().size()]; 299 for (int i = 0; i < getParameters().size(); i++) { 300 IParameter param = getParameters().get(i); 301 if (param != null) { 302 params[i] = param.translateQueryParametersIntoServerArgument(theRequest, this); 303 } 304 } 305 306 Object resultObj = invokeServer(theServer, theRequest, params); 307 308 Integer count = RestfulServerUtils.extractCountParameter(theRequest); 309 310 final ResourceOrDstu1Bundle responseObject; 311 312 switch (getReturnType()) { 313 case BUNDLE: { 314 315 /* 316 * Figure out the self-link for this request 317 */ 318 String serverBase = theRequest.getServerBaseForRequest(); 319 String linkSelf; 320 StringBuilder b = new StringBuilder(); 321 b.append(serverBase); 322 if (isNotBlank(theRequest.getRequestPath())) { 323 b.append('/'); 324 b.append(theRequest.getRequestPath()); 325 } 326 // For POST the URL parameters get jumbled with the post body parameters so don't include them, they might be huge 327 if (theRequest.getRequestType() == RequestTypeEnum.GET) { 328 boolean first = true; 329 Map<String, String[]> parameters = theRequest.getParameters(); 330 for (String nextParamName : new TreeSet<String>(parameters.keySet())) { 331 for (String nextParamValue : parameters.get(nextParamName)) { 332 if (first) { 333 b.append('?'); 334 first = false; 335 } else { 336 b.append('&'); 337 } 338 b.append(UrlUtil.escape(nextParamName)); 339 b.append('='); 340 b.append(UrlUtil.escape(nextParamValue)); 341 } 342 } 343 } 344 linkSelf = b.toString(); 345 346 if (getMethodReturnType() == MethodReturnTypeEnum.BUNDLE_RESOURCE) { 347 IBaseResource resource; 348 IPrimitiveType<Date> lastUpdated; 349 if (resultObj instanceof IBundleProvider) { 350 IBundleProvider result = (IBundleProvider) resultObj; 351 resource = result.getResources(0, 1).get(0); 352 lastUpdated = result.getPublished(); 353 } else { 354 resource = (IBaseResource) resultObj; 355 lastUpdated = theServer.getFhirContext().getVersion().getLastUpdated(resource); 356 } 357 358 /* 359 * 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. 360 */ 361 IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory(); 362 bundleFactory.initializeWithBundleResource(resource); 363 bundleFactory.addRootPropertiesToBundle(null, theRequest.getFhirServerBase(), linkSelf, count, getResponseBundleType(), lastUpdated); 364 365 responseObject = new ResourceOrDstu1Bundle(resource); 366 } else { 367 Set<Include> includes = getRequestIncludesFromParams(params); 368 369 IBundleProvider result = (IBundleProvider) resultObj; 370 if (count == null) { 371 count = result.preferredPageSize(); 372 } 373 374 Integer offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_PAGINGOFFSET); 375 if (offsetI == null || offsetI < 0) { 376 offsetI = 0; 377 } 378 379 Integer resultSize = result.size(); 380 int start; 381 if (resultSize != null) { 382 start = Math.max(0, Math.min(offsetI, resultSize - 1)); 383 } else { 384 start = offsetI; 385 } 386 387 IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory(); 388 389 ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest, theServer.getDefaultResponseEncoding()); 390 EncodingEnum linkEncoding = theRequest.getParameters().containsKey(Constants.PARAM_FORMAT) && responseEncoding != null ? responseEncoding.getEncoding() : null; 391 392 boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theServer, theRequest); 393 bundleFactory.initializeBundleFromBundleProvider(theServer, result, linkEncoding, theRequest.getFhirServerBase(), linkSelf, prettyPrint, start, count, null, getResponseBundleType(), 394 includes); 395 Bundle bundle = bundleFactory.getDstu1Bundle(); 396 if (bundle != null) { 397 responseObject = new ResourceOrDstu1Bundle(bundle); 398 } else { 399 IBaseResource resBundle = bundleFactory.getResourceBundle(); 400 responseObject = new ResourceOrDstu1Bundle(resBundle); 401 } 402 } 403 break; 404 } 405 case RESOURCE: { 406 IBundleProvider result = (IBundleProvider) resultObj; 407 if (result.size() == 0) { 408 throw new ResourceNotFoundException(theRequest.getId()); 409 } else if (result.size() > 1) { 410 throw new InternalErrorException("Method returned multiple resources"); 411 } 412 413 IBaseResource resource = result.getResources(0, 1).get(0); 414 responseObject = new ResourceOrDstu1Bundle(resource); 415 break; 416 } 417 default: 418 throw new IllegalStateException(); // should not happen 419 } 420 return responseObject; 421 } 422 423 public abstract Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException; 424 425 /** 426 * 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 427 */ 428 protected boolean isAddContentLocationHeader() { 429 return true; 430 } 431 432 protected void setResourceName(String theResourceName) { 433 myResourceName = theResourceName; 434 } 435 436 public enum MethodReturnTypeEnum { 437 BUNDLE, BUNDLE_PROVIDER, BUNDLE_RESOURCE, LIST_OF_RESOURCES, METHOD_OUTCOME, RESOURCE 438 } 439 440 public static class ResourceOrDstu1Bundle { 441 442 private final Bundle myDstu1Bundle; 443 private final IBaseResource myResource; 444 445 public ResourceOrDstu1Bundle(Bundle theBundle) { 446 myDstu1Bundle = theBundle; 447 myResource = null; 448 } 449 450 public ResourceOrDstu1Bundle(IBaseResource theResource) { 451 myResource = theResource; 452 myDstu1Bundle = null; 453 } 454 455 public Bundle getDstu1Bundle() { 456 return myDstu1Bundle; 457 } 458 459 public IBaseResource getResource() { 460 return myResource; 461 } 462 463 } 464 465 public enum ReturnTypeEnum { 466 BUNDLE, RESOURCE 467 } 468 469}