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 */
022import static org.apache.commons.lang3.StringUtils.isBlank;
023import static org.apache.commons.lang3.StringUtils.isNotBlank;
024
025import java.lang.annotation.Annotation;
026import java.lang.reflect.Method;
027import java.lang.reflect.Modifier;
028import java.util.*;
029
030import org.hl7.fhir.instance.model.api.*;
031
032import ca.uhn.fhir.context.ConfigurationException;
033import ca.uhn.fhir.context.FhirContext;
034import ca.uhn.fhir.model.api.annotation.Description;
035import ca.uhn.fhir.model.valueset.BundleTypeEnum;
036import ca.uhn.fhir.rest.annotation.*;
037import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
038import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation;
039import ca.uhn.fhir.rest.param.ParameterUtil;
040import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
041import ca.uhn.fhir.util.FhirTerser;
042
043public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
044
045        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class);
046        private BundleTypeEnum myBundleType;
047        private boolean myCanOperateAtInstanceLevel;
048        private boolean myCanOperateAtServerLevel;
049        private boolean myCanOperateAtTypeLevel;
050        private String myDescription;
051        private final boolean myIdempotent;
052        private final Integer myIdParamIndex;
053        private final String myName;
054        private final RestOperationTypeEnum myOtherOperatiopnType;
055        private List<ReturnType> myReturnParams;
056        private final ReturnTypeEnum myReturnType;
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                        OperationParam[] theReturnParams, BundleTypeEnum theBundleType) {
061                super(theReturnResourceType, theMethod, theContext, theProvider);
062
063                myBundleType = theBundleType;
064                myIdempotent = theIdempotent;
065                myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
066                if (myIdParamIndex != null) {
067                        for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) {
068                                if (next instanceof IdParam) {
069                                        myCanOperateAtTypeLevel = ((IdParam) next).optional() == true;
070                                }
071                        }
072                } else {
073                        myCanOperateAtTypeLevel = true;
074                }
075
076                Description description = theMethod.getAnnotation(Description.class);
077                if (description != null) {
078                        myDescription = description.formalDefinition();
079                        if (isBlank(myDescription)) {
080                                myDescription = description.shortDefinition();
081                        }
082                }
083                if (isBlank(myDescription)) {
084                        myDescription = null;
085                }
086
087                if (isBlank(theOperationName)) {
088                        throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName()
089                                        + " but this annotation has no name defined");
090                }
091                if (theOperationName.startsWith("$") == false) {
092                        theOperationName = "$" + theOperationName;
093                }
094                myName = theOperationName;
095
096                if (theReturnTypeFromRp != null) {
097                        setResourceName(theContext.getResourceDefinition(theReturnTypeFromRp).getName());
098                } else {
099                        if (Modifier.isAbstract(theOperationType.getModifiers()) == false) {
100                                setResourceName(theContext.getResourceDefinition(theOperationType).getName());
101                        } else {
102                                setResourceName(null);
103                        }
104                }
105
106                myReturnType = ReturnTypeEnum.RESOURCE;
107
108                if (getResourceName() == null) {
109                        myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER;
110                } else if (myIdParamIndex == null) {
111                        myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
112                } else {
113                        myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
114                }
115
116                myReturnParams = new ArrayList<OperationMethodBinding.ReturnType>();
117                if (theReturnParams != null) {
118                        for (OperationParam next : theReturnParams) {
119                                ReturnType type = new ReturnType();
120                                type.setName(next.name());
121                                type.setMin(next.min());
122                                type.setMax(next.max());
123                                if (type.getMax() == OperationParam.MAX_DEFAULT) {
124                                        type.setMax(1);
125                                }
126                                if (!next.type().equals(IBase.class)) {
127                                        if (next.type().isInterface() || Modifier.isAbstract(next.type().getModifiers())) {
128                                                throw new ConfigurationException("Invalid value for @OperationParam.type(): " + next.type().getName());
129                                        }
130                                        type.setType(theContext.getElementDefinition(next.type()).getName());
131                                }
132                                myReturnParams.add(type);
133                        }
134                }
135
136                if (myIdParamIndex != null) {
137                        myCanOperateAtInstanceLevel = true;
138                }
139                if (getResourceName() == null) {
140                        myCanOperateAtServerLevel = true;
141                }
142
143        }
144
145        public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider,
146                        Operation theAnnotation) {
147                this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.returnParameters(),
148                                theAnnotation.bundleType());
149        }
150
151        public String getDescription() {
152                return myDescription;
153        }
154
155        /**
156         * Returns the name of the operation, starting with "$"
157         */
158        public String getName() {
159                return myName;
160        }
161
162        @Override
163        protected BundleTypeEnum getResponseBundleType() {
164                return myBundleType;
165        }
166
167        @Override
168        public RestOperationTypeEnum getRestOperationType() {
169                return myOtherOperatiopnType;
170        }
171
172        public List<ReturnType> getReturnParams() {
173                return Collections.unmodifiableList(myReturnParams);
174        }
175
176        @Override
177        public ReturnTypeEnum getReturnType() {
178                return myReturnType;
179        }
180
181        @Override
182        public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
183                String id = null;
184                if (myIdParamIndex != null) {
185                        IIdType idDt = (IIdType) theArgs[myIdParamIndex];
186                        id = idDt.getValue();
187                }
188                IBaseParameters parameters = (IBaseParameters) getContext().getResourceDefinition("Parameters").newInstance();
189
190                if (theArgs != null) {
191                        for (int idx = 0; idx < theArgs.length; idx++) {
192                                IParameter nextParam = getParameters().get(idx);
193                                nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, parameters);
194                        }
195                }
196
197                return createOperationInvocation(getContext(), getResourceName(), id, null, myName, parameters, false);
198        }
199
200        public boolean isCanOperateAtInstanceLevel() {
201                return this.myCanOperateAtInstanceLevel;
202        }
203
204        public boolean isCanOperateAtServerLevel() {
205                return this.myCanOperateAtServerLevel;
206        }
207
208        public boolean isCanOperateAtTypeLevel() {
209                return myCanOperateAtTypeLevel;
210        }
211
212        public boolean isIdempotent() {
213                return myIdempotent;
214        }
215
216        public void setDescription(String theDescription) {
217                myDescription = theDescription;
218        }
219
220        public static BaseHttpClientInvocation createOperationInvocation(FhirContext theContext, String theResourceName, String theId, String theVersion, String theOperationName, IBaseParameters theInput,
221                        boolean theUseHttpGet) {
222                StringBuilder b = new StringBuilder();
223                if (theResourceName != null) {
224                        b.append(theResourceName);
225                        if (isNotBlank(theId)) {
226                                b.append('/');
227                                b.append(theId);
228                                if (isNotBlank(theVersion)) {
229                                        b.append("/_history/");
230                                        b.append(theVersion);
231                                }
232                        }
233                }
234                if (b.length() > 0) {
235                        b.append('/');
236                }
237                if (!theOperationName.startsWith("$")) {
238                        b.append("$");
239                }
240                b.append(theOperationName);
241
242                if (!theUseHttpGet) {
243                        return new HttpPostClientInvocation(theContext, theInput, b.toString());
244                }
245                FhirTerser t = theContext.newTerser();
246                List<Object> parameters = t.getValues(theInput, "Parameters.parameter");
247
248                Map<String, List<String>> params = new LinkedHashMap<String, List<String>>();
249                for (Object nextParameter : parameters) {
250                        IPrimitiveType<?> nextNameDt = (IPrimitiveType<?>) t.getSingleValueOrNull((IBase) nextParameter, "name");
251                        if (nextNameDt == null || nextNameDt.isEmpty()) {
252                                ourLog.warn("Ignoring input parameter with no value in Parameters.parameter.name in operation client invocation");
253                                continue;
254                        }
255                        String nextName = nextNameDt.getValueAsString();
256                        if (!params.containsKey(nextName)) {
257                                params.put(nextName, new ArrayList<String>());
258                        }
259
260                        IBaseDatatype value = (IBaseDatatype) t.getSingleValueOrNull((IBase) nextParameter, "value[x]");
261                        if (value == null) {
262                                continue;
263                        }
264                        if (!(value instanceof IPrimitiveType)) {
265                                throw new IllegalArgumentException(
266                                                "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());
267                        }
268                        IPrimitiveType<?> primitive = (IPrimitiveType<?>) value;
269                        params.get(nextName).add(primitive.getValueAsString());
270                }
271                return new HttpGetClientInvocation(theContext, params, b.toString());
272        }
273
274        public static BaseHttpClientInvocation createProcessMsgInvocation(FhirContext theContext, String theOperationName, IBaseBundle theInput, Map<String, List<String>> urlParams) {
275                StringBuilder b = new StringBuilder();
276
277                if (b.length() > 0) {
278                        b.append('/');
279                }
280                if (!theOperationName.startsWith("$")) {
281                        b.append("$");
282                }
283                b.append(theOperationName);
284
285                BaseHttpClientInvocation.appendExtraParamsWithQuestionMark(urlParams, b, b.indexOf("?") == -1);
286
287                return new HttpPostClientInvocation(theContext, theInput, b.toString());
288
289        }
290
291        public static class ReturnType {
292                private int myMax;
293                private int myMin;
294                private String myName;
295                /**
296                 * http://hl7-fhir.github.io/valueset-operation-parameter-type.html
297                 */
298                private String myType;
299
300                public int getMax() {
301                        return myMax;
302                }
303
304                public int getMin() {
305                        return myMin;
306                }
307
308                public String getName() {
309                        return myName;
310                }
311
312                public String getType() {
313                        return myType;
314                }
315
316                public void setMax(int theMax) {
317                        myMax = theMax;
318                }
319
320                public void setMin(int theMin) {
321                        myMin = theMin;
322                }
323
324                public void setName(String theName) {
325                        myName = theName;
326                }
327
328                public void setType(String theType) {
329                        myType = theType;
330                }
331        }
332
333}