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