001package ca.uhn.fhir.rest.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2016 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.ArrayList;
029import java.util.Collections;
030import java.util.LinkedHashMap;
031import java.util.List;
032import java.util.Map;
033
034import org.hl7.fhir.instance.model.api.IBase;
035import org.hl7.fhir.instance.model.api.IBaseDatatype;
036import org.hl7.fhir.instance.model.api.IBaseParameters;
037import org.hl7.fhir.instance.model.api.IBaseResource;
038import org.hl7.fhir.instance.model.api.IIdType;
039import org.hl7.fhir.instance.model.api.IPrimitiveType;
040
041import ca.uhn.fhir.context.ConfigurationException;
042import ca.uhn.fhir.context.FhirContext;
043import ca.uhn.fhir.context.FhirVersionEnum;
044import ca.uhn.fhir.model.api.Bundle;
045import ca.uhn.fhir.model.api.annotation.Description;
046import ca.uhn.fhir.model.valueset.BundleTypeEnum;
047import ca.uhn.fhir.rest.annotation.IdParam;
048import ca.uhn.fhir.rest.annotation.Operation;
049import ca.uhn.fhir.rest.annotation.OperationParam;
050import ca.uhn.fhir.rest.api.RequestTypeEnum;
051import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
052import ca.uhn.fhir.rest.client.BaseHttpClientInvocation;
053import ca.uhn.fhir.rest.server.IBundleProvider;
054import ca.uhn.fhir.rest.server.IRestfulServer;
055import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
056import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
057import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
058import ca.uhn.fhir.util.FhirTerser;
059
060public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
061
062        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class);
063        private boolean myCanOperateAtInstanceLevel;
064        private boolean myCanOperateAtServerLevel;
065        private String myDescription;
066        private final boolean myIdempotent;
067        private final Integer myIdParamIndex;
068        private final String myName;
069        private final RestOperationTypeEnum myOtherOperatiopnType;
070        private List<ReturnType> myReturnParams;
071        private final ReturnTypeEnum myReturnType;
072        private boolean myCanOperateAtTypeLevel;
073
074        protected OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType,
075                        OperationParam[] theReturnParams) {
076                super(theReturnResourceType, theMethod, theContext, theProvider);
077
078                myIdempotent = theIdempotent;
079                myIdParamIndex = MethodUtil.findIdParameterIndex(theMethod, getContext());
080                if (myIdParamIndex != null) {
081                        for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) {
082                                if (next instanceof IdParam) {
083                                        myCanOperateAtTypeLevel = ((IdParam) next).optional() == true;
084                                }
085                        }
086                } else {
087                        myCanOperateAtTypeLevel = true;
088                }
089                
090                Description description = theMethod.getAnnotation(Description.class);
091                if (description != null) {
092                        myDescription = description.formalDefinition();
093                        if (isBlank(myDescription)) {
094                                myDescription = description.shortDefinition();
095                        }
096                }
097                if (isBlank(myDescription)) {
098                        myDescription = null;
099                }
100
101                if (isBlank(theOperationName)) {
102                        throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName() + " but this annotation has no name defined");
103                }
104                if (theOperationName.startsWith("$") == false) {
105                        theOperationName = "$" + theOperationName;
106                }
107                myName = theOperationName;
108
109                if (theContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU1)) {
110                        throw new ConfigurationException("@" + Operation.class.getSimpleName() + " methods are not supported on servers for FHIR version " + theContext.getVersion().getVersion().name());
111                }
112
113                if (theReturnTypeFromRp != null) {
114                        setResourceName(theContext.getResourceDefinition(theReturnTypeFromRp).getName());
115                } else {
116                        if (Modifier.isAbstract(theOperationType.getModifiers()) == false) {
117                                setResourceName(theContext.getResourceDefinition(theOperationType).getName());
118                        } else {
119                                setResourceName(null);
120                        }
121                }
122
123                if (theMethod.getReturnType().isAssignableFrom(Bundle.class)) {
124                        throw new ConfigurationException("Can not return a DSTU1 bundle from an @" + Operation.class.getSimpleName() + " method. Found in method " + theMethod.getName() + " defined in type " + theMethod.getDeclaringClass().getName());
125                }
126
127                if (theMethod.getReturnType().equals(IBundleProvider.class)) {
128                        myReturnType = ReturnTypeEnum.BUNDLE;
129                } else {
130                        myReturnType = ReturnTypeEnum.RESOURCE;
131                }
132
133                if (getResourceName() == null) {
134                        myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER;
135                } else if (myIdParamIndex == null) {
136                        myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
137                } else {
138                        myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
139                }
140
141                myReturnParams = new ArrayList<OperationMethodBinding.ReturnType>();
142                if (theReturnParams != null) {
143                        for (OperationParam next : theReturnParams) {
144                                ReturnType type = new ReturnType();
145                                type.setName(next.name());
146                                type.setMin(next.min());
147                                type.setMax(next.max());
148                                if (!next.type().equals(IBase.class)) {
149                                        if (next.type().isInterface() || Modifier.isAbstract(next.type().getModifiers())) {
150                                                throw new ConfigurationException("Invalid value for @OperationParam.type(): " + next.type().getName());
151                                        }
152                                        type.setType(theContext.getElementDefinition(next.type()).getName());
153                                }
154                                myReturnParams.add(type);
155                        }
156                }
157
158                if (myIdParamIndex != null) {
159                        myCanOperateAtInstanceLevel = true;
160                }
161                if (getResourceName() == null) {
162                        myCanOperateAtServerLevel = true;
163                }
164
165        }
166
167        public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, Operation theAnnotation) {
168                this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.returnParameters());
169        }
170
171        public String getDescription() {
172                return myDescription;
173        }
174
175        /**
176         * Returns the name of the operation, starting with "$"
177         */
178        public String getName() {
179                return myName;
180        }
181
182        @Override
183        public RestOperationTypeEnum getRestOperationType() {
184                return myOtherOperatiopnType;
185        }
186        
187        @Override
188        protected BundleTypeEnum getResponseBundleType() {
189                return BundleTypeEnum.COLLECTION;
190        }
191
192        public List<ReturnType> getReturnParams() {
193                return Collections.unmodifiableList(myReturnParams);
194        }
195
196        @Override
197        public ReturnTypeEnum getReturnType() {
198                return myReturnType;
199        }
200
201        @Override
202        public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
203                if (getResourceName() == null) {
204                        if (isNotBlank(theRequest.getResourceName())) {
205                                return false;
206                        }
207                } else if (!getResourceName().equals(theRequest.getResourceName())) {
208                        return false;
209                }
210
211                if (!myName.equals(theRequest.getOperation())) {
212                        return false;
213                }
214                
215                boolean requestHasId = theRequest.getId() != null;
216                if (requestHasId) {
217                        if (isCanOperateAtInstanceLevel() == false) {
218                                return false;
219                        }
220                } else {
221                        if (myCanOperateAtTypeLevel == false) {
222                                return false;
223                        }
224                }
225
226                return true;
227        }
228
229        @Override
230        public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
231                String id = null;
232                if (myIdParamIndex != null) {
233                        IIdType idDt = (IIdType) theArgs[myIdParamIndex];
234                        id = idDt.getValue();
235                }
236                IBaseParameters parameters = (IBaseParameters) getContext().getResourceDefinition("Parameters").newInstance();
237
238                if (theArgs != null) {
239                        for (int idx = 0; idx < theArgs.length; idx++) {
240                                IParameter nextParam = getParameters().get(idx);
241                                nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, parameters);
242                        }
243                }
244
245                return createOperationInvocation(getContext(), getResourceName(), id, myName, parameters, false);
246        }
247
248        @Override
249        public Object invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException {
250                if (theRequest.getRequestType() == RequestTypeEnum.POST) {
251                        // always ok
252                } else if (theRequest.getRequestType() == RequestTypeEnum.GET) {
253                        if (!myIdempotent) {
254                                String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name());
255                                throw new MethodNotAllowedException(message, RequestTypeEnum.POST);
256                        }
257                } else {
258                        if (!myIdempotent) {
259                                String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name());
260                                throw new MethodNotAllowedException(message, RequestTypeEnum.POST);
261                        } else {
262                                String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.GET.name(), RequestTypeEnum.POST.name());
263                                throw new MethodNotAllowedException(message, RequestTypeEnum.GET, RequestTypeEnum.POST);
264                        }
265                }
266
267                if (myIdParamIndex != null) {
268                        theMethodParams[myIdParamIndex] = theRequest.getId();
269                }
270
271                Object response = invokeServerMethod(theServer, theRequest, theMethodParams);
272                IBundleProvider retVal = toResourceList(response);
273                return retVal;
274        }
275
276        public boolean isCanOperateAtInstanceLevel() {
277                return this.myCanOperateAtInstanceLevel;
278        }
279
280        public boolean isCanOperateAtServerLevel() {
281                return this.myCanOperateAtServerLevel;
282        }
283
284        public boolean isIdempotent() {
285                return myIdempotent;
286        }
287
288        public void setDescription(String theDescription) {
289                myDescription = theDescription;
290        }
291
292        public static BaseHttpClientInvocation createOperationInvocation(FhirContext theContext, String theResourceName, String theId, String theOperationName, IBaseParameters theInput, boolean theUseHttpGet) {
293                StringBuilder b = new StringBuilder();
294                if (theResourceName != null) {
295                        b.append(theResourceName);
296                        if (isNotBlank(theId)) {
297                                b.append('/');
298                                b.append(theId);
299                        }
300                }
301                if (b.length() > 0) {
302                        b.append('/');
303                }
304                if (!theOperationName.startsWith("$")) {
305                        b.append("$");
306                }
307                b.append(theOperationName);
308
309                if (!theUseHttpGet) {
310                        return new HttpPostClientInvocation(theContext, theInput, b.toString());
311                } else {
312                        FhirTerser t = theContext.newTerser();
313                        List<Object> parameters = t.getValues(theInput, "Parameters.parameter");
314
315                        Map<String, List<String>> params = new LinkedHashMap<String, List<String>>();
316                        for (Object nextParameter : parameters) {
317                                IPrimitiveType<?> nextNameDt = (IPrimitiveType<?>) t.getSingleValueOrNull((IBase) nextParameter, "name");
318                                if (nextNameDt == null || nextNameDt.isEmpty()) {
319                                        ourLog.warn("Ignoring input parameter with no value in Parameters.parameter.name in operation client invocation");
320                                        continue;
321                                }
322                                String nextName = nextNameDt.getValueAsString();
323                                if (!params.containsKey(nextName)) {
324                                        params.put(nextName, new ArrayList<String>());
325                                }
326
327                                IBaseDatatype value = (IBaseDatatype) t.getSingleValueOrNull((IBase) nextParameter, "value[x]");
328                                if (value == null) {
329                                        continue;
330                                }
331                                if (!(value instanceof IPrimitiveType)) {
332                                        throw new IllegalArgumentException("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());
333                                }
334                                IPrimitiveType<?> primitive = (IPrimitiveType<?>) value;
335                                params.get(nextName).add(primitive.getValueAsString());
336                        }
337                        return new HttpGetClientInvocation(params, b.toString());
338                }
339        }
340
341        public static class ReturnType {
342                private int myMax;
343                private int myMin;
344                private String myName;
345                /**
346                 * http://hl7-fhir.github.io/valueset-operation-parameter-type.html
347                 */
348                private String myType;
349
350                public int getMax() {
351                        return myMax;
352                }
353
354                public int getMin() {
355                        return myMin;
356                }
357
358                public String getName() {
359                        return myName;
360                }
361
362                public String getType() {
363                        return myType;
364                }
365
366                public void setMax(int theMax) {
367                        myMax = theMax;
368                }
369
370                public void setMin(int theMin) {
371                        myMin = theMin;
372                }
373
374                public void setName(String theName) {
375                        myName = theName;
376                }
377
378                public void setType(String theType) {
379                        myType = theType;
380                }
381        }
382
383}