001/*-
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.server.method;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.i18n.Msg;
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 jakarta.annotation.Nonnull;
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 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(
067                        Class<? extends IBaseResource> theAnnotatedResourceType,
068                        Method theMethod,
069                        FhirContext theContext,
070                        Object theProvider) {
071                super(theAnnotatedResourceType, theMethod, theContext, theProvider);
072
073                Validate.notNull(theMethod, "Method must not be null");
074
075                Integer idIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext());
076
077                Class<?>[] parameterTypes = theMethod.getParameterTypes();
078
079                mySupportsVersion = theMethod.getAnnotation(Read.class).version();
080                myIdIndex = idIndex;
081
082                if (myIdIndex == null) {
083                        throw new ConfigurationException(
084                                        Msg.code(382) + "@" + Read.class.getSimpleName() + " method " + theMethod.getName() + " on type \""
085                                                        + theMethod.getDeclaringClass().getName() + "\" does not have a parameter annotated with @"
086                                                        + IdParam.class.getSimpleName());
087                }
088                myIdParameterType = (Class<? extends IIdType>) parameterTypes[myIdIndex];
089
090                if (!IIdType.class.isAssignableFrom(myIdParameterType)) {
091                        throw new ConfigurationException(
092                                        Msg.code(383) + "ID parameter must be of type IdDt or IdType - Found: " + myIdParameterType);
093                }
094        }
095
096        @Override
097        public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) {
098                if (mySupportsVersion && theRequestDetails.getId().hasVersionIdPart()) {
099                        return RestOperationTypeEnum.VREAD;
100                }
101                return RestOperationTypeEnum.READ;
102        }
103
104        @Override
105        public List<Class<?>> getAllowableParamAnnotations() {
106                ArrayList<Class<?>> retVal = new ArrayList<>();
107                retVal.add(IdParam.class);
108                retVal.add(Elements.class);
109                return retVal;
110        }
111
112        @Nonnull
113        @Override
114        public RestOperationTypeEnum getRestOperationType() {
115                return isVread() ? RestOperationTypeEnum.VREAD : RestOperationTypeEnum.READ;
116        }
117
118        @Override
119        public ReturnTypeEnum getReturnType() {
120                return ReturnTypeEnum.RESOURCE;
121        }
122
123        @Override
124        public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) {
125                if (!theRequest.getResourceName().equals(getResourceName())) {
126                        return MethodMatchEnum.NONE;
127                }
128                for (String next : theRequest.getParameters().keySet()) {
129                        if (!next.startsWith("_")) {
130                                return MethodMatchEnum.NONE;
131                        }
132                }
133                if (theRequest.getId() == null) {
134                        return MethodMatchEnum.NONE;
135                }
136                if (mySupportsVersion == false) {
137                        if (theRequest.getId().hasVersionIdPart()) {
138                                return MethodMatchEnum.NONE;
139                        }
140                }
141                if (isNotBlank(theRequest.getCompartmentName())) {
142                        return MethodMatchEnum.NONE;
143                }
144                if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.HEAD) {
145                        ourLog.trace(
146                                        "Method {} doesn't match because request type is not GET or HEAD: {}",
147                                        theRequest.getId(),
148                                        theRequest.getRequestType());
149                        return MethodMatchEnum.NONE;
150                }
151                if (Constants.PARAM_HISTORY.equals(theRequest.getOperation())) {
152                        if (mySupportsVersion == false) {
153                                return MethodMatchEnum.NONE;
154                        } else if (theRequest.getId().hasVersionIdPart() == false) {
155                                return MethodMatchEnum.NONE;
156                        }
157                } else if (!StringUtils.isBlank(theRequest.getOperation())) {
158                        return MethodMatchEnum.NONE;
159                }
160                return MethodMatchEnum.EXACT;
161        }
162
163        @Override
164        public IBundleProvider invokeServer(
165                        IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams)
166                        throws InvalidRequestException, InternalErrorException {
167                IIdType requestId = theRequest.getId();
168                FhirContext ctx = theRequest.getServer().getFhirContext();
169
170                String[] invalidQueryStringParams = new String[] {
171                        Constants.PARAM_CONTAINED,
172                        Constants.PARAM_COUNT,
173                        Constants.PARAM_INCLUDE,
174                        Constants.PARAM_REVINCLUDE,
175                        Constants.PARAM_SORT,
176                        Constants.PARAM_SEARCH_TOTAL_MODE
177                };
178                List<String> invalidQueryStringParamsInRequest = new ArrayList<>();
179                Set<String> queryStringParamsInRequest = theRequest.getParameters().keySet();
180
181                for (String queryStringParamName : queryStringParamsInRequest) {
182                        String lowercaseQueryStringParamName = queryStringParamName.toLowerCase();
183                        if (StringUtils.startsWithAny(lowercaseQueryStringParamName, invalidQueryStringParams)) {
184                                invalidQueryStringParamsInRequest.add(queryStringParamName);
185                        }
186                }
187
188                if (!invalidQueryStringParamsInRequest.isEmpty()) {
189                        throw new InvalidRequestException(Msg.code(384)
190                                        + ctx.getLocalizer()
191                                                        .getMessage(
192                                                                        ReadMethodBinding.class,
193                                                                        "invalidParamsInRequest",
194                                                                        invalidQueryStringParamsInRequest));
195                }
196
197                theMethodParams[myIdIndex] = ParameterUtil.convertIdToType(requestId, myIdParameterType);
198
199                Object response = invokeServerMethod(theRequest, theMethodParams);
200                IBundleProvider retVal = toResourceList(response);
201
202                if (Integer.valueOf(1).equals(retVal.size())) {
203                        List<IBaseResource> responseResources = retVal.getResources(0, 1);
204                        IBaseResource responseResource = responseResources.get(0);
205
206                        // If-None-Match
207                        if (theRequest.getServer().getETagSupport() == ETagSupportEnum.ENABLED) {
208                                String ifNoneMatch = theRequest.getHeader(Constants.HEADER_IF_NONE_MATCH_LC);
209                                if (StringUtils.isNotBlank(ifNoneMatch)) {
210                                        ifNoneMatch = ParameterUtil.parseETagValue(ifNoneMatch);
211                                        String versionIdPart = responseResource.getIdElement().getVersionIdPart();
212                                        if (StringUtils.isBlank(versionIdPart)) {
213                                                versionIdPart = responseResource.getMeta().getVersionId();
214                                        }
215                                        if (ifNoneMatch.equals(versionIdPart)) {
216                                                ourLog.debug(
217                                                                "Returning HTTP 304 because request specified {}={}",
218                                                                Constants.HEADER_IF_NONE_MATCH,
219                                                                ifNoneMatch);
220                                                throw new NotModifiedException(Msg.code(385) + "Not Modified");
221                                        }
222                                }
223                        }
224
225                        // If-Modified-Since
226                        String ifModifiedSince = theRequest.getHeader(Constants.HEADER_IF_MODIFIED_SINCE_LC);
227                        if (isNotBlank(ifModifiedSince)) {
228                                Date ifModifiedSinceDate = DateUtils.parseDate(ifModifiedSince);
229                                Date lastModified = null;
230                                if (responseResource instanceof IResource) {
231                                        InstantDt lastModifiedDt = ResourceMetadataKeyEnum.UPDATED.get((IResource) responseResource);
232                                        if (lastModifiedDt != null) {
233                                                lastModified = lastModifiedDt.getValue();
234                                        }
235                                } else {
236                                        lastModified = responseResource.getMeta().getLastUpdated();
237                                }
238
239                                if (lastModified != null && lastModified.getTime() <= ifModifiedSinceDate.getTime()) {
240                                        ourLog.debug("Returning HTTP 304 because If-Modified-Since does not match");
241                                        throw new NotModifiedException(Msg.code(386) + "Not Modified");
242                                }
243                        }
244                } // if we have at least 1 result
245
246                return retVal;
247        }
248
249        public boolean isVread() {
250                return mySupportsVersion;
251        }
252
253        @Override
254        protected BundleTypeEnum getResponseBundleType() {
255                return null;
256        }
257}