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}