001package ca.uhn.fhir.rest.server.method;
002
003/*
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2021 Smile CDR, Inc.
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.valueset.BundleTypeEnum;
026import ca.uhn.fhir.parser.DataFormatException;
027import ca.uhn.fhir.rest.annotation.IdParam;
028import ca.uhn.fhir.rest.annotation.Operation;
029import ca.uhn.fhir.rest.annotation.OperationParam;
030import ca.uhn.fhir.rest.annotation.OptionalParam;
031import ca.uhn.fhir.rest.annotation.RequiredParam;
032import ca.uhn.fhir.rest.api.RequestTypeEnum;
033import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
034import ca.uhn.fhir.rest.api.server.IBundleProvider;
035import ca.uhn.fhir.rest.api.server.IRestfulServer;
036import ca.uhn.fhir.rest.api.server.RequestDetails;
037import ca.uhn.fhir.rest.param.ParameterUtil;
038import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
039import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
040import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
041import ca.uhn.fhir.util.ParametersUtil;
042import org.apache.commons.lang3.builder.ToStringBuilder;
043import org.apache.commons.lang3.builder.ToStringStyle;
044import org.hl7.fhir.instance.model.api.IBase;
045import org.hl7.fhir.instance.model.api.IBaseResource;
046
047import javax.annotation.Nonnull;
048import java.io.IOException;
049import java.lang.annotation.Annotation;
050import java.lang.reflect.Method;
051import java.lang.reflect.Modifier;
052import java.util.ArrayList;
053import java.util.Collections;
054import java.util.List;
055
056import static org.apache.commons.lang3.StringUtils.isBlank;
057import static org.apache.commons.lang3.StringUtils.isNotBlank;
058
059public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
060
061        public static final String WILDCARD_NAME = "$" + Operation.NAME_MATCH_ALL;
062        private final boolean myIdempotent;
063        private final Integer myIdParamIndex;
064        private final String myName;
065        private final RestOperationTypeEnum myOtherOperationType;
066        private final ReturnTypeEnum myReturnType;
067        private final String myShortDescription;
068        private boolean myGlobal;
069        private BundleTypeEnum myBundleType;
070        private boolean myCanOperateAtInstanceLevel;
071        private boolean myCanOperateAtServerLevel;
072        private boolean myCanOperateAtTypeLevel;
073        private String myDescription;
074        private List<ReturnType> myReturnParams;
075        private boolean myManualRequestMode;
076        private boolean myManualResponseMode;
077
078        /**
079         * Constructor - This is the constructor that is called when binding a
080         * standard @Operation method.
081         */
082        public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider,
083                                                                                        Operation theAnnotation) {
084                this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.typeName(), theAnnotation.returnParameters(),
085                        theAnnotation.bundleType(), theAnnotation.global());
086
087                myManualRequestMode = theAnnotation.manualRequest();
088                myManualResponseMode = theAnnotation.manualResponse();
089        }
090
091        protected OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider,
092                                                                                                boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType, String theOperationTypeName,
093                                                                                                OperationParam[] theReturnParams, BundleTypeEnum theBundleType, boolean theGlobal) {
094                super(theReturnResourceType, theMethod, theContext, theProvider);
095
096                myBundleType = theBundleType;
097                myIdempotent = theIdempotent;
098                myDescription = ParametersUtil.extractDescription(theMethod);
099                myShortDescription = ParametersUtil.extractShortDefinition(theMethod);
100                myGlobal = theGlobal;
101
102                for (Annotation[] nextParamAnnotations : theMethod.getParameterAnnotations()) {
103                        for (Annotation nextParam : nextParamAnnotations) {
104                                if (nextParam instanceof OptionalParam || nextParam instanceof RequiredParam) {
105                                        throw new ConfigurationException("Illegal method parameter annotation @" + nextParam.annotationType().getSimpleName() + " on method: " + theMethod.toString());
106                                }
107                        }
108                }
109
110                if (isBlank(theOperationName)) {
111                        throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName()
112                                + " but this annotation has no name defined");
113                }
114                if (theOperationName.startsWith("$") == false) {
115                        theOperationName = "$" + theOperationName;
116                }
117                myName = theOperationName;
118
119                try {
120                        if (theReturnTypeFromRp != null) {
121                                setResourceName(theContext.getResourceType(theReturnTypeFromRp));
122                        } else if (theOperationType != null && Modifier.isAbstract(theOperationType.getModifiers()) == false) {
123                                setResourceName(theContext.getResourceType(theOperationType));
124                        } else if (isNotBlank(theOperationTypeName)) {
125                                setResourceName(theContext.getResourceType(theOperationTypeName));
126                        } else {
127                                setResourceName(null);
128                        }
129                } catch (DataFormatException e) {
130                        throw new ConfigurationException("Failed to bind method " + theMethod + " - " + e.getMessage(), e);
131                }
132
133                if (theMethod.getReturnType().equals(IBundleProvider.class)) {
134                        myReturnType = ReturnTypeEnum.BUNDLE;
135                } else {
136                        myReturnType = ReturnTypeEnum.RESOURCE;
137                }
138
139                myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
140                if (getResourceName() == null) {
141                        myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER;
142                        if (myIdParamIndex != null) {
143                                myCanOperateAtInstanceLevel = true;
144                        } else {
145                                myCanOperateAtServerLevel = true;
146                        }
147                } else if (myIdParamIndex == null) {
148                        myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
149                        myCanOperateAtTypeLevel = true;
150                } else {
151                        myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
152                        myCanOperateAtInstanceLevel = true;
153                        for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) {
154                                if (next instanceof IdParam) {
155                                        myCanOperateAtTypeLevel = ((IdParam) next).optional() == true;
156                                }
157                        }
158                }
159
160                myReturnParams = new ArrayList<>();
161                if (theReturnParams != null) {
162                        for (OperationParam next : theReturnParams) {
163                                ReturnType type = new ReturnType();
164                                type.setName(next.name());
165                                type.setMin(next.min());
166                                type.setMax(next.max());
167                                if (type.getMax() == OperationParam.MAX_DEFAULT) {
168                                        type.setMax(1);
169                                }
170                                if (!next.type().equals(IBase.class)) {
171                                        if (next.type().isInterface() || Modifier.isAbstract(next.type().getModifiers())) {
172                                                throw new ConfigurationException("Invalid value for @OperationParam.type(): " + next.type().getName());
173                                        }
174                                        type.setType(theContext.getElementDefinition(next.type()).getName());
175                                }
176                                myReturnParams.add(type);
177                        }
178                }
179
180                // Parameter Validation
181                if (myCanOperateAtInstanceLevel && !isGlobalMethod() && getResourceName() == null) {
182                        throw new ConfigurationException("@" + Operation.class.getSimpleName() + " method is an instance level method (it has an @" + IdParam.class.getSimpleName() + " parameter) but is not marked as global() and is not declared in a resource provider: " + theMethod.getName());
183                }
184
185        }
186
187        public String getShortDescription() {
188                return myShortDescription;
189        }
190
191        @Override
192        public boolean isGlobalMethod() {
193                return myGlobal;
194        }
195
196        public String getDescription() {
197                return myDescription;
198        }
199
200        public void setDescription(String theDescription) {
201                myDescription = theDescription;
202        }
203
204        /**
205         * Returns the name of the operation, starting with "$"
206         */
207        public String getName() {
208                return myName;
209        }
210
211        @Override
212        protected BundleTypeEnum getResponseBundleType() {
213                return myBundleType;
214        }
215
216        @Nonnull
217        @Override
218        public RestOperationTypeEnum getRestOperationType() {
219                return myOtherOperationType;
220        }
221
222        public List<ReturnType> getReturnParams() {
223                return Collections.unmodifiableList(myReturnParams);
224        }
225
226        @Override
227        public ReturnTypeEnum getReturnType() {
228                return myReturnType;
229        }
230
231        @Override
232        public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
233                if (isBlank(theRequest.getOperation())) {
234                        return MethodMatchEnum.NONE;
235                }
236
237                if (!myName.equals(theRequest.getOperation())) {
238                        if (!myName.equals(WILDCARD_NAME)) {
239                                return MethodMatchEnum.NONE;
240                        }
241                }
242
243                if (getResourceName() == null) {
244                        if (isNotBlank(theRequest.getResourceName())) {
245                                if (!isGlobalMethod()) {
246                                        return MethodMatchEnum.NONE;
247                                }
248                        }
249                }
250
251                if (getResourceName() != null && !getResourceName().equals(theRequest.getResourceName())) {
252                        return MethodMatchEnum.NONE;
253                }
254
255                RequestTypeEnum requestType = theRequest.getRequestType();
256                if (requestType != RequestTypeEnum.GET && requestType != RequestTypeEnum.POST) {
257                        // Operations can only be invoked with GET and POST
258                        return MethodMatchEnum.NONE;
259                }
260
261                boolean requestHasId = theRequest.getId() != null;
262                if (requestHasId) {
263                        return myCanOperateAtInstanceLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE;
264                }
265                if (isNotBlank(theRequest.getResourceName())) {
266                        return myCanOperateAtTypeLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE;
267                }
268                return myCanOperateAtServerLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE;
269        }
270
271        @Override
272        public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) {
273                RestOperationTypeEnum retVal = super.getRestOperationType(theRequestDetails);
274
275                if (retVal == RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE) {
276                        if (theRequestDetails.getId() == null) {
277                                retVal = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
278                        }
279                }
280
281                if (myGlobal && theRequestDetails.getId() != null && theRequestDetails.getId().hasIdPart()) {
282                        retVal = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
283                } else if (myGlobal && isNotBlank(theRequestDetails.getResourceName())) {
284                        retVal = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
285                }
286
287                return retVal;
288        }
289
290        @Override
291        public String toString() {
292                return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
293                        .append("name", myName)
294                        .append("methodName", getMethod().getDeclaringClass().getSimpleName() + "." + getMethod().getName())
295                        .append("serverLevel", myCanOperateAtServerLevel)
296                        .append("typeLevel", myCanOperateAtTypeLevel)
297                        .append("instanceLevel", myCanOperateAtInstanceLevel)
298                        .toString();
299        }
300
301        @Override
302        public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException {
303                if (theRequest.getRequestType() == RequestTypeEnum.POST && !myManualRequestMode) {
304                        IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, this, null);
305                        theRequest.getUserData().put(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY, requestContents);
306                }
307                return super.invokeServer(theServer, theRequest);
308        }
309
310        @Override
311        public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException {
312                if (theRequest.getRequestType() == RequestTypeEnum.POST) {
313                        // all good
314                } else if (theRequest.getRequestType() == RequestTypeEnum.GET) {
315                        if (!myIdempotent) {
316                                String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name());
317                                throw new MethodNotAllowedException(message, RequestTypeEnum.POST);
318                        }
319                } else {
320                        if (!myIdempotent) {
321                                String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name());
322                                throw new MethodNotAllowedException(message, RequestTypeEnum.POST);
323                        }
324                        String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.GET.name(), RequestTypeEnum.POST.name());
325                        throw new MethodNotAllowedException(message, RequestTypeEnum.GET, RequestTypeEnum.POST);
326                }
327
328                if (myIdParamIndex != null) {
329                        theMethodParams[myIdParamIndex] = theRequest.getId();
330                }
331
332                Object response = invokeServerMethod(theRequest, theMethodParams);
333                if (myManualResponseMode) {
334                        return null;
335                }
336
337                IBundleProvider retVal = toResourceList(response);
338                return retVal;
339        }
340
341        public boolean isCanOperateAtInstanceLevel() {
342                return this.myCanOperateAtInstanceLevel;
343        }
344
345        public boolean isCanOperateAtServerLevel() {
346                return this.myCanOperateAtServerLevel;
347        }
348
349        public boolean isCanOperateAtTypeLevel() {
350                return myCanOperateAtTypeLevel;
351        }
352
353        public boolean isIdempotent() {
354                return myIdempotent;
355        }
356
357        @Override
358        protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) {
359                super.populateActionRequestDetailsForInterceptor(theRequestDetails, theDetails, theMethodParams);
360                IBaseResource resource = (IBaseResource) theRequestDetails.getUserData().get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY);
361                theRequestDetails.setResource(resource);
362                if (theDetails != null) {
363                        theDetails.setResource(resource);
364                }
365        }
366
367        public boolean isManualRequestMode() {
368                return myManualRequestMode;
369        }
370
371        public static class ReturnType {
372                private int myMax;
373                private int myMin;
374                private String myName;
375                /**
376                 * http://hl7-fhir.github.io/valueset-operation-parameter-type.html
377                 */
378                private String myType;
379
380                public int getMax() {
381                        return myMax;
382                }
383
384                public void setMax(int theMax) {
385                        myMax = theMax;
386                }
387
388                public int getMin() {
389                        return myMin;
390                }
391
392                public void setMin(int theMin) {
393                        myMin = theMin;
394                }
395
396                public String getName() {
397                        return myName;
398                }
399
400                public void setName(String theName) {
401                        myName = theName;
402                }
403
404                public String getType() {
405                        return myType;
406                }
407
408                public void setType(String theType) {
409                        myType = theType;
410                }
411        }
412
413}