001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2024 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 (theRequest.getRequestType() == RequestTypeEnum.GET 281 || method.isManualRequestMode() 282 || method.isDeleteEnabled()) { 283 translateQueryParametersIntoServerArgumentForGet(theRequest, matchingParamValues); 284 } else { 285 translateQueryParametersIntoServerArgumentForPost(theRequest, matchingParamValues); 286 } 287 288 if (matchingParamValues.isEmpty()) { 289 return null; 290 } 291 292 if (myInnerCollectionType == null) { 293 return matchingParamValues.get(0); 294 } 295 296 Collection<Object> retVal = ReflectionUtil.newInstance(myInnerCollectionType); 297 retVal.addAll(matchingParamValues); 298 return retVal; 299 } 300 301 private void translateQueryParametersIntoServerArgumentForGet( 302 RequestDetails theRequest, List<Object> matchingParamValues) { 303 if (mySearchParameterBinding != null) { 304 305 List<QualifiedParamList> params = new ArrayList<QualifiedParamList>(); 306 String nameWithQualifierColon = myName + ":"; 307 308 for (String nextParamName : theRequest.getParameters().keySet()) { 309 String qualifier; 310 if (nextParamName.equals(myName)) { 311 qualifier = null; 312 } else if (nextParamName.startsWith(nameWithQualifierColon)) { 313 qualifier = nextParamName.substring(nextParamName.indexOf(':')); 314 } else { 315 // This is some other parameter, not the one bound by this instance 316 continue; 317 } 318 String[] values = theRequest.getParameters().get(nextParamName); 319 if (values != null) { 320 for (String nextValue : values) { 321 params.add(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, nextValue)); 322 } 323 } 324 } 325 if (!params.isEmpty()) { 326 for (QualifiedParamList next : params) { 327 Object values = mySearchParameterBinding.parse(myContext, Collections.singletonList(next)); 328 addValueToList(matchingParamValues, values); 329 } 330 } 331 332 } else { 333 String[] paramValues = theRequest.getParameters().get(myName); 334 if (paramValues != null && paramValues.length > 0) { 335 if (myAllowGet) { 336 337 if (DateRangeParam.class.isAssignableFrom(myParameterType)) { 338 List<QualifiedParamList> parameters = new ArrayList<>(); 339 parameters.add(QualifiedParamList.singleton(paramValues[0])); 340 if (paramValues.length > 1) { 341 parameters.add(QualifiedParamList.singleton(paramValues[1])); 342 } 343 DateRangeParam dateRangeParam = new DateRangeParam(); 344 FhirContext ctx = theRequest.getServer().getFhirContext(); 345 dateRangeParam.setValuesAsQueryTokens(ctx, myName, parameters); 346 matchingParamValues.add(dateRangeParam); 347 348 } else if (IBaseReference.class.isAssignableFrom(myParameterType)) { 349 350 processAllCommaSeparatedValues(paramValues, t -> { 351 IBaseReference param = (IBaseReference) ReflectionUtil.newInstance(myParameterType); 352 param.setReference(t); 353 matchingParamValues.add(param); 354 }); 355 356 } else if (IBaseCoding.class.isAssignableFrom(myParameterType)) { 357 358 processAllCommaSeparatedValues(paramValues, t -> { 359 TokenParam tokenParam = new TokenParam(); 360 tokenParam.setValueAsQueryToken(myContext, myName, null, t); 361 362 IBaseCoding param = (IBaseCoding) ReflectionUtil.newInstance(myParameterType); 363 param.setSystem(tokenParam.getSystem()); 364 param.setCode(tokenParam.getValue()); 365 matchingParamValues.add(param); 366 }); 367 368 } else if (String.class.isAssignableFrom(myParameterType)) { 369 370 matchingParamValues.addAll(Arrays.asList(paramValues)); 371 372 } else if (ValidationModeEnum.class.equals(myParameterType)) { 373 374 if (isNotBlank(paramValues[0])) { 375 ValidationModeEnum validationMode = ValidationModeEnum.forCode(paramValues[0]); 376 if (validationMode != null) { 377 matchingParamValues.add(validationMode); 378 } else { 379 throwInvalidMode(paramValues[0]); 380 } 381 } 382 383 } else { 384 for (String nextValue : paramValues) { 385 FhirContext ctx = theRequest.getServer().getFhirContext(); 386 RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) 387 ctx.getElementDefinition(myParameterType.asSubclass(IBase.class)); 388 IPrimitiveType<?> instance = def.newInstance(); 389 instance.setValueAsString(nextValue); 390 matchingParamValues.add(instance); 391 } 392 } 393 } else { 394 HapiLocalizer localizer = 395 theRequest.getServer().getFhirContext().getLocalizer(); 396 String msg = localizer.getMessage( 397 OperationParameter.class, "urlParamNotPrimitive", myOperationName, myName); 398 throw new MethodNotAllowedException(Msg.code(363) + msg, RequestTypeEnum.POST); 399 } 400 } 401 } 402 } 403 404 /** 405 * This method is here to mediate between the POST form of operation parameters (i.e. elements within a <code>Parameters</code> 406 * resource) and the GET form (i.e. URL parameters). 407 * <p> 408 * Essentially we want to allow comma-separated values as is done with searches on URLs. 409 * </p> 410 */ 411 private void processAllCommaSeparatedValues(String[] theParamValues, Consumer<String> theHandler) { 412 for (String nextValue : theParamValues) { 413 QualifiedParamList qualifiedParamList = 414 QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextValue); 415 for (String nextSplitValue : qualifiedParamList) { 416 theHandler.accept(nextSplitValue); 417 } 418 } 419 } 420 421 private void translateQueryParametersIntoServerArgumentForPost( 422 RequestDetails theRequest, List<Object> matchingParamValues) { 423 IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY); 424 if (requestContents != null) { 425 RuntimeResourceDefinition def = myContext.getResourceDefinition(requestContents); 426 if (def.getName().equals("Parameters")) { 427 428 BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter"); 429 BaseRuntimeElementCompositeDefinition<?> paramChildElem = 430 (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter"); 431 432 RuntimeChildPrimitiveDatatypeDefinition nameChild = 433 (RuntimeChildPrimitiveDatatypeDefinition) paramChildElem.getChildByName("name"); 434 BaseRuntimeChildDefinition valueChild = paramChildElem.getChildByName("value[x]"); 435 BaseRuntimeChildDefinition resourceChild = paramChildElem.getChildByName("resource"); 436 437 IAccessor paramChildAccessor = paramChild.getAccessor(); 438 List<IBase> values = paramChildAccessor.getValues(requestContents); 439 for (IBase nextParameter : values) { 440 List<IBase> nextNames = nameChild.getAccessor().getValues(nextParameter); 441 if (nextNames != null && nextNames.size() > 0) { 442 IPrimitiveType<?> nextName = (IPrimitiveType<?>) nextNames.get(0); 443 if (myName.equals(nextName.getValueAsString())) { 444 445 if (myParameterType.isAssignableFrom(nextParameter.getClass())) { 446 matchingParamValues.add(nextParameter); 447 } else { 448 List<IBase> paramValues = 449 valueChild.getAccessor().getValues(nextParameter); 450 List<IBase> paramResources = 451 resourceChild.getAccessor().getValues(nextParameter); 452 if (paramValues != null && paramValues.size() > 0) { 453 tryToAddValues(paramValues, matchingParamValues); 454 } else if (paramResources != null && paramResources.size() > 0) { 455 tryToAddValues(paramResources, matchingParamValues); 456 } 457 } 458 } 459 } 460 } 461 462 } else { 463 464 if (myParameterType.isAssignableFrom(requestContents.getClass())) { 465 tryToAddValues(Arrays.asList(requestContents), matchingParamValues); 466 } 467 } 468 } 469 } 470 471 @SuppressWarnings("unchecked") 472 private void tryToAddValues(List<IBase> theParamValues, List<Object> theMatchingParamValues) { 473 for (Object nextValue : theParamValues) { 474 if (nextValue == null) { 475 continue; 476 } 477 if (myConverter != null) { 478 nextValue = myConverter.incomingServer(nextValue); 479 } 480 if (myParameterType.equals(String.class)) { 481 if (nextValue instanceof IPrimitiveType<?>) { 482 IPrimitiveType<?> source = (IPrimitiveType<?>) nextValue; 483 theMatchingParamValues.add(source.getValueAsString()); 484 continue; 485 } 486 } 487 if (!myParameterType.isAssignableFrom(nextValue.getClass())) { 488 Class<? extends IBaseDatatype> sourceType = (Class<? extends IBaseDatatype>) nextValue.getClass(); 489 Class<? extends IBaseDatatype> targetType = (Class<? extends IBaseDatatype>) myParameterType; 490 BaseRuntimeElementDefinition<?> sourceTypeDef = myContext.getElementDefinition(sourceType); 491 BaseRuntimeElementDefinition<?> targetTypeDef = myContext.getElementDefinition(targetType); 492 if (targetTypeDef instanceof IRuntimeDatatypeDefinition 493 && sourceTypeDef instanceof IRuntimeDatatypeDefinition) { 494 IRuntimeDatatypeDefinition targetTypeDtDef = (IRuntimeDatatypeDefinition) targetTypeDef; 495 if (targetTypeDtDef.isProfileOf(sourceType)) { 496 FhirTerser terser = myContext.newTerser(); 497 IBase newTarget = targetTypeDef.newInstance(); 498 terser.cloneInto((IBase) nextValue, newTarget, true); 499 theMatchingParamValues.add(newTarget); 500 continue; 501 } 502 } 503 throwWrongParamType(nextValue); 504 } 505 506 addValueToList(theMatchingParamValues, nextValue); 507 } 508 } 509 510 public String getDescription() { 511 return myDescription; 512 } 513 514 public List<String> getExampleValues() { 515 return myExampleValues; 516 } 517 518 interface IOperationParamConverter { 519 520 Object incomingServer(Object theObject); 521 522 Object outgoingClient(Object theObject); 523 } 524 525 class OperationParamConverter implements IOperationParamConverter { 526 527 public OperationParamConverter() { 528 Validate.isTrue(mySearchParameterBinding != null); 529 } 530 531 @Override 532 public Object incomingServer(Object theObject) { 533 IPrimitiveType<?> obj = (IPrimitiveType<?>) theObject; 534 List<QualifiedParamList> paramList = Collections.singletonList( 535 QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, obj.getValueAsString())); 536 return mySearchParameterBinding.parse(myContext, paramList); 537 } 538 539 @Override 540 public Object outgoingClient(Object theObject) { 541 IQueryParameterType obj = (IQueryParameterType) theObject; 542 IPrimitiveType<?> retVal = 543 (IPrimitiveType<?>) myContext.getElementDefinition("string").newInstance(); 544 retVal.setValueAsString(obj.getValueAsQueryToken(myContext)); 545 return retVal; 546 } 547 } 548 549 public static void throwInvalidMode(String paramValues) { 550 throw new InvalidRequestException(Msg.code(364) + "Invalid mode value: \"" + paramValues + "\""); 551 } 552}