
001package ca.uhn.fhir.rest.server.method; 002 003/* 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 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.i18n.Msg; 024import ca.uhn.fhir.context.ConfigurationException; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.model.valueset.BundleTypeEnum; 027import ca.uhn.fhir.parser.DataFormatException; 028import ca.uhn.fhir.rest.annotation.IdParam; 029import ca.uhn.fhir.rest.annotation.Operation; 030import ca.uhn.fhir.rest.annotation.OperationParam; 031import ca.uhn.fhir.rest.annotation.OptionalParam; 032import ca.uhn.fhir.rest.annotation.RequiredParam; 033import ca.uhn.fhir.rest.api.RequestTypeEnum; 034import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 035import ca.uhn.fhir.rest.api.server.IBundleProvider; 036import ca.uhn.fhir.rest.api.server.IRestfulServer; 037import ca.uhn.fhir.rest.api.server.RequestDetails; 038import ca.uhn.fhir.rest.param.ParameterUtil; 039import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 040import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 041import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; 042import ca.uhn.fhir.util.ParametersUtil; 043import org.apache.commons.lang3.builder.ToStringBuilder; 044import org.apache.commons.lang3.builder.ToStringStyle; 045import org.hl7.fhir.instance.model.api.IBase; 046import org.hl7.fhir.instance.model.api.IBaseResource; 047 048import javax.annotation.Nonnull; 049import java.io.IOException; 050import java.lang.annotation.Annotation; 051import java.lang.reflect.Method; 052import java.lang.reflect.Modifier; 053import java.util.ArrayList; 054import java.util.Collections; 055import java.util.List; 056 057import static org.apache.commons.lang3.StringUtils.isBlank; 058import static org.apache.commons.lang3.StringUtils.isNotBlank; 059 060public class OperationMethodBinding extends BaseResourceReturningMethodBinding { 061 062 public static final String WILDCARD_NAME = "$" + Operation.NAME_MATCH_ALL; 063 private final boolean myIdempotent; 064 private final Integer myIdParamIndex; 065 private final String myName; 066 private final RestOperationTypeEnum myOtherOperationType; 067 private final ReturnTypeEnum myReturnType; 068 private final String myShortDescription; 069 private boolean myGlobal; 070 private BundleTypeEnum myBundleType; 071 private boolean myCanOperateAtInstanceLevel; 072 private boolean myCanOperateAtServerLevel; 073 private boolean myCanOperateAtTypeLevel; 074 private String myDescription; 075 private List<ReturnType> myReturnParams; 076 private boolean myManualRequestMode; 077 private boolean myManualResponseMode; 078 079 /** 080 * Constructor - This is the constructor that is called when binding a 081 * standard @Operation method. 082 */ 083 public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, 084 Operation theAnnotation) { 085 this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.typeName(), theAnnotation.returnParameters(), 086 theAnnotation.bundleType(), theAnnotation.global()); 087 088 myManualRequestMode = theAnnotation.manualRequest(); 089 myManualResponseMode = theAnnotation.manualResponse(); 090 } 091 092 protected OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, 093 boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType, String theOperationTypeName, 094 OperationParam[] theReturnParams, BundleTypeEnum theBundleType, boolean theGlobal) { 095 super(theReturnResourceType, theMethod, theContext, theProvider); 096 097 myBundleType = theBundleType; 098 myIdempotent = theIdempotent; 099 myDescription = ParametersUtil.extractDescription(theMethod); 100 myShortDescription = ParametersUtil.extractShortDefinition(theMethod); 101 myGlobal = theGlobal; 102 103 for (Annotation[] nextParamAnnotations : theMethod.getParameterAnnotations()) { 104 for (Annotation nextParam : nextParamAnnotations) { 105 if (nextParam instanceof OptionalParam || nextParam instanceof RequiredParam) { 106 throw new ConfigurationException(Msg.code(421) + "Illegal method parameter annotation @" + nextParam.annotationType().getSimpleName() + " on method: " + theMethod.toString()); 107 } 108 } 109 } 110 111 if (isBlank(theOperationName)) { 112 throw new ConfigurationException(Msg.code(422) + "Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName() 113 + " but this annotation has no name defined"); 114 } 115 if (theOperationName.startsWith("$") == false) { 116 theOperationName = "$" + theOperationName; 117 } 118 myName = theOperationName; 119 120 try { 121 if (theReturnTypeFromRp != null) { 122 setResourceName(theContext.getResourceType(theReturnTypeFromRp)); 123 } else if (theOperationType != null && Modifier.isAbstract(theOperationType.getModifiers()) == false) { 124 setResourceName(theContext.getResourceType(theOperationType)); 125 } else if (isNotBlank(theOperationTypeName)) { 126 setResourceName(theContext.getResourceType(theOperationTypeName)); 127 } else { 128 setResourceName(null); 129 } 130 } catch (DataFormatException e) { 131 throw new ConfigurationException(Msg.code(423) + "Failed to bind method " + theMethod + " - " + e.getMessage(), e); 132 } 133 134 if (theMethod.getReturnType().equals(IBundleProvider.class)) { 135 myReturnType = ReturnTypeEnum.BUNDLE; 136 } else { 137 myReturnType = ReturnTypeEnum.RESOURCE; 138 } 139 140 myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); 141 if (getResourceName() == null) { 142 myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; 143 if (myIdParamIndex != null) { 144 myCanOperateAtInstanceLevel = true; 145 } else { 146 myCanOperateAtServerLevel = true; 147 } 148 } else if (myIdParamIndex == null) { 149 myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; 150 myCanOperateAtTypeLevel = true; 151 } else { 152 myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; 153 myCanOperateAtInstanceLevel = true; 154 for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) { 155 if (next instanceof IdParam) { 156 myCanOperateAtTypeLevel = ((IdParam) next).optional() == true; 157 } 158 } 159 } 160 161 myReturnParams = new ArrayList<>(); 162 if (theReturnParams != null) { 163 for (OperationParam next : theReturnParams) { 164 ReturnType type = new ReturnType(); 165 type.setName(next.name()); 166 type.setMin(next.min()); 167 type.setMax(next.max()); 168 if (type.getMax() == OperationParam.MAX_DEFAULT) { 169 type.setMax(1); 170 } 171 Class<? extends IBase> returnType = next.type(); 172 if (!returnType.equals(IBase.class)) { 173 if (returnType.isInterface() || Modifier.isAbstract(returnType.getModifiers())) { 174 throw new ConfigurationException(Msg.code(424) + "Invalid value for @OperationParam.type(): " + returnType.getName()); 175 } 176 OperationParameter.validateTypeIsAppropriateVersionForContext(theMethod, returnType, theContext, "return"); 177 type.setType(theContext.getElementDefinition(returnType).getName()); 178 } 179 myReturnParams.add(type); 180 } 181 } 182 183 // Parameter Validation 184 if (myCanOperateAtInstanceLevel && !isGlobalMethod() && getResourceName() == null) { 185 throw new ConfigurationException(Msg.code(425) + "@" + Operation.class.getSimpleName() + " method is an instance level method (it has an @" + IdParam.class.getSimpleName() + " parameter) but is not marked as global() and is not declared in a resource provider: " + theMethod.getName()); 186 } 187 188 } 189 190 public String getShortDescription() { 191 return myShortDescription; 192 } 193 194 @Override 195 public boolean isGlobalMethod() { 196 return myGlobal; 197 } 198 199 public String getDescription() { 200 return myDescription; 201 } 202 203 public void setDescription(String theDescription) { 204 myDescription = theDescription; 205 } 206 207 /** 208 * Returns the name of the operation, starting with "$" 209 */ 210 public String getName() { 211 return myName; 212 } 213 214 @Override 215 protected BundleTypeEnum getResponseBundleType() { 216 return myBundleType; 217 } 218 219 @Nonnull 220 @Override 221 public RestOperationTypeEnum getRestOperationType() { 222 return myOtherOperationType; 223 } 224 225 public List<ReturnType> getReturnParams() { 226 return Collections.unmodifiableList(myReturnParams); 227 } 228 229 @Override 230 public ReturnTypeEnum getReturnType() { 231 return myReturnType; 232 } 233 234 @Override 235 public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) { 236 if (isBlank(theRequest.getOperation())) { 237 return MethodMatchEnum.NONE; 238 } 239 240 if (!myName.equals(theRequest.getOperation())) { 241 if (!myName.equals(WILDCARD_NAME)) { 242 return MethodMatchEnum.NONE; 243 } 244 } 245 246 if (getResourceName() == null) { 247 if (isNotBlank(theRequest.getResourceName())) { 248 if (!isGlobalMethod()) { 249 return MethodMatchEnum.NONE; 250 } 251 } 252 } 253 254 if (getResourceName() != null && !getResourceName().equals(theRequest.getResourceName())) { 255 return MethodMatchEnum.NONE; 256 } 257 258 RequestTypeEnum requestType = theRequest.getRequestType(); 259 if (requestType != RequestTypeEnum.GET && requestType != RequestTypeEnum.POST) { 260 // Operations can only be invoked with GET and POST 261 return MethodMatchEnum.NONE; 262 } 263 264 boolean requestHasId = theRequest.getId() != null; 265 if (requestHasId) { 266 return myCanOperateAtInstanceLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE; 267 } 268 if (isNotBlank(theRequest.getResourceName())) { 269 return myCanOperateAtTypeLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE; 270 } 271 return myCanOperateAtServerLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE; 272 } 273 274 @Override 275 public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) { 276 RestOperationTypeEnum retVal = super.getRestOperationType(theRequestDetails); 277 278 if (retVal == RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE) { 279 if (theRequestDetails.getId() == null) { 280 retVal = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; 281 } 282 } 283 284 if (myGlobal && theRequestDetails.getId() != null && theRequestDetails.getId().hasIdPart()) { 285 retVal = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; 286 } else if (myGlobal && isNotBlank(theRequestDetails.getResourceName())) { 287 retVal = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; 288 } 289 290 return retVal; 291 } 292 293 @Override 294 public String toString() { 295 return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 296 .append("name", myName) 297 .append("methodName", getMethod().getDeclaringClass().getSimpleName() + "." + getMethod().getName()) 298 .append("serverLevel", myCanOperateAtServerLevel) 299 .append("typeLevel", myCanOperateAtTypeLevel) 300 .append("instanceLevel", myCanOperateAtInstanceLevel) 301 .toString(); 302 } 303 304 @Override 305 public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { 306 if (theRequest.getRequestType() == RequestTypeEnum.POST && !myManualRequestMode) { 307 IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, this, null); 308 theRequest.getUserData().put(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY, requestContents); 309 } 310 return super.invokeServer(theServer, theRequest); 311 } 312 313 @Override 314 public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException { 315 if (theRequest.getRequestType() == RequestTypeEnum.POST) { 316 // all good 317 } else if (theRequest.getRequestType() == RequestTypeEnum.GET) { 318 if (!myIdempotent) { 319 String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name()); 320 throw new MethodNotAllowedException(Msg.code(426) + message, RequestTypeEnum.POST); 321 } 322 } else { 323 if (!myIdempotent) { 324 String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name()); 325 throw new MethodNotAllowedException(Msg.code(427) + message, RequestTypeEnum.POST); 326 } 327 String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.GET.name(), RequestTypeEnum.POST.name()); 328 throw new MethodNotAllowedException(Msg.code(428) + message, RequestTypeEnum.GET, RequestTypeEnum.POST); 329 } 330 331 if (myIdParamIndex != null) { 332 theMethodParams[myIdParamIndex] = theRequest.getId(); 333 } 334 335 Object response = invokeServerMethod(theRequest, theMethodParams); 336 if (myManualResponseMode) { 337 return null; 338 } 339 340 IBundleProvider retVal = toResourceList(response); 341 return retVal; 342 } 343 344 public boolean isCanOperateAtInstanceLevel() { 345 return this.myCanOperateAtInstanceLevel; 346 } 347 348 public boolean isCanOperateAtServerLevel() { 349 return this.myCanOperateAtServerLevel; 350 } 351 352 public boolean isCanOperateAtTypeLevel() { 353 return myCanOperateAtTypeLevel; 354 } 355 356 public boolean isIdempotent() { 357 return myIdempotent; 358 } 359 360 @Override 361 protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) { 362 super.populateActionRequestDetailsForInterceptor(theRequestDetails, theDetails, theMethodParams); 363 IBaseResource resource = (IBaseResource) theRequestDetails.getUserData().get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY); 364 theRequestDetails.setResource(resource); 365 if (theDetails != null) { 366 theDetails.setResource(resource); 367 } 368 } 369 370 public boolean isManualRequestMode() { 371 return myManualRequestMode; 372 } 373 374 public static class ReturnType { 375 private int myMax; 376 private int myMin; 377 private String myName; 378 /** 379 * http://hl7-fhir.github.io/valueset-operation-parameter-type.html 380 */ 381 private String myType; 382 383 public int getMax() { 384 return myMax; 385 } 386 387 public void setMax(int theMax) { 388 myMax = theMax; 389 } 390 391 public int getMin() { 392 return myMin; 393 } 394 395 public void setMin(int theMin) { 396 myMin = theMin; 397 } 398 399 public String getName() { 400 return myName; 401 } 402 403 public void setName(String theName) { 404 myName = theName; 405 } 406 407 public String getType() { 408 return myType; 409 } 410 411 public void setType(String theType) { 412 myType = theType; 413 } 414 } 415 416}