001package ca.uhn.fhir.rest.client.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Client Framework
006 * %%
007 * Copyright (C) 2014 - 2018 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, 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 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                        }
229                }
230                if (b.length() > 0) {
231                        b.append('/');
232                }
233                if (!theOperationName.startsWith("$")) {
234                        b.append("$");
235                }
236                b.append(theOperationName);
237
238                if (!theUseHttpGet) {
239                        return new HttpPostClientInvocation(theContext, theInput, b.toString());
240                }
241                FhirTerser t = theContext.newTerser();
242                List<Object> parameters = t.getValues(theInput, "Parameters.parameter");
243
244                Map<String, List<String>> params = new LinkedHashMap<String, List<String>>();
245                for (Object nextParameter : parameters) {
246                        IPrimitiveType<?> nextNameDt = (IPrimitiveType<?>) t.getSingleValueOrNull((IBase) nextParameter, "name");
247                        if (nextNameDt == null || nextNameDt.isEmpty()) {
248                                ourLog.warn("Ignoring input parameter with no value in Parameters.parameter.name in operation client invocation");
249                                continue;
250                        }
251                        String nextName = nextNameDt.getValueAsString();
252                        if (!params.containsKey(nextName)) {
253                                params.put(nextName, new ArrayList<String>());
254                        }
255
256                        IBaseDatatype value = (IBaseDatatype) t.getSingleValueOrNull((IBase) nextParameter, "value[x]");
257                        if (value == null) {
258                                continue;
259                        }
260                        if (!(value instanceof IPrimitiveType)) {
261                                throw new IllegalArgumentException(
262                                                "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());
263                        }
264                        IPrimitiveType<?> primitive = (IPrimitiveType<?>) value;
265                        params.get(nextName).add(primitive.getValueAsString());
266                }
267                return new HttpGetClientInvocation(theContext, params, b.toString());
268        }
269
270        public static BaseHttpClientInvocation createProcessMsgInvocation(FhirContext theContext, String theOperationName, IBaseBundle theInput, Map<String, List<String>> urlParams) {
271                StringBuilder b = new StringBuilder();
272
273                if (b.length() > 0) {
274                        b.append('/');
275                }
276                if (!theOperationName.startsWith("$")) {
277                        b.append("$");
278                }
279                b.append(theOperationName);
280
281                BaseHttpClientInvocation.appendExtraParamsWithQuestionMark(urlParams, b, b.indexOf("?") == -1);
282
283                return new HttpPostClientInvocation(theContext, theInput, b.toString());
284
285        }
286
287        public static class ReturnType {
288                private int myMax;
289                private int myMin;
290                private String myName;
291                /**
292                 * http://hl7-fhir.github.io/valueset-operation-parameter-type.html
293                 */
294                private String myType;
295
296                public int getMax() {
297                        return myMax;
298                }
299
300                public int getMin() {
301                        return myMin;
302                }
303
304                public String getName() {
305                        return myName;
306                }
307
308                public String getType() {
309                        return myType;
310                }
311
312                public void setMax(int theMax) {
313                        myMax = theMax;
314                }
315
316                public void setMin(int theMin) {
317                        myMin = theMin;
318                }
319
320                public void setName(String theName) {
321                        myName = theName;
322                }
323
324                public void setType(String theType) {
325                        myType = theType;
326                }
327        }
328
329}