001/* 002 * #%L 003 * HAPI FHIR JPA - Search Parameters 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.jpa.searchparam; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.model.api.IQueryParameterAnd; 024import ca.uhn.fhir.model.api.IQueryParameterOr; 025import ca.uhn.fhir.model.api.IQueryParameterType; 026import ca.uhn.fhir.model.api.Include; 027import ca.uhn.fhir.rest.api.Constants; 028import ca.uhn.fhir.rest.api.SearchContainedModeEnum; 029import ca.uhn.fhir.rest.api.SearchTotalModeEnum; 030import ca.uhn.fhir.rest.api.SortOrderEnum; 031import ca.uhn.fhir.rest.api.SortSpec; 032import ca.uhn.fhir.rest.api.SummaryEnum; 033import ca.uhn.fhir.rest.param.DateParam; 034import ca.uhn.fhir.rest.param.DateRangeParam; 035import ca.uhn.fhir.rest.param.ParamPrefixEnum; 036import ca.uhn.fhir.rest.param.QuantityParam; 037import ca.uhn.fhir.rest.param.TokenParamModifier; 038import ca.uhn.fhir.util.UrlUtil; 039import com.fasterxml.jackson.annotation.JsonIgnore; 040import jakarta.annotation.Nonnull; 041import org.apache.commons.lang3.StringUtils; 042import org.apache.commons.lang3.Validate; 043import org.apache.commons.lang3.builder.CompareToBuilder; 044import org.apache.commons.lang3.builder.ToStringBuilder; 045import org.apache.commons.lang3.builder.ToStringStyle; 046 047import java.io.Serializable; 048import java.util.ArrayList; 049import java.util.Collection; 050import java.util.Collections; 051import java.util.Comparator; 052import java.util.HashMap; 053import java.util.HashSet; 054import java.util.LinkedHashMap; 055import java.util.List; 056import java.util.Map; 057import java.util.Set; 058import java.util.stream.Collectors; 059 060import static ca.uhn.fhir.rest.param.ParamPrefixEnum.GREATERTHAN_OR_EQUALS; 061import static ca.uhn.fhir.rest.param.ParamPrefixEnum.LESSTHAN_OR_EQUALS; 062import static ca.uhn.fhir.rest.param.ParamPrefixEnum.NOT_EQUAL; 063import static org.apache.commons.lang3.StringUtils.defaultString; 064import static org.apache.commons.lang3.StringUtils.isBlank; 065import static org.apache.commons.lang3.StringUtils.isNotBlank; 066 067public class SearchParameterMap implements Serializable { 068 public static final Integer INTEGER_0 = 0; 069 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParameterMap.class); 070 private static final long serialVersionUID = 1L; 071 private final HashMap<String, List<List<IQueryParameterType>>> mySearchParameterMap = new LinkedHashMap<>(); 072 private Integer myCount; 073 private Integer myOffset; 074 private EverythingModeEnum myEverythingMode = null; 075 private Set<Include> myIncludes; 076 private DateRangeParam myLastUpdated; 077 private boolean myLoadSynchronous; 078 private Integer myLoadSynchronousUpTo; 079 private Set<Include> myRevIncludes; 080 private SortSpec mySort; 081 private SummaryEnum mySummaryMode; 082 private SearchTotalModeEnum mySearchTotalMode; 083 private QuantityParam myNearDistanceParam; 084 private boolean myLastN; 085 private Integer myLastNMax; 086 private boolean myDeleteExpunge; 087 private SearchContainedModeEnum mySearchContainedMode = SearchContainedModeEnum.FALSE; 088 089 /** 090 * Constructor 091 */ 092 public SearchParameterMap() { 093 super(); 094 } 095 096 /** 097 * Constructor 098 */ 099 public SearchParameterMap(String theName, IQueryParameterType theParam) { 100 add(theName, theParam); 101 } 102 103 /** 104 * Creates and returns a copy of this map 105 */ 106 @JsonIgnore 107 @Override 108 public SearchParameterMap clone() { 109 SearchParameterMap map = new SearchParameterMap(); 110 map.setSummaryMode(getSummaryMode()); 111 map.setSort(getSort()); 112 map.setSearchTotalMode(getSearchTotalMode()); 113 map.setRevIncludes(getRevIncludes()); 114 map.setIncludes(getIncludes()); 115 map.setEverythingMode(getEverythingMode()); 116 map.setCount(getCount()); 117 map.setDeleteExpunge(isDeleteExpunge()); 118 map.setLastN(isLastN()); 119 map.setLastNMax(getLastNMax()); 120 map.setLastUpdated(getLastUpdated()); 121 map.setLoadSynchronous(isLoadSynchronous()); 122 map.setNearDistanceParam(getNearDistanceParam()); 123 map.setLoadSynchronousUpTo(getLoadSynchronousUpTo()); 124 map.setOffset(getOffset()); 125 map.setSearchContainedMode(getSearchContainedMode()); 126 127 for (Map.Entry<String, List<List<IQueryParameterType>>> entry : mySearchParameterMap.entrySet()) { 128 List<List<IQueryParameterType>> andParams = entry.getValue(); 129 List<List<IQueryParameterType>> newAndParams = new ArrayList<>(); 130 for (List<IQueryParameterType> orParams : andParams) { 131 List<IQueryParameterType> newOrParams = new ArrayList<>(orParams); 132 newAndParams.add(newOrParams); 133 } 134 map.put(entry.getKey(), newAndParams); 135 } 136 137 return map; 138 } 139 140 public SummaryEnum getSummaryMode() { 141 return mySummaryMode; 142 } 143 144 public void setSummaryMode(SummaryEnum theSummaryMode) { 145 mySummaryMode = theSummaryMode; 146 } 147 148 public SearchTotalModeEnum getSearchTotalMode() { 149 return mySearchTotalMode; 150 } 151 152 public void setSearchTotalMode(SearchTotalModeEnum theSearchTotalMode) { 153 mySearchTotalMode = theSearchTotalMode; 154 } 155 156 public SearchParameterMap add(String theName, DateParam theDateParam) { 157 add(theName, (IQueryParameterOr<?>) theDateParam); 158 return this; 159 } 160 161 @SuppressWarnings("unchecked") 162 public SearchParameterMap add(String theName, IQueryParameterAnd<?> theAnd) { 163 if (theAnd == null) { 164 return this; 165 } 166 if (!containsKey(theName)) { 167 put(theName, new ArrayList<>()); 168 } 169 170 List<List<IQueryParameterType>> paramList = get(theName); 171 for (IQueryParameterOr<?> next : theAnd.getValuesAsQueryTokens()) { 172 if (next == null) { 173 continue; 174 } 175 paramList.add((List<IQueryParameterType>) next.getValuesAsQueryTokens()); 176 } 177 178 return this; 179 } 180 181 public SearchParameterMap add(String theName, IQueryParameterOr<?> theOr) { 182 if (theOr == null) { 183 return this; 184 } 185 if (!containsKey(theName)) { 186 put(theName, new ArrayList<>()); 187 } 188 189 get(theName).add((List<IQueryParameterType>) theOr.getValuesAsQueryTokens()); 190 return this; 191 } 192 193 public Collection<List<List<IQueryParameterType>>> values() { 194 return mySearchParameterMap.values(); 195 } 196 197 public SearchParameterMap add(String theName, IQueryParameterType theParam) { 198 assert !Constants.PARAM_LASTUPDATED.equals(theName); // this has it's own field in the map 199 200 if (theParam == null) { 201 return this; 202 } 203 if (!containsKey(theName)) { 204 put(theName, new ArrayList<>()); 205 } 206 ArrayList<IQueryParameterType> list = new ArrayList<>(); 207 list.add(theParam); 208 get(theName).add(list); 209 210 return this; 211 } 212 213 public SearchParameterMap addInclude(Include theInclude) { 214 getIncludes().add(theInclude); 215 return this; 216 } 217 218 private void addLastUpdateParam(StringBuilder theBuilder, ParamPrefixEnum thePrefix, DateParam theDateParam) { 219 if (theDateParam != null && isNotBlank(theDateParam.getValueAsString())) { 220 addUrlParamSeparator(theBuilder); 221 theBuilder.append(Constants.PARAM_LASTUPDATED); 222 theBuilder.append('='); 223 theBuilder.append(thePrefix.getValue()); 224 theBuilder.append(theDateParam.getValueAsString()); 225 } 226 } 227 228 public SearchParameterMap addRevInclude(Include theInclude) { 229 getRevIncludes().add(theInclude); 230 return this; 231 } 232 233 private void addUrlIncludeParams(StringBuilder b, String paramName, Set<Include> theList) { 234 ArrayList<Include> list = new ArrayList<>(theList); 235 236 list.sort(new IncludeComparator()); 237 for (Include nextInclude : list) { 238 addUrlParamSeparator(b); 239 b.append(paramName); 240 if (nextInclude.isRecurse()) { 241 b.append(Constants.PARAM_INCLUDE_QUALIFIER_RECURSE); 242 } 243 b.append('='); 244 if (Constants.INCLUDE_STAR.equals(nextInclude.getValue())) { 245 b.append(Constants.INCLUDE_STAR); 246 } else { 247 b.append(UrlUtil.escapeUrlParam(nextInclude.getParamType())); 248 b.append(':'); 249 b.append(UrlUtil.escapeUrlParam(nextInclude.getParamName())); 250 if (isNotBlank(nextInclude.getParamTargetType())) { 251 b.append(':'); 252 b.append(nextInclude.getParamTargetType()); 253 } 254 } 255 } 256 } 257 258 private void addUrlParamSeparator(StringBuilder theB) { 259 if (theB.length() == 0) { 260 theB.append('?'); 261 } else { 262 theB.append('&'); 263 } 264 } 265 266 public Integer getCount() { 267 return myCount; 268 } 269 270 public SearchParameterMap setCount(Integer theCount) { 271 myCount = theCount; 272 return this; 273 } 274 275 public Integer getOffset() { 276 return myOffset; 277 } 278 279 public void setOffset(Integer theOffset) { 280 myOffset = theOffset; 281 } 282 283 public EverythingModeEnum getEverythingMode() { 284 return myEverythingMode; 285 } 286 287 public void setEverythingMode(EverythingModeEnum theConsolidateMatches) { 288 myEverythingMode = theConsolidateMatches; 289 } 290 291 public Set<Include> getIncludes() { 292 if (myIncludes == null) { 293 myIncludes = new HashSet<>(); 294 } 295 return myIncludes; 296 } 297 298 public void setIncludes(Set<Include> theIncludes) { 299 myIncludes = theIncludes; 300 } 301 302 /** 303 * Returns null if there is no last updated value 304 */ 305 public DateRangeParam getLastUpdated() { 306 if (myLastUpdated != null) { 307 if (myLastUpdated.isEmpty()) { 308 myLastUpdated = null; 309 } 310 } 311 return myLastUpdated; 312 } 313 314 public void setLastUpdated(DateRangeParam theLastUpdated) { 315 myLastUpdated = theLastUpdated; 316 } 317 318 /** 319 * If set, tells the server to load these results synchronously, and not to load 320 * more than X results 321 */ 322 public Integer getLoadSynchronousUpTo() { 323 return myLoadSynchronousUpTo; 324 } 325 326 /** 327 * If set, tells the server to load these results synchronously, and not to load 328 * more than X results. Note that setting this to a value will also set 329 * {@link #setLoadSynchronous(boolean)} to true 330 */ 331 public SearchParameterMap setLoadSynchronousUpTo(Integer theLoadSynchronousUpTo) { 332 myLoadSynchronousUpTo = theLoadSynchronousUpTo; 333 if (myLoadSynchronousUpTo != null) { 334 setLoadSynchronous(true); 335 } 336 return this; 337 } 338 339 public Set<Include> getRevIncludes() { 340 if (myRevIncludes == null) { 341 myRevIncludes = new HashSet<>(); 342 } 343 return myRevIncludes; 344 } 345 346 public void setRevIncludes(Set<Include> theRevIncludes) { 347 myRevIncludes = theRevIncludes; 348 } 349 350 public SortSpec getSort() { 351 return mySort; 352 } 353 354 public SearchParameterMap setSort(SortSpec theSort) { 355 mySort = theSort; 356 return this; 357 } 358 359 /** 360 * If set, tells the server to load these results synchronously, and not to load 361 * more than X results 362 */ 363 public boolean isLoadSynchronous() { 364 return myLoadSynchronous; 365 } 366 367 /** 368 * If set, tells the server to load these results synchronously, and not to load 369 * more than X results 370 */ 371 public SearchParameterMap setLoadSynchronous(boolean theLoadSynchronous) { 372 myLoadSynchronous = theLoadSynchronous; 373 return this; 374 } 375 376 /** 377 * If set, tells the server to use an Elasticsearch query to generate a list of 378 * Resource IDs for the LastN operation 379 */ 380 public boolean isLastN() { 381 return myLastN; 382 } 383 384 /** 385 * If set, tells the server to use an Elasticsearch query to generate a list of 386 * Resource IDs for the LastN operation 387 */ 388 public SearchParameterMap setLastN(boolean theLastN) { 389 myLastN = theLastN; 390 return this; 391 } 392 393 /** 394 * If set, tells the server the maximum number of observations to return for each 395 * observation code in the result set of a lastn operation 396 */ 397 public Integer getLastNMax() { 398 return myLastNMax; 399 } 400 401 /** 402 * If set, tells the server the maximum number of observations to return for each 403 * observation code in the result set of a lastn operation 404 */ 405 public SearchParameterMap setLastNMax(Integer theLastNMax) { 406 myLastNMax = theLastNMax; 407 return this; 408 } 409 410 /** 411 * This method creates a URL query string representation of the parameters in this 412 * object, excluding the part before the parameters, e.g. 413 * <p> 414 * <code>?name=smith&_sort=Patient:family</code> 415 * </p> 416 * <p> 417 * This method <b>excludes</b> the <code>_count</code> parameter, 418 * as it doesn't affect the substance of the results returned 419 * </p> 420 */ 421 public String toNormalizedQueryString(FhirContext theCtx) { 422 StringBuilder b = new StringBuilder(); 423 424 ArrayList<String> keys = new ArrayList<>(keySet()); 425 Collections.sort(keys); 426 for (String nextKey : keys) { 427 428 List<List<IQueryParameterType>> nextValuesAndsIn = get(nextKey); 429 List<List<IQueryParameterType>> nextValuesAndsOut = new ArrayList<>(); 430 431 for (List<? extends IQueryParameterType> nextValuesAndIn : nextValuesAndsIn) { 432 433 List<IQueryParameterType> nextValuesOrsOut = new ArrayList<>(); 434 435 nextValuesOrsOut.addAll(nextValuesAndIn); 436 437 nextValuesOrsOut.sort(new QueryParameterTypeComparator(theCtx)); 438 439 if (nextValuesOrsOut.size() > 0) { 440 nextValuesAndsOut.add(nextValuesOrsOut); 441 } 442 } // for AND 443 444 nextValuesAndsOut.sort(new QueryParameterOrComparator(theCtx)); 445 446 for (List<IQueryParameterType> nextValuesAnd : nextValuesAndsOut) { 447 addUrlParamSeparator(b); 448 IQueryParameterType firstValue = nextValuesAnd.get(0); 449 b.append(UrlUtil.escapeUrlParam(nextKey)); 450 451 if (firstValue.getMissing() != null) { 452 b.append(Constants.PARAMQUALIFIER_MISSING); 453 b.append('='); 454 if (firstValue.getMissing()) { 455 b.append(Constants.PARAMQUALIFIER_MISSING_TRUE); 456 } else { 457 b.append(Constants.PARAMQUALIFIER_MISSING_FALSE); 458 } 459 continue; 460 } 461 462 if (isNotBlank(firstValue.getQueryParameterQualifier())) { 463 b.append(firstValue.getQueryParameterQualifier()); 464 } 465 466 b.append('='); 467 468 for (int i = 0; i < nextValuesAnd.size(); i++) { 469 IQueryParameterType nextValueOr = nextValuesAnd.get(i); 470 if (i > 0) { 471 b.append(','); 472 } 473 String valueAsQueryToken = nextValueOr.getValueAsQueryToken(theCtx); 474 valueAsQueryToken = defaultString(valueAsQueryToken); 475 b.append(UrlUtil.escapeUrlParam(valueAsQueryToken)); 476 } 477 } 478 } // for keys 479 480 SortSpec sort = getSort(); 481 boolean first = true; 482 while (sort != null) { 483 484 if (isNotBlank(sort.getParamName())) { 485 if (first) { 486 addUrlParamSeparator(b); 487 b.append(Constants.PARAM_SORT); 488 b.append('='); 489 first = false; 490 } else { 491 b.append(','); 492 } 493 if (sort.getOrder() == SortOrderEnum.DESC) { 494 b.append('-'); 495 } 496 b.append(sort.getParamName()); 497 } 498 499 Validate.isTrue(sort != sort.getChain()); // just in case, shouldn't happen 500 sort = sort.getChain(); 501 } 502 503 if (hasIncludes()) { 504 addUrlIncludeParams(b, Constants.PARAM_INCLUDE, getIncludes()); 505 } 506 addUrlIncludeParams(b, Constants.PARAM_REVINCLUDE, getRevIncludes()); 507 508 if (getLastUpdated() != null) { 509 DateParam lb = getLastUpdated().getLowerBound(); 510 DateParam ub = getLastUpdated().getUpperBound(); 511 512 if (isNotEqualsComparator(lb, ub)) { 513 addLastUpdateParam(b, NOT_EQUAL, getLastUpdated().getLowerBound()); 514 } else { 515 addLastUpdateParam(b, GREATERTHAN_OR_EQUALS, lb); 516 addLastUpdateParam(b, LESSTHAN_OR_EQUALS, ub); 517 } 518 } 519 520 if (getCount() != null) { 521 addUrlParamSeparator(b); 522 b.append(Constants.PARAM_COUNT); 523 b.append('='); 524 b.append(getCount()); 525 } 526 527 if (getOffset() != null) { 528 addUrlParamSeparator(b); 529 b.append(Constants.PARAM_OFFSET); 530 b.append('='); 531 b.append(getOffset()); 532 } 533 534 // Summary mode (_summary) 535 if (getSummaryMode() != null) { 536 addUrlParamSeparator(b); 537 b.append(Constants.PARAM_SUMMARY); 538 b.append('='); 539 b.append(getSummaryMode().getCode()); 540 } 541 542 // Search count mode (_total) 543 if (getSearchTotalMode() != null) { 544 addUrlParamSeparator(b); 545 b.append(Constants.PARAM_SEARCH_TOTAL_MODE); 546 b.append('='); 547 b.append(getSearchTotalMode().getCode()); 548 } 549 550 // Contained mode 551 // For some reason, instead of null here, we default to false. That said, ommitting it is identical to setting 552 // it to false. 553 if (getSearchContainedMode() != SearchContainedModeEnum.FALSE) { 554 addUrlParamSeparator(b); 555 b.append(Constants.PARAM_CONTAINED); 556 b.append("="); 557 b.append(getSearchContainedMode().getCode()); 558 } 559 560 if (b.length() == 0) { 561 b.append('?'); 562 } 563 564 return b.toString(); 565 } 566 567 private boolean isNotEqualsComparator(DateParam theLowerBound, DateParam theUpperBound) { 568 return theLowerBound != null 569 && theUpperBound != null 570 && theLowerBound.getPrefix().equals(NOT_EQUAL) 571 && theUpperBound.getPrefix().equals(NOT_EQUAL); 572 } 573 574 /** 575 * @since 5.5.0 576 */ 577 public boolean hasIncludes() { 578 return myIncludes != null && !myIncludes.isEmpty(); 579 } 580 581 /** 582 * @since 6.2.0 583 */ 584 public boolean hasRevIncludes() { 585 return myRevIncludes != null && !myRevIncludes.isEmpty(); 586 } 587 588 @Override 589 public String toString() { 590 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 591 if (!isEmpty()) { 592 b.append("params", mySearchParameterMap); 593 } 594 if (!getIncludes().isEmpty()) { 595 b.append("includes", getIncludes()); 596 } 597 return b.toString(); 598 } 599 600 public void clean() { 601 for (Map.Entry<String, List<List<IQueryParameterType>>> nextParamEntry : this.entrySet()) { 602 String nextParamName = nextParamEntry.getKey(); 603 List<List<IQueryParameterType>> andOrParams = nextParamEntry.getValue(); 604 cleanParameter(nextParamName, andOrParams); 605 } 606 } 607 608 /* 609 * Given a particular named parameter, e.g. `name`, iterate over AndOrParams and remove any which are empty. 610 */ 611 private void cleanParameter(String theParamName, List<List<IQueryParameterType>> theAndOrParams) { 612 theAndOrParams.forEach(orList -> { 613 List<IQueryParameterType> emptyParameters = orList.stream() 614 .filter(nextOr -> nextOr.getMissing() == null) 615 .filter(nextOr -> nextOr instanceof QuantityParam) 616 .filter(nextOr -> isBlank(((QuantityParam) nextOr).getValueAsString())) 617 .collect(Collectors.toList()); 618 619 ourLog.debug("Ignoring empty parameter: {}", theParamName); 620 orList.removeAll(emptyParameters); 621 }); 622 theAndOrParams.removeIf(List::isEmpty); 623 } 624 625 public QuantityParam getNearDistanceParam() { 626 return myNearDistanceParam; 627 } 628 629 public void setNearDistanceParam(QuantityParam theQuantityParam) { 630 myNearDistanceParam = theQuantityParam; 631 } 632 633 public boolean isWantOnlyCount() { 634 return SummaryEnum.COUNT.equals(getSummaryMode()) || INTEGER_0.equals(getCount()); 635 } 636 637 public boolean isDeleteExpunge() { 638 return myDeleteExpunge; 639 } 640 641 public SearchParameterMap setDeleteExpunge(boolean theDeleteExpunge) { 642 myDeleteExpunge = theDeleteExpunge; 643 return this; 644 } 645 646 public List<List<IQueryParameterType>> get(String theName) { 647 return mySearchParameterMap.get(theName); 648 } 649 650 public void put(String theName, List<List<IQueryParameterType>> theParams) { 651 mySearchParameterMap.put(theName, theParams); 652 } 653 654 public boolean containsKey(String theName) { 655 return mySearchParameterMap.containsKey(theName); 656 } 657 658 public Set<String> keySet() { 659 return mySearchParameterMap.keySet(); 660 } 661 662 public boolean isEmpty() { 663 return mySearchParameterMap.isEmpty(); 664 } 665 666 // Wrapper methods 667 668 public Set<Map.Entry<String, List<List<IQueryParameterType>>>> entrySet() { 669 return mySearchParameterMap.entrySet(); 670 } 671 672 public List<List<IQueryParameterType>> remove(String theName) { 673 return mySearchParameterMap.remove(theName); 674 } 675 676 /** 677 * Variant of removeByNameAndModifier for unmodified params. 678 * 679 * @param theName the query parameter key 680 * @return an And/Or List of Query Parameters matching the name with no modifier. 681 */ 682 public List<List<IQueryParameterType>> removeByNameUnmodified(String theName) { 683 return this.removeByNameAndModifier(theName, ""); 684 } 685 686 /** 687 * Given a search parameter name and modifier (e.g. :text), 688 * get and remove all Search Parameters matching this name and modifier 689 * 690 * @param theName the query parameter key 691 * @param theModifier the qualifier you want to remove - nullable for unmodified params. 692 * @return an And/Or List of Query Parameters matching the qualifier. 693 */ 694 public List<List<IQueryParameterType>> removeByNameAndModifier(String theName, String theModifier) { 695 theModifier = StringUtils.defaultString(theModifier, ""); 696 697 List<List<IQueryParameterType>> remainderParameters = new ArrayList<>(); 698 List<List<IQueryParameterType>> matchingParameters = new ArrayList<>(); 699 700 // pull all of them out, partition by match against the qualifier 701 List<List<IQueryParameterType>> andList = mySearchParameterMap.remove(theName); 702 if (andList != null) { 703 for (List<IQueryParameterType> orList : andList) { 704 if (!orList.isEmpty() 705 && StringUtils.defaultString(orList.get(0).getQueryParameterQualifier(), "") 706 .equals(theModifier)) { 707 matchingParameters.add(orList); 708 } else { 709 remainderParameters.add(orList); 710 } 711 } 712 } 713 714 // put the unmatched back in. 715 if (!remainderParameters.isEmpty()) { 716 mySearchParameterMap.put(theName, remainderParameters); 717 } 718 return matchingParameters; 719 } 720 721 public List<List<IQueryParameterType>> removeByNameAndModifier( 722 String theName, @Nonnull TokenParamModifier theModifier) { 723 return removeByNameAndModifier(theName, theModifier.getValue()); 724 } 725 726 /** 727 * For each search parameter in the map, extract any which have the given qualifier. 728 * e.g. Take the url: Observation?code:text=abc&code=123&code:text=def&reason:text=somereason 729 * <p> 730 * If we call this function with `:text`, it will return a map that looks like: 731 * <p> 732 * code -> [[code:text=abc], [code:text=def]] 733 * reason -> [[reason:text=somereason]] 734 * <p> 735 * and the remaining search parameters in the map will be: 736 * <p> 737 * code -> [[code=123]] 738 * 739 * @param theQualifier 740 * @return 741 */ 742 public Map<String, List<List<IQueryParameterType>>> removeByQualifier(String theQualifier) { 743 744 Map<String, List<List<IQueryParameterType>>> retVal = new HashMap<>(); 745 Set<String> parameterNames = mySearchParameterMap.keySet(); 746 for (String parameterName : parameterNames) { 747 List<List<IQueryParameterType>> paramsWithQualifier = removeByNameAndModifier(parameterName, theQualifier); 748 retVal.put(parameterName, paramsWithQualifier); 749 } 750 751 return retVal; 752 } 753 754 public Map<String, List<List<IQueryParameterType>>> removeByQualifier(@Nonnull TokenParamModifier theModifier) { 755 return removeByQualifier(theModifier.getValue()); 756 } 757 758 public int size() { 759 return mySearchParameterMap.size(); 760 } 761 762 public SearchContainedModeEnum getSearchContainedMode() { 763 return mySearchContainedMode; 764 } 765 766 public void setSearchContainedMode(SearchContainedModeEnum theSearchContainedMode) { 767 if (theSearchContainedMode == null) { 768 mySearchContainedMode = SearchContainedModeEnum.FALSE; 769 } else { 770 this.mySearchContainedMode = theSearchContainedMode; 771 } 772 } 773 774 /** 775 * Returns true if {@link #getOffset()} and {@link #getCount()} both return a non null response 776 * 777 * @since 5.5.0 778 */ 779 public boolean isOffsetQuery() { 780 return getOffset() != null && getCount() != null; 781 } 782 783 public enum EverythingModeEnum { 784 /* 785 * Don't reorder! We rely on the ordinals 786 */ 787 ENCOUNTER_INSTANCE(false, true, true), 788 ENCOUNTER_TYPE(false, true, false), 789 PATIENT_INSTANCE(true, false, true), 790 PATIENT_TYPE(true, false, false); 791 792 private final boolean myEncounter; 793 794 private final boolean myInstance; 795 796 private final boolean myPatient; 797 798 EverythingModeEnum(boolean thePatient, boolean theEncounter, boolean theInstance) { 799 assert thePatient ^ theEncounter; 800 myPatient = thePatient; 801 myEncounter = theEncounter; 802 myInstance = theInstance; 803 } 804 805 public boolean isEncounter() { 806 return myEncounter; 807 } 808 809 public boolean isInstance() { 810 return myInstance; 811 } 812 813 public boolean isPatient() { 814 return myPatient; 815 } 816 } 817 818 static int compare(FhirContext theCtx, IQueryParameterType theO1, IQueryParameterType theO2) { 819 CompareToBuilder b = new CompareToBuilder(); 820 b.append(theO1.getMissing(), theO2.getMissing()); 821 b.append(theO1.getQueryParameterQualifier(), theO2.getQueryParameterQualifier()); 822 if (b.toComparison() == 0) { 823 b.append(theO1.getValueAsQueryToken(theCtx), theO2.getValueAsQueryToken(theCtx)); 824 } 825 826 return b.toComparison(); 827 } 828 829 public static SearchParameterMap newSynchronous() { 830 SearchParameterMap retVal = new SearchParameterMap(); 831 retVal.setLoadSynchronous(true); 832 return retVal; 833 } 834 835 public static SearchParameterMap newSynchronous(String theName, IQueryParameterType theParam) { 836 SearchParameterMap retVal = new SearchParameterMap(); 837 retVal.setLoadSynchronous(true); 838 retVal.add(theName, theParam); 839 return retVal; 840 } 841 842 public static class IncludeComparator implements Comparator<Include> { 843 844 @Override 845 public int compare(Include theO1, Include theO2) { 846 int retVal = StringUtils.compare(theO1.getParamType(), theO2.getParamType()); 847 if (retVal == 0) { 848 retVal = StringUtils.compare(theO1.getParamName(), theO2.getParamName()); 849 } 850 if (retVal == 0) { 851 retVal = StringUtils.compare(theO1.getParamTargetType(), theO2.getParamTargetType()); 852 } 853 return retVal; 854 } 855 } 856 857 public static class QueryParameterOrComparator implements Comparator<List<IQueryParameterType>> { 858 private final FhirContext myCtx; 859 860 QueryParameterOrComparator(FhirContext theCtx) { 861 myCtx = theCtx; 862 } 863 864 @Override 865 public int compare(List<IQueryParameterType> theO1, List<IQueryParameterType> theO2) { 866 // These lists will never be empty 867 return SearchParameterMap.compare(myCtx, theO1.get(0), theO2.get(0)); 868 } 869 } 870 871 public static class QueryParameterTypeComparator implements Comparator<IQueryParameterType> { 872 873 private final FhirContext myCtx; 874 875 QueryParameterTypeComparator(FhirContext theCtx) { 876 myCtx = theCtx; 877 } 878 879 @Override 880 public int compare(IQueryParameterType theO1, IQueryParameterType theO2) { 881 return SearchParameterMap.compare(myCtx, theO1, theO2); 882 } 883 } 884 885 public List<SortSpec> getAllChainsInOrder() { 886 final List<SortSpec> allChainsInOrder = new ArrayList<>(); 887 for (SortSpec sortSpec = getSort(); sortSpec != null; sortSpec = sortSpec.getChain()) { 888 allChainsInOrder.add(sortSpec); 889 } 890 891 return Collections.unmodifiableList(allChainsInOrder); 892 } 893}