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}