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