001package ca.uhn.fhir.rest.method; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2017 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 139 String clientPreference = theRequest.getHeader(Constants.HEADER_PREFER); 140 boolean lenientHandling = false; 141 if(clientPreference != null) 142 { 143 String[] preferences = clientPreference.split(";"); 144 for( String p : preferences){ 145 if("handling:lenient".equalsIgnoreCase(p)) 146 { 147 lenientHandling = true; 148 break; 149 } 150 } 151 } 152 153 if (theRequest.getId() != null && myIdParamIndex == null) { 154 ourLog.trace("Method {} doesn't match because ID is not null: {}", theRequest.getId()); 155 return false; 156 } 157 if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { 158 ourLog.trace("Method {} doesn't match because request type is GET but operation is not null: {}", theRequest.getId(), theRequest.getOperation()); 159 return false; 160 } 161 if (theRequest.getRequestType() == RequestTypeEnum.POST && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) { 162 ourLog.trace("Method {} doesn't match because request type is POST but operation is not _search: {}", theRequest.getId(), theRequest.getOperation()); 163 return false; 164 } 165 if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) { 166 ourLog.trace("Method {} doesn't match because request type is {}", getMethod()); 167 return false; 168 } 169 if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) { 170 ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", new Object[] { getMethod(), myCompartmentName, theRequest.getCompartmentName() }); 171 return false; 172 } 173 // This is used to track all the parameters so we can reject queries that 174 // have additional params we don't understand 175 Set<String> methodParamsTemp = new HashSet<String>(); 176 177 Set<String> unqualifiedNames = theRequest.getUnqualifiedToQualifiedNames().keySet(); 178 Set<String> qualifiedParamNames = theRequest.getParameters().keySet(); 179 for (int i = 0; i < this.getParameters().size(); i++) { 180 if (!(getParameters().get(i) instanceof BaseQueryParameter)) { 181 continue; 182 } 183 BaseQueryParameter temp = (BaseQueryParameter) getParameters().get(i); 184 String name = temp.getName(); 185 if (temp.isRequired()) { 186 187 if (qualifiedParamNames.contains(name)) { 188 QualifierDetails qualifiers = extractQualifiersFromParameterName(name); 189 if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) { 190 methodParamsTemp.add(name); 191 } 192 } 193 if (unqualifiedNames.contains(name)) { 194 List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name); 195 qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist()); 196 methodParamsTemp.addAll(qualifiedNames); 197 } 198 if (!qualifiedParamNames.contains(name) && !unqualifiedNames.contains(name)) 199 { 200 ourLog.trace("Method {} doesn't match param '{}' is not present", getMethod().getName(), name); 201 return false; 202 } 203 204 } else { 205 if (qualifiedParamNames.contains(name)) { 206 QualifierDetails qualifiers = extractQualifiersFromParameterName(name); 207 if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) { 208 methodParamsTemp.add(name); 209 } 210 } 211 if (unqualifiedNames.contains(name)) { 212 List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name); 213 qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist()); 214 methodParamsTemp.addAll(qualifiedNames); 215 } 216 if (!qualifiedParamNames.contains(name)) { 217 methodParamsTemp.add(name); 218 } 219 } 220 } 221 if (myQueryName != null) { 222 String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY); 223 if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) { 224 String queryName = queryNameValues[0]; 225 if (!myQueryName.equals(queryName)) { 226 ourLog.trace("Query name does not match {}", myQueryName); 227 return false; 228 } 229 methodParamsTemp.add(Constants.PARAM_QUERY); 230 } else { 231 ourLog.trace("Query name does not match {}", myQueryName); 232 return false; 233 } 234 } else { 235 String[] queryNameValues = theRequest.getParameters().get(Constants.PARAM_QUERY); 236 if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) { 237 ourLog.trace("Query has name"); 238 return false; 239 } 240 } 241 for (String next : theRequest.getParameters().keySet()) { 242 if (ALLOWED_PARAMS.contains(next)) { 243 methodParamsTemp.add(next); 244 } 245 } 246 Set<String> keySet = theRequest.getParameters().keySet(); 247 if(lenientHandling == true) 248 return true; 249 250 if (myAllowUnknownParams == false) { 251 for (String next : keySet) { 252 if (!methodParamsTemp.contains(next)) { 253 return false; 254 } 255 } 256 } 257 return true; 258 } 259 260 @Override 261 public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { 262 assert (myQueryName == null || ((theArgs != null ? theArgs.length : 0) == getParameters().size())) : "Wrong number of arguments: " + (theArgs != null ? theArgs.length : "null"); 263 264 Map<String, List<String>> queryStringArgs = new LinkedHashMap<String, List<String>>(); 265 266 if (myQueryName != null) { 267 queryStringArgs.put(Constants.PARAM_QUERY, Collections.singletonList(myQueryName)); 268 } 269 270 IdDt id = (IdDt) (myIdParamIndex != null ? theArgs[myIdParamIndex] : null); 271 272 String resourceName = getResourceName(); 273 if (theArgs != null) { 274 for (int idx = 0; idx < theArgs.length; idx++) { 275 IParameter nextParam = getParameters().get(idx); 276 nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], queryStringArgs, null); 277 } 278 } 279 280 BaseHttpClientInvocation retVal = createSearchInvocation(getContext(), resourceName, queryStringArgs, id, myCompartmentName, null); 281 282 return retVal; 283 } 284 285 @Override 286 public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException { 287 if (myIdParamIndex != null) { 288 theMethodParams[myIdParamIndex] = theRequest.getId(); 289 } 290 291 Object response = invokeServerMethod(theServer, theRequest, theMethodParams); 292 293 return toResourceList(response); 294 295 } 296 297 @Override 298 protected boolean isAddContentLocationHeader() { 299 return false; 300 } 301 302 private List<String> processWhitelistAndBlacklist(List<String> theQualifiedNames, Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) { 303 if (theQualifierWhitelist == null && theQualifierBlacklist == null) { 304 return theQualifiedNames; 305 } 306 ArrayList<String> retVal = new ArrayList<String>(theQualifiedNames.size()); 307 for (String next : theQualifiedNames) { 308 QualifierDetails qualifiers = extractQualifiersFromParameterName(next); 309 if (!qualifiers.passes(theQualifierWhitelist, theQualifierBlacklist)) { 310 continue; 311 } 312 retVal.add(next); 313 } 314 return retVal; 315 } 316 317 @Override 318 public String toString() { 319 return getMethod().toString(); 320 } 321 322 public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theResourceName, Map<String, List<String>> theParameters, IdDt theId, String theCompartmentName, 323 SearchStyleEnum theSearchStyle) { 324 SearchStyleEnum searchStyle = theSearchStyle; 325 if (searchStyle == null) { 326 int length = 0; 327 for (Entry<String, List<String>> nextEntry : theParameters.entrySet()) { 328 length += nextEntry.getKey().length(); 329 for (String next : nextEntry.getValue()) { 330 length += next.length(); 331 } 332 } 333 334 if (length < 5000) { 335 searchStyle = SearchStyleEnum.GET; 336 } else { 337 searchStyle = SearchStyleEnum.POST; 338 } 339 } 340 341 BaseHttpClientInvocation invocation; 342 343 boolean compartmentSearch = false; 344 if (theCompartmentName != null) { 345 if (theId == null || !theId.hasIdPart()) { 346 String msg = theContext.getLocalizer().getMessage(SearchMethodBinding.class.getName() + ".idNullForCompartmentSearch"); 347 throw new InvalidRequestException(msg); 348 } 349 compartmentSearch = true; 350 } 351 352 /* 353 * 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) 354 */ 355 switch (searchStyle) { 356 case GET: 357 default: 358 if (compartmentSearch) { 359 invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName); 360 } else { 361 invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName); 362 } 363 break; 364 case GET_WITH_SEARCH: 365 if (compartmentSearch) { 366 invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH); 367 } else { 368 invocation = new HttpGetClientInvocation(theContext, theParameters, theResourceName, Constants.PARAM_SEARCH); 369 } 370 break; 371 case POST: 372 if (compartmentSearch) { 373 invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, theId.getIdPart(), theCompartmentName, Constants.PARAM_SEARCH); 374 } else { 375 invocation = new HttpPostClientInvocation(theContext, theParameters, theResourceName, Constants.PARAM_SEARCH); 376 } 377 } 378 379 return invocation; 380 } 381 382 public static QualifierDetails extractQualifiersFromParameterName(String theParamName) { 383 QualifierDetails retVal = new QualifierDetails(); 384 if (theParamName == null || theParamName.length() == 0) { 385 return retVal; 386 } 387 388 int dotIdx = -1; 389 int colonIdx = -1; 390 for (int idx = 0; idx < theParamName.length(); idx++) { 391 char nextChar = theParamName.charAt(idx); 392 if (nextChar == '.' && dotIdx == -1) { 393 dotIdx = idx; 394 } else if (nextChar == ':' && colonIdx == -1) { 395 colonIdx = idx; 396 } 397 } 398 399 if (dotIdx != -1 && colonIdx != -1) { 400 if (dotIdx < colonIdx) { 401 retVal.setDotQualifier(theParamName.substring(dotIdx, colonIdx)); 402 retVal.setColonQualifier(theParamName.substring(colonIdx)); 403 retVal.setParamName(theParamName.substring(0, dotIdx)); 404 retVal.setWholeQualifier(theParamName.substring(dotIdx)); 405 } else { 406 retVal.setColonQualifier(theParamName.substring(colonIdx, dotIdx)); 407 retVal.setDotQualifier(theParamName.substring(dotIdx)); 408 retVal.setParamName(theParamName.substring(0, colonIdx)); 409 retVal.setWholeQualifier(theParamName.substring(colonIdx)); 410 } 411 } else if (dotIdx != -1) { 412 retVal.setDotQualifier(theParamName.substring(dotIdx)); 413 retVal.setParamName(theParamName.substring(0, dotIdx)); 414 retVal.setWholeQualifier(theParamName.substring(dotIdx)); 415 } else if (colonIdx != -1) { 416 retVal.setColonQualifier(theParamName.substring(colonIdx)); 417 retVal.setParamName(theParamName.substring(0, colonIdx)); 418 retVal.setWholeQualifier(theParamName.substring(colonIdx)); 419 } else { 420 retVal.setParamName(theParamName); 421 retVal.setColonQualifier(null); 422 retVal.setDotQualifier(null); 423 retVal.setWholeQualifier(null); 424 } 425 426 return retVal; 427 } 428 429 public static class QualifierDetails { 430 431 private String myColonQualifier; 432 private String myDotQualifier; 433 private String myParamName; 434 private String myWholeQualifier; 435 436 public boolean passes(Set<String> theQualifierWhitelist, Set<String> theQualifierBlacklist) { 437 if (theQualifierWhitelist != null) { 438 if (!theQualifierWhitelist.contains(".*")) { 439 if (myDotQualifier != null) { 440 if (!theQualifierWhitelist.contains(myDotQualifier)) { 441 return false; 442 } 443 } else { 444 if (!theQualifierWhitelist.contains(".")) { 445 return false; 446 } 447 } 448 } 449 /* 450 * This was removed Sep 9 2015, as I don't see any way it could possibly be triggered. 451 if (!theQualifierWhitelist.contains(SearchParameter.QUALIFIER_ANY_TYPE)) { 452 if (myColonQualifier != null) { 453 if (!theQualifierWhitelist.contains(myColonQualifier)) { 454 return false; 455 } 456 } else { 457 if (!theQualifierWhitelist.contains(":")) { 458 return false; 459 } 460 } 461 } 462 */ 463 } 464 if (theQualifierBlacklist != null) { 465 if (myDotQualifier != null) { 466 if (theQualifierBlacklist.contains(myDotQualifier)) { 467 return false; 468 } 469 } 470 if (myColonQualifier != null) { 471 if (theQualifierBlacklist.contains(myColonQualifier)) { 472 return false; 473 } 474 } 475 } 476 477 return true; 478 } 479 480 public void setParamName(String theParamName) { 481 myParamName = theParamName; 482 } 483 484 public String getParamName() { 485 return myParamName; 486 } 487 488 public void setColonQualifier(String theColonQualifier) { 489 myColonQualifier = theColonQualifier; 490 } 491 492 public void setDotQualifier(String theDotQualifier) { 493 myDotQualifier = theDotQualifier; 494 } 495 496 public String getWholeQualifier() { 497 return myWholeQualifier; 498 } 499 500 public void setWholeQualifier(String theWholeQualifier) { 501 myWholeQualifier = theWholeQualifier; 502 } 503 504 } 505 506 public static BaseHttpClientInvocation createSearchInvocation(FhirContext theContext, String theSearchUrl, Map<String, List<String>> theParams) { 507 return new HttpGetClientInvocation(theContext, theParams, theSearchUrl); 508 } 509 510}