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