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