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