001package ca.uhn.fhir.rest.client.method; 002 003/* 004 * #%L 005 * HAPI FHIR - Client Framework 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 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.i18n.Msg; 024import ca.uhn.fhir.context.ConfigurationException; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.model.api.annotation.Description; 027import ca.uhn.fhir.model.valueset.BundleTypeEnum; 028import ca.uhn.fhir.rest.annotation.Operation; 029import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 030import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; 031import ca.uhn.fhir.rest.param.ParameterUtil; 032import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 033import ca.uhn.fhir.util.FhirTerser; 034import ca.uhn.fhir.util.ParametersUtil; 035import org.hl7.fhir.instance.model.api.*; 036 037import java.lang.reflect.Method; 038import java.lang.reflect.Modifier; 039import java.util.ArrayList; 040import java.util.LinkedHashMap; 041import java.util.List; 042import java.util.Map; 043 044import static org.apache.commons.lang3.StringUtils.isBlank; 045import static org.apache.commons.lang3.StringUtils.isNotBlank; 046 047public class OperationMethodBinding extends BaseResourceReturningMethodBinding { 048 049 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class); 050 private final boolean myIdempotent; 051 private final Integer myIdParamIndex; 052 private final String myName; 053 private final RestOperationTypeEnum myOtherOperationType; 054 private final ReturnTypeEnum myReturnType; 055 private BundleTypeEnum myBundleType; 056 private String myDescription; 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 BundleTypeEnum theBundleType) { 061 super(theReturnResourceType, theMethod, theContext, theProvider); 062 063 myBundleType = theBundleType; 064 myIdempotent = theIdempotent; 065 myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); 066 067 Description description = theMethod.getAnnotation(Description.class); 068 if (description != null) { 069 myDescription = ParametersUtil.extractDescription(description); 070 } 071 if (isBlank(myDescription)) { 072 myDescription = null; 073 } 074 075 if (isBlank(theOperationName)) { 076 throw new ConfigurationException(Msg.code(1452) + "Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName() 077 + " but this annotation has no name defined"); 078 } 079 if (theOperationName.startsWith("$") == false) { 080 theOperationName = "$" + theOperationName; 081 } 082 myName = theOperationName; 083 084 if (theReturnTypeFromRp != null) { 085 setResourceName(theContext.getResourceType(theReturnTypeFromRp)); 086 } else { 087 if (Modifier.isAbstract(theOperationType.getModifiers()) == false) { 088 setResourceName(theContext.getResourceType(theOperationType)); 089 } else { 090 setResourceName(null); 091 } 092 } 093 094 myReturnType = ReturnTypeEnum.RESOURCE; 095 096 if (getResourceName() == null) { 097 myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; 098 } else if (myIdParamIndex == null) { 099 myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; 100 } else { 101 myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; 102 } 103 104 } 105 106 public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, 107 Operation theAnnotation) { 108 this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.bundleType()); 109 } 110 111 public String getDescription() { 112 return myDescription; 113 } 114 115 public void setDescription(String theDescription) { 116 myDescription = theDescription; 117 } 118 119 /** 120 * Returns the name of the operation, starting with "$" 121 */ 122 public String getName() { 123 return myName; 124 } 125 126 @Override 127 protected BundleTypeEnum getResponseBundleType() { 128 return myBundleType; 129 } 130 131 @Override 132 public RestOperationTypeEnum getRestOperationType() { 133 return myOtherOperationType; 134 } 135 136 @Override 137 public ReturnTypeEnum getReturnType() { 138 return myReturnType; 139 } 140 141 @Override 142 public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { 143 String id = null; 144 if (myIdParamIndex != null) { 145 IIdType idDt = (IIdType) theArgs[myIdParamIndex]; 146 id = idDt.getValue(); 147 } 148 IBaseParameters parameters = (IBaseParameters) getContext().getResourceDefinition("Parameters").newInstance(); 149 150 if (theArgs != null) { 151 for (int idx = 0; idx < theArgs.length; idx++) { 152 IParameter nextParam = getParameters().get(idx); 153 nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, parameters); 154 } 155 } 156 157 return createOperationInvocation(getContext(), getResourceName(), id, null, myName, parameters, false); 158 } 159 160 public boolean isIdempotent() { 161 return myIdempotent; 162 } 163 164 public static BaseHttpClientInvocation createOperationInvocation(FhirContext theContext, String theResourceName, String theId, String theVersion, String theOperationName, IBaseParameters theInput, 165 boolean theUseHttpGet) { 166 StringBuilder b = new StringBuilder(); 167 if (theResourceName != null) { 168 b.append(theResourceName); 169 if (isNotBlank(theId)) { 170 b.append('/'); 171 b.append(theId); 172 if (isNotBlank(theVersion)) { 173 b.append("/_history/"); 174 b.append(theVersion); 175 } 176 } 177 } 178 if (b.length() > 0) { 179 b.append('/'); 180 } 181 if (!theOperationName.startsWith("$")) { 182 b.append("$"); 183 } 184 b.append(theOperationName); 185 186 if (!theUseHttpGet) { 187 return new HttpPostClientInvocation(theContext, theInput, b.toString()); 188 } 189 FhirTerser t = theContext.newTerser(); 190 List<IBase> parameters = t.getValues(theInput, "Parameters.parameter"); 191 192 Map<String, List<String>> params = new LinkedHashMap<>(); 193 for (Object nextParameter : parameters) { 194 IPrimitiveType<?> nextNameDt = (IPrimitiveType<?>) t.getSingleValueOrNull((IBase) nextParameter, "name"); 195 if (nextNameDt == null || nextNameDt.isEmpty()) { 196 ourLog.warn("Ignoring input parameter with no value in Parameters.parameter.name in operation client invocation"); 197 continue; 198 } 199 String nextName = nextNameDt.getValueAsString(); 200 if (!params.containsKey(nextName)) { 201 params.put(nextName, new ArrayList<>()); 202 } 203 204 IBaseDatatype value = (IBaseDatatype) t.getSingleValueOrNull((IBase) nextParameter, "value[x]"); 205 if (value == null) { 206 continue; 207 } 208 if (!(value instanceof IPrimitiveType)) { 209 throw new IllegalArgumentException(Msg.code(1453) + "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()); 210 } 211 IPrimitiveType<?> primitive = (IPrimitiveType<?>) value; 212 params.get(nextName).add(primitive.getValueAsString()); 213 } 214 return new HttpGetClientInvocation(theContext, params, b.toString()); 215 } 216 217 public static BaseHttpClientInvocation createProcessMsgInvocation(FhirContext theContext, String theOperationName, IBaseBundle theInput, Map<String, List<String>> urlParams) { 218 StringBuilder b = new StringBuilder(); 219 220 if (b.length() > 0) { 221 b.append('/'); 222 } 223 if (!theOperationName.startsWith("$")) { 224 b.append("$"); 225 } 226 b.append(theOperationName); 227 228 BaseHttpClientInvocation.appendExtraParamsWithQuestionMark(urlParams, b, b.indexOf("?") == -1); 229 230 return new HttpPostClientInvocation(theContext, theInput, b.toString()); 231 232 } 233 234}