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 org.hl7.fhir.instance.model.api.IBaseResource; 040import org.hl7.fhir.instance.model.api.IPrimitiveType; 041 042import javax.servlet.ServletRequest; 043import javax.servlet.ServletResponse; 044import java.lang.annotation.Annotation; 045import java.lang.reflect.Method; 046import java.util.ArrayList; 047import java.util.Collection; 048import java.util.Date; 049import java.util.List; 050 051import static org.apache.commons.lang3.StringUtils.isNotBlank; 052 053public class MethodUtil { 054 055 public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) { 056 for (Annotation annotation : theAnnotations) { 057 if (annotation instanceof Description) { 058 Description desc = (Description) annotation; 059 if (isNotBlank(desc.formalDefinition())) { 060 theParameter.setDescription(desc.formalDefinition()); 061 } else { 062 theParameter.setDescription(desc.shortDefinition()); 063 } 064 } 065 } 066 } 067 068 069 @SuppressWarnings("unchecked") 070 public static List<IParameter> getResourceParameters(final FhirContext theContext, Method theMethod, Object theProvider, RestOperationTypeEnum theRestfulOperationTypeEnum) { 071 List<IParameter> parameters = new ArrayList<IParameter>(); 072 073 Class<?>[] parameterTypes = theMethod.getParameterTypes(); 074 int paramIndex = 0; 075 for (Annotation[] annotations : theMethod.getParameterAnnotations()) { 076 077 IParameter param = null; 078 Class<?> parameterType = parameterTypes[paramIndex]; 079 Class<? extends java.util.Collection<?>> outerCollectionType = null; 080 Class<? extends java.util.Collection<?>> innerCollectionType = null; 081 if (TagList.class.isAssignableFrom(parameterType)) { 082 // TagList is handled directly within the method bindings 083 param = new NullParameter(); 084 } else { 085 if (Collection.class.isAssignableFrom(parameterType)) { 086 innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType; 087 parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); 088 } 089 if (Collection.class.isAssignableFrom(parameterType)) { 090 outerCollectionType = innerCollectionType; 091 innerCollectionType = (Class<? extends java.util.Collection<?>>) parameterType; 092 parameterType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); 093 } 094 if (Collection.class.isAssignableFrom(parameterType)) { 095 throw new ConfigurationException("Argument #" + paramIndex + " of Method '" + theMethod.getName() + "' in type '" + theMethod.getDeclaringClass().getCanonicalName() 096 + "' is of an invalid generic type (can not be a collection of a collection of a collection)"); 097 } 098 099 /* 100 * If the user is trying to bind IPrimitiveType they are probably 101 * trying to write code that is compatible across versions of FHIR. 102 * We'll try and come up with an appropriate subtype to give 103 * them. 104 * 105 * This gets tested in HistoryR4Test 106 */ 107 if (IPrimitiveType.class.equals(parameterType)) { 108 Class<?> genericType = ReflectionUtil.getGenericCollectionTypeOfMethodParameter(theMethod, paramIndex); 109 if (Date.class.equals(genericType)) { 110 BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("dateTime"); 111 parameterType = dateTimeDef.getImplementingClass(); 112 } else if (String.class.equals(genericType) || genericType == null) { 113 BaseRuntimeElementDefinition<?> dateTimeDef = theContext.getElementDefinition("string"); 114 parameterType = dateTimeDef.getImplementingClass(); 115 } 116 } 117 } 118 119 if (ServletRequest.class.isAssignableFrom(parameterType)) { 120 param = new ServletRequestParameter(); 121 } else if (ServletResponse.class.isAssignableFrom(parameterType)) { 122 param = new ServletResponseParameter(); 123 } else if (parameterType.equals(RequestDetails.class) || parameterType.equals(ServletRequestDetails.class)) { 124 param = new RequestDetailsParameter(); 125 } else if (parameterType.equals(IInterceptorBroadcaster.class)) { 126 param = new InterceptorBroadcasterParameter(); 127 } else if (parameterType.equals(SummaryEnum.class)) { 128 param = new SummaryEnumParameter(); 129 } else if (parameterType.equals(PatchTypeEnum.class)) { 130 param = new PatchTypeParameter(); 131 } else if (parameterType.equals(SearchTotalModeEnum.class)) { 132 param = new SearchTotalModeParameter(); 133 } else { 134 for (int i = 0; i < annotations.length && param == null; i++) { 135 Annotation nextAnnotation = annotations[i]; 136 137 if (nextAnnotation instanceof RequiredParam) { 138 SearchParameter parameter = new SearchParameter(); 139 parameter.setName(((RequiredParam) nextAnnotation).name()); 140 parameter.setRequired(true); 141 parameter.setDeclaredTypes(((RequiredParam) nextAnnotation).targetTypes()); 142 parameter.setCompositeTypes(((RequiredParam) nextAnnotation).compositeTypes()); 143 parameter.setChainlists(((RequiredParam) nextAnnotation).chainWhitelist(), ((RequiredParam) nextAnnotation).chainBlacklist()); 144 parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); 145 MethodUtil.extractDescription(parameter, annotations); 146 param = parameter; 147 } else if (nextAnnotation instanceof OptionalParam) { 148 SearchParameter parameter = new SearchParameter(); 149 parameter.setName(((OptionalParam) nextAnnotation).name()); 150 parameter.setRequired(false); 151 parameter.setDeclaredTypes(((OptionalParam) nextAnnotation).targetTypes()); 152 parameter.setCompositeTypes(((OptionalParam) nextAnnotation).compositeTypes()); 153 parameter.setChainlists(((OptionalParam) nextAnnotation).chainWhitelist(), ((OptionalParam) nextAnnotation).chainBlacklist()); 154 parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); 155 MethodUtil.extractDescription(parameter, annotations); 156 param = parameter; 157 } else if (nextAnnotation instanceof RawParam) { 158 param = new RawParamsParmeter(parameters); 159 } else if (nextAnnotation instanceof IncludeParam) { 160 Class<? extends Collection<Include>> instantiableCollectionType; 161 Class<?> specType; 162 163 if (parameterType == String.class) { 164 instantiableCollectionType = null; 165 specType = String.class; 166 } else if ((parameterType != Include.class) || innerCollectionType == null || outerCollectionType != null) { 167 throw new ConfigurationException("Method '" + theMethod.getName() + "' is annotated with @" + IncludeParam.class.getSimpleName() + " but has a type other than Collection<" 168 + Include.class.getSimpleName() + ">"); 169 } else { 170 instantiableCollectionType = (Class<? extends Collection<Include>>) CollectionBinder.getInstantiableCollectionType(innerCollectionType, "Method '" + theMethod.getName() + "'"); 171 specType = parameterType; 172 } 173 174 param = new IncludeParameter((IncludeParam) nextAnnotation, instantiableCollectionType, specType); 175 } else if (nextAnnotation instanceof ResourceParam) { 176 Mode mode; 177 if (IBaseResource.class.isAssignableFrom(parameterType)) { 178 mode = Mode.RESOURCE; 179 } else if (String.class.equals(parameterType)) { 180 mode = ResourceParameter.Mode.BODY; 181 } else if (byte[].class.equals(parameterType)) { 182 mode = ResourceParameter.Mode.BODY_BYTE_ARRAY; 183 } else if (EncodingEnum.class.equals(parameterType)) { 184 mode = Mode.ENCODING; 185 } else { 186 StringBuilder b = new StringBuilder(); 187 b.append("Method '"); 188 b.append(theMethod.getName()); 189 b.append("' is annotated with @"); 190 b.append(ResourceParam.class.getSimpleName()); 191 b.append(" but has a type that is not an implemtation of "); 192 b.append(IBaseResource.class.getCanonicalName()); 193 b.append(" or String or byte[]"); 194 throw new ConfigurationException(b.toString()); 195 } 196 boolean methodIsOperation = theMethod.getAnnotation(Operation.class) != null; 197 param = new ResourceParameter((Class<? extends IBaseResource>) parameterType, theProvider, mode, methodIsOperation); 198 } else if (nextAnnotation instanceof IdParam) { 199 param = new NullParameter(); 200 } else if (nextAnnotation instanceof ServerBase) { 201 param = new ServerBaseParamBinder(); 202 } else if (nextAnnotation instanceof Elements) { 203 param = new ElementsParameter(); 204 } else if (nextAnnotation instanceof Since) { 205 param = new SinceParameter(); 206 ((SinceParameter) param).setType(theContext, parameterType, innerCollectionType, outerCollectionType); 207 } else if (nextAnnotation instanceof At) { 208 param = new AtParameter(); 209 ((AtParameter) param).setType(theContext, parameterType, innerCollectionType, outerCollectionType); 210 } else if (nextAnnotation instanceof Count) { 211 param = new CountParameter(); 212 } else if (nextAnnotation instanceof GraphQLQuery) { 213 param = new GraphQLQueryParameter(); 214 } else if (nextAnnotation instanceof Sort) { 215 param = new SortParameter(theContext); 216 } else if (nextAnnotation instanceof TransactionParam) { 217 param = new TransactionParameter(theContext); 218 } else if (nextAnnotation instanceof ConditionalUrlParam) { 219 param = new ConditionalParamBinder(theRestfulOperationTypeEnum, ((ConditionalUrlParam) nextAnnotation).supportsMultiple()); 220 } else if (nextAnnotation instanceof OperationParam) { 221 Operation op = theMethod.getAnnotation(Operation.class); 222 param = new OperationParameter(theContext, op.name(), ((OperationParam) nextAnnotation)); 223 } else if (nextAnnotation instanceof Validate.Mode) { 224 if (parameterType.equals(ValidationModeEnum.class) == false) { 225 throw new ConfigurationException( 226 "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName()); 227 } 228 param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IOperationParamConverter() { 229 @Override 230 public Object incomingServer(Object theObject) { 231 if (isNotBlank(theObject.toString())) { 232 ValidationModeEnum retVal = ValidationModeEnum.forCode(theObject.toString()); 233 if (retVal == null) { 234 OperationParameter.throwInvalidMode(theObject.toString()); 235 } 236 return retVal; 237 } 238 return null; 239 } 240 241 @Override 242 public Object outgoingClient(Object theObject) { 243 return ParametersUtil.createString(theContext, ((ValidationModeEnum) theObject).getCode()); 244 } 245 }); 246 } else if (nextAnnotation instanceof Validate.Profile) { 247 if (parameterType.equals(String.class) == false) { 248 throw new ConfigurationException( 249 "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName()); 250 } 251 param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1).setConverter(new IOperationParamConverter() { 252 @Override 253 public Object incomingServer(Object theObject) { 254 return theObject.toString(); 255 } 256 257 @Override 258 public Object outgoingClient(Object theObject) { 259 return ParametersUtil.createString(theContext, theObject.toString()); 260 } 261 }); 262 } else { 263 continue; 264 } 265 266 } 267 268 } 269 270 if (param == null) { 271 throw new ConfigurationException( 272 "Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() 273 + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter"); 274 } 275 276 param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); 277 parameters.add(param); 278 279 paramIndex++; 280 } 281 return parameters; 282 } 283 284 285}