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