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