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                                if (!next.type().equals(IBase.class)) {
172                                        if (next.type().isInterface() || Modifier.isAbstract(next.type().getModifiers())) {
173                                                throw new ConfigurationException(Msg.code(424) + "Invalid value for @OperationParam.type(): " + next.type().getName());
174                                        }
175                                        type.setType(theContext.getElementDefinition(next.type()).getName());
176                                }
177                                myReturnParams.add(type);
178                        }
179                }
180
181                // Parameter Validation
182                if (myCanOperateAtInstanceLevel && !isGlobalMethod() && getResourceName() == null) {
183                        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());
184                }
185
186        }
187
188        public String getShortDescription() {
189                return myShortDescription;
190        }
191
192        @Override
193        public boolean isGlobalMethod() {
194                return myGlobal;
195        }
196
197        public String getDescription() {
198                return myDescription;
199        }
200
201        public void setDescription(String theDescription) {
202                myDescription = theDescription;
203        }
204
205        /**
206         * Returns the name of the operation, starting with "$"
207         */
208        public String getName() {
209                return myName;
210        }
211
212        @Override
213        protected BundleTypeEnum getResponseBundleType() {
214                return myBundleType;
215        }
216
217        @Nonnull
218        @Override
219        public RestOperationTypeEnum getRestOperationType() {
220                return myOtherOperationType;
221        }
222
223        public List<ReturnType> getReturnParams() {
224                return Collections.unmodifiableList(myReturnParams);
225        }
226
227        @Override
228        public ReturnTypeEnum getReturnType() {
229                return myReturnType;
230        }
231
232        @Override
233        public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
234                if (isBlank(theRequest.getOperation())) {
235                        return MethodMatchEnum.NONE;
236                }
237
238                if (!myName.equals(theRequest.getOperation())) {
239                        if (!myName.equals(WILDCARD_NAME)) {
240                                return MethodMatchEnum.NONE;
241                        }
242                }
243
244                if (getResourceName() == null) {
245                        if (isNotBlank(theRequest.getResourceName())) {
246                                if (!isGlobalMethod()) {
247                                        return MethodMatchEnum.NONE;
248                                }
249                        }
250                }
251
252                if (getResourceName() != null && !getResourceName().equals(theRequest.getResourceName())) {
253                        return MethodMatchEnum.NONE;
254                }
255
256                RequestTypeEnum requestType = theRequest.getRequestType();
257                if (requestType != RequestTypeEnum.GET && requestType != RequestTypeEnum.POST) {
258                        // Operations can only be invoked with GET and POST
259                        return MethodMatchEnum.NONE;
260                }
261
262                boolean requestHasId = theRequest.getId() != null;
263                if (requestHasId) {
264                        return myCanOperateAtInstanceLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE;
265                }
266                if (isNotBlank(theRequest.getResourceName())) {
267                        return myCanOperateAtTypeLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE;
268                }
269                return myCanOperateAtServerLevel ? MethodMatchEnum.EXACT : MethodMatchEnum.NONE;
270        }
271
272        @Override
273        public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) {
274                RestOperationTypeEnum retVal = super.getRestOperationType(theRequestDetails);
275
276                if (retVal == RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE) {
277                        if (theRequestDetails.getId() == null) {
278                                retVal = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
279                        }
280                }
281
282                if (myGlobal && theRequestDetails.getId() != null && theRequestDetails.getId().hasIdPart()) {
283                        retVal = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE;
284                } else if (myGlobal && isNotBlank(theRequestDetails.getResourceName())) {
285                        retVal = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE;
286                }
287
288                return retVal;
289        }
290
291        @Override
292        public String toString() {
293                return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
294                        .append("name", myName)
295                        .append("methodName", getMethod().getDeclaringClass().getSimpleName() + "." + getMethod().getName())
296                        .append("serverLevel", myCanOperateAtServerLevel)
297                        .append("typeLevel", myCanOperateAtTypeLevel)
298                        .append("instanceLevel", myCanOperateAtInstanceLevel)
299                        .toString();
300        }
301
302        @Override
303        public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest) throws BaseServerResponseException, IOException {
304                if (theRequest.getRequestType() == RequestTypeEnum.POST && !myManualRequestMode) {
305                        IBaseResource requestContents = ResourceParameter.loadResourceFromRequest(theRequest, this, null);
306                        theRequest.getUserData().put(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY, requestContents);
307                }
308                return super.invokeServer(theServer, theRequest);
309        }
310
311        @Override
312        public Object invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws BaseServerResponseException {
313                if (theRequest.getRequestType() == RequestTypeEnum.POST) {
314                        // all good
315                } else if (theRequest.getRequestType() == RequestTypeEnum.GET) {
316                        if (!myIdempotent) {
317                                String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name());
318                                throw new MethodNotAllowedException(Msg.code(426) + message, RequestTypeEnum.POST);
319                        }
320                } else {
321                        if (!myIdempotent) {
322                                String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name());
323                                throw new MethodNotAllowedException(Msg.code(427) + message, RequestTypeEnum.POST);
324                        }
325                        String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.GET.name(), RequestTypeEnum.POST.name());
326                        throw new MethodNotAllowedException(Msg.code(428) + message, RequestTypeEnum.GET, RequestTypeEnum.POST);
327                }
328
329                if (myIdParamIndex != null) {
330                        theMethodParams[myIdParamIndex] = theRequest.getId();
331                }
332
333                Object response = invokeServerMethod(theRequest, theMethodParams);
334                if (myManualResponseMode) {
335                        return null;
336                }
337
338                IBundleProvider retVal = toResourceList(response);
339                return retVal;
340        }
341
342        public boolean isCanOperateAtInstanceLevel() {
343                return this.myCanOperateAtInstanceLevel;
344        }
345
346        public boolean isCanOperateAtServerLevel() {
347                return this.myCanOperateAtServerLevel;
348        }
349
350        public boolean isCanOperateAtTypeLevel() {
351                return myCanOperateAtTypeLevel;
352        }
353
354        public boolean isIdempotent() {
355                return myIdempotent;
356        }
357
358        @Override
359        protected void populateActionRequestDetailsForInterceptor(RequestDetails theRequestDetails, ActionRequestDetails theDetails, Object[] theMethodParams) {
360                super.populateActionRequestDetailsForInterceptor(theRequestDetails, theDetails, theMethodParams);
361                IBaseResource resource = (IBaseResource) theRequestDetails.getUserData().get(OperationParameter.REQUEST_CONTENTS_USERDATA_KEY);
362                theRequestDetails.setResource(resource);
363                if (theDetails != null) {
364                        theDetails.setResource(resource);
365                }
366        }
367
368        public boolean isManualRequestMode() {
369                return myManualRequestMode;
370        }
371
372        public static class ReturnType {
373                private int myMax;
374                private int myMin;
375                private String myName;
376                /**
377                 * http://hl7-fhir.github.io/valueset-operation-parameter-type.html
378                 */
379                private String myType;
380
381                public int getMax() {
382                        return myMax;
383                }
384
385                public void setMax(int theMax) {
386                        myMax = theMax;
387                }
388
389                public int getMin() {
390                        return myMin;
391                }
392
393                public void setMin(int theMin) {
394                        myMin = theMin;
395                }
396
397                public String getName() {
398                        return myName;
399                }
400
401                public void setName(String theName) {
402                        myName = theName;
403                }
404
405                public String getType() {
406                        return myType;
407                }
408
409                public void setType(String theType) {
410                        myType = theType;
411                }
412        }
413
414}