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