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