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