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.valueset.BundleTypeEnum; 026import ca.uhn.fhir.rest.annotation.Search; 027import ca.uhn.fhir.rest.api.Constants; 028import ca.uhn.fhir.rest.api.RequestTypeEnum; 029import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 030import ca.uhn.fhir.rest.api.server.IBundleProvider; 031import ca.uhn.fhir.rest.api.server.IRestfulServer; 032import ca.uhn.fhir.rest.api.server.RequestDetails; 033import ca.uhn.fhir.rest.param.ParameterUtil; 034import ca.uhn.fhir.rest.param.QualifierDetails; 035import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 036import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 037import ca.uhn.fhir.util.ParametersUtil; 038import jakarta.annotation.Nonnull; 039import org.apache.commons.lang3.StringUtils; 040import org.hl7.fhir.instance.model.api.IAnyResource; 041import org.hl7.fhir.instance.model.api.IBaseResource; 042 043import java.lang.reflect.Method; 044import java.util.Collections; 045import java.util.HashSet; 046import java.util.List; 047import java.util.Set; 048import java.util.stream.Collectors; 049 050import static org.apache.commons.lang3.StringUtils.isBlank; 051import static org.apache.commons.lang3.StringUtils.isNotBlank; 052 053public class SearchMethodBinding extends BaseResourceReturningMethodBinding { 054 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class); 055 056 private static final Set<String> SPECIAL_SEARCH_PARAMS; 057 058 static { 059 HashSet<String> specialSearchParams = new HashSet<>(); 060 specialSearchParams.add(IAnyResource.SP_RES_ID); 061 specialSearchParams.add(Constants.PARAM_INCLUDE); 062 specialSearchParams.add(Constants.PARAM_REVINCLUDE); 063 SPECIAL_SEARCH_PARAMS = Collections.unmodifiableSet(specialSearchParams); 064 } 065 066 private final String myResourceProviderResourceName; 067 private final List<String> myRequiredParamNames; 068 private final List<String> myOptionalParamNames; 069 private final String myCompartmentName; 070 private String myDescription; 071 private final Integer myIdParamIndex; 072 private final String myQueryName; 073 private final boolean myAllowUnknownParams; 074 075 public SearchMethodBinding( 076 Class<? extends IBaseResource> theReturnResourceType, 077 Class<? extends IBaseResource> theResourceProviderResourceType, 078 Method theMethod, 079 FhirContext theContext, 080 Object theProvider) { 081 super(theReturnResourceType, theMethod, theContext, theProvider); 082 Search search = theMethod.getAnnotation(Search.class); 083 this.myQueryName = StringUtils.defaultIfBlank(search.queryName(), null); 084 this.myCompartmentName = StringUtils.defaultIfBlank(search.compartmentName(), null); 085 this.myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); 086 this.myAllowUnknownParams = search.allowUnknownParams(); 087 this.myDescription = ParametersUtil.extractDescription(theMethod); 088 089 /* 090 * Only compartment searching methods may have an ID parameter 091 */ 092 if (isBlank(myCompartmentName) && myIdParamIndex != null) { 093 String msg = theContext 094 .getLocalizer() 095 .getMessage( 096 getClass().getName() + ".idWithoutCompartment", 097 theMethod.getName(), 098 theMethod.getDeclaringClass()); 099 throw new ConfigurationException(Msg.code(412) + msg); 100 } 101 102 if (theResourceProviderResourceType != null) { 103 this.myResourceProviderResourceName = theContext.getResourceType(theResourceProviderResourceType); 104 } else { 105 this.myResourceProviderResourceName = null; 106 } 107 108 myRequiredParamNames = getQueryParameters().stream() 109 .filter(t -> t.isRequired()) 110 .map(t -> t.getName()) 111 .collect(Collectors.toList()); 112 myOptionalParamNames = getQueryParameters().stream() 113 .filter(t -> !t.isRequired()) 114 .map(t -> t.getName()) 115 .collect(Collectors.toList()); 116 } 117 118 public String getDescription() { 119 return myDescription; 120 } 121 122 public String getQueryName() { 123 return myQueryName; 124 } 125 126 public String getResourceProviderResourceName() { 127 return myResourceProviderResourceName; 128 } 129 130 @Nonnull 131 @Override 132 public RestOperationTypeEnum getRestOperationType() { 133 return RestOperationTypeEnum.SEARCH_TYPE; 134 } 135 136 @Override 137 protected BundleTypeEnum getResponseBundleType() { 138 return BundleTypeEnum.SEARCHSET; 139 } 140 141 @Override 142 public ReturnTypeEnum getReturnType() { 143 return ReturnTypeEnum.BUNDLE; 144 } 145 146 @Override 147 public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) { 148 149 if (!mightBeSearchRequest(theRequest)) { 150 return MethodMatchEnum.NONE; 151 } 152 153 if (theRequest.getId() != null && myIdParamIndex == null) { 154 ourLog.trace("Method {} doesn't match because ID is not null: {}", getMethod(), theRequest.getId()); 155 return MethodMatchEnum.NONE; 156 } 157 if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) { 158 ourLog.trace( 159 "Method {} doesn't match because it is for compartment {} but request is compartment {}", 160 getMethod(), 161 myCompartmentName, 162 theRequest.getCompartmentName()); 163 return MethodMatchEnum.NONE; 164 } 165 166 if (myQueryName != null) { 167 String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY); 168 if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) { 169 String queryName = queryNameValues[0]; 170 if (!myQueryName.equals(queryName)) { 171 ourLog.trace("Query name does not match {}", myQueryName); 172 return MethodMatchEnum.NONE; 173 } 174 } else { 175 ourLog.trace("Query name does not match {}", myQueryName); 176 return MethodMatchEnum.NONE; 177 } 178 } else { 179 String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY); 180 if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) { 181 ourLog.trace("Query has name"); 182 return MethodMatchEnum.NONE; 183 } 184 } 185 186 Set<String> unqualifiedNames = 187 theRequest.getUnqualifiedToQualifiedNames().keySet(); 188 Set<String> qualifiedParamNames = theRequest.getParameters().keySet(); 189 190 MethodMatchEnum retVal = MethodMatchEnum.EXACT; 191 for (String nextRequestParam : theRequest.getParameters().keySet()) { 192 String nextUnqualifiedRequestParam = ParameterUtil.stripModifierPart(nextRequestParam); 193 if (nextRequestParam.startsWith("_") && !SPECIAL_SEARCH_PARAMS.contains(nextUnqualifiedRequestParam)) { 194 continue; 195 } 196 197 boolean parameterMatches = false; 198 boolean approx = false; 199 for (BaseQueryParameter nextMethodParam : getQueryParameters()) { 200 201 if (nextRequestParam.equals(nextMethodParam.getName())) { 202 QualifierDetails qualifiers = QualifierDetails.extractQualifiersFromParameterName(nextRequestParam); 203 if (qualifiers.passes( 204 nextMethodParam.getQualifierWhitelist(), nextMethodParam.getQualifierBlacklist())) { 205 parameterMatches = true; 206 } 207 } else if (nextUnqualifiedRequestParam.equals(nextMethodParam.getName())) { 208 List<String> qualifiedNames = 209 theRequest.getUnqualifiedToQualifiedNames().get(nextUnqualifiedRequestParam); 210 if (passesWhitelistAndBlacklist( 211 qualifiedNames, 212 nextMethodParam.getQualifierWhitelist(), 213 nextMethodParam.getQualifierBlacklist())) { 214 parameterMatches = true; 215 } 216 } 217 218 // Repetitions supplied by URL but not supported by this parameter 219 if (theRequest.getParameters().get(nextRequestParam).length > 1 220 != nextMethodParam.supportsRepetition()) { 221 approx = true; 222 } 223 } 224 225 if (parameterMatches) { 226 227 if (approx) { 228 retVal = retVal.weakerOf(MethodMatchEnum.APPROXIMATE); 229 } 230 231 } else { 232 233 if (myAllowUnknownParams) { 234 retVal = retVal.weakerOf(MethodMatchEnum.APPROXIMATE); 235 } else { 236 retVal = retVal.weakerOf(MethodMatchEnum.NONE); 237 } 238 } 239 240 if (retVal == MethodMatchEnum.NONE) { 241 break; 242 } 243 } 244 245 if (retVal != MethodMatchEnum.NONE) { 246 for (String nextRequiredParamName : myRequiredParamNames) { 247 if (!qualifiedParamNames.contains(nextRequiredParamName)) { 248 if (!unqualifiedNames.contains(nextRequiredParamName)) { 249 retVal = MethodMatchEnum.NONE; 250 break; 251 } 252 } 253 } 254 } 255 if (retVal != MethodMatchEnum.NONE) { 256 for (String nextRequiredParamName : myOptionalParamNames) { 257 if (!qualifiedParamNames.contains(nextRequiredParamName)) { 258 if (!unqualifiedNames.contains(nextRequiredParamName)) { 259 retVal = retVal.weakerOf(MethodMatchEnum.APPROXIMATE); 260 } 261 } 262 } 263 } 264 265 return retVal; 266 } 267 268 /** 269 * Is this request a request for a normal search - Ie. not a named search, nor a compartment 270 * search, just a plain old search. 271 * 272 * @since 5.4.0 273 */ 274 public static boolean isPlainSearchRequest(RequestDetails theRequest) { 275 if (theRequest.getId() != null) { 276 return false; 277 } 278 if (isNotBlank(theRequest.getCompartmentName())) { 279 return false; 280 } 281 return mightBeSearchRequest(theRequest); 282 } 283 284 private static boolean mightBeSearchRequest(RequestDetails theRequest) { 285 if (theRequest.getRequestType() == RequestTypeEnum.GET 286 && theRequest.getOperation() != null 287 && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { 288 return false; 289 } 290 if (theRequest.getRequestType() == RequestTypeEnum.POST 291 && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { 292 return false; 293 } 294 if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) { 295 return false; 296 } 297 if (theRequest.getParameters().get(Constants.PARAM_PAGINGACTION) != null) { 298 return false; 299 } 300 return true; 301 } 302 303 @Override 304 public IBundleProvider invokeServer( 305 IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) 306 throws InvalidRequestException, InternalErrorException { 307 if (myIdParamIndex != null) { 308 theMethodParams[myIdParamIndex] = theRequest.getId(); 309 } 310 311 Object response = invokeServerMethod(theRequest, theMethodParams); 312 313 return toResourceList(response); 314 } 315 316 @Override 317 protected boolean isAddContentLocationHeader() { 318 return false; 319 } 320 321 private boolean passesWhitelistAndBlacklist( 322 List<String> theQualifiedNames, Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) { 323 if (theQualifierWhitelist == null && theQualifierBlacklist == null) { 324 return true; 325 } 326 for (String next : theQualifiedNames) { 327 QualifierDetails qualifiers = QualifierDetails.extractQualifiersFromParameterName(next); 328 if (!qualifiers.passes(theQualifierWhitelist, theQualifierBlacklist)) { 329 return false; 330 } 331 } 332 return true; 333 } 334 335 @Override 336 public String toString() { 337 return getMethod().toString(); 338 } 339}