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