
001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.server.method; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeChildDefinition.IAccessor; 024import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 025import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 026import ca.uhn.fhir.context.ConfigurationException; 027import ca.uhn.fhir.context.FhirContext; 028import ca.uhn.fhir.context.FhirVersionEnum; 029import ca.uhn.fhir.context.IRuntimeDatatypeDefinition; 030import ca.uhn.fhir.context.RuntimeChildPrimitiveDatatypeDefinition; 031import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition; 032import ca.uhn.fhir.context.RuntimeResourceDefinition; 033import ca.uhn.fhir.i18n.HapiLocalizer; 034import ca.uhn.fhir.i18n.Msg; 035import ca.uhn.fhir.model.api.IQueryParameterAnd; 036import ca.uhn.fhir.model.api.IQueryParameterOr; 037import ca.uhn.fhir.model.api.IQueryParameterType; 038import ca.uhn.fhir.rest.annotation.OperationParam; 039import ca.uhn.fhir.rest.api.QualifiedParamList; 040import ca.uhn.fhir.rest.api.RequestTypeEnum; 041import ca.uhn.fhir.rest.api.ValidationModeEnum; 042import ca.uhn.fhir.rest.api.server.RequestDetails; 043import ca.uhn.fhir.rest.param.BaseAndListParam; 044import ca.uhn.fhir.rest.param.DateRangeParam; 045import ca.uhn.fhir.rest.param.TokenParam; 046import ca.uhn.fhir.rest.param.binder.CollectionBinder; 047import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 048import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 049import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 050import ca.uhn.fhir.util.FhirTerser; 051import ca.uhn.fhir.util.ReflectionUtil; 052import org.apache.commons.lang3.Validate; 053import org.hl7.fhir.instance.model.api.IBase; 054import org.hl7.fhir.instance.model.api.IBaseCoding; 055import org.hl7.fhir.instance.model.api.IBaseDatatype; 056import org.hl7.fhir.instance.model.api.IBaseReference; 057import org.hl7.fhir.instance.model.api.IBaseResource; 058import org.hl7.fhir.instance.model.api.IPrimitiveType; 059 060import java.lang.reflect.Method; 061import java.lang.reflect.Modifier; 062import java.util.ArrayList; 063import java.util.Arrays; 064import java.util.Collection; 065import java.util.Collections; 066import java.util.List; 067import java.util.function.Consumer; 068 069import static org.apache.commons.lang3.StringUtils.isNotBlank; 070 071public class OperationParameter implements IParameter { 072 073 static final String REQUEST_CONTENTS_USERDATA_KEY = OperationParam.class.getName() + "_PARSED_RESOURCE"; 074 075 @SuppressWarnings("unchecked") 076 private static final Class<? extends IQueryParameterType>[] COMPOSITE_TYPES = new Class[0]; 077 078 private final FhirContext myContext; 079 private final String myName; 080 private final String myOperationName; 081 private boolean myAllowGet; 082 private IOperationParamConverter myConverter; 083 084 @SuppressWarnings("rawtypes") 085 private Class<? extends Collection> myInnerCollectionType; 086 087 private int myMax; 088 private int myMin; 089 private Class<?> myParameterType; 090 private String myParamType; 091 private SearchParameter mySearchParameterBinding; 092 private String myDescription; 093 private List<String> myExampleValues; 094 095 OperationParameter( 096 FhirContext theCtx, 097 String theOperationName, 098 String theParameterName, 099 int theMin, 100 int theMax, 101 String theDescription, 102 List<String> theExampleValues) { 103 myOperationName = theOperationName; 104 myName = theParameterName; 105 myMin = theMin; 106 myMax = theMax; 107 myContext = theCtx; 108 myDescription = theDescription; 109 110 List<String> exampleValues = new ArrayList<>(); 111 if (theExampleValues != null) { 112 exampleValues.addAll(theExampleValues); 113 } 114 myExampleValues = Collections.unmodifiableList(exampleValues); 115 } 116 117 @SuppressWarnings({"rawtypes", "unchecked"}) 118 private void addValueToList(List<Object> matchingParamValues, Object values) { 119 if (values != null) { 120 if (BaseAndListParam.class.isAssignableFrom(myParameterType) && matchingParamValues.size() > 0) { 121 BaseAndListParam existing = (BaseAndListParam<?>) matchingParamValues.get(0); 122 BaseAndListParam<?> newAndList = (BaseAndListParam<?>) values; 123 for (IQueryParameterOr nextAnd : newAndList.getValuesAsQueryTokens()) { 124 existing.addAnd(nextAnd); 125 } 126 } else { 127 matchingParamValues.add(values); 128 } 129 } 130 } 131 132 protected FhirContext getContext() { 133 return myContext; 134 } 135 136 public int getMax() { 137 return myMax; 138 } 139 140 public int getMin() { 141 return myMin; 142 } 143 144 public String getName() { 145 return myName; 146 } 147 148 public String getParamType() { 149 return myParamType; 150 } 151 152 public String getSearchParamType() { 153 if (mySearchParameterBinding != null) { 154 return mySearchParameterBinding.getParamType().getCode(); 155 } 156 return null; 157 } 158 159 @SuppressWarnings("unchecked") 160 @Override 161 public void initializeTypes( 162 Method theMethod, 163 Class<? extends Collection<?>> theOuterCollectionType, 164 Class<? extends Collection<?>> theInnerCollectionType, 165 Class<?> theParameterType) { 166 FhirContext context = getContext(); 167 validateTypeIsAppropriateVersionForContext(theMethod, theParameterType, context, "parameter"); 168 169 myParameterType = theParameterType; 170 if (theInnerCollectionType != null) { 171 myInnerCollectionType = CollectionBinder.getInstantiableCollectionType(theInnerCollectionType, myName); 172 if (myMax == OperationParam.MAX_DEFAULT) { 173 myMax = OperationParam.MAX_UNLIMITED; 174 } 175 } else if (IQueryParameterAnd.class.isAssignableFrom(myParameterType)) { 176 if (myMax == OperationParam.MAX_DEFAULT) { 177 myMax = OperationParam.MAX_UNLIMITED; 178 } 179 } else { 180 if (myMax == OperationParam.MAX_DEFAULT) { 181 myMax = 1; 182 } 183 } 184 185 boolean typeIsConcrete = !myParameterType.isInterface() && !Modifier.isAbstract(myParameterType.getModifiers()); 186 187 boolean isSearchParam = IQueryParameterType.class.isAssignableFrom(myParameterType) 188 || IQueryParameterOr.class.isAssignableFrom(myParameterType) 189 || IQueryParameterAnd.class.isAssignableFrom(myParameterType); 190 191 /* 192 * Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also 193 * extend this interface. I'm not sure if they should in the end.. but they do, so we 194 * exclude them. 195 */ 196 isSearchParam &= typeIsConcrete && !IBase.class.isAssignableFrom(myParameterType); 197 198 myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) 199 || String.class.equals(myParameterType) 200 || isSearchParam 201 || ValidationModeEnum.class.equals(myParameterType); 202 203 /* 204 * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We 205 * should probably clean this up.. 206 */ 207 if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) { 208 if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) { 209 myParamType = "Resource"; 210 } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { 211 myParamType = "Reference"; 212 myAllowGet = true; 213 } else if (IBaseCoding.class.isAssignableFrom(myParameterType)) { 214 myParamType = "Coding"; 215 myAllowGet = true; 216 } else if (DateRangeParam.class.isAssignableFrom(myParameterType)) { 217 myParamType = "date"; 218 myMax = 2; 219 myAllowGet = true; 220 } else if (myParameterType.equals(ValidationModeEnum.class)) { 221 myParamType = "code"; 222 } else if (IBase.class.isAssignableFrom(myParameterType) && typeIsConcrete) { 223 myParamType = myContext 224 .getElementDefinition((Class<? extends IBase>) myParameterType) 225 .getName(); 226 } else if (isSearchParam) { 227 myParamType = "string"; 228 mySearchParameterBinding = new SearchParameter(myName, myMin > 0); 229 mySearchParameterBinding.setCompositeTypes(COMPOSITE_TYPES); 230 mySearchParameterBinding.setType( 231 myContext, theParameterType, theInnerCollectionType, theOuterCollectionType); 232 myConverter = new OperationParamConverter(); 233 } else { 234 throw new ConfigurationException(Msg.code(361) + "Invalid type for @OperationParam on method " 235 + theMethod + ": " + myParameterType.getName()); 236 } 237 } 238 } 239 240 public static void validateTypeIsAppropriateVersionForContext( 241 Method theMethod, Class<?> theParameterType, FhirContext theContext, String theUseDescription) { 242 if (theParameterType != null) { 243 if (theParameterType.isInterface()) { 244 // TODO: we could probably be a bit more nuanced here but things like 245 // IBaseResource are often used and they aren't version specific 246 return; 247 } 248 249 FhirVersionEnum elementVersion = FhirVersionEnum.determineVersionForType(theParameterType); 250 if (elementVersion != null) { 251 if (elementVersion != theContext.getVersion().getVersion()) { 252 throw new ConfigurationException(Msg.code(360) + "Incorrect use of type " 253 + theParameterType.getSimpleName() + " as " + theUseDescription 254 + " type for method when theContext is for version " 255 + theContext.getVersion().getVersion().name() + " in method: " + theMethod.toString()); 256 } 257 } 258 } 259 } 260 261 public OperationParameter setConverter(IOperationParamConverter theConverter) { 262 myConverter = theConverter; 263 return this; 264 } 265 266 private void throwWrongParamType(Object nextValue) { 267 throw new InvalidRequestException(Msg.code(362) + "Request has parameter " + myName + " of type " 268 + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName()); 269 } 270 271 @SuppressWarnings("unchecked") 272 @Override 273 public Object translateQueryParametersIntoServerArgument( 274 RequestDetails theRequest, BaseMethodBinding theMethodBinding) 275 throws InternalErrorException, InvalidRequestException { 276 List<Object> matchingParamValues = new ArrayList<>(); 277 278 OperationMethodBinding method = (OperationMethodBinding) theMethodBinding; 279 280 // If the request body is a Parameters resource, check if it has any 281 // values for us 282 if (theRequest.getRequestType() != RequestTypeEnum.GET 283 && !method.isManualRequestMode() 284 && !method.isDeleteEnabled()) { 285 translateQueryParametersIntoServerArgumentForPost(theRequest, matchingParamValues); 286 } 287 288 // We always look at the URL to see if any matching parameters were provided there 289 translateQueryParametersIntoServerArgumentForGet(theRequest, matchingParamValues); 290 291 if (matchingParamValues.isEmpty()) { 292 return null; 293 } 294 295 if (myInnerCollectionType == null) { 296 return matchingParamValues.get(0); 297 } 298 299 Collection<Object> retVal = ReflectionUtil.newInstance(myInnerCollectionType); 300 retVal.addAll(matchingParamValues); 301 return retVal; 302 } 303 304 private void translateQueryParametersIntoServerArgumentForGet( 305 RequestDetails theRequest, List<Object> matchingParamValues) { 306 if (mySearchParameterBinding != null) { 307 308 List<QualifiedParamList> params = new ArrayList<QualifiedParamList>(); 309 String nameWithQualifierColon = myName + ":"; 310 311 for (String nextParamName : theRequest.getParameters().keySet()) { 312 String qualifier; 313 if (nextParamName.equals(myName)) { 314 qualifier = null; 315 } else if (nextParamName.startsWith(nameWithQualifierColon)) { 316 qualifier = nextParamName.substring(nextParamName.indexOf(':')); 317 } else { 318 // This is some other parameter, not the one bound by this instance 319 continue; 320 } 321 String[] values = theRequest.getParameters().get(nextParamName); 322 if (values != null) { 323 for (String nextValue : values) { 324 params.add(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, nextValue)); 325 } 326 } 327 } 328 if (!params.isEmpty()) { 329 for (QualifiedParamList next : params) { 330 Object values = mySearchParameterBinding.parse(myContext, Collections.singletonList(next)); 331 addValueToList(matchingParamValues, values); 332 } 333 } 334 335 } else { 336 String[] paramValues = theRequest.getParameters().get(myName); 337 if (paramValues != null && paramValues.length > 0) { 338 if (myAllowGet) { 339 340 if (DateRangeParam.class.isAssignableFrom(myParameterType)) { 341 List<QualifiedParamList> parameters = new ArrayList<>(); 342 parameters.add(QualifiedParamList.singleton(paramValues[0])); 343 if (paramValues.length > 1) { 344 parameters.add(QualifiedParamList.singleton(paramValues[1])); 345 } 346 DateRangeParam dateRangeParam = new DateRangeParam(); 347 FhirContext ctx = theRequest.getServer().getFhirContext(); 348 dateRangeParam.setValuesAsQueryTokens(ctx, myName, parameters); 349 matchingParamValues.add(dateRangeParam); 350 351 } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { 352 353 processAllCommaSeparatedValues(paramValues, t -> { 354 IBaseReference param = (IBaseReference) ReflectionUtil.newInstance(myParameterType); 355 param.setReference(t); 356 matchingParamValues.add(param); 357 }); 358 359 } else if (IBaseCoding.class.isAssignableFrom(myParameterType)) { 360 361 processAllCommaSeparatedValues(paramValues, t -> { 362 TokenParam tokenParam = new TokenParam(); 363 tokenParam.setValueAsQueryToken(myContext, myName, null, t); 364 365 IBaseCoding param = (IBaseCoding) ReflectionUtil.newInstance(myParameterType); 366 param.setSystem(tokenParam.getSystem()); 367 param.setCode(tokenParam.getValue()); 368 matchingParamValues.add(param); 369 }); 370 371 } else if (String.class.isAssignableFrom(myParameterType)) { 372 373 matchingParamValues.addAll(Arrays.asList(paramValues)); 374 375 } else if (ValidationModeEnum.class.equals(myParameterType)) { 376 377 if (isNotBlank(paramValues[0])) { 378 ValidationModeEnum validationMode = ValidationModeEnum.forCode(paramValues[0]); 379 if (validationMode != null) { 380 matchingParamValues.add(validationMode); 381 } else { 382 throwInvalidMode(paramValues[0]); 383 } 384 } 385 386 } else { 387 for (String nextValue : paramValues) { 388 FhirContext ctx = theRequest.getServer().getFhirContext(); 389 RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) 390 ctx.getElementDefinition(myParameterType.asSubclass(IBase.class)); 391 IPrimitiveType<?> instance = def.newInstance(); 392 instance.setValueAsString(nextValue); 393 matchingParamValues.add(instance); 394 } 395 } 396 } else { 397 HapiLocalizer localizer = 398 theRequest.getServer().getFhirContext().getLocalizer(); 399 String msg = localizer.getMessage( 400 OperationParameter.class, "urlParamNotPrimitive", myOperationName, myName); 401 throw new MethodNotAllowedException(Msg.code(363) + msg, RequestTypeEnum.POST); 402 } 403 } 404 } 405 } 406 407 /** 408 * This method is here to mediate between the POST form of operation parameters (i.e. elements within a <code>Parameters</code> 409 * resource) and the GET form (i.e. URL parameters). 410 * <p> 411 * Essentially we want to allow comma-separated values as is done with searches on URLs. 412 * </p> 413 */ 414 private void processAllCommaSeparatedValues(String[] theParamValues, Consumer<String> theHandler) { 415 for (String nextValue : theParamValues) { 416 QualifiedParamList qualifiedParamList = 417 QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextValue); 418 for (String nextSplitValue : qualifiedParamList) { 419 theHandler.accept(nextSplitValue); 420 } 421 } 422 } 423 424 private void translateQueryParametersIntoServerArgumentForPost( 425 RequestDetails theRequest, List<Object> matchingParamValues) { 426 IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY); 427 if (requestContents != null) { 428 RuntimeResourceDefinition def = myContext.getResourceDefinition(requestContents); 429 if (def.getName().equals("Parameters")) { 430 431 BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter"); 432 BaseRuntimeElementCompositeDefinition<?> paramChildElem = 433 (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter"); 434 435 RuntimeChildPrimitiveDatatypeDefinition nameChild = 436 (RuntimeChildPrimitiveDatatypeDefinition) paramChildElem.getChildByName("name"); 437 BaseRuntimeChildDefinition valueChild = paramChildElem.getChildByName("value[x]"); 438 BaseRuntimeChildDefinition resourceChild = paramChildElem.getChildByName("resource"); 439 440 IAccessor paramChildAccessor = paramChild.getAccessor(); 441 List<IBase> values = paramChildAccessor.getValues(requestContents); 442 for (IBase nextParameter : values) { 443 List<IBase> nextNames = nameChild.getAccessor().getValues(nextParameter); 444 if (nextNames != null && nextNames.size() > 0) { 445 IPrimitiveType<?> nextName = (IPrimitiveType<?>) nextNames.get(0); 446 if (myName.equals(nextName.getValueAsString())) { 447 448 if (myParameterType.isAssignableFrom(nextParameter.getClass())) { 449 matchingParamValues.add(nextParameter); 450 } else { 451 List<IBase> paramValues = 452 valueChild.getAccessor().getValues(nextParameter); 453 List<IBase> paramResources = 454 resourceChild.getAccessor().getValues(nextParameter); 455 if (paramValues != null && paramValues.size() > 0) { 456 tryToAddValues(paramValues, matchingParamValues); 457 } else if (paramResources != null && paramResources.size() > 0) { 458 tryToAddValues(paramResources, matchingParamValues); 459 } 460 } 461 } 462 } 463 } 464 465 } else { 466 467 if (myParameterType.isAssignableFrom(requestContents.getClass())) { 468 tryToAddValues(Arrays.asList(requestContents), matchingParamValues); 469 } 470 } 471 } 472 } 473 474 @SuppressWarnings("unchecked") 475 private void tryToAddValues(List<IBase> theParamValues, List<Object> theMatchingParamValues) { 476 for (Object nextValue : theParamValues) { 477 if (nextValue == null) { 478 continue; 479 } 480 if (myConverter != null) { 481 nextValue = myConverter.incomingServer(nextValue); 482 } 483 if (myParameterType.equals(String.class)) { 484 if (nextValue instanceof IPrimitiveType<?>) { 485 IPrimitiveType<?> source = (IPrimitiveType<?>) nextValue; 486 theMatchingParamValues.add(source.getValueAsString()); 487 continue; 488 } 489 } 490 if (!myParameterType.isAssignableFrom(nextValue.getClass())) { 491 Class<? extends IBaseDatatype> sourceType = (Class<? extends IBaseDatatype>) nextValue.getClass(); 492 Class<? extends IBaseDatatype> targetType = (Class<? extends IBaseDatatype>) myParameterType; 493 BaseRuntimeElementDefinition<?> sourceTypeDef = myContext.getElementDefinition(sourceType); 494 BaseRuntimeElementDefinition<?> targetTypeDef = myContext.getElementDefinition(targetType); 495 if (targetTypeDef instanceof IRuntimeDatatypeDefinition 496 && sourceTypeDef instanceof IRuntimeDatatypeDefinition) { 497 IRuntimeDatatypeDefinition targetTypeDtDef = (IRuntimeDatatypeDefinition) targetTypeDef; 498 if (targetTypeDtDef.isProfileOf(sourceType)) { 499 FhirTerser terser = myContext.newTerser(); 500 IBase newTarget = targetTypeDef.newInstance(); 501 terser.cloneInto((IBase) nextValue, newTarget, true); 502 theMatchingParamValues.add(newTarget); 503 continue; 504 } 505 } 506 throwWrongParamType(nextValue); 507 } 508 509 addValueToList(theMatchingParamValues, nextValue); 510 } 511 } 512 513 public String getDescription() { 514 return myDescription; 515 } 516 517 public List<String> getExampleValues() { 518 return myExampleValues; 519 } 520 521 interface IOperationParamConverter { 522 523 Object incomingServer(Object theObject); 524 525 Object outgoingClient(Object theObject); 526 } 527 528 class OperationParamConverter implements IOperationParamConverter { 529 530 public OperationParamConverter() { 531 Validate.isTrue(mySearchParameterBinding != null); 532 } 533 534 @Override 535 public Object incomingServer(Object theObject) { 536 IPrimitiveType<?> obj = (IPrimitiveType<?>) theObject; 537 List<QualifiedParamList> paramList = Collections.singletonList( 538 QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, obj.getValueAsString())); 539 return mySearchParameterBinding.parse(myContext, paramList); 540 } 541 542 @Override 543 public Object outgoingClient(Object theObject) { 544 IQueryParameterType obj = (IQueryParameterType) theObject; 545 IPrimitiveType<?> retVal = 546 (IPrimitiveType<?>) myContext.getElementDefinition("string").newInstance(); 547 retVal.setValueAsString(obj.getValueAsQueryToken(myContext)); 548 return retVal; 549 } 550 } 551 552 public static void throwInvalidMode(String paramValues) { 553 throw new InvalidRequestException(Msg.code(364) + "Invalid mode value: \"" + paramValues + "\""); 554 } 555}