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