001package ca.uhn.fhir.rest.client.method; 002 003import static org.apache.commons.lang3.StringUtils.isNotBlank; 004 005import java.io.*; 006import java.lang.annotation.Annotation; 007import java.lang.reflect.Method; 008import java.util.*; 009import java.util.Map.Entry; 010 011import org.apache.commons.lang3.StringUtils; 012import org.hl7.fhir.instance.model.api.*; 013 014import ca.uhn.fhir.context.*; 015import ca.uhn.fhir.model.api.*; 016import ca.uhn.fhir.model.api.annotation.Description; 017import ca.uhn.fhir.model.primitive.IdDt; 018import ca.uhn.fhir.model.primitive.InstantDt; 019import ca.uhn.fhir.parser.IParser; 020import ca.uhn.fhir.rest.annotation.*; 021import ca.uhn.fhir.rest.api.*; 022import ca.uhn.fhir.rest.client.api.IHttpRequest; 023import ca.uhn.fhir.rest.client.method.OperationParameter.IOperationParamConverter; 024import ca.uhn.fhir.rest.param.ParameterUtil; 025import ca.uhn.fhir.rest.param.binder.CollectionBinder; 026import ca.uhn.fhir.util.*; 027 028/* 029 * #%L 030 * HAPI FHIR - Client Framework 031 * %% 032 * Copyright (C) 2014 - 2019 University Health Network 033 * %% 034 * Licensed under the Apache License, Version 2.0 (the "License"); 035 * you may not use this file except in compliance with the License. 036 * You may obtain a copy of the License at 037 * 038 * http://www.apache.org/licenses/LICENSE-2.0 039 * 040 * Unless required by applicable law or agreed to in writing, software 041 * distributed under the License is distributed on an "AS IS" BASIS, 042 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 043 * See the License for the specific language governing permissions and 044 * limitations under the License. 045 * #L% 046 */ 047 048@SuppressWarnings("deprecation") 049public class MethodUtil { 050 051 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MethodUtil.class); 052 053 /** Non instantiable */ 054 private MethodUtil() { 055 // nothing 056 } 057 058 public static void addAcceptHeaderToRequest(EncodingEnum theEncoding, IHttpRequest theHttpRequest, 059 FhirContext theContext) { 060 if (theEncoding == null) { 061 if (theContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2_1) == false) { 062 theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_XML_OR_JSON_LEGACY); 063 } else { 064 theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_XML_OR_JSON_NON_LEGACY); 065 } 066 } else if (theEncoding == EncodingEnum.JSON) { 067 if (theContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2_1) == false) { 068 theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_JSON); 069 } else { 070 theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_JSON_NON_LEGACY); 071 } 072 } else if (theEncoding == EncodingEnum.XML) { 073 if (theContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2_1) == false) { 074 theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.CT_FHIR_XML); 075 } else { 076 theHttpRequest.addHeader(Constants.HEADER_ACCEPT, Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY); 077 } 078 } 079 080 } 081 082 public static HttpGetClientInvocation createConformanceInvocation(FhirContext theContext) { 083 return new HttpGetClientInvocation(theContext, "metadata"); 084 } 085 086 public static HttpPostClientInvocation createCreateInvocation(IBaseResource theResource, FhirContext theContext) { 087 return createCreateInvocation(theResource, null, theContext); 088 } 089 090 public static HttpPostClientInvocation createCreateInvocation(IBaseResource theResource, String theResourceBody, 091 FhirContext theContext) { 092 RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource); 093 String resourceName = def.getName(); 094 095 StringBuilder urlExtension = new StringBuilder(); 096 urlExtension.append(resourceName); 097 098 HttpPostClientInvocation retVal; 099 if (StringUtils.isBlank(theResourceBody)) { 100 retVal = new HttpPostClientInvocation(theContext, theResource, urlExtension.toString()); 101 } else { 102 retVal = new HttpPostClientInvocation(theContext, theResourceBody, false, urlExtension.toString()); 103 } 104 105 retVal.setOmitResourceId(true); 106 107 return retVal; 108 } 109 110 public static HttpPostClientInvocation createCreateInvocation(IBaseResource theResource, String theResourceBody, 111 FhirContext theContext, Map<String, List<String>> theIfNoneExistParams) { 112 HttpPostClientInvocation retVal = createCreateInvocation(theResource, theResourceBody, theContext); 113 retVal.setIfNoneExistParams(theIfNoneExistParams); 114 return retVal; 115 } 116 117 public static HttpPostClientInvocation createCreateInvocation(IBaseResource theResource, String theResourceBody, 118 FhirContext theContext, String theIfNoneExistUrl) { 119 HttpPostClientInvocation retVal = createCreateInvocation(theResource, theResourceBody, theContext); 120 retVal.setIfNoneExistString(theIfNoneExistUrl); 121 return retVal; 122 } 123 124 public static HttpPatchClientInvocation createPatchInvocation(FhirContext theContext, IIdType theId, 125 PatchTypeEnum thePatchType, String theBody) { 126 return PatchMethodBinding.createPatchInvocation(theContext, theId, thePatchType, theBody); 127 } 128 129 public static HttpPatchClientInvocation createPatchInvocation(FhirContext theContext, PatchTypeEnum thePatchType, 130 String theBody, String theResourceType, Map<String, List<String>> theMatchParams) { 131 return PatchMethodBinding.createPatchInvocation(theContext, thePatchType, theBody, theResourceType, 132 theMatchParams); 133 } 134 135 public static HttpPatchClientInvocation createPatchInvocation(FhirContext theContext, String theUrl, 136 PatchTypeEnum thePatchType, String theBody) { 137 return PatchMethodBinding.createPatchInvocation(theContext, theUrl, thePatchType, theBody); 138 } 139 140 public static HttpPutClientInvocation createUpdateInvocation(FhirContext theContext, IBaseResource theResource, 141 String theResourceBody, Map<String, List<String>> theMatchParams) { 142 String resourceType = theContext.getResourceDefinition(theResource).getName(); 143 144 StringBuilder b = createUrl(resourceType, theMatchParams); 145 146 HttpPutClientInvocation retVal; 147 if (StringUtils.isBlank(theResourceBody)) { 148 retVal = new HttpPutClientInvocation(theContext, theResource, b.toString()); 149 } else { 150 retVal = new HttpPutClientInvocation(theContext, theResourceBody, false, b.toString()); 151 } 152 153 return retVal; 154 } 155 156 public static HttpPutClientInvocation createUpdateInvocation(FhirContext theContext, IBaseResource theResource, 157 String theResourceBody, String theMatchUrl) { 158 HttpPutClientInvocation retVal; 159 if (StringUtils.isBlank(theResourceBody)) { 160 retVal = new HttpPutClientInvocation(theContext, theResource, theMatchUrl); 161 } else { 162 retVal = new HttpPutClientInvocation(theContext, theResourceBody, false, theMatchUrl); 163 } 164 165 return retVal; 166 } 167 168 public static HttpPutClientInvocation createUpdateInvocation(IBaseResource theResource, String theResourceBody, 169 IIdType theId, FhirContext theContext) { 170 String resourceName = theContext.getResourceDefinition(theResource).getName(); 171 StringBuilder urlBuilder = new StringBuilder(); 172 urlBuilder.append(resourceName); 173 urlBuilder.append('/'); 174 urlBuilder.append(theId.getIdPart()); 175 String urlExtension = urlBuilder.toString(); 176 177 HttpPutClientInvocation retVal; 178 if (StringUtils.isBlank(theResourceBody)) { 179 retVal = new HttpPutClientInvocation(theContext, theResource, urlExtension); 180 } else { 181 retVal = new HttpPutClientInvocation(theContext, theResourceBody, false, urlExtension); 182 } 183 184 retVal.setForceResourceId(theId); 185 186 if (theId.hasVersionIdPart()) { 187 retVal.addHeader(Constants.HEADER_IF_MATCH, '"' + theId.getVersionIdPart() + '"'); 188 } 189 190 return retVal; 191 } 192 193 public static StringBuilder createUrl(String theResourceType, Map<String, List<String>> theMatchParams) { 194 StringBuilder b = new StringBuilder(); 195 196 b.append(theResourceType); 197 198 boolean haveQuestionMark = false; 199 for (Entry<String, List<String>> nextEntry : theMatchParams.entrySet()) { 200 for (String nextValue : nextEntry.getValue()) { 201 b.append(haveQuestionMark ? '&' : '?'); 202 haveQuestionMark = true; 203 b.append(UrlUtil.escapeUrlParam(nextEntry.getKey())); 204 b.append('='); 205 b.append(UrlUtil.escapeUrlParam(nextValue)); 206 } 207 } 208 return b; 209 } 210 211 public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) { 212 for (Annotation annotation : theAnnotations) { 213 if (annotation instanceof Description) { 214 Description desc = (Description) annotation; 215 if (isNotBlank(desc.formalDefinition())) { 216 theParameter.setDescription(desc.formalDefinition()); 217 } else { 218 theParameter.setDescription(desc.shortDefinition()); 219 } 220 } 221 } 222 } 223 224 @SuppressWarnings("unchecked") 225 public static List<IParameter> getResourceParameters(final FhirContext theContext, Method theMethod, 226 Object theProvider, RestOperationTypeEnum theRestfulOperationTypeEnum) { 227 List<IParameter> parameters = new ArrayList<IParameter>(); 228 229 Class<?>[] parameterTypes = theMethod.getParameterTypes(); 230 int paramIndex = 0; 231 for (Annotation[] annotations : theMethod.getParameterAnnotations()) { 232 233 IParameter param = null; 234 Class<?> parameterType = parameterTypes[paramIndex]; 235 Class<? extends java.util.Collection<?>> outerCollectionType = null; 236 Class<? extends java.util.Collection<?>> innerCollectionType = null; 237 if (TagList.class.isAssignableFrom(parameterType)) { 238 // TagList is handled directly within the method bindings 239 param = new NullParameter(); 240 } else { 241 if (Collection.class.isAssignableFrom(parameterType)) { 242 innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType; 243 parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); 244 } 245 if (Collection.class.isAssignableFrom(parameterType)) { 246 outerCollectionType = innerCollectionType; 247 innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType; 248 parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); 249 } 250 if (Collection.class.isAssignableFrom(parameterType)) { 251 throw new ConfigurationException("Argument #" + paramIndex + " of Method '" + theMethod.getName() 252 + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() 253 + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); 254 } 255 } 256 257 if (parameterType.equals(SummaryEnum.class)) { 258 param = new SummaryEnumParameter(); 259 } else if (parameterType.equals(PatchTypeEnum.class)) { 260 param = new PatchTypeParameter(); 261 } else { 262 for (int i = 0; i < annotations.length && param == null; i++) { 263 Annotation nextAnnotation = annotations[i]; 264 265 if (nextAnnotation instanceof RequiredParam) { 266 SearchParameter parameter = new SearchParameter(); 267 parameter.setName(((RequiredParam) nextAnnotation).name()); 268 parameter.setRequired(true); 269 parameter.setDeclaredTypes(((RequiredParam) nextAnnotation).targetTypes()); 270 parameter.setCompositeTypes(((RequiredParam) nextAnnotation).compositeTypes()); 271 parameter.setChainlists(((RequiredParam) nextAnnotation).chainWhitelist(), 272 ((RequiredParam) nextAnnotation).chainBlacklist()); 273 parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); 274 MethodUtil.extractDescription(parameter, annotations); 275 param = parameter; 276 } else if (nextAnnotation instanceof OptionalParam) { 277 SearchParameter parameter = new SearchParameter(); 278 parameter.setName(((OptionalParam) nextAnnotation).name()); 279 parameter.setRequired(false); 280 parameter.setDeclaredTypes(((OptionalParam) nextAnnotation).targetTypes()); 281 parameter.setCompositeTypes(((OptionalParam) nextAnnotation).compositeTypes()); 282 parameter.setChainlists(((OptionalParam) nextAnnotation).chainWhitelist(), 283 ((OptionalParam) nextAnnotation).chainBlacklist()); 284 parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); 285 MethodUtil.extractDescription(parameter, annotations); 286 param = parameter; 287 } else if (nextAnnotation instanceof RawParam) { 288 param = new RawParamsParmeter(); 289 } else if (nextAnnotation instanceof IncludeParam) { 290 Class<? extends Collection<Include>> instantiableCollectionType; 291 Class<?> specType; 292 293 if (parameterType == String.class) { 294 instantiableCollectionType = null; 295 specType = String.class; 296 } else if ((parameterType != Include.class) || innerCollectionType == null 297 || outerCollectionType != null) { 298 throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" 299 + IncludeParam.class.getSimpleName() + " but has a type other than Collection<" 300 + Include.class.getSimpleName() + ">"); 301 } else { 302 instantiableCollectionType = (Class<? extends Collection<Include>>) CollectionBinder 303 .getInstantiableCollectionType(innerCollectionType, 304 "Method '" + theMethod.getName() + "'"); 305 specType = parameterType; 306 } 307 308 param = new IncludeParameter((IncludeParam) nextAnnotation, instantiableCollectionType, 309 specType); 310 } else if (nextAnnotation instanceof ResourceParam) { 311 if (IBaseResource.class.isAssignableFrom(parameterType)) { 312 // good 313 } else if (String.class.equals(parameterType)) { 314 // good 315 } else if (byte[].class.equals(parameterType)) { 316 // good 317 } else if (EncodingEnum.class.equals(parameterType)) { 318 // good 319 } else { 320 StringBuilder b = new StringBuilder(); 321 b.append("Method '"); 322 b.append(theMethod.getName()); 323 b.append("' is annotated with @"); 324 b.append(ResourceParam.class.getSimpleName()); 325 b.append(" but has a type that is not an implemtation of "); 326 b.append(IBaseResource.class.getCanonicalName()); 327 b.append(" or String or byte[]"); 328 throw new ConfigurationException(b.toString()); 329 } 330 param = new ResourceParameter(parameterType); 331 } else if (nextAnnotation instanceof IdParam) { 332 param = new NullParameter(); 333 } else if (nextAnnotation instanceof ServerBase) { 334 param = new ServerBaseParamBinder(); 335 } else if (nextAnnotation instanceof Elements) { 336 param = new ElementsParameter(); 337 } else if (nextAnnotation instanceof Since) { 338 param = new SinceParameter(); 339 ((SinceParameter) param).setType(theContext, parameterType, innerCollectionType, 340 outerCollectionType); 341 } else if (nextAnnotation instanceof At) { 342 param = new AtParameter(); 343 ((AtParameter) param).setType(theContext, parameterType, innerCollectionType, 344 outerCollectionType); 345 } else if (nextAnnotation instanceof Count) { 346 param = new CountParameter(); 347 } else if (nextAnnotation instanceof Sort) { 348 param = new SortParameter(theContext); 349 } else if (nextAnnotation instanceof TransactionParam) { 350 param = new TransactionParameter(theContext); 351 } else if (nextAnnotation instanceof ConditionalUrlParam) { 352 param = new ConditionalParamBinder(theRestfulOperationTypeEnum, 353 ((ConditionalUrlParam) nextAnnotation).supportsMultiple()); 354 } else if (nextAnnotation instanceof OperationParam) { 355 Operation op = theMethod.getAnnotation(Operation.class); 356 param = new OperationParameter(theContext, op.name(), ((OperationParam) nextAnnotation)); 357 } else if (nextAnnotation instanceof Validate.Mode) { 358 if (parameterType.equals(ValidationModeEnum.class) == false) { 359 throw new ConfigurationException("Parameter annotated with @" 360 + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() 361 + " must be of type " + ValidationModeEnum.class.getName()); 362 } 363 param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, 364 Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IOperationParamConverter() { 365 @Override 366 public Object incomingServer(Object theObject) { 367 if (isNotBlank(theObject.toString())) { 368 ValidationModeEnum retVal = ValidationModeEnum 369 .forCode(theObject.toString()); 370 if (retVal == null) { 371 OperationParameter.throwInvalidMode(theObject.toString()); 372 } 373 return retVal; 374 } 375 return null; 376 } 377 378 @Override 379 public Object outgoingClient(Object theObject) { 380 return ParametersUtil.createString(theContext, 381 ((ValidationModeEnum) theObject).getCode()); 382 } 383 }); 384 } else if (nextAnnotation instanceof Validate.Profile) { 385 if (parameterType.equals(String.class) == false) { 386 throw new ConfigurationException("Parameter annotated with @" 387 + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() 388 + " must be of type " + String.class.getName()); 389 } 390 param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, 391 Constants.EXTOP_VALIDATE_PROFILE, 0, 1).setConverter(new IOperationParamConverter() { 392 @Override 393 public Object incomingServer(Object theObject) { 394 return theObject.toString(); 395 } 396 397 @Override 398 public Object outgoingClient(Object theObject) { 399 return ParametersUtil.createString(theContext, theObject.toString()); 400 } 401 }); 402 } else { 403 continue; 404 } 405 406 } 407 408 } 409 410 if (param == null) { 411 throw new ConfigurationException("Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) 412 + " of method '" + theMethod.getName() + "' on type '" 413 + theMethod.getDeclaringClass().getCanonicalName() 414 + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter"); 415 } 416 417 param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); 418 parameters.add(param); 419 420 paramIndex++; 421 } 422 return parameters; 423 } 424 425 public static void parseClientRequestResourceHeaders(IIdType theRequestedId, Map<String, List<String>> theHeaders, 426 IBaseResource resource) { 427 List<String> lmHeaders = theHeaders.get(Constants.HEADER_LAST_MODIFIED_LOWERCASE); 428 if (lmHeaders != null && lmHeaders.size() > 0 && StringUtils.isNotBlank(lmHeaders.get(0))) { 429 String headerValue = lmHeaders.get(0); 430 Date headerDateValue; 431 try { 432 headerDateValue = DateUtils.parseDate(headerValue); 433 if (resource instanceof IResource) { 434 IResource iResource = (IResource) resource; 435 InstantDt existing = ResourceMetadataKeyEnum.UPDATED.get(iResource); 436 if (existing == null || existing.isEmpty()) { 437 InstantDt lmValue = new InstantDt(headerDateValue); 438 iResource.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, lmValue); 439 } 440 } else if (resource instanceof IAnyResource) { 441 IAnyResource anyResource = (IAnyResource) resource; 442 if (anyResource.getMeta().getLastUpdated() == null) { 443 anyResource.getMeta().setLastUpdated(headerDateValue); 444 } 445 } 446 } catch (Exception e) { 447 ourLog.warn("Unable to parse date string '{}'. Error is: {}", headerValue, e.toString()); 448 } 449 } 450 451 List<String> clHeaders = theHeaders.get(Constants.HEADER_CONTENT_LOCATION_LC); 452 if (clHeaders != null && clHeaders.size() > 0 && StringUtils.isNotBlank(clHeaders.get(0))) { 453 String headerValue = clHeaders.get(0); 454 if (isNotBlank(headerValue)) { 455 new IdDt(headerValue).applyTo(resource); 456 } 457 } 458 459 List<String> locationHeaders = theHeaders.get(Constants.HEADER_LOCATION_LC); 460 if (locationHeaders != null && locationHeaders.size() > 0 && StringUtils.isNotBlank(locationHeaders.get(0))) { 461 String headerValue = locationHeaders.get(0); 462 if (isNotBlank(headerValue)) { 463 new IdDt(headerValue).applyTo(resource); 464 } 465 } 466 467 IdDt existing = IdDt.of(resource); 468 469 List<String> eTagHeaders = theHeaders.get(Constants.HEADER_ETAG_LC); 470 String eTagVersion = null; 471 if (eTagHeaders != null && eTagHeaders.size() > 0) { 472 eTagVersion = ParameterUtil.parseETagValue(eTagHeaders.get(0)); 473 } 474 if (isNotBlank(eTagVersion)) { 475 if (existing == null || existing.isEmpty()) { 476 if (theRequestedId != null) { 477 theRequestedId.withVersion(eTagVersion).applyTo(resource); 478 } 479 } else if (existing.hasVersionIdPart() == false) { 480 existing.withVersion(eTagVersion).applyTo(resource); 481 } 482 } else if (existing == null || existing.isEmpty()) { 483 if (theRequestedId != null) { 484 theRequestedId.applyTo(resource); 485 } 486 } 487 488 } 489 490 public static MethodOutcome process2xxResponse(FhirContext theContext, int theResponseStatusCode, 491 String theResponseMimeType, InputStream theResponseReader, Map<String, List<String>> theHeaders) { 492 List<String> locationHeaders = new ArrayList<>(); 493 List<String> lh = theHeaders.get(Constants.HEADER_LOCATION_LC); 494 if (lh != null) { 495 locationHeaders.addAll(lh); 496 } 497 List<String> clh = theHeaders.get(Constants.HEADER_CONTENT_LOCATION_LC); 498 if (clh != null) { 499 locationHeaders.addAll(clh); 500 } 501 502 MethodOutcome retVal = new MethodOutcome(); 503 if (locationHeaders.size() > 0) { 504 String locationHeader = locationHeaders.get(0); 505 BaseOutcomeReturningMethodBinding.parseContentLocation(theContext, retVal, locationHeader); 506 } 507 if (theResponseStatusCode != Constants.STATUS_HTTP_204_NO_CONTENT) { 508 EncodingEnum ct = EncodingEnum.forContentType(theResponseMimeType); 509 if (ct != null) { 510 PushbackInputStream reader = new PushbackInputStream(theResponseReader); 511 512 try { 513 int firstByte = reader.read(); 514 if (firstByte == -1) { 515 BaseOutcomeReturningMethodBinding.ourLog.debug("No content in response, not going to read"); 516 reader = null; 517 } else { 518 reader.unread(firstByte); 519 } 520 } catch (IOException e) { 521 BaseOutcomeReturningMethodBinding.ourLog.debug("No content in response, not going to read", e); 522 reader = null; 523 } 524 525 if (reader != null) { 526 IParser parser = ct.newParser(theContext); 527 IBaseResource outcome = parser.parseResource(reader); 528 if (outcome instanceof IBaseOperationOutcome) { 529 retVal.setOperationOutcome((IBaseOperationOutcome) outcome); 530 } else { 531 retVal.setResource(outcome); 532 } 533 } 534 535 } else { 536 BaseOutcomeReturningMethodBinding.ourLog.debug("Ignoring response content of type: {}", 537 theResponseMimeType); 538 } 539 } 540 return retVal; 541 } 542 543}