001package ca.uhn.fhir.rest.server.method; 002 003/*- 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2019 University Health Network 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 024import ca.uhn.fhir.context.ConfigurationException; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 027import ca.uhn.fhir.model.api.Include; 028import ca.uhn.fhir.model.api.TagList; 029import ca.uhn.fhir.model.api.annotation.Description; 030import ca.uhn.fhir.rest.annotation.*; 031import ca.uhn.fhir.rest.api.*; 032import ca.uhn.fhir.rest.api.server.RequestDetails; 033import ca.uhn.fhir.rest.param.binder.CollectionBinder; 034import ca.uhn.fhir.rest.server.method.OperationParameter.IOperationParamConverter; 035import ca.uhn.fhir.rest.server.method.ResourceParameter.Mode; 036import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 037import ca.uhn.fhir.util.ParametersUtil; 038import ca.uhn.fhir.util.ReflectionUtil; 039import ca.uhn.fhir.util.ValidateUtil; 040import org.hl7.fhir.instance.model.api.IBaseResource; 041import org.hl7.fhir.instance.model.api.IPrimitiveType; 042 043import javax.servlet.ServletRequest; 044import javax.servlet.ServletResponse; 045import java.lang.annotation.Annotation; 046import java.lang.reflect.Method; 047import java.util.ArrayList; 048import java.util.Collection; 049import java.util.Date; 050import java.util.List; 051 052import static org.apache.commons.lang3.StringUtils.isNotBlank; 053 054public class MethodUtil { 055 056 public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) { 057 for (Annotation annotation : theAnnotations) { 058 if (annotation instanceof Description) { 059 Description desc = (Description) annotation; 060 if (isNotBlank(desc.formalDefinition())) { 061 theParameter.setDescription(desc.formalDefinition()); 062 } else { 063 theParameter.setDescription(desc.shortDefinition()); 064 } 065 } 066 } 067 } 068 069 070 @SuppressWarnings("unchecked") 071 public static List<IParameter> getResourceParameters(final FhirContext theContext, Method theMethod, Object theProvider, RestOperationTypeEnum theRestfulOperationTypeEnum) { 072 List<IParameter> parameters = new ArrayList<>(); 073 074 Class<?>[] parameterTypes = theMethod.getParameterTypes(); 075 int paramIndex = 0; 076 for (Annotation[] annotations : theMethod.getParameterAnnotations()) { 077 078 IParameter param = null; 079 Class<?> declaredParameterType = parameterTypes[paramIndex]; 080 Class<?> parameterType = declaredParameterType; 081 Class<? extends java.util.Collection<?>> outerCollectionType = null; 082 Class<? extends java.util.Collection<?>> innerCollectionType = null; 083 if (TagList.class.isAssignableFrom(parameterType)) { 084 // TagList is handled directly within the method bindings 085 param = new NullParameter(); 086 } else { 087 if (Collection.class.isAssignableFrom(parameterType)) { 088 innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType; 089 parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); 090 declaredParameterType = parameterType; 091 } 092 if (Collection.class.isAssignableFrom(parameterType)) { 093 outerCollectionType = innerCollectionType; 094 innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType; 095 parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); 096 declaredParameterType = parameterType; 097 } 098 if (Collection.class.isAssignableFrom(parameterType)) { 099 throw new ConfigurationException("Argument #" + paramIndex + " of Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() 100 + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); 101 } 102 103 /* 104 * If the user is trying to bind IPrimitiveType they are probably 105 * trying to write code that is compatible across versions of FHIR. 106 * We'll try and come up with an appropriate subtype to give 107 * them. 108 * 109 * This gets tested in HistoryR4Test 110 */ 111 if (IPrimitiveType.class.equals(parameterType)) { 112 Class<?> genericType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); 113 if (Date.class.equals(genericType)) { 114 BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("dateTime"); 115 parameterType = dateTimeDef.getImplementingClass(); 116 } else if (String.class.equals(genericType) || genericType == null) { 117 BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("string"); 118 parameterType = dateTimeDef.getImplementingClass(); 119 } 120 } 121 } 122 123 if (ServletRequest.class.isAssignableFrom(parameterType)) { 124 param = new ServletRequestParameter(); 125 } else if (ServletResponse.class.isAssignableFrom(parameterType)) { 126 param = new ServletResponseParameter(); 127 } else if (parameterType.equals(RequestDetails.class) || parameterType.equals(ServletRequestDetails.class)) { 128 param = new RequestDetailsParameter(); 129 } else if (parameterType.equals(IInterceptorBroadcaster.class)) { 130 param = new InterceptorBroadcasterParameter(); 131 } else if (parameterType.equals(SummaryEnum.class)) { 132 param = new SummaryEnumParameter(); 133 } else if (parameterType.equals(PatchTypeEnum.class)) { 134 param = new PatchTypeParameter(); 135 } else if (parameterType.equals(SearchTotalModeEnum.class)) { 136 param = new SearchTotalModeParameter(); 137 } else { 138 for (int i = 0; i < annotations.length && param == null; i++) { 139 Annotation nextAnnotation = annotations[i]; 140 141 if (nextAnnotation instanceof RequiredParam) { 142 SearchParameter parameter = new SearchParameter(); 143 parameter.setName(((RequiredParam) nextAnnotation).name()); 144 parameter.setRequired(true); 145 parameter.setDeclaredTypes(((RequiredParam) nextAnnotation).targetTypes()); 146 parameter.setCompositeTypes(((RequiredParam) nextAnnotation).compositeTypes()); 147 parameter.setChainlists(((RequiredParam) nextAnnotation).chainWhitelist(), ((RequiredParam) nextAnnotation).chainBlacklist()); 148 parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); 149 MethodUtil.extractDescription(parameter, annotations); 150 param = parameter; 151 } else if (nextAnnotation instanceof OptionalParam) { 152 SearchParameter parameter = new SearchParameter(); 153 parameter.setName(((OptionalParam) nextAnnotation).name()); 154 parameter.setRequired(false); 155 parameter.setDeclaredTypes(((OptionalParam) nextAnnotation).targetTypes()); 156 parameter.setCompositeTypes(((OptionalParam) nextAnnotation).compositeTypes()); 157 parameter.setChainlists(((OptionalParam) nextAnnotation).chainWhitelist(), ((OptionalParam) nextAnnotation).chainBlacklist()); 158 parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); 159 MethodUtil.extractDescription(parameter, annotations); 160 param = parameter; 161 } else if (nextAnnotation instanceof RawParam) { 162 param = new RawParamsParmeter(parameters); 163 } else if (nextAnnotation instanceof IncludeParam) { 164 Class<? extends Collection<Include>> instantiableCollectionType; 165 Class<?> specType; 166 167 if (parameterType == String.class) { 168 instantiableCollectionType = null; 169 specType = String.class; 170 } else if ((parameterType != Include.class) || innerCollectionType == null || outerCollectionType != null) { 171 throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<" 172 + Include.class.getSimpleName() + ">"); 173 } else { 174 instantiableCollectionType = (Class<? extends Collection<Include>>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() + "'"); 175 specType = parameterType; 176 } 177 178 param = new IncludeParameter((IncludeParam) nextAnnotation, instantiableCollectionType, specType); 179 } else if (nextAnnotation instanceof ResourceParam) { 180 Mode mode; 181 if (IBaseResource.class.isAssignableFrom(parameterType)) { 182 mode = Mode.RESOURCE; 183 } else if (String.class.equals(parameterType)) { 184 mode = ResourceParameter.Mode.BODY; 185 } else if (byte[].class.equals(parameterType)) { 186 mode = ResourceParameter.Mode.BODY_BYTE_ARRAY; 187 } else if (EncodingEnum.class.equals(parameterType)) { 188 mode = Mode.ENCODING; 189 } else { 190 StringBuilder b = new StringBuilder(); 191 b.append("Method '"); 192 b.append(theMethod.getName()); 193 b.append("' is annotated with @"); 194 b.append(ResourceParam.class.getSimpleName()); 195 b.append(" but has a type that is not an implemtation of "); 196 b.append(IBaseResource.class.getCanonicalName()); 197 b.append(" or String or byte[]"); 198 throw new ConfigurationException(b.toString()); 199 } 200 boolean methodIsOperation = theMethod.getAnnotation(Operation.class) != null; 201 param = new ResourceParameter((Class<? extends IBaseResource>) parameterType, theProvider, mode, methodIsOperation); 202 } else if (nextAnnotation instanceof IdParam) { 203 param = new NullParameter(); 204 } else if (nextAnnotation instanceof ServerBase) { 205 param = new ServerBaseParamBinder(); 206 } else if (nextAnnotation instanceof Elements) { 207 param = new ElementsParameter(); 208 } else if (nextAnnotation instanceof Since) { 209 param = new SinceParameter(); 210 ((SinceParameter) param).setType(theContext, parameterType, innerCollectionType, outerCollectionType); 211 } else if (nextAnnotation instanceof At) { 212 param = new AtParameter(); 213 ((AtParameter) param).setType(theContext, parameterType, innerCollectionType, outerCollectionType); 214 } else if (nextAnnotation instanceof Count) { 215 param = new CountParameter(); 216 } else if (nextAnnotation instanceof GraphQLQuery) { 217 param = new GraphQLQueryParameter(); 218 } else if (nextAnnotation instanceof Sort) { 219 param = new SortParameter(theContext); 220 } else if (nextAnnotation instanceof TransactionParam) { 221 param = new TransactionParameter(theContext); 222 } else if (nextAnnotation instanceof ConditionalUrlParam) { 223 param = new ConditionalParamBinder(theRestfulOperationTypeEnum, ((ConditionalUrlParam) nextAnnotation).supportsMultiple()); 224 } else if (nextAnnotation instanceof OperationParam) { 225 Operation op = theMethod.getAnnotation(Operation.class); 226 OperationParam operationParam = (OperationParam) nextAnnotation; 227 param = new OperationParameter(theContext, op.name(), operationParam); 228 if (isNotBlank(operationParam.typeName())) { 229 BaseRuntimeElementDefinition<?> elementDefinition = theContext.getElementDefinition(operationParam.typeName()); 230 if (elementDefinition == null) { 231 elementDefinition = theContext.getResourceDefinition(operationParam.typeName()); 232 } 233 org.apache.commons.lang3.Validate.notNull(elementDefinition, "Unknown type name in @OperationParam: typeName=\"%s\"", operationParam.typeName()); 234 235 Class<?> newParameterType = elementDefinition.getImplementingClass(); 236 if (!declaredParameterType.isAssignableFrom(newParameterType)) { 237 throw new ConfigurationException("Non assignable parameter typeName=\"" + operationParam.typeName() + "\" specified on method " + theMethod); 238 } 239 parameterType = newParameterType; 240 } 241 } else if (nextAnnotation instanceof Validate.Mode) { 242 if (parameterType.equals(ValidationModeEnum.class) == false) { 243 throw new ConfigurationException( 244 "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName()); 245 } 246 param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IOperationParamConverter() { 247 @Override 248 public Object incomingServer(Object theObject) { 249 if (isNotBlank(theObject.toString())) { 250 ValidationModeEnum retVal = ValidationModeEnum.forCode(theObject.toString()); 251 if (retVal == null) { 252 OperationParameter.throwInvalidMode(theObject.toString()); 253 } 254 return retVal; 255 } 256 return null; 257 } 258 259 @Override 260 public Object outgoingClient(Object theObject) { 261 return ParametersUtil.createString(theContext, ((ValidationModeEnum) theObject).getCode()); 262 } 263 }); 264 } else if (nextAnnotation instanceof Validate.Profile) { 265 if (parameterType.equals(String.class) == false) { 266 throw new ConfigurationException( 267 "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName()); 268 } 269 param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1).setConverter(new IOperationParamConverter() { 270 @Override 271 public Object incomingServer(Object theObject) { 272 return theObject.toString(); 273 } 274 275 @Override 276 public Object outgoingClient(Object theObject) { 277 return ParametersUtil.createString(theContext, theObject.toString()); 278 } 279 }); 280 } else { 281 continue; 282 } 283 284 } 285 286 } 287 288 if (param == null) { 289 throw new ConfigurationException( 290 "Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() 291 + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter"); 292 } 293 294 param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); 295 parameters.add(param); 296 297 paramIndex++; 298 } 299 return parameters; 300 } 301 302 303}