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