
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 * @deprecated Use {@link #toNormalizedQueryString()} instead. 421 */ 422 @Deprecated(since = "8.6.0", forRemoval = true) 423 public String toNormalizedQueryString(FhirContext theCtx) { 424 return toNormalizedQueryString(); 425 } 426 427 /** 428 * This method creates a URL query string representation of the parameters in this 429 * object, excluding the part before the parameters, e.g. 430 * <p> 431 * <code>?name=smith&_sort=Patient:family</code> 432 * </p> 433 * <p> 434 * This method <b>excludes</b> the <code>_count</code> parameter, 435 * as it doesn't affect the substance of the results returned 436 * </p> 437 */ 438 public String toNormalizedQueryString() { 439 StringBuilder b = new StringBuilder(); 440 441 ArrayList<String> keys = new ArrayList<>(keySet()); 442 Collections.sort(keys); 443 for (String nextKey : keys) { 444 445 List<List<IQueryParameterType>> nextValuesAndsIn = get(nextKey); 446 List<List<IQueryParameterType>> nextValuesAndsOut = new ArrayList<>(); 447 448 for (List<? extends IQueryParameterType> nextValuesAndIn : nextValuesAndsIn) { 449 450 List<IQueryParameterType> nextValuesOrsOut = new ArrayList<>(nextValuesAndIn); 451 452 nextValuesOrsOut.sort(new QueryParameterTypeComparator()); 453 454 if (!nextValuesOrsOut.isEmpty()) { 455 nextValuesAndsOut.add(nextValuesOrsOut); 456 } 457 } // for AND 458 459 nextValuesAndsOut.sort(new QueryParameterOrComparator()); 460 461 for (List<IQueryParameterType> nextValuesAnd : nextValuesAndsOut) { 462 addUrlParamSeparator(b); 463 IQueryParameterType firstValue = nextValuesAnd.get(0); 464 b.append(UrlUtil.escapeUrlParam(nextKey)); 465 466 if (firstValue.getMissing() != null) { 467 b.append(Constants.PARAMQUALIFIER_MISSING); 468 b.append('='); 469 if (firstValue.getMissing()) { 470 b.append(Constants.PARAMQUALIFIER_MISSING_TRUE); 471 } else { 472 b.append(Constants.PARAMQUALIFIER_MISSING_FALSE); 473 } 474 continue; 475 } 476 477 if (isNotBlank(firstValue.getQueryParameterQualifier())) { 478 b.append(firstValue.getQueryParameterQualifier()); 479 } 480 481 b.append('='); 482 483 for (int i = 0; i < nextValuesAnd.size(); i++) { 484 IQueryParameterType nextValueOr = nextValuesAnd.get(i); 485 if (i > 0) { 486 b.append(','); 487 } 488 String valueAsQueryToken = nextValueOr.getValueAsQueryToken(); 489 valueAsQueryToken = defaultString(valueAsQueryToken); 490 b.append(UrlUtil.escapeUrlParam(valueAsQueryToken, false)); 491 } 492 } 493 } // for keys 494 495 SortSpec sort = getSort(); 496 boolean first = true; 497 while (sort != null) { 498 499 if (isNotBlank(sort.getParamName())) { 500 if (first) { 501 addUrlParamSeparator(b); 502 b.append(Constants.PARAM_SORT); 503 b.append('='); 504 first = false; 505 } else { 506 b.append(','); 507 } 508 if (sort.getOrder() == SortOrderEnum.DESC) { 509 b.append('-'); 510 } 511 b.append(sort.getParamName()); 512 } 513 514 Validate.isTrue(sort != sort.getChain()); // just in case, shouldn't happen 515 sort = sort.getChain(); 516 } 517 518 if (hasIncludes()) { 519 addUrlIncludeParams(b, Constants.PARAM_INCLUDE, getIncludes()); 520 } 521 addUrlIncludeParams(b, Constants.PARAM_REVINCLUDE, getRevIncludes()); 522 523 if (getLastUpdated() != null) { 524 DateParam lb = getLastUpdated().getLowerBound(); 525 DateParam ub = getLastUpdated().getUpperBound(); 526 527 if (isNotEqualsComparator(lb, ub)) { 528 addLastUpdateParam(b, NOT_EQUAL, getLastUpdated().getLowerBound()); 529 } else { 530 addLastUpdateParam(b, GREATERTHAN_OR_EQUALS, lb); 531 addLastUpdateParam(b, LESSTHAN_OR_EQUALS, ub); 532 } 533 } 534 535 if (getCount() != null) { 536 addUrlParamSeparator(b); 537 b.append(Constants.PARAM_COUNT); 538 b.append('='); 539 b.append(getCount()); 540 } 541 542 if (getOffset() != null) { 543 addUrlParamSeparator(b); 544 b.append(Constants.PARAM_OFFSET); 545 b.append('='); 546 b.append(getOffset()); 547 } 548 549 // Summary mode (_summary) 550 if (getSummaryMode() != null) { 551 addUrlParamSeparator(b); 552 b.append(Constants.PARAM_SUMMARY); 553 b.append('='); 554 b.append(getSummaryMode().getCode()); 555 } 556 557 // Search count mode (_total) 558 if (getSearchTotalMode() != null) { 559 addUrlParamSeparator(b); 560 b.append(Constants.PARAM_SEARCH_TOTAL_MODE); 561 b.append('='); 562 b.append(getSearchTotalMode().getCode()); 563 } 564 565 // Contained mode 566 // For some reason, instead of null here, we default to false. That said, ommitting it is identical to setting 567 // it to false. 568 if (getSearchContainedMode() != SearchContainedModeEnum.FALSE) { 569 addUrlParamSeparator(b); 570 b.append(Constants.PARAM_CONTAINED); 571 b.append("="); 572 b.append(getSearchContainedMode().getCode()); 573 } 574 575 if (getSearchIncludeDeletedMode() != null) { 576 addUrlParamSeparator(b); 577 b.append(Constants.PARAM_INCLUDE_DELETED); 578 b.append("="); 579 b.append(getSearchIncludeDeletedMode().getCode()); 580 } 581 582 if (b.isEmpty()) { 583 b.append('?'); 584 } 585 586 return b.toString(); 587 } 588 589 private boolean isNotEqualsComparator(DateParam theLowerBound, DateParam theUpperBound) { 590 return theLowerBound != null 591 && theUpperBound != null 592 && NOT_EQUAL.equals(theLowerBound.getPrefix()) 593 && NOT_EQUAL.equals(theUpperBound.getPrefix()); 594 } 595 596 /** 597 * @since 5.5.0 598 */ 599 public boolean hasIncludes() { 600 return myIncludes != null && !myIncludes.isEmpty(); 601 } 602 603 /** 604 * @since 6.2.0 605 */ 606 public boolean hasRevIncludes() { 607 return myRevIncludes != null && !myRevIncludes.isEmpty(); 608 } 609 610 @Override 611 public String toString() { 612 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 613 if (!isEmpty()) { 614 b.append("params", mySearchParameterMap); 615 } 616 if (!getIncludes().isEmpty()) { 617 b.append("includes", getIncludes()); 618 } 619 return b.toString(); 620 } 621 622 public void clean() { 623 for (Map.Entry<String, List<List<IQueryParameterType>>> nextParamEntry : this.entrySet()) { 624 String nextParamName = nextParamEntry.getKey(); 625 List<List<IQueryParameterType>> andOrParams = nextParamEntry.getValue(); 626 cleanParameter(nextParamName, andOrParams); 627 } 628 } 629 630 /* 631 * Given a particular named parameter, e.g. `name`, iterate over AndOrParams and remove any which are empty. 632 */ 633 private void cleanParameter(String theParamName, List<List<IQueryParameterType>> theAndOrParams) { 634 theAndOrParams.forEach(orList -> { 635 List<IQueryParameterType> emptyParameters = orList.stream() 636 .filter(nextOr -> nextOr.getMissing() == null) 637 .filter(nextOr -> nextOr instanceof QuantityParam) 638 .filter(nextOr -> isBlank(((QuantityParam) nextOr).getValueAsString())) 639 .toList(); 640 641 ourLog.debug("Ignoring empty parameter: {}", theParamName); 642 orList.removeAll(emptyParameters); 643 }); 644 theAndOrParams.removeIf(List::isEmpty); 645 } 646 647 public QuantityParam getNearDistanceParam() { 648 return myNearDistanceParam; 649 } 650 651 public void setNearDistanceParam(QuantityParam theQuantityParam) { 652 myNearDistanceParam = theQuantityParam; 653 } 654 655 public boolean isWantOnlyCount() { 656 return SummaryEnum.COUNT.equals(getSummaryMode()) || INTEGER_0.equals(getCount()); 657 } 658 659 public boolean isDeleteExpunge() { 660 return myDeleteExpunge; 661 } 662 663 public SearchParameterMap setDeleteExpunge(boolean theDeleteExpunge) { 664 myDeleteExpunge = theDeleteExpunge; 665 return this; 666 } 667 668 public List<List<IQueryParameterType>> get(String theName) { 669 return mySearchParameterMap.get(theName); 670 } 671 672 public void put(String theName, List<List<IQueryParameterType>> theParams) { 673 mySearchParameterMap.put(theName, theParams); 674 } 675 676 public boolean containsKey(String theName) { 677 return mySearchParameterMap.containsKey(theName); 678 } 679 680 public Set<String> keySet() { 681 return mySearchParameterMap.keySet(); 682 } 683 684 public boolean isEmpty() { 685 return mySearchParameterMap.isEmpty(); 686 } 687 688 // Wrapper methods 689 690 public Set<Map.Entry<String, List<List<IQueryParameterType>>>> entrySet() { 691 return mySearchParameterMap.entrySet(); 692 } 693 694 public List<List<IQueryParameterType>> remove(String theName) { 695 return mySearchParameterMap.remove(theName); 696 } 697 698 /** 699 * Variant of removeByNameAndModifier for unmodified params. 700 * 701 * @param theName the query parameter key 702 * @return an And/Or List of Query Parameters matching the name with no modifier. 703 */ 704 public List<List<IQueryParameterType>> removeByNameUnmodified(String theName) { 705 return this.removeByNameAndModifier(theName, ""); 706 } 707 708 /** 709 * Given a search parameter name and modifier (e.g. :text), 710 * get and remove all Search Parameters matching this name and modifier 711 * 712 * @param theName the query parameter key 713 * @param theModifier the qualifier you want to remove - nullable for unmodified params. 714 * @return an And/Or List of Query Parameters matching the qualifier. 715 */ 716 public List<List<IQueryParameterType>> removeByNameAndModifier(String theName, String theModifier) { 717 theModifier = Objects.toString(theModifier, ""); 718 719 List<List<IQueryParameterType>> remainderParameters = new ArrayList<>(); 720 List<List<IQueryParameterType>> matchingParameters = new ArrayList<>(); 721 722 // pull all of them out, partition by match against the qualifier 723 List<List<IQueryParameterType>> andList = mySearchParameterMap.remove(theName); 724 if (andList != null) { 725 for (List<IQueryParameterType> orList : andList) { 726 if (!orList.isEmpty() 727 && Objects.toString(orList.get(0).getQueryParameterQualifier(), "") 728 .equals(theModifier)) { 729 matchingParameters.add(orList); 730 } else { 731 remainderParameters.add(orList); 732 } 733 } 734 } 735 736 // put the unmatched back in. 737 if (!remainderParameters.isEmpty()) { 738 mySearchParameterMap.put(theName, remainderParameters); 739 } 740 return matchingParameters; 741 } 742 743 public List<List<IQueryParameterType>> removeByNameAndModifier( 744 String theName, @Nonnull TokenParamModifier theModifier) { 745 return removeByNameAndModifier(theName, theModifier.getValue()); 746 } 747 748 /** 749 * For each search parameter in the map, extract any which have the given qualifier. 750 * e.g. Take the url: {@code Observation?code:text=abc&code=123&code:text=def&reason:text=somereason} 751 * <p> 752 * If we call this function with `:text`, it will return a map that looks like: 753 * <p> 754 * code -> [[code:text=abc], [code:text=def]] 755 * reason -> [[reason:text=somereason]] 756 * <p> 757 * and the remaining search parameters in the map will be: 758 * <p> 759 * code -> [[code=123]] 760 */ 761 public Map<String, List<List<IQueryParameterType>>> removeByQualifier(String theQualifier) { 762 763 Map<String, List<List<IQueryParameterType>>> retVal = new HashMap<>(); 764 Set<String> parameterNames = mySearchParameterMap.keySet(); 765 for (String parameterName : parameterNames) { 766 List<List<IQueryParameterType>> paramsWithQualifier = removeByNameAndModifier(parameterName, theQualifier); 767 retVal.put(parameterName, paramsWithQualifier); 768 } 769 770 return retVal; 771 } 772 773 public Map<String, List<List<IQueryParameterType>>> removeByQualifier(@Nonnull TokenParamModifier theModifier) { 774 return removeByQualifier(theModifier.getValue()); 775 } 776 777 public int size() { 778 return mySearchParameterMap.size(); 779 } 780 781 public SearchContainedModeEnum getSearchContainedMode() { 782 return mySearchContainedMode; 783 } 784 785 public void setSearchContainedMode(SearchContainedModeEnum theSearchContainedMode) { 786 this.mySearchContainedMode = Objects.requireNonNullElse(theSearchContainedMode, SearchContainedModeEnum.FALSE); 787 } 788 789 public SearchIncludeDeletedEnum getSearchIncludeDeletedMode() { 790 return mySearchIncludeDeletedMode; 791 } 792 793 public void setSearchIncludeDeletedMode(SearchIncludeDeletedEnum theSearchIncludeDeletedMode) { 794 this.mySearchIncludeDeletedMode = theSearchIncludeDeletedMode; 795 } 796 797 /** 798 * Returns true if {@link #getOffset()} and {@link #getCount()} both return a non null response 799 * 800 * @since 5.5.0 801 */ 802 public boolean isOffsetQuery() { 803 return getOffset() != null && getCount() != null; 804 } 805 806 public enum EverythingModeEnum { 807 /* 808 * Don't reorder! We rely on the ordinals 809 */ 810 ENCOUNTER_INSTANCE(false, true, true), 811 ENCOUNTER_TYPE(false, true, false), 812 PATIENT_INSTANCE(true, false, true), 813 PATIENT_TYPE(true, false, false); 814 815 private final boolean myEncounter; 816 817 private final boolean myInstance; 818 819 private final boolean myPatient; 820 821 EverythingModeEnum(boolean thePatient, boolean theEncounter, boolean theInstance) { 822 assert thePatient ^ theEncounter; 823 myPatient = thePatient; 824 myEncounter = theEncounter; 825 myInstance = theInstance; 826 } 827 828 public boolean isEncounter() { 829 return myEncounter; 830 } 831 832 public boolean isInstance() { 833 return myInstance; 834 } 835 836 public boolean isPatient() { 837 return myPatient; 838 } 839 } 840 841 static int compare(IQueryParameterType theO1, IQueryParameterType theO2) { 842 CompareToBuilder b = new CompareToBuilder(); 843 b.append(theO1.getMissing(), theO2.getMissing()); 844 b.append(theO1.getQueryParameterQualifier(), theO2.getQueryParameterQualifier()); 845 if (b.toComparison() == 0) { 846 b.append(theO1.getValueAsQueryToken(), theO2.getValueAsQueryToken()); 847 } 848 849 return b.toComparison(); 850 } 851 852 public static SearchParameterMap newSynchronous() { 853 SearchParameterMap retVal = new SearchParameterMap(); 854 retVal.setLoadSynchronous(true); 855 return retVal; 856 } 857 858 public static SearchParameterMap newSynchronous(String theName, IQueryParameterType theParam) { 859 SearchParameterMap retVal = new SearchParameterMap(); 860 retVal.setLoadSynchronous(true); 861 retVal.add(theName, theParam); 862 return retVal; 863 } 864 865 public static class IncludeComparator implements Comparator<Include> { 866 867 @Override 868 public int compare(Include theO1, Include theO2) { 869 int retVal = Strings.CS.compare(theO1.getParamType(), theO2.getParamType()); 870 if (retVal == 0) { 871 retVal = Strings.CS.compare(theO1.getParamName(), theO2.getParamName()); 872 } 873 if (retVal == 0) { 874 retVal = Strings.CS.compare(theO1.getParamTargetType(), theO2.getParamTargetType()); 875 } 876 return retVal; 877 } 878 } 879 880 public static class QueryParameterOrComparator implements Comparator<List<IQueryParameterType>> { 881 882 /** 883 * Constructor 884 */ 885 QueryParameterOrComparator() { 886 super(); 887 } 888 889 @Override 890 public int compare(List<IQueryParameterType> theO1, List<IQueryParameterType> theO2) { 891 // These lists will never be empty 892 return SearchParameterMap.compare(theO1.get(0), theO2.get(0)); 893 } 894 } 895 896 public static class QueryParameterTypeComparator implements Comparator<IQueryParameterType> { 897 898 /** 899 * Constructor 900 */ 901 QueryParameterTypeComparator() { 902 super(); 903 } 904 905 @Override 906 public int compare(IQueryParameterType theO1, IQueryParameterType theO2) { 907 return SearchParameterMap.compare(theO1, theO2); 908 } 909 } 910 911 public List<SortSpec> getAllChainsInOrder() { 912 final List<SortSpec> allChainsInOrder = new ArrayList<>(); 913 for (SortSpec sortSpec = getSort(); sortSpec != null; sortSpec = sortSpec.getChain()) { 914 allChainsInOrder.add(sortSpec); 915 } 916 917 return Collections.unmodifiableList(allChainsInOrder); 918 } 919 920 @Override 921 public boolean equals(Object theO) { 922 if (!(theO instanceof SearchParameterMap that)) return false; 923 return myLoadSynchronous == that.myLoadSynchronous 924 && myLastN == that.myLastN 925 && myDeleteExpunge == that.myDeleteExpunge 926 && Objects.equals(mySearchParameterMap, that.mySearchParameterMap) 927 && Objects.equals(myCount, that.myCount) 928 && Objects.equals(myOffset, that.myOffset) 929 && myEverythingMode == that.myEverythingMode 930 && Objects.equals(myIncludes, that.myIncludes) 931 && Objects.equals(myLastUpdated, that.myLastUpdated) 932 && Objects.equals(myLoadSynchronousUpTo, that.myLoadSynchronousUpTo) 933 && Objects.equals(myRevIncludes, that.myRevIncludes) 934 && Objects.equals(mySort, that.mySort) 935 && mySummaryMode == that.mySummaryMode 936 && mySearchTotalMode == that.mySearchTotalMode 937 && Objects.equals(myNearDistanceParam, that.myNearDistanceParam) 938 && Objects.equals(myLastNMax, that.myLastNMax) 939 && mySearchContainedMode == that.mySearchContainedMode 940 && mySearchIncludeDeletedMode == that.mySearchIncludeDeletedMode; 941 } 942 943 @Override 944 public int hashCode() { 945 return Objects.hash( 946 mySearchParameterMap, 947 myCount, 948 myOffset, 949 myEverythingMode, 950 myIncludes, 951 myLastUpdated, 952 myLoadSynchronous, 953 myLoadSynchronousUpTo, 954 myRevIncludes, 955 mySort, 956 mySummaryMode, 957 mySearchTotalMode, 958 myNearDistanceParam, 959 myLastN, 960 myLastNMax, 961 myDeleteExpunge, 962 mySearchContainedMode, 963 mySearchIncludeDeletedMode); 964 } 965}