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