
001/*- 002 * #%L 003 * HAPI FHIR - Core Library 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.interceptor.model; 021 022import ca.uhn.fhir.model.api.IModelJson; 023import ca.uhn.fhir.rest.api.Constants; 024import ca.uhn.fhir.util.JsonUtil; 025import com.fasterxml.jackson.annotation.JsonProperty; 026import com.fasterxml.jackson.core.JsonProcessingException; 027import com.fasterxml.jackson.databind.ObjectMapper; 028import jakarta.annotation.Nonnull; 029import jakarta.annotation.Nullable; 030import org.apache.commons.collections4.CollectionUtils; 031import org.apache.commons.lang3.Validate; 032import org.apache.commons.lang3.builder.EqualsBuilder; 033import org.apache.commons.lang3.builder.HashCodeBuilder; 034import org.apache.commons.lang3.builder.ToStringBuilder; 035import org.apache.commons.lang3.builder.ToStringStyle; 036import org.hl7.fhir.instance.model.api.IBaseResource; 037 038import java.time.LocalDate; 039import java.util.ArrayList; 040import java.util.Arrays; 041import java.util.Collection; 042import java.util.Collections; 043import java.util.List; 044import java.util.Objects; 045import java.util.Optional; 046import java.util.stream.Collectors; 047import java.util.stream.Stream; 048 049import static org.apache.commons.lang3.ObjectUtils.getIfNull; 050 051/** 052 * @since 5.0.0 053 */ 054public class RequestPartitionId implements IModelJson { 055 private static final RequestPartitionId ALL_PARTITIONS = new RequestPartitionId(); 056 private static final ObjectMapper ourObjectMapper = 057 new ObjectMapper().registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule()); 058 059 @JsonProperty("partitionDate") 060 private final LocalDate myPartitionDate; 061 062 @JsonProperty("allPartitions") 063 private final boolean myAllPartitions; 064 065 @JsonProperty("partitionIds") 066 private final List<Integer> myPartitionIds; 067 068 @JsonProperty("partitionNames") 069 private final List<String> myPartitionNames; 070 071 /** 072 * Constructor for a single partition 073 */ 074 private RequestPartitionId( 075 @Nullable String thePartitionName, @Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) { 076 this(toListOrNull(thePartitionName), toListOrNull(thePartitionId), thePartitionDate); 077 } 078 079 /** 080 * Constructor for a multiple partition 081 */ 082 private RequestPartitionId( 083 @Nullable List<String> thePartitionName, 084 @Nullable List<Integer> thePartitionId, 085 @Nullable LocalDate thePartitionDate) { 086 this(thePartitionName, thePartitionId, thePartitionDate, false); 087 } 088 089 /** 090 * Constructor for a multiple partitions with explicit "all partitions" flag 091 */ 092 private RequestPartitionId( 093 @Nullable List<String> thePartitionName, 094 @Nullable List<Integer> thePartitionId, 095 @Nullable LocalDate thePartitionDate, 096 boolean theAllPartitions) { 097 myPartitionIds = toListOrNull(thePartitionId); 098 myPartitionNames = toListOrNull(thePartitionName); 099 myPartitionDate = thePartitionDate; 100 myAllPartitions = theAllPartitions; 101 } 102 103 /** 104 * Constructor for all partitions 105 */ 106 private RequestPartitionId() { 107 super(); 108 myPartitionDate = null; 109 myPartitionNames = null; 110 myPartitionIds = null; 111 myAllPartitions = true; 112 } 113 114 @Nonnull 115 public static Optional<RequestPartitionId> getPartitionIfAssigned(IBaseResource theFromResource) { 116 return Optional.ofNullable((RequestPartitionId) theFromResource.getUserData(Constants.RESOURCE_PARTITION_ID)); 117 } 118 119 /** 120 * Creates a new RequestPartitionId which includes all partition IDs from 121 * this {@link RequestPartitionId} but also includes all IDs from the given 122 * {@link RequestPartitionId}. Any duplicates are only included once, and 123 * partition names and dates are ignored and not returned. This {@link RequestPartitionId} 124 * and {@literal theOther} are not modified. 125 * 126 * @since 7.4.0 127 */ 128 public RequestPartitionId mergeIds(RequestPartitionId theOther) { 129 if ((isAllPartitions() && !hasPartitionIds()) || (theOther.isAllPartitions() && !theOther.hasPartitionIds())) { 130 return RequestPartitionId.allPartitions(); 131 } 132 133 // don't know why this is required - otherwise PartitionedStrictTransactionR4Test fails 134 if (this.equals(theOther)) { 135 return this; 136 } 137 138 List<Integer> thisPartitionIds = getPartitionIds(); 139 List<Integer> otherPartitionIds = theOther.getPartitionIds(); 140 List<Integer> newPartitionIds = Stream.concat(thisPartitionIds.stream(), otherPartitionIds.stream()) 141 .distinct() 142 .collect(Collectors.toList()); 143 boolean newAllPartitions = isAllPartitions() || theOther.isAllPartitions(); 144 if (newAllPartitions) { 145 return RequestPartitionId.allPartitionsWithPartitionIds(newPartitionIds); 146 } else { 147 return RequestPartitionId.fromPartitionIds(newPartitionIds); 148 } 149 } 150 151 public static RequestPartitionId fromJson(String theJson) throws JsonProcessingException { 152 return ourObjectMapper.readValue(theJson, RequestPartitionId.class); 153 } 154 155 public boolean isAllPartitions() { 156 return myAllPartitions; 157 } 158 159 public boolean isPartitionCovered(Integer thePartitionId) { 160 return isAllPartitions() || getPartitionIds().contains(thePartitionId); 161 } 162 163 @Nullable 164 public LocalDate getPartitionDate() { 165 return myPartitionDate; 166 } 167 168 @Nullable 169 public List<String> getPartitionNames() { 170 return myPartitionNames; 171 } 172 173 @Nonnull 174 public List<Integer> getPartitionIds() { 175 Validate.notNull(myPartitionIds, "Partition IDs have not been set"); 176 return myPartitionIds; 177 } 178 179 @Override 180 public String toString() { 181 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 182 if (hasPartitionIds()) { 183 b.append("ids", getPartitionIds()); 184 } 185 if (hasPartitionNames()) { 186 b.append("names", getPartitionNames()); 187 } 188 if (myAllPartitions) { 189 b.append("allPartitions", myAllPartitions); 190 } 191 return b.build(); 192 } 193 194 /** 195 * Returns true if this partition definition contains the other. 196 * Compatible with equals: {@code a.contains(b) && b.contains(a) ==> a.equals(b)}. 197 * We can't implement Comparable because this is only a partial order. 198 */ 199 public boolean contains(RequestPartitionId theOther) { 200 if (this.isAllPartitions()) { 201 return true; 202 } 203 if (theOther.isAllPartitions()) { 204 return false; 205 } 206 if (hasPartitionNames() && theOther.hasPartitionNames()) { 207 return CollectionUtils.containsAll(myPartitionNames, theOther.myPartitionNames); 208 } 209 if (hasPartitionNames() || theOther.hasPartitionNames()) { 210 return false; 211 } 212 return CollectionUtils.containsAll(this.myPartitionIds, theOther.myPartitionIds); 213 } 214 215 @Override 216 public boolean equals(Object theO) { 217 if (this == theO) { 218 return true; 219 } 220 221 if (theO == null || getClass() != theO.getClass()) { 222 return false; 223 } 224 225 RequestPartitionId that = (RequestPartitionId) theO; 226 227 EqualsBuilder b = new EqualsBuilder(); 228 b.append(myAllPartitions, that.myAllPartitions); 229 b.append(myPartitionDate, that.myPartitionDate); 230 b.append(myPartitionIds, that.myPartitionIds); 231 b.append(myPartitionNames, that.myPartitionNames); 232 return b.isEquals(); 233 } 234 235 @Override 236 public int hashCode() { 237 return new HashCodeBuilder(17, 37) 238 .append(myPartitionDate) 239 .append(myAllPartitions) 240 .append(myPartitionIds) 241 .append(myPartitionNames) 242 .toHashCode(); 243 } 244 245 public String toJson() { 246 return JsonUtil.serializeOrInvalidRequest(this); 247 } 248 249 @Nullable 250 public Integer getFirstPartitionIdOrNull() { 251 if (myPartitionIds != null) { 252 return myPartitionIds.get(0); 253 } 254 return null; 255 } 256 257 public String getFirstPartitionNameOrNull() { 258 if (myPartitionNames != null) { 259 return myPartitionNames.get(0); 260 } 261 return null; 262 } 263 264 /** 265 * Returns true if this request partition contains only one partition ID and it is the DEFAULT partition ID (null) 266 * 267 * @deprecated use {@link #isPartition(Integer)} or {@link IDefaultPartitionSettings#isDefaultPartition(RequestPartitionId)} 268 * instead 269 * . 270 */ 271 @Deprecated(since = "2025.02.R01") 272 public boolean isDefaultPartition() { 273 return isPartition(null); 274 } 275 276 /** 277 * Test whether this request partition is for the given partition ID. 278 * 279 * @param thePartitionId is the partition id to be tested against 280 * @return <code>true</code> if the request partition contains exactly one partition ID and the partition ID is 281 * <code>thePartitionId</code>. 282 */ 283 public boolean isPartition(@Nullable Integer thePartitionId) { 284 if (isAllPartitions() && !hasPartitionIds()) { 285 return false; 286 } 287 return hasPartitionIds() 288 && getPartitionIds().size() == 1 289 && Objects.equals(getPartitionIds().get(0), thePartitionId); 290 } 291 292 public boolean hasPartitionId(Integer thePartitionId) { 293 Validate.notNull(myPartitionIds, "Partition IDs not set"); 294 return myPartitionIds.contains(thePartitionId); 295 } 296 297 public boolean hasPartitionIds() { 298 return myPartitionIds != null; 299 } 300 301 public boolean hasPartitionNames() { 302 return myPartitionNames != null; 303 } 304 305 /** 306 * Verifies that one of the requested partition is the default partition which is assumed to have a default value of 307 * null. 308 * 309 * @return true if one of the requested partition is the default partition(null). 310 * 311 * @deprecated use {@link #hasDefaultPartitionId(Integer)} or {@link IDefaultPartitionSettings#hasDefaultPartitionId} 312 * instead 313 */ 314 @Deprecated(since = "2025.02.R01") 315 public boolean hasDefaultPartitionId() { 316 return hasDefaultPartitionId(null); 317 } 318 319 /** 320 * Test whether this request partition has the default partition as one of its targeted partitions. 321 * 322 * This method can be directly invoked on a requestPartition object providing that <code>theDefaultPartitionId</code> 323 * is known or through {@link IDefaultPartitionSettings#hasDefaultPartitionId} where the implementer of the interface 324 * will provide the default partition id (see {@link IDefaultPartitionSettings#getDefaultPartitionId}). 325 * 326 * @param theDefaultPartitionId is the ID that was given to the default partition. The default partition ID can be 327 * NULL as per default or specifically assigned another value. 328 * See PartitionSettings#setDefaultPartitionId. 329 * @return <code>true</code> if the request partition has the default partition as one of the targeted partition. 330 */ 331 public boolean hasDefaultPartitionId(@Nullable Integer theDefaultPartitionId) { 332 return getPartitionIds().contains(theDefaultPartitionId); 333 } 334 335 public List<Integer> getPartitionIdsWithoutDefault() { 336 return getPartitionIds().stream().filter(Objects::nonNull).collect(Collectors.toList()); 337 } 338 339 @Nullable 340 private static <T> List<T> toListOrNull(@Nullable Collection<T> theList) { 341 if (theList != null) { 342 if (theList.size() == 1) { 343 return Collections.singletonList(theList.iterator().next()); 344 } 345 return Collections.unmodifiableList(new ArrayList<>(theList)); 346 } 347 return null; 348 } 349 350 @Nullable 351 private static <T> List<T> toListOrNull(@Nullable T theObject) { 352 if (theObject != null) { 353 return Collections.singletonList(theObject); 354 } 355 return null; 356 } 357 358 @SafeVarargs 359 @Nullable 360 private static <T> List<T> toListOrNull(@Nullable T... theObject) { 361 if (theObject != null) { 362 return Arrays.asList(theObject); 363 } 364 return null; 365 } 366 367 @Nonnull 368 public static RequestPartitionId allPartitions() { 369 return ALL_PARTITIONS; 370 } 371 372 /** 373 * Creates a new RequestPartitionId which indicates "all partitions" and explicitly lists them 374 * 375 * @since 8.8.0 376 */ 377 @Nonnull 378 public static RequestPartitionId allPartitionsWithPartitionIds(Integer... thePartitionIds) { 379 return allPartitionsWithPartitionIds(toListOrNull(thePartitionIds)); 380 } 381 382 /** 383 * Creates a new RequestPartitionId which indicates "all partitions" and explicitly lists them 384 * 385 * @since 8.8.0 386 */ 387 @Nonnull 388 public static RequestPartitionId allPartitionsWithPartitionIds(List<Integer> thePartitionIds) { 389 return new RequestPartitionId(null, thePartitionIds, null, true); 390 } 391 392 /** 393 * @deprecated use {@link RequestPartitionId#defaultPartition(IDefaultPartitionSettings)} instead 394 */ 395 @Deprecated 396 @Nonnull 397 // TODO GGG: This is a now-bad usage and we should remove it. we cannot assume null means default. 398 public static RequestPartitionId defaultPartition() { 399 return fromPartitionIds(Collections.singletonList(null)); 400 } 401 402 /** 403 * Creates a RequestPartitionId for the default partition using the provided partition settings. 404 * This method uses the default partition ID from the given settings to create the RequestPartitionId. 405 * 406 * @param theDefaultPartitionSettings the partition settings containing the default partition ID 407 * @return a RequestPartitionId for the default partition 408 */ 409 @Nonnull 410 public static RequestPartitionId defaultPartition(IDefaultPartitionSettings theDefaultPartitionSettings) { 411 return fromPartitionId(theDefaultPartitionSettings.getDefaultPartitionId()); 412 } 413 414 @Deprecated 415 @Nonnull 416 // TODO GGG: This is a now-bad usage and we should remove it. we cannot assume null means default. 417 public static RequestPartitionId defaultPartition(@Nullable LocalDate thePartitionDate) { 418 return fromPartitionIds(Collections.singletonList(null), thePartitionDate); 419 } 420 421 @Nonnull 422 public static RequestPartitionId fromPartitionId(@Nullable Integer thePartitionId) { 423 return fromPartitionIds(Collections.singletonList(thePartitionId)); 424 } 425 426 @Nonnull 427 public static RequestPartitionId fromPartitionId( 428 @Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) { 429 return new RequestPartitionId(null, Collections.singletonList(thePartitionId), thePartitionDate); 430 } 431 432 @Nonnull 433 public static RequestPartitionId fromPartitionIds(@Nonnull Collection<Integer> thePartitionIds) { 434 return fromPartitionIds(thePartitionIds, null); 435 } 436 437 @Nonnull 438 public static RequestPartitionId fromPartitionIds( 439 @Nonnull Collection<Integer> thePartitionIds, @Nullable LocalDate thePartitionDate) { 440 return new RequestPartitionId(null, toListOrNull(thePartitionIds), thePartitionDate); 441 } 442 443 @Nonnull 444 public static RequestPartitionId fromPartitionIds(Integer... thePartitionIds) { 445 return new RequestPartitionId(null, toListOrNull(thePartitionIds), null); 446 } 447 448 @Nonnull 449 public static RequestPartitionId fromPartitionName(@Nullable String thePartitionName) { 450 return fromPartitionName(thePartitionName, null); 451 } 452 453 @Nonnull 454 public static RequestPartitionId fromPartitionName( 455 @Nullable String thePartitionName, @Nullable LocalDate thePartitionDate) { 456 return new RequestPartitionId(thePartitionName, null, thePartitionDate); 457 } 458 459 @Nonnull 460 public static RequestPartitionId fromPartitionNames(@Nullable List<String> thePartitionNames) { 461 return new RequestPartitionId(toListOrNull(thePartitionNames), null, null); 462 } 463 464 @Nonnull 465 public static RequestPartitionId fromPartitionNames(String... thePartitionNames) { 466 return new RequestPartitionId(toListOrNull(thePartitionNames), null, null); 467 } 468 469 @Nonnull 470 public static RequestPartitionId fromPartitionIdAndName( 471 @Nullable Integer thePartitionId, @Nullable String thePartitionName) { 472 return new RequestPartitionId(thePartitionName, thePartitionId, null); 473 } 474 475 @Nonnull 476 public static RequestPartitionId forPartitionIdAndName( 477 @Nullable Integer thePartitionId, @Nullable String thePartitionName, @Nullable LocalDate thePartitionDate) { 478 return new RequestPartitionId(thePartitionName, thePartitionId, thePartitionDate); 479 } 480 481 @Nonnull 482 public static RequestPartitionId forPartitionIdsAndNames( 483 List<String> thePartitionNames, List<Integer> thePartitionIds, LocalDate thePartitionDate) { 484 return new RequestPartitionId(thePartitionNames, thePartitionIds, thePartitionDate); 485 } 486 487 @Nonnull 488 public static RequestPartitionId forPartitionIdsAndNames( 489 List<String> thePartitionNames, 490 List<Integer> thePartitionIds, 491 LocalDate thePartitionDate, 492 boolean theAllPartitions) { 493 return new RequestPartitionId(thePartitionNames, thePartitionIds, thePartitionDate, theAllPartitions); 494 } 495 496 /** 497 * Create a string representation suitable for use as a cache key. Null aware. 498 * <p> 499 * Returns the partition IDs (numeric) as a joined string with a space between, using the string "null" for any null values 500 */ 501 public static String stringifyForKey(@Nonnull RequestPartitionId theRequestPartitionId) { 502 String retVal; 503 if (theRequestPartitionId.hasPartitionIds()) { 504 assert theRequestPartitionId.hasPartitionIds(); 505 retVal = theRequestPartitionId.getPartitionIds().stream() 506 .map(t -> getIfNull(t, "null").toString()) 507 .collect(Collectors.joining(" ")); 508 if (theRequestPartitionId.isAllPartitions()) { 509 retVal = "(all) " + retVal; 510 } 511 } else { 512 retVal = "(all)"; 513 } 514 return retVal; 515 } 516 517 public String asJson() throws JsonProcessingException { 518 return ourObjectMapper.writeValueAsString(this); 519 } 520}