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