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