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.binder.CollectionBinder; 018import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 019import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 020import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 021import ca.uhn.fhir.util.FhirTerser; 022import ca.uhn.fhir.util.ReflectionUtil; 023import org.apache.commons.lang3.Validate; 024import org.hl7.fhir.instance.model.api.IBase; 025import org.hl7.fhir.instance.model.api.IBaseDatatype; 026import org.hl7.fhir.instance.model.api.IBaseResource; 027import org.hl7.fhir.instance.model.api.IPrimitiveType; 028 029import java.lang.reflect.Method; 030import java.lang.reflect.Modifier; 031import java.util.*; 032 033import static org.apache.commons.lang3.StringUtils.isNotBlank; 034 035/* 036 * #%L 037 * HAPI FHIR - Server Framework 038 * %% 039 * Copyright (C) 2014 - 2019 University Health Network 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 073 public OperationParameter(FhirContext theCtx, String theOperationName, OperationParam theOperationParam) { 074 this(theCtx, theOperationName, theOperationParam.name(), theOperationParam.min(), theOperationParam.max()); 075 } 076 077 OperationParameter(FhirContext theCtx, String theOperationName, String theParameterName, int theMin, int theMax) { 078 myOperationName = theOperationName; 079 myName = theParameterName; 080 myMin = theMin; 081 myMax = theMax; 082 myContext = theCtx; 083 } 084 085 @SuppressWarnings({"rawtypes", "unchecked"}) 086 private void addValueToList(List<Object> matchingParamValues, Object values) { 087 if (values != null) { 088 if (BaseAndListParam.class.isAssignableFrom(myParameterType) && matchingParamValues.size() > 0) { 089 BaseAndListParam existing = (BaseAndListParam<?>) matchingParamValues.get(0); 090 BaseAndListParam<?> newAndList = (BaseAndListParam<?>) values; 091 for (IQueryParameterOr nextAnd : newAndList.getValuesAsQueryTokens()) { 092 existing.addAnd(nextAnd); 093 } 094 } else { 095 matchingParamValues.add(values); 096 } 097 } 098 } 099 100 protected FhirContext getContext() { 101 return myContext; 102 } 103 104 public int getMax() { 105 return myMax; 106 } 107 108 public int getMin() { 109 return myMin; 110 } 111 112 public String getName() { 113 return myName; 114 } 115 116 public String getParamType() { 117 return myParamType; 118 } 119 120 public String getSearchParamType() { 121 if (mySearchParameterBinding != null) { 122 return mySearchParameterBinding.getParamType().getCode(); 123 } 124 return null; 125 } 126 127 @SuppressWarnings("unchecked") 128 @Override 129 public void initializeTypes(Method theMethod, Class<? extends Collection<?>> theOuterCollectionType, Class<? extends Collection<?>> theInnerCollectionType, Class<?> theParameterType) { 130 if (getContext().getVersion().getVersion().isRi()) { 131 if (IDatatype.class.isAssignableFrom(theParameterType)) { 132 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()); 133 } 134 } 135 136 myParameterType = theParameterType; 137 if (theInnerCollectionType != null) { 138 myInnerCollectionType = CollectionBinder.getInstantiableCollectionType(theInnerCollectionType, myName); 139 if (myMax == OperationParam.MAX_DEFAULT) { 140 myMax = OperationParam.MAX_UNLIMITED; 141 } 142 } else if (IQueryParameterAnd.class.isAssignableFrom(myParameterType)) { 143 if (myMax == OperationParam.MAX_DEFAULT) { 144 myMax = OperationParam.MAX_UNLIMITED; 145 } 146 } else { 147 if (myMax == OperationParam.MAX_DEFAULT) { 148 myMax = 1; 149 } 150 } 151 152 boolean typeIsConcrete = !myParameterType.isInterface() && !Modifier.isAbstract(myParameterType.getModifiers()); 153 154 //@formatter:off 155 boolean isSearchParam = 156 IQueryParameterType.class.isAssignableFrom(myParameterType) || 157 IQueryParameterOr.class.isAssignableFrom(myParameterType) || 158 IQueryParameterAnd.class.isAssignableFrom(myParameterType); 159 //@formatter:off 160 161 /* 162 * Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also 163 * extend this interface. I'm not sure if they should in the end.. but they do, so we 164 * exclude them. 165 */ 166 isSearchParam &= typeIsConcrete && !IBase.class.isAssignableFrom(myParameterType); 167 168 myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) || String.class.equals(myParameterType) || isSearchParam || 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 (DateRangeParam.class.isAssignableFrom(myParameterType)) { 178 myParamType = "date"; 179 myMax = 2; 180 myAllowGet = true; 181 } else if (myParameterType.equals(ValidationModeEnum.class)) { 182 myParamType = "code"; 183 } else if (IBase.class.isAssignableFrom(myParameterType) && typeIsConcrete) { 184 myParamType = myContext.getElementDefinition((Class<? extends IBase>) myParameterType).getName(); 185 } else if (isSearchParam) { 186 myParamType = "string"; 187 mySearchParameterBinding = new SearchParameter(myName, myMin > 0); 188 mySearchParameterBinding.setCompositeTypes(COMPOSITE_TYPES); 189 mySearchParameterBinding.setType(myContext, theParameterType, theInnerCollectionType, theOuterCollectionType); 190 myConverter = new OperationParamConverter(); 191 } else { 192 throw new ConfigurationException("Invalid type for @OperationParam: " + myParameterType.getName()); 193 } 194 195 } 196 197 } 198 199 public OperationParameter setConverter(IOperationParamConverter theConverter) { 200 myConverter = theConverter; 201 return this; 202 } 203 204 private void throwWrongParamType(Object nextValue) { 205 throw new InvalidRequestException("Request has parameter " + myName + " of type " + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName()); 206 } 207 208 @SuppressWarnings("unchecked") 209 @Override 210 public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding) throws InternalErrorException, InvalidRequestException { 211 List<Object> matchingParamValues = new ArrayList<Object>(); 212 213 if (theRequest.getRequestType() == RequestTypeEnum.GET) { 214 translateQueryParametersIntoServerArgumentForGet(theRequest, matchingParamValues); 215 } else { 216 translateQueryParametersIntoServerArgumentForPost(theRequest, matchingParamValues); 217 } 218 219 if (matchingParamValues.isEmpty()) { 220 return null; 221 } 222 223 if (myInnerCollectionType == null) { 224 return matchingParamValues.get(0); 225 } 226 227 Collection<Object> retVal = ReflectionUtil.newInstance(myInnerCollectionType); 228 retVal.addAll(matchingParamValues); 229 return retVal; 230 } 231 232 private void translateQueryParametersIntoServerArgumentForGet(RequestDetails theRequest, List<Object> matchingParamValues) { 233 if (mySearchParameterBinding != null) { 234 235 List<QualifiedParamList> params = new ArrayList<QualifiedParamList>(); 236 String nameWithQualifierColon = myName + ":"; 237 238 for (String nextParamName : theRequest.getParameters().keySet()) { 239 String qualifier; 240 if (nextParamName.equals(myName)) { 241 qualifier = null; 242 } else if (nextParamName.startsWith(nameWithQualifierColon)) { 243 qualifier = nextParamName.substring(nextParamName.indexOf(':')); 244 } else { 245 // This is some other parameter, not the one bound by this instance 246 continue; 247 } 248 String[] values = theRequest.getParameters().get(nextParamName); 249 if (values != null) { 250 for (String nextValue : values) { 251 params.add(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, nextValue)); 252 } 253 } 254 } 255 if (!params.isEmpty()) { 256 for (QualifiedParamList next : params) { 257 Object values = mySearchParameterBinding.parse(myContext, Collections.singletonList(next)); 258 addValueToList(matchingParamValues, values); 259 } 260 261 } 262 263 } else { 264 String[] paramValues = theRequest.getParameters().get(myName); 265 if (paramValues != null && paramValues.length > 0) { 266 if (myAllowGet) { 267 268 if (DateRangeParam.class.isAssignableFrom(myParameterType)) { 269 List<QualifiedParamList> parameters = new ArrayList<QualifiedParamList>(); 270 parameters.add(QualifiedParamList.singleton(paramValues[0])); 271 if (paramValues.length > 1) { 272 parameters.add(QualifiedParamList.singleton(paramValues[1])); 273 } 274 DateRangeParam dateRangeParam = new DateRangeParam(); 275 FhirContext ctx = theRequest.getServer().getFhirContext(); 276 dateRangeParam.setValuesAsQueryTokens(ctx, myName, parameters); 277 matchingParamValues.add(dateRangeParam); 278 } else if (String.class.isAssignableFrom(myParameterType)) { 279 280 for (String next : paramValues) { 281 matchingParamValues.add(next); 282 } 283 } else if (ValidationModeEnum.class.equals(myParameterType)) { 284 285 if (isNotBlank(paramValues[0])) { 286 ValidationModeEnum validationMode = ValidationModeEnum.forCode(paramValues[0]); 287 if (validationMode != null) { 288 matchingParamValues.add(validationMode); 289 } else { 290 throwInvalidMode(paramValues[0]); 291 } 292 } 293 294 } else { 295 for (String nextValue : paramValues) { 296 FhirContext ctx = theRequest.getServer().getFhirContext(); 297 RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) ctx.getElementDefinition(myParameterType.asSubclass(IBase.class)); 298 IPrimitiveType<?> instance = def.newInstance(); 299 instance.setValueAsString(nextValue); 300 matchingParamValues.add(instance); 301 } 302 } 303 } else { 304 HapiLocalizer localizer = theRequest.getServer().getFhirContext().getLocalizer(); 305 String msg = localizer.getMessage(OperationParameter.class, "urlParamNotPrimitive", myOperationName, myName); 306 throw new MethodNotAllowedException(msg, RequestTypeEnum.POST); 307 } 308 } 309 } 310 } 311 312 private void translateQueryParametersIntoServerArgumentForPost(RequestDetails theRequest, List<Object> matchingParamValues) { 313 IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY); 314 RuntimeResourceDefinition def = myContext.getResourceDefinition(requestContents); 315 if (def.getName().equals("Parameters")) { 316 317 BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter"); 318 BaseRuntimeElementCompositeDefinition<?> paramChildElem = (BaseRuntimeElementCompositeDefinition<?>) paramChild.getChildByName("parameter"); 319 320 RuntimeChildPrimitiveDatatypeDefinition nameChild = (RuntimeChildPrimitiveDatatypeDefinition) paramChildElem.getChildByName("name"); 321 BaseRuntimeChildDefinition valueChild = paramChildElem.getChildByName("value[x]"); 322 BaseRuntimeChildDefinition resourceChild = paramChildElem.getChildByName("resource"); 323 324 IAccessor paramChildAccessor = paramChild.getAccessor(); 325 List<IBase> values = paramChildAccessor.getValues(requestContents); 326 for (IBase nextParameter : values) { 327 List<IBase> nextNames = nameChild.getAccessor().getValues(nextParameter); 328 if (nextNames != null && nextNames.size() > 0) { 329 IPrimitiveType<?> nextName = (IPrimitiveType<?>) nextNames.get(0); 330 if (myName.equals(nextName.getValueAsString())) { 331 332 if (myParameterType.isAssignableFrom(nextParameter.getClass())) { 333 matchingParamValues.add(nextParameter); 334 } else { 335 List<IBase> paramValues = valueChild.getAccessor().getValues(nextParameter); 336 List<IBase> paramResources = resourceChild.getAccessor().getValues(nextParameter); 337 if (paramValues != null && paramValues.size() > 0) { 338 tryToAddValues(paramValues, matchingParamValues); 339 } else if (paramResources != null && paramResources.size() > 0) { 340 tryToAddValues(paramResources, matchingParamValues); 341 } 342 } 343 344 } 345 } 346 } 347 348 } else { 349 350 if (myParameterType.isAssignableFrom(requestContents.getClass())) { 351 tryToAddValues(Arrays.asList((IBase) requestContents), matchingParamValues); 352 } 353 354 } 355 } 356 357 @SuppressWarnings("unchecked") 358 private void tryToAddValues(List<IBase> theParamValues, List<Object> theMatchingParamValues) { 359 for (Object nextValue : theParamValues) { 360 if (nextValue == null) { 361 continue; 362 } 363 if (myConverter != null) { 364 nextValue = myConverter.incomingServer(nextValue); 365 } 366 if (myParameterType.equals(String.class)) { 367 if (nextValue instanceof IPrimitiveType<?>) { 368 IPrimitiveType<?> source = (IPrimitiveType<?>) nextValue; 369 theMatchingParamValues.add(source.getValueAsString()); 370 continue; 371 } 372 } 373 if (!myParameterType.isAssignableFrom(nextValue.getClass())) { 374 Class<? extends IBaseDatatype> sourceType = (Class<? extends IBaseDatatype>) nextValue.getClass(); 375 Class<? extends IBaseDatatype> targetType = (Class<? extends IBaseDatatype>) myParameterType; 376 BaseRuntimeElementDefinition<?> sourceTypeDef = myContext.getElementDefinition(sourceType); 377 BaseRuntimeElementDefinition<?> targetTypeDef = myContext.getElementDefinition(targetType); 378 if (targetTypeDef instanceof IRuntimeDatatypeDefinition && sourceTypeDef instanceof IRuntimeDatatypeDefinition) { 379 IRuntimeDatatypeDefinition targetTypeDtDef = (IRuntimeDatatypeDefinition) targetTypeDef; 380 if (targetTypeDtDef.isProfileOf(sourceType)) { 381 FhirTerser terser = myContext.newTerser(); 382 IBase newTarget = targetTypeDef.newInstance(); 383 terser.cloneInto((IBase) nextValue, newTarget, true); 384 theMatchingParamValues.add(newTarget); 385 continue; 386 } 387 } 388 throwWrongParamType(nextValue); 389 } 390 391 addValueToList(theMatchingParamValues, nextValue); 392 } 393 } 394 395 public static void throwInvalidMode(String paramValues) { 396 throw new InvalidRequestException("Invalid mode value: \"" + paramValues + "\""); 397 } 398 399 interface IOperationParamConverter { 400 401 Object incomingServer(Object theObject); 402 403 Object outgoingClient(Object theObject); 404 405 } 406 407 class OperationParamConverter implements IOperationParamConverter { 408 409 public OperationParamConverter() { 410 Validate.isTrue(mySearchParameterBinding != null); 411 } 412 413 @Override 414 public Object incomingServer(Object theObject) { 415 IPrimitiveType<?> obj = (IPrimitiveType<?>) theObject; 416 List<QualifiedParamList> paramList = Collections.singletonList(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, obj.getValueAsString())); 417 return mySearchParameterBinding.parse(myContext, paramList); 418 } 419 420 @Override 421 public Object outgoingClient(Object theObject) { 422 IQueryParameterType obj = (IQueryParameterType) theObject; 423 IPrimitiveType<?> retVal = (IPrimitiveType<?>) myContext.getElementDefinition("string").newInstance(); 424 retVal.setValueAsString(obj.getValueAsQueryToken(myContext)); 425 return retVal; 426 } 427 428 } 429 430 431}