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