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