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