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