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 if (!next.type().equals(IBase.class)) { 172 if (next.type().isInterface() || Modifier.isAbstract(next.type().getModifiers())) { 173 throw new ConfigurationException(Msg.code(424) + "Invalid value for @OperationParam.type(): " + next.type().getName()); 174 } 175 type.setType(theContext.getElementDefinition(next.type()).getName()); 176 } 177 myReturnParams.add(type); 178 } 179 } 180 181 // Parameter Validation 182 if (myCanOperateAtInstanceLevel && !isGlobalMethod() && getResourceName() == null) { 183 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()); 184 } 185 186 } 187 188 public String getShortDescription() { 189 return myShortDescription; 190 } 191 192 @Override 193 public boolean isGlobalMethod() { 194 return myGlobal; 195 } 196 197 public String getDescription() { 198 return myDescription; 199 } 200 201 public void setDescription(String theDescription) { 202 myDescription = theDescription; 203 } 204 205 /** 206 * Returns the name of the operation, starting with "$" 207 */ 208 public String getName() { 209 return myName; 210 } 211 212 @Override 213 protected BundleTypeEnum getResponseBundleType() { 214 return myBundleType; 215 } 216 217 @Nonnull 218 @Override 219 public RestOperationTypeEnum getRestOperationType() { 220 return myOtherOperationType; 221 } 222 223 public List<ReturnType> getReturnParams() { 224 return Collections.unmodifiableList(myReturnParams); 225 } 226 227 @Override 228 public ReturnTypeEnum getReturnType() { 229 return myReturnType; 230 } 231 232 @Override 233 public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) { 234 if (isBlank(theRequest.getOperation())) { 235 return MethodMatchEnum.NONE; 236 } 237 238 if (!myName.equals(theRequest.getOperation())) { 239 if (!myName.equals(WILDCARD_NAME)) { 240 return MethodMatchEnum.NONE; 241 } 242 } 243 244 if (getResourceName() == null) { 245 if (isNotBlank(theRequest.getResourceName())) { 246 if (!isGlobalMethod()) { 247 return MethodMatchEnum.NONE; 248 } 249 } 250 } 251 252 if (getResourceName() != null && !getResourceName().equals(theRequest.getResourceName())) { 253 return MethodMatchEnum.NONE; 254 } 255 256 RequestTypeEnum requestType = theRequest.getRequestType(); 257 if (requestType != RequestTypeEnum.GET && requestType != RequestTypeEnum.POST) { 258 // Operations can only be invoked with GET and POST 259 return MethodMatchEnum.NONE; 260 } 261 262 boolean requestHasId = theRequest.getId() != null; 263 if (requestHasId) { 264 return myCanOperateAtInstanceLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE; 265 } 266 if (isNotBlank(theRequest.getResourceName())) { 267 return myCanOperateAtTypeLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE; 268 } 269 return myCanOperateAtServerLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE; 270 } 271 272 @Override 273 public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) { 274 RestOperationTypeEnum retVal = super.getRestOperationType(theRequestDetails); 275 276 if (retVal == RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE) { 277 if (theRequestDetails.getId() == null) { 278 retVal = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; 279 } 280 } 281 282 if (myGlobal && theRequestDetails.getId() != null && theRequestDetails.getId().hasIdPart()) { 283 retVal = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; 284 } else if (myGlobal && isNotBlank(theRequestDetails.getResourceName())) { 285 retVal = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; 286 } 287 288 return retVal; 289 } 290 291 @Override 292 public String toString() { 293 return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 294 .append("name", myName) 295 .append("methodName", getMethod().getDeclaringClass().getSimpleName() + "." + getMethod().getName()) 296 .append("serverLevel", myCanOperateAtServerLevel) 297 .append("typeLevel", myCanOperateAtTypeLevel) 298 .append("instanceLevel", myCanOperateAtInstanceLevel) 299 .toString(); 300 } 301 302 @Override 303 public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { 304 if (theRequest.getRequestType() == RequestTypeEnum.POST && !myManualRequestMode) { 305 IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, this, null); 306 theRequest.getUserData().put(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY, requestContents); 307 } 308 return super.invokeServer(theServer, theRequest); 309 } 310 311 @Override 312 public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException { 313 if (theRequest.getRequestType() == RequestTypeEnum.POST) { 314 // all good 315 } else if (theRequest.getRequestType() == RequestTypeEnum.GET) { 316 if (!myIdempotent) { 317 String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name()); 318 throw new MethodNotAllowedException(Msg.code(426) + message, RequestTypeEnum.POST); 319 } 320 } else { 321 if (!myIdempotent) { 322 String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name()); 323 throw new MethodNotAllowedException(Msg.code(427) + message, RequestTypeEnum.POST); 324 } 325 String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.GET.name(), RequestTypeEnum.POST.name()); 326 throw new MethodNotAllowedException(Msg.code(428) + message, RequestTypeEnum.GET, RequestTypeEnum.POST); 327 } 328 329 if (myIdParamIndex != null) { 330 theMethodParams[myIdParamIndex] = theRequest.getId(); 331 } 332 333 Object response = invokeServerMethod(theRequest, theMethodParams); 334 if (myManualResponseMode) { 335 return null; 336 } 337 338 IBundleProvider retVal = toResourceList(response); 339 return retVal; 340 } 341 342 public boolean isCanOperateAtInstanceLevel() { 343 return this.myCanOperateAtInstanceLevel; 344 } 345 346 public boolean isCanOperateAtServerLevel() { 347 return this.myCanOperateAtServerLevel; 348 } 349 350 public boolean isCanOperateAtTypeLevel() { 351 return myCanOperateAtTypeLevel; 352 } 353 354 public boolean isIdempotent() { 355 return myIdempotent; 356 } 357 358 @Override 359 protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) { 360 super.populateActionRequestDetailsForInterceptor(theRequestDetails, theDetails, theMethodParams); 361 IBaseResource resource = (IBaseResource) theRequestDetails.getUserData().get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY); 362 theRequestDetails.setResource(resource); 363 if (theDetails != null) { 364 theDetails.setResource(resource); 365 } 366 } 367 368 public boolean isManualRequestMode() { 369 return myManualRequestMode; 370 } 371 372 public static class ReturnType { 373 private int myMax; 374 private int myMin; 375 private String myName; 376 /** 377 * http://hl7-fhir.github.io/valueset-operation-parameter-type.html 378 */ 379 private String myType; 380 381 public int getMax() { 382 return myMax; 383 } 384 385 public void setMax(int theMax) { 386 myMax = theMax; 387 } 388 389 public int getMin() { 390 return myMin; 391 } 392 393 public void setMin(int theMin) { 394 myMin = theMin; 395 } 396 397 public String getName() { 398 return myName; 399 } 400 401 public void setName(String theName) { 402 myName = theName; 403 } 404 405 public String getType() { 406 return myType; 407 } 408 409 public void setType(String theType) { 410 myType = theType; 411 } 412 } 413 414}