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}