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