001package ca.uhn.fhir.rest.server.method;
002
003/*-
004 * #%L
005 * HAPI FHIR - Server Framework
006 * %%
007 * Copyright (C) 2014 - 2021 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.model.api.IResource;
026import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
027import ca.uhn.fhir.model.primitive.InstantDt;
028import ca.uhn.fhir.model.valueset.BundleTypeEnum;
029import ca.uhn.fhir.rest.annotation.Elements;
030import ca.uhn.fhir.rest.annotation.IdParam;
031import ca.uhn.fhir.rest.annotation.Read;
032import ca.uhn.fhir.rest.api.Constants;
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.ETagSupportEnum;
040import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
041import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
042import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
043import ca.uhn.fhir.util.DateUtils;
044import org.apache.commons.lang3.StringUtils;
045import org.apache.commons.lang3.Validate;
046import org.hl7.fhir.instance.model.api.IBaseResource;
047import org.hl7.fhir.instance.model.api.IIdType;
048
049import javax.annotation.Nonnull;
050import java.lang.reflect.Method;
051import java.util.ArrayList;
052import java.util.Date;
053import java.util.List;
054import java.util.Set;
055
056import static org.apache.commons.lang3.StringUtils.isNotBlank;
057
058public class ReadMethodBinding extends BaseResourceReturningMethodBinding {
059        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ReadMethodBinding.class);
060
061        private Integer myIdIndex;
062        private boolean mySupportsVersion;
063        private Class<? extends IIdType> myIdParameterType;
064
065        @SuppressWarnings("unchecked")
066        public ReadMethodBinding(Class<? extends IBaseResource> theAnnotatedResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
067                super(theAnnotatedResourceType, theMethod, theContext, theProvider);
068
069                Validate.notNull(theMethod, "Method must not be null");
070
071                Integer idIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
072
073                Class<?>[] parameterTypes = theMethod.getParameterTypes();
074
075                mySupportsVersion = theMethod.getAnnotation(Read.class).version();
076                myIdIndex = idIndex;
077
078                if (myIdIndex == null) {
079                        throw new ConfigurationException("@" + Read.class.getSimpleName() + " method " + theMethod.getName() + " on type \"" + theMethod.getDeclaringClass().getName() + "\" does not have a parameter annotated with @" + IdParam.class.getSimpleName());
080                }
081                myIdParameterType = (Class<? extends IIdType>) parameterTypes[myIdIndex];
082
083                if (!IIdType.class.isAssignableFrom(myIdParameterType)) {
084                        throw new ConfigurationException("ID parameter must be of type IdDt or IdType - Found: " + myIdParameterType);
085                }
086
087        }
088
089        @Override
090        public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) {
091                if (mySupportsVersion && theRequestDetails.getId().hasVersionIdPart()) {
092                        return RestOperationTypeEnum.VREAD;
093                }
094                return RestOperationTypeEnum.READ;
095        }
096
097        @Override
098        public List<Class<?>> getAllowableParamAnnotations() {
099                ArrayList<Class<?>> retVal = new ArrayList<Class<?>>();
100                retVal.add(IdParam.class);
101                retVal.add(Elements.class);
102                return retVal;
103        }
104
105        @Nonnull
106        @Override
107        public RestOperationTypeEnum getRestOperationType() {
108                return isVread() ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ;
109        }
110
111        @Override
112        public ReturnTypeEnum getReturnType() {
113                return ReturnTypeEnum.RESOURCE;
114        }
115
116        @Override
117        public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
118                if (!theRequest.getResourceName().equals(getResourceName())) {
119                        return MethodMatchEnum.NONE;
120                }
121                for (String next : theRequest.getParameters().keySet()) {
122                        if (!next.startsWith("_")) {
123                                return MethodMatchEnum.NONE;
124                        }
125                }
126                if (theRequest.getId() == null) {
127                        return MethodMatchEnum.NONE;
128                }
129                if (mySupportsVersion == false) {
130                        if (theRequest.getId().hasVersionIdPart()) {
131                                return MethodMatchEnum.NONE;
132                        }
133                }
134                if (isNotBlank(theRequest.getCompartmentName())) {
135                        return MethodMatchEnum.NONE;
136                }
137                if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.HEAD ) {
138                        ourLog.trace("Method {} doesn't match because request type is not GET or HEAD: {}", theRequest.getId(), theRequest.getRequestType());
139                        return MethodMatchEnum.NONE;
140                }
141                if (Constants.PARAM_HISTORY.equals(theRequest.getOperation())) {
142                        if (mySupportsVersion == false) {
143                                return MethodMatchEnum.NONE;
144                        } else if (theRequest.getId().hasVersionIdPart() == false) {
145                                return MethodMatchEnum.NONE;
146                        }
147                } else if (!StringUtils.isBlank(theRequest.getOperation())) {
148                        return MethodMatchEnum.NONE;
149                }
150                return MethodMatchEnum.EXACT;
151        }
152
153
154        @Override
155        public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException {
156                IIdType requestId = theRequest.getId();
157                FhirContext ctx = theRequest.getServer().getFhirContext();
158
159                String[] invalidQueryStringParams = new String[]{Constants.PARAM_CONTAINED, Constants.PARAM_COUNT, Constants.PARAM_INCLUDE, Constants.PARAM_REVINCLUDE, Constants.PARAM_SORT, Constants.PARAM_SEARCH_TOTAL_MODE};
160                List<String> invalidQueryStringParamsInRequest = new ArrayList<>();
161                Set<String> queryStringParamsInRequest = theRequest.getParameters().keySet();
162
163                for (String queryStringParamName : queryStringParamsInRequest) {
164                        String lowercaseQueryStringParamName = queryStringParamName.toLowerCase();
165                        if (StringUtils.startsWithAny(lowercaseQueryStringParamName, invalidQueryStringParams)) {
166                                invalidQueryStringParamsInRequest.add(queryStringParamName);
167                        }
168                }
169
170                if (!invalidQueryStringParamsInRequest.isEmpty()) {
171                        throw new InvalidRequestException(ctx.getLocalizer().getMessage(ReadMethodBinding.class, "invalidParamsInRequest", invalidQueryStringParamsInRequest));
172                }
173
174                theMethodParams[myIdIndex] = ParameterUtil.convertIdToType(requestId, myIdParameterType);
175
176                Object response = invokeServerMethod(theRequest, theMethodParams);
177                IBundleProvider retVal = toResourceList(response);
178
179
180                if (Integer.valueOf(1).equals(retVal.size())) {
181                        List<IBaseResource> responseResources = retVal.getResources(0, 1);
182                        IBaseResource responseResource = responseResources.get(0);
183
184                        // If-None-Match
185                        if (theRequest.getServer().getETagSupport() == ETagSupportEnum.ENABLED) {
186                                String ifNoneMatch = theRequest.getHeader(Constants.HEADER_IF_NONE_MATCH_LC);
187                                if (StringUtils.isNotBlank(ifNoneMatch)) {
188                                        ifNoneMatch = ParameterUtil.parseETagValue(ifNoneMatch);
189                                        String versionIdPart = responseResource.getIdElement().getVersionIdPart();
190                                        if (StringUtils.isBlank(versionIdPart)) {
191                                                versionIdPart = responseResource.getMeta().getVersionId();
192                                        }
193                                        if (ifNoneMatch.equals(versionIdPart)) {
194                                                ourLog.debug("Returning HTTP 304 because request specified {}={}", Constants.HEADER_IF_NONE_MATCH, ifNoneMatch);
195                                                throw new NotModifiedException("Not Modified");
196                                        }
197                                }
198                        }
199                                
200                        // If-Modified-Since
201                        String ifModifiedSince = theRequest.getHeader(Constants.HEADER_IF_MODIFIED_SINCE_LC);
202                        if (isNotBlank(ifModifiedSince)) {
203                                Date ifModifiedSinceDate = DateUtils.parseDate(ifModifiedSince);
204                                Date lastModified = null;
205                                if (responseResource instanceof IResource) {
206                                        InstantDt lastModifiedDt = ResourceMetadataKeyEnum.UPDATED.get((IResource) responseResource);
207                                        if (lastModifiedDt != null) {
208                                                lastModified = lastModifiedDt.getValue();
209                                        }
210                                } else {
211                                        lastModified = responseResource.getMeta().getLastUpdated();
212                                }
213                                
214                                if (lastModified != null && lastModified.getTime() <= ifModifiedSinceDate.getTime()) {
215                                        ourLog.debug("Returning HTTP 304 because If-Modified-Since does not match");
216                                        throw new NotModifiedException("Not Modified");
217                                }
218                        }
219                                
220                } // if we have at least 1 result
221
222                
223                return retVal;
224        }
225
226        public boolean isVread() {
227                return mySupportsVersion;
228        }
229
230        @Override
231        protected BundleTypeEnum getResponseBundleType() {
232                return null;
233        }
234
235}