001package ca.uhn.fhir.rest.method; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2016 University Health Network 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 */ 022import static org.apache.commons.lang3.StringUtils.isBlank; 023import static org.apache.commons.lang3.StringUtils.isNotBlank; 024 025import java.lang.reflect.Method; 026import java.util.ArrayList; 027import java.util.Collections; 028import java.util.HashSet; 029import java.util.LinkedHashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Map.Entry; 033import java.util.Set; 034 035import org.apache.commons.lang3.StringUtils; 036import org.hl7.fhir.instance.model.api.IBaseResource; 037 038import ca.uhn.fhir.context.ConfigurationException; 039import ca.uhn.fhir.context.FhirContext; 040import ca.uhn.fhir.model.api.annotation.Description; 041import ca.uhn.fhir.model.primitive.IdDt; 042import ca.uhn.fhir.model.valueset.BundleTypeEnum; 043import ca.uhn.fhir.rest.annotation.Search; 044import ca.uhn.fhir.rest.api.RequestTypeEnum; 045import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 046import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; 047import ca.uhn.fhir.rest.param.BaseQueryParameter; 048import ca.uhn.fhir.rest.server.Constants; 049import ca.uhn.fhir.rest.server.IBundleProvider; 050import ca.uhn.fhir.rest.server.IRestfulServer; 051import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 052import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 053 054public class SearchMethodBinding extends BaseResourceReturningMethodBinding { 055 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class); 056 057 private String myCompartmentName; 058 private String myDescription; 059 private Integer myIdParamIndex; 060 private String myQueryName; 061 private boolean myAllowUnknownParams; 062 063 public SearchMethodBinding(Class<? extends IBaseResource> theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { 064 super(theReturnResourceType, theMethod, theContext, theProvider); 065 Search search = theMethod.getAnnotation(Search.class); 066 this.myQueryName = StringUtils.defaultIfBlank(search.queryName(), null); 067 this.myCompartmentName = StringUtils.defaultIfBlank(search.compartmentName(), null); 068 this.myIdParamIndex = MethodUtil.findIdParameterIndex(theMethod, getContext()); 069 this.myAllowUnknownParams = search.allowUnknownParams(); 070 071 Description desc = theMethod.getAnnotation(Description.class); 072 if (desc != null) { 073 if (isNotBlank(desc.formalDefinition())) { 074 myDescription = StringUtils.defaultIfBlank(desc.formalDefinition(), null); 075 } else { 076 myDescription = StringUtils.defaultIfBlank(desc.shortDefinition(), null); 077 } 078 } 079 080 /* 081 * Check for parameter combinations and names that are invalid 082 */ 083 List<IParameter> parameters = getParameters(); 084 // List<SearchParameter> searchParameters = new ArrayList<SearchParameter>(); 085 for (int i = 0; i < parameters.size(); i++) { 086 IParameter next = parameters.get(i); 087 if (!(next instanceof SearchParameter)) { 088 continue; 089 } 090 091 SearchParameter sp = (SearchParameter) next; 092 if (sp.getName().startsWith("_")) { 093 if (ALLOWED_PARAMS.contains(sp.getName())) { 094 String msg = getContext().getLocalizer().getMessage(getClass().getName() + ".invalidSpecialParamName", theMethod.getName(), theMethod.getDeclaringClass().getSimpleName(), 095 sp.getName()); 096 throw new ConfigurationException(msg); 097 } 098 } 099 100 // searchParameters.add(sp); 101 } 102 // for (int i = 0; i < searchParameters.size(); i++) { 103 // SearchParameter next = searchParameters.get(i); 104 // // next. 105 // } 106 107 /* 108 * Only compartment searching methods may have an ID parameter 109 */ 110 if (isBlank(myCompartmentName) && myIdParamIndex != null) { 111 String msg = theContext.getLocalizer().getMessage(getClass().getName() + ".idWithoutCompartment", theMethod.getName(), theMethod.getDeclaringClass()); 112 throw new ConfigurationException(msg); 113 } 114 115 } 116 117 public String getDescription() { 118 return myDescription; 119 } 120 121 @Override 122 public RestOperationTypeEnum getRestOperationType() { 123 return RestOperationTypeEnum.SEARCH_TYPE; 124 } 125 126 @Override 127 protected BundleTypeEnum getResponseBundleType() { 128 return BundleTypeEnum.SEARCHSET; 129 } 130 131 @Override 132 public ReturnTypeEnum getReturnType() { 133 return ReturnTypeEnum.BUNDLE; 134 } 135 136 @Override 137 public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) { 138 if (theRequest.getId() != null && myIdParamIndex == null) { 139 ourLog.trace("Method {} doesn't match because ID is not null: {}", theRequest.getId()); 140 return false; 141 } 142 if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { 143 ourLog.trace("Method {} doesn't match because request type is GET but operation is not null: {}", theRequest.getId(), theRequest.getOperation()); 144 return false; 145 } 146 if (theRequest.getRequestType() == RequestTypeEnum.POST && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { 147 ourLog.trace("Method {} doesn't match because request type is POST but operation is not _search: {}", theRequest.getId(), theRequest.getOperation()); 148 return false; 149 } 150 if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) { 151 ourLog.trace("Method {} doesn't match because request type is {}", getMethod()); 152 return false; 153 } 154 if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) { 155 ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", new Object[] { getMethod(), myCompartmentName, theRequest.getCompartmentName() }); 156 return false; 157 } 158 // This is used to track all the parameters so we can reject queries that 159 // have additional params we don't understand 160 Set<String> methodParamsTemp = new HashSet<String>(); 161 162 Set<String> unqualifiedNames = theRequest.getUnqualifiedToQualifiedNames().keySet(); 163 Set<String> qualifiedParamNames = theRequest.getParameters().keySet(); 164 for (int i = 0; i < this.getParameters().size(); i++) { 165 if (!(getParameters().get(i) instanceof BaseQueryParameter)) { 166 continue; 167 } 168 BaseQueryParameter temp = (BaseQueryParameter) getParameters().get(i); 169 String name = temp.getName(); 170 if (temp.isRequired()) { 171 172 if (qualifiedParamNames.contains(name)) { 173 QualifierDetails qualifiers = extractQualifiersFromParameterName(name); 174 if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) { 175 methodParamsTemp.add(name); 176 } 177 } 178 if (unqualifiedNames.contains(name)) { 179 List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name); 180 qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist()); 181 methodParamsTemp.addAll(qualifiedNames); 182 } 183 if (!qualifiedParamNames.contains(name) && !unqualifiedNames.contains(name)) 184 { 185 ourLog.trace("Method {} doesn't match param '{}' is not present", getMethod().getName(), name); 186 return false; 187 } 188 189 } else { 190 if (qualifiedParamNames.contains(name)) { 191 QualifierDetails qualifiers = extractQualifiersFromParameterName(name); 192 if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) { 193 methodParamsTemp.add(name); 194 } 195 } 196 if (unqualifiedNames.contains(name)) { 197 List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name); 198 qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist()); 199 methodParamsTemp.addAll(qualifiedNames); 200 } 201 if (!qualifiedParamNames.contains(name) && !qualifiedParamNames.contains(name)) { 202 methodParamsTemp.add(name); 203 } 204 } 205 } 206 if (myQueryName != null) { 207 String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY); 208 if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) { 209 String queryName = queryNameValues[0]; 210 if (!myQueryName.equals(queryName)) { 211 ourLog.trace("Query name does not match {}", myQueryName); 212 return false; 213 } else { 214 methodParamsTemp.add(Constants.PARAM_QUERY); 215 } 216 } else { 217 ourLog.trace("Query name does not match {}", myQueryName); 218 return false; 219 } 220 } else { 221 String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY); 222 if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) { 223 ourLog.trace("Query has name"); 224 return false; 225 } 226 } 227 for (String next : theRequest.getParameters().keySet()) { 228 if (ALLOWED_PARAMS.contains(next)) { 229 methodParamsTemp.add(next); 230 } 231 } 232 Set<String> keySet = theRequest.getParameters().keySet(); 233 if (myAllowUnknownParams == false) { 234 for (String next : keySet) { 235 if (!methodParamsTemp.contains(next)) { 236 return false; 237 } 238 } 239 } 240 return true; 241 } 242 243 @Override 244 public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { 245 assert (myQueryName == null || ((theArgs != null ? theArgs.length : 0) == getParameters().size())) : "Wrong number of arguments: " + (theArgs != null ? theArgs.length : "null"); 246 247 Map<String, List<String>> queryStringArgs = new LinkedHashMap<String, List<String>>(); 248 249 if (myQueryName != null) { 250 queryStringArgs.put(Constants.PARAM_QUERY, Collections.singletonList(myQueryName)); 251 } 252 253 IdDt id = (IdDt) (myIdParamIndex != null ? theArgs[myIdParamIndex] : null); 254 255 String resourceName = getResourceName(); 256 if (theArgs != null) { 257 for (int idx = 0; idx < theArgs.length; idx++) { 258 IParameter nextParam = getParameters().get(idx); 259 nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], queryStringArgs, null); 260 } 261 } 262 263 BaseHttpClientInvocation retVal = createSearchInvocation(getContext(), resourceName, queryStringArgs, id, myCompartmentName, null); 264 265 return retVal; 266 } 267 268 @Override 269 public IBundleProvider invokeServer(IRestfulServer theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { 270 if (myIdParamIndex != null) { 271 theMethodParams[myIdParamIndex] = theRequest.getId(); 272 } 273 274 Object response = invokeServerMethod(theServer, theRequest, theMethodParams); 275 276 return toResourceList(response); 277 278 } 279 280 @Override 281 protected boolean isAddContentLocationHeader() { 282 return false; 283 } 284 285 private List<String> processWhitelistAndBlacklist(List<String> theQualifiedNames, Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) { 286 if (theQualifierWhitelist == null && theQualifierBlacklist == null) { 287 return theQualifiedNames; 288 } 289 ArrayList<String> retVal = new ArrayList<String>(theQualifiedNames.size()); 290 for (String next : theQualifiedNames) { 291 QualifierDetails qualifiers = extractQualifiersFromParameterName(next); 292 if (!qualifiers.passes(theQualifierWhitelist, theQualifierBlacklist)) { 293 continue; 294 } 295 retVal.add(next); 296 } 297 return retVal; 298 } 299 300 @Override 301 public String toString() { 302 return getMethod().toString(); 303 } 304 305 public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theResourceName, Map<String, List<String>> theParameters, IdDt theId, String theCompartmentName, 306 SearchStyleEnum theSearchStyle) { 307 SearchStyleEnum searchStyle = theSearchStyle; 308 if (searchStyle == null) { 309 int length = 0; 310 for (Entry<String, List<String>> nextEntry : theParameters.entrySet()) { 311 length += nextEntry.getKey().length(); 312 for (String next : nextEntry.getValue()) { 313 length += next.length(); 314 } 315 } 316 317 if (length < 5000) { 318 searchStyle = SearchStyleEnum.GET; 319 } else { 320 searchStyle = SearchStyleEnum.POST; 321 } 322 } 323 324 BaseHttpClientInvocation invocation; 325 326 boolean compartmentSearch = false; 327 if (theCompartmentName != null) { 328 if (theId == null || !theId.hasIdPart()) { 329 String msg = theContext.getLocalizer().getMessage(SearchMethodBinding.class.getName() + ".idNullForCompartmentSearch"); 330 throw new InvalidRequestException(msg); 331 } else { 332 compartmentSearch = true; 333 } 334 } 335 336 /* 337 * Are we doing a get (GET [base]/Patient?name=foo) or a get with search (GET [base]/Patient/_search?name=foo) or a post (POST [base]/Patient with parameters in the POST body) 338 */ 339 switch (searchStyle) { 340 case GET: 341 default: 342 if (compartmentSearch) { 343 invocation = new HttpGetClientInvocation(theParameters, theResourceName, theId.getIdPart(), theCompartmentName); 344 } else { 345 invocation = new HttpGetClientInvocation(theParameters, theResourceName); 346 } 347 break; 348 case GET_WITH_SEARCH: 349 if (compartmentSearch) { 350 invocation = new HttpGetClientInvocation(theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH); 351 } else { 352 invocation = new HttpGetClientInvocation(theParameters, theResourceName, Constants.PARAM_SEARCH); 353 } 354 break; 355 case POST: 356 if (compartmentSearch) { 357 invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH); 358 } else { 359 invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, Constants.PARAM_SEARCH); 360 } 361 } 362 363 return invocation; 364 } 365 366 public static QualifierDetails extractQualifiersFromParameterName(String theParamName) { 367 QualifierDetails retVal = new QualifierDetails(); 368 if (theParamName == null || theParamName.length() == 0) { 369 return retVal; 370 } 371 372 int dotIdx = -1; 373 int colonIdx = -1; 374 for (int idx = 0; idx < theParamName.length(); idx++) { 375 char nextChar = theParamName.charAt(idx); 376 if (nextChar == '.' && dotIdx == -1) { 377 dotIdx = idx; 378 } else if (nextChar == ':' && colonIdx == -1) { 379 colonIdx = idx; 380 } 381 } 382 383 if (dotIdx != -1 && colonIdx != -1) { 384 if (dotIdx < colonIdx) { 385 retVal.setDotQualifier(theParamName.substring(dotIdx, colonIdx)); 386 retVal.setColonQualifier(theParamName.substring(colonIdx)); 387 } else { 388 retVal.setColonQualifier(theParamName.substring(colonIdx, dotIdx)); 389 retVal.setDotQualifier(theParamName.substring(dotIdx)); 390 } 391 } else if (dotIdx != -1) { 392 retVal.setDotQualifier(theParamName.substring(dotIdx)); 393 } else if (colonIdx != -1) { 394 retVal.setColonQualifier(theParamName.substring(colonIdx)); 395 } 396 397 return retVal; 398 } 399 400 public static class QualifierDetails { 401 402 private String myColonQualifier; 403 private String myDotQualifier; 404 405 public boolean passes(Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) { 406 if (theQualifierWhitelist != null) { 407 if (!theQualifierWhitelist.contains(".*")) { 408 if (myDotQualifier != null) { 409 if (!theQualifierWhitelist.contains(myDotQualifier)) { 410 return false; 411 } 412 } else { 413 if (!theQualifierWhitelist.contains(".")) { 414 return false; 415 } 416 } 417 } 418 /* 419 * This was removed Sep 9 2015, as I don't see any way it could possibly be triggered. 420 if (!theQualifierWhitelist.contains(SearchParameter.QUALIFIER_ANY_TYPE)) { 421 if (myColonQualifier != null) { 422 if (!theQualifierWhitelist.contains(myColonQualifier)) { 423 return false; 424 } 425 } else { 426 if (!theQualifierWhitelist.contains(":")) { 427 return false; 428 } 429 } 430 } 431 */ 432 } 433 if (theQualifierBlacklist != null) { 434 if (myDotQualifier != null) { 435 if (theQualifierBlacklist.contains(myDotQualifier)) { 436 return false; 437 } 438 } 439 if (myColonQualifier != null) { 440 if (theQualifierBlacklist.contains(myColonQualifier)) { 441 return false; 442 } 443 } 444 } 445 446 return true; 447 } 448 449 public void setColonQualifier(String theColonQualifier) { 450 myColonQualifier = theColonQualifier; 451 } 452 453 public void setDotQualifier(String theDotQualifier) { 454 myDotQualifier = theDotQualifier; 455 } 456 457 } 458 459 public static BaseHttpClientInvocation createSearchInvocation(String theSearchUrl, Map<String, List<String>> theParams) { 460 return new HttpGetClientInvocation(theParams, theSearchUrl); 461 } 462 463}