001/* 002 * #%L 003 * HAPI FHIR - Client Framework 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.client.method; 021 022import ca.uhn.fhir.context.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.model.api.annotation.Description; 026import ca.uhn.fhir.model.valueset.BundleTypeEnum; 027import ca.uhn.fhir.rest.annotation.Operation; 028import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 029import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; 030import ca.uhn.fhir.rest.param.ParameterUtil; 031import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 032import ca.uhn.fhir.util.FhirTerser; 033import ca.uhn.fhir.util.ParametersUtil; 034import org.hl7.fhir.instance.model.api.IBase; 035import org.hl7.fhir.instance.model.api.IBaseBundle; 036import org.hl7.fhir.instance.model.api.IBaseDatatype; 037import org.hl7.fhir.instance.model.api.IBaseParameters; 038import org.hl7.fhir.instance.model.api.IBaseResource; 039import org.hl7.fhir.instance.model.api.IIdType; 040import org.hl7.fhir.instance.model.api.IPrimitiveType; 041 042import java.lang.reflect.Method; 043import java.lang.reflect.Modifier; 044import java.util.ArrayList; 045import java.util.LinkedHashMap; 046import java.util.List; 047import java.util.Map; 048 049import static org.apache.commons.lang3.StringUtils.isBlank; 050import static org.apache.commons.lang3.StringUtils.isNotBlank; 051 052public class OperationMethodBinding extends BaseResourceReturningMethodBinding { 053 054 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class); 055 private final boolean myIdempotent; 056 private final Integer myIdParamIndex; 057 private final String myName; 058 private final RestOperationTypeEnum myOtherOperationType; 059 private final ReturnTypeEnum myReturnType; 060 private BundleTypeEnum myBundleType; 061 private String myDescription; 062 063 protected OperationMethodBinding( 064 Class<?> theReturnResourceType, 065 Class<? extends IBaseResource> theReturnTypeFromRp, 066 Method theMethod, 067 FhirContext theContext, 068 Object theProvider, 069 boolean theIdempotent, 070 String theOperationName, 071 Class<? extends IBaseResource> theOperationType, 072 BundleTypeEnum theBundleType) { 073 super(theReturnResourceType, theMethod, theContext, theProvider); 074 075 myBundleType = theBundleType; 076 myIdempotent = theIdempotent; 077 myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); 078 079 Description description = theMethod.getAnnotation(Description.class); 080 if (description != null) { 081 myDescription = ParametersUtil.extractDescription(description); 082 } 083 if (isBlank(myDescription)) { 084 myDescription = null; 085 } 086 087 if (isBlank(theOperationName)) { 088 throw new ConfigurationException(Msg.code(1452) + "Method '" + theMethod.getName() + "' on type " 089 + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName() 090 + " but this annotation has no name defined"); 091 } 092 if (theOperationName.startsWith("$") == false) { 093 theOperationName = "$" + theOperationName; 094 } 095 myName = theOperationName; 096 097 if (theReturnTypeFromRp != null) { 098 setResourceName(theContext.getResourceType(theReturnTypeFromRp)); 099 } else { 100 if (Modifier.isAbstract(theOperationType.getModifiers()) == false) { 101 setResourceName(theContext.getResourceType(theOperationType)); 102 } else { 103 setResourceName(null); 104 } 105 } 106 107 myReturnType = ReturnTypeEnum.RESOURCE; 108 109 if (getResourceName() == null) { 110 myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; 111 } else if (myIdParamIndex == null) { 112 myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; 113 } else { 114 myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; 115 } 116 } 117 118 public OperationMethodBinding( 119 Class<?> theReturnResourceType, 120 Class<? extends IBaseResource> theReturnTypeFromRp, 121 Method theMethod, 122 FhirContext theContext, 123 Object theProvider, 124 Operation theAnnotation) { 125 this( 126 theReturnResourceType, 127 theReturnTypeFromRp, 128 theMethod, 129 theContext, 130 theProvider, 131 theAnnotation.idempotent(), 132 theAnnotation.name(), 133 theAnnotation.type(), 134 theAnnotation.bundleType()); 135 } 136 137 public String getDescription() { 138 return myDescription; 139 } 140 141 public void setDescription(String theDescription) { 142 myDescription = theDescription; 143 } 144 145 /** 146 * Returns the name of the operation, starting with "$" 147 */ 148 public String getName() { 149 return myName; 150 } 151 152 @Override 153 protected BundleTypeEnum getResponseBundleType() { 154 return myBundleType; 155 } 156 157 @Override 158 public RestOperationTypeEnum getRestOperationType() { 159 return myOtherOperationType; 160 } 161 162 @Override 163 public ReturnTypeEnum getReturnType() { 164 return myReturnType; 165 } 166 167 @Override 168 public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { 169 String id = null; 170 if (myIdParamIndex != null) { 171 IIdType idDt = (IIdType) theArgs[myIdParamIndex]; 172 id = idDt.getValue(); 173 } 174 IBaseParameters parameters = (IBaseParameters) 175 getContext().getResourceDefinition("Parameters").newInstance(); 176 177 if (theArgs != null) { 178 for (int idx = 0; idx < theArgs.length; idx++) { 179 IParameter nextParam = getParameters().get(idx); 180 nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, parameters); 181 } 182 } 183 184 return createOperationInvocation(getContext(), getResourceName(), id, null, myName, parameters, false); 185 } 186 187 public boolean isIdempotent() { 188 return myIdempotent; 189 } 190 191 public static BaseHttpClientInvocation createOperationInvocation( 192 FhirContext theContext, 193 String theResourceName, 194 String theId, 195 String theVersion, 196 String theOperationName, 197 IBaseParameters theInput, 198 boolean theUseHttpGet) { 199 StringBuilder b = new StringBuilder(); 200 if (theResourceName != null) { 201 b.append(theResourceName); 202 if (isNotBlank(theId)) { 203 b.append('/'); 204 b.append(theId); 205 if (isNotBlank(theVersion)) { 206 b.append("/_history/"); 207 b.append(theVersion); 208 } 209 } 210 } 211 if (b.length() > 0) { 212 b.append('/'); 213 } 214 if (!theOperationName.startsWith("$")) { 215 b.append("$"); 216 } 217 b.append(theOperationName); 218 219 if (!theUseHttpGet) { 220 return new HttpPostClientInvocation(theContext, theInput, b.toString()); 221 } 222 FhirTerser t = theContext.newTerser(); 223 List<IBase> parameters = t.getValues(theInput, "Parameters.parameter"); 224 225 Map<String, List<String>> params = new LinkedHashMap<>(); 226 for (Object nextParameter : parameters) { 227 IPrimitiveType<?> nextNameDt = (IPrimitiveType<?>) t.getSingleValueOrNull((IBase) nextParameter, "name"); 228 if (nextNameDt == null || nextNameDt.isEmpty()) { 229 ourLog.warn( 230 "Ignoring input parameter with no value in Parameters.parameter.name in operation client invocation"); 231 continue; 232 } 233 String nextName = nextNameDt.getValueAsString(); 234 if (!params.containsKey(nextName)) { 235 params.put(nextName, new ArrayList<>()); 236 } 237 238 IBaseDatatype value = (IBaseDatatype) t.getSingleValueOrNull((IBase) nextParameter, "value[x]"); 239 if (value == null) { 240 continue; 241 } 242 if (!(value instanceof IPrimitiveType)) { 243 throw new IllegalArgumentException(Msg.code(1453) 244 + "Can not invoke operation as HTTP GET when it has parameters with a composite (non priitive) datatype as the value. Found value: " 245 + value.getClass().getName()); 246 } 247 IPrimitiveType<?> primitive = (IPrimitiveType<?>) value; 248 params.get(nextName).add(primitive.getValueAsString()); 249 } 250 return new HttpGetClientInvocation(theContext, params, b.toString()); 251 } 252 253 public static BaseHttpClientInvocation createProcessMsgInvocation( 254 FhirContext theContext, 255 String theOperationName, 256 IBaseBundle theInput, 257 Map<String, List<String>> urlParams) { 258 StringBuilder b = new StringBuilder(); 259 260 if (b.length() > 0) { 261 b.append('/'); 262 } 263 if (!theOperationName.startsWith("$")) { 264 b.append("$"); 265 } 266 b.append(theOperationName); 267 268 BaseHttpClientInvocation.appendExtraParamsWithQuestionMark(urlParams, b, b.indexOf("?") == -1); 269 270 return new HttpPostClientInvocation(theContext, theInput, b.toString()); 271 } 272}