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