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}