
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<Class<?>>(); 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}