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