001package ca.uhn.fhir.rest.method; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2017 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 */ 022import static org.apache.commons.lang3.StringUtils.isBlank; 023import static org.apache.commons.lang3.StringUtils.isNotBlank; 024 025import java.io.IOException; 026import java.lang.annotation.Annotation; 027import java.lang.reflect.Method; 028import java.lang.reflect.Modifier; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.LinkedHashMap; 032import java.util.List; 033import java.util.Map; 034 035import org.hl7.fhir.instance.model.api.IBase; 036import org.hl7.fhir.instance.model.api.IBaseBundle; 037import org.hl7.fhir.instance.model.api.IBaseDatatype; 038import org.hl7.fhir.instance.model.api.IBaseParameters; 039import org.hl7.fhir.instance.model.api.IBaseResource; 040import org.hl7.fhir.instance.model.api.IIdType; 041import org.hl7.fhir.instance.model.api.IPrimitiveType; 042 043import ca.uhn.fhir.context.ConfigurationException; 044import ca.uhn.fhir.context.FhirContext; 045import ca.uhn.fhir.context.FhirVersionEnum; 046import ca.uhn.fhir.model.api.Bundle; 047import ca.uhn.fhir.model.api.annotation.Description; 048import ca.uhn.fhir.model.valueset.BundleTypeEnum; 049import ca.uhn.fhir.rest.annotation.IdParam; 050import ca.uhn.fhir.rest.annotation.Operation; 051import ca.uhn.fhir.rest.annotation.OperationParam; 052import ca.uhn.fhir.rest.api.RequestTypeEnum; 053import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 054import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; 055import ca.uhn.fhir.rest.param.ResourceParameter; 056import ca.uhn.fhir.rest.server.IBundleProvider; 057import ca.uhn.fhir.rest.server.IRestfulServer; 058import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 059import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 060import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 061import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; 062import ca.uhn.fhir.util.FhirTerser; 063 064public class OperationMethodBinding extends BaseResourceReturningMethodBinding { 065 066 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class); 067 private BundleTypeEnum myBundleType; 068 private boolean myCanOperateAtInstanceLevel; 069 private boolean myCanOperateAtServerLevel; 070 private boolean myCanOperateAtTypeLevel; 071 private String myDescription; 072 private final boolean myIdempotent; 073 private final Integer myIdParamIndex; 074 private final String myName; 075 private final RestOperationTypeEnum myOtherOperatiopnType; 076 private List<ReturnType> myReturnParams; 077 private final ReturnTypeEnum myReturnType; 078 079 protected OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, 080 boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType, 081 OperationParam[] theReturnParams, BundleTypeEnum theBundleType) { 082 super(theReturnResourceType, theMethod, theContext, theProvider); 083 084 myBundleType = theBundleType; 085 myIdempotent = theIdempotent; 086 myIdParamIndex = MethodUtil.findIdParameterIndex(theMethod, getContext()); 087 if (myIdParamIndex != null) { 088 for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) { 089 if (next instanceof IdParam) { 090 myCanOperateAtTypeLevel = ((IdParam) next).optional() == true; 091 } 092 } 093 } else { 094 myCanOperateAtTypeLevel = true; 095 } 096 097 Description description = theMethod.getAnnotation(Description.class); 098 if (description != null) { 099 myDescription = description.formalDefinition(); 100 if (isBlank(myDescription)) { 101 myDescription = description.shortDefinition(); 102 } 103 } 104 if (isBlank(myDescription)) { 105 myDescription = null; 106 } 107 108 if (isBlank(theOperationName)) { 109 throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName() 110 + " but this annotation has no name defined"); 111 } 112 if (theOperationName.startsWith("$") == false) { 113 theOperationName = "$" + theOperationName; 114 } 115 myName = theOperationName; 116 117 if (theContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU1)) { 118 throw new ConfigurationException("@" + Operation.class.getSimpleName() + " methods are not supported on servers for FHIR version " + theContext.getVersion().getVersion().name() 119 + " - Found one on class " + theMethod.getDeclaringClass().getName()); 120 } 121 122 if (theReturnTypeFromRp != null) { 123 setResourceName(theContext.getResourceDefinition(theReturnTypeFromRp).getName()); 124 } else { 125 if (Modifier.isAbstract(theOperationType.getModifiers()) == false) { 126 setResourceName(theContext.getResourceDefinition(theOperationType).getName()); 127 } else { 128 setResourceName(null); 129 } 130 } 131 132 if (theMethod.getReturnType().isAssignableFrom(Bundle.class)) { 133 throw new ConfigurationException("Can not return a DSTU1 bundle from an @" + Operation.class.getSimpleName() + " method. Found in method " + theMethod.getName() + " defined in type " 134 + theMethod.getDeclaringClass().getName()); 135 } 136 137 if (theMethod.getReturnType().equals(IBundleProvider.class)) { 138 myReturnType = ReturnTypeEnum.BUNDLE; 139 } else { 140 myReturnType = ReturnTypeEnum.RESOURCE; 141 } 142 143 if (getResourceName() == null) { 144 myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; 145 } else if (myIdParamIndex == null) { 146 myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; 147 } else { 148 myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; 149 } 150 151 myReturnParams = new ArrayList<OperationMethodBinding.ReturnType>(); 152 if (theReturnParams != null) { 153 for (OperationParam next : theReturnParams) { 154 ReturnType type = new ReturnType(); 155 type.setName(next.name()); 156 type.setMin(next.min()); 157 type.setMax(next.max()); 158 if (type.getMax() == OperationParam.MAX_DEFAULT) { 159 type.setMax(1); 160 } 161 if (!next.type().equals(IBase.class)) { 162 if (next.type().isInterface() || Modifier.isAbstract(next.type().getModifiers())) { 163 throw new ConfigurationException("Invalid value for @OperationParam.type(): " + next.type().getName()); 164 } 165 type.setType(theContext.getElementDefinition(next.type()).getName()); 166 } 167 myReturnParams.add(type); 168 } 169 } 170 171 if (myIdParamIndex != null) { 172 myCanOperateAtInstanceLevel = true; 173 } 174 if (getResourceName() == null) { 175 myCanOperateAtServerLevel = true; 176 } 177 178 } 179 180 public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, 181 Operation theAnnotation) { 182 this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.returnParameters(), 183 theAnnotation.bundleType()); 184 } 185 186 public String getDescription() { 187 return myDescription; 188 } 189 190 /** 191 * Returns the name of the operation, starting with "$" 192 */ 193 public String getName() { 194 return myName; 195 } 196 197 @Override 198 protected BundleTypeEnum getResponseBundleType() { 199 return myBundleType; 200 } 201 202 @Override 203 public RestOperationTypeEnum getRestOperationType() { 204 return myOtherOperatiopnType; 205 } 206 207 public List<ReturnType> getReturnParams() { 208 return Collections.unmodifiableList(myReturnParams); 209 } 210 211 @Override 212 public ReturnTypeEnum getReturnType() { 213 return myReturnType; 214 } 215 216 @Override 217 public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) { 218 if (getResourceName() == null) { 219 if (isNotBlank(theRequest.getResourceName())) { 220 return false; 221 } 222 } else if (!getResourceName().equals(theRequest.getResourceName())) { 223 return false; 224 } 225 226 if (!myName.equals(theRequest.getOperation())) { 227 return false; 228 } 229 230 RequestTypeEnum requestType = theRequest.getRequestType(); 231 if (requestType != RequestTypeEnum.GET && requestType != RequestTypeEnum.POST) { 232 // Operations can only be invoked with GET and POST 233 return false; 234 } 235 236 boolean requestHasId = theRequest.getId() != null; 237 if (requestHasId) { 238 if (isCanOperateAtInstanceLevel() == false) { 239 return false; 240 } 241 } else { 242 if (myCanOperateAtTypeLevel == false) { 243 return false; 244 } 245 } 246 247 return true; 248 } 249 250 @Override 251 public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { 252 String id = null; 253 if (myIdParamIndex != null) { 254 IIdType idDt = (IIdType) theArgs[myIdParamIndex]; 255 id = idDt.getValue(); 256 } 257 IBaseParameters parameters = (IBaseParameters) getContext().getResourceDefinition("Parameters").newInstance(); 258 259 if (theArgs != null) { 260 for (int idx = 0; idx < theArgs.length; idx++) { 261 IParameter nextParam = getParameters().get(idx); 262 nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, parameters); 263 } 264 } 265 266 return createOperationInvocation(getContext(), getResourceName(), id, myName, parameters, false); 267 } 268 269 @Override 270 public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException { 271 if (theRequest.getRequestType() == RequestTypeEnum.POST) { 272 IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, this, null); 273 theRequest.getUserData().put(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY, requestContents); 274 } 275 return super.invokeServer(theServer, theRequest); 276 } 277 278 @Override 279 public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException { 280 if (theRequest.getRequestType() == RequestTypeEnum.POST) { 281 // all good 282 } else if (theRequest.getRequestType() == RequestTypeEnum.GET) { 283 if (!myIdempotent) { 284 String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name()); 285 throw new MethodNotAllowedException(message, RequestTypeEnum.POST); 286 } 287 } else { 288 if (!myIdempotent) { 289 String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name()); 290 throw new MethodNotAllowedException(message, RequestTypeEnum.POST); 291 } 292 String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.GET.name(), RequestTypeEnum.POST.name()); 293 throw new MethodNotAllowedException(message, RequestTypeEnum.GET, RequestTypeEnum.POST); 294 } 295 296 if (myIdParamIndex != null) { 297 theMethodParams[myIdParamIndex] = theRequest.getId(); 298 } 299 300 Object response = invokeServerMethod(theServer, theRequest, theMethodParams); 301 IBundleProvider retVal = toResourceList(response); 302 return retVal; 303 } 304 305 public boolean isCanOperateAtInstanceLevel() { 306 return this.myCanOperateAtInstanceLevel; 307 } 308 309 public boolean isCanOperateAtServerLevel() { 310 return this.myCanOperateAtServerLevel; 311 } 312 313 public boolean isCanOperateAtTypeLevel() { 314 return myCanOperateAtTypeLevel; 315 } 316 317 public boolean isIdempotent() { 318 return myIdempotent; 319 } 320 321 @Override 322 protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) { 323 super.populateActionRequestDetailsForInterceptor(theRequestDetails, theDetails, theMethodParams); 324 theDetails.setResource((IBaseResource) theRequestDetails.getUserData().get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY)); 325 } 326 327 public void setDescription(String theDescription) { 328 myDescription = theDescription; 329 } 330 331 public static BaseHttpClientInvocation createOperationInvocation(FhirContext theContext, String theResourceName, String theId, String theOperationName, IBaseParameters theInput, 332 boolean theUseHttpGet) { 333 StringBuilder b = new StringBuilder(); 334 if (theResourceName != null) { 335 b.append(theResourceName); 336 if (isNotBlank(theId)) { 337 b.append('/'); 338 b.append(theId); 339 } 340 } 341 if (b.length() > 0) { 342 b.append('/'); 343 } 344 if (!theOperationName.startsWith("$")) { 345 b.append("$"); 346 } 347 b.append(theOperationName); 348 349 if (!theUseHttpGet) { 350 return new HttpPostClientInvocation(theContext, theInput, b.toString()); 351 } 352 FhirTerser t = theContext.newTerser(); 353 List<Object> parameters = t.getValues(theInput, "Parameters.parameter"); 354 355 Map<String, List<String>> params = new LinkedHashMap<String, List<String>>(); 356 for (Object nextParameter : parameters) { 357 IPrimitiveType<?> nextNameDt = (IPrimitiveType<?>) t.getSingleValueOrNull((IBase) nextParameter, "name"); 358 if (nextNameDt == null || nextNameDt.isEmpty()) { 359 ourLog.warn("Ignoring input parameter with no value in Parameters.parameter.name in operation client invocation"); 360 continue; 361 } 362 String nextName = nextNameDt.getValueAsString(); 363 if (!params.containsKey(nextName)) { 364 params.put(nextName, new ArrayList<String>()); 365 } 366 367 IBaseDatatype value = (IBaseDatatype) t.getSingleValueOrNull((IBase) nextParameter, "value[x]"); 368 if (value == null) { 369 continue; 370 } 371 if (!(value instanceof IPrimitiveType)) { 372 throw new IllegalArgumentException( 373 "Can not invoke operation as HTTP GET when it has parameters with a composite (non priitive) datatype as the value. Found value: " + value.getClass().getName()); 374 } 375 IPrimitiveType<?> primitive = (IPrimitiveType<?>) value; 376 params.get(nextName).add(primitive.getValueAsString()); 377 } 378 return new HttpGetClientInvocation(theContext, params, b.toString()); 379 } 380 381 public static BaseHttpClientInvocation createProcessMsgInvocation(FhirContext theContext, String theOperationName, IBaseBundle theInput, Map<String, List<String>> urlParams) { 382 StringBuilder b = new StringBuilder(); 383 384 if (b.length() > 0) { 385 b.append('/'); 386 } 387 if (!theOperationName.startsWith("$")) { 388 b.append("$"); 389 } 390 b.append(theOperationName); 391 392 BaseHttpClientInvocation.appendExtraParamsWithQuestionMark(urlParams, b, b.indexOf("?") == -1); 393 394 return new HttpPostClientInvocation(theContext, theInput, b.toString()); 395 396 } 397 398 public static class ReturnType { 399 private int myMax; 400 private int myMin; 401 private String myName; 402 /** 403 * http://hl7-fhir.github.io/valueset-operation-parameter-type.html 404 */ 405 private String myType; 406 407 public int getMax() { 408 return myMax; 409 } 410 411 public int getMin() { 412 return myMin; 413 } 414 415 public String getName() { 416 return myName; 417 } 418 419 public String getType() { 420 return myType; 421 } 422 423 public void setMax(int theMax) { 424 myMax = theMax; 425 } 426 427 public void setMin(int theMin) { 428 myMin = theMin; 429 } 430 431 public void setName(String theName) { 432 myName = theName; 433 } 434 435 public void setType(String theType) { 436 myType = theType; 437 } 438 } 439 440}