
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 defintion contains the other. 182 * Compatible with equals: 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 #isDefaultPartition(Integer)} or {@link IRequestPartitionHelperSvc.isDefaultPartition} 247 * instead 248 * . 249 */ 250 @Deprecated(since = "2025.02.R01") 251 public boolean isDefaultPartition() { 252 return isDefaultPartition(null); 253 } 254 255 /** 256 * Test whether this request partition is for a given default partition ID. 257 * 258 * This method can be directly invoked on a requestPartition object providing that <code>theDefaultPartitionId</code> 259 * is known or through {@link IRequestPartitionHelperSvc.isDefaultPartition} where the implementer of the interface 260 * will provide the default partition id (see {@link IRequestPartitionHelperSvc.getDefaultPartition}). 261 * 262 * @param theDefaultPartitionId is the ID that was given to the default partition. The default partition ID can be 263 * NULL as per default or specifically assigned another value. 264 * See PartitionSettings#setDefaultPartitionId. 265 * @return <code>true</code> if the request partition contains only one partition ID and the partition ID is 266 * <code>theDefaultPartitionId</code>. 267 */ 268 public boolean isDefaultPartition(@Nullable Integer theDefaultPartitionId) { 269 if (isAllPartitions()) { 270 return false; 271 } 272 return hasPartitionIds() 273 && getPartitionIds().size() == 1 274 && Objects.equals(getPartitionIds().get(0), theDefaultPartitionId); 275 } 276 277 public boolean hasPartitionId(Integer thePartitionId) { 278 Validate.notNull(myPartitionIds, "Partition IDs not set"); 279 return myPartitionIds.contains(thePartitionId); 280 } 281 282 public boolean hasPartitionIds() { 283 return myPartitionIds != null; 284 } 285 286 public boolean hasPartitionNames() { 287 return myPartitionNames != null; 288 } 289 290 /** 291 * Verifies that one of the requested partition is the default partition which is assumed to have a default value of 292 * null. 293 * 294 * @return true if one of the requested partition is the default partition(null). 295 * 296 * @deprecated use {@link #hasDefaultPartitionId(Integer)} or {@link IRequestPartitionHelperSvc.hasDefaultPartitionId} 297 * instead 298 */ 299 @Deprecated(since = "2025.02.R01") 300 public boolean hasDefaultPartitionId() { 301 return hasDefaultPartitionId(null); 302 } 303 304 /** 305 * Test whether this request partition has the default partition as one of its targeted partitions. 306 * 307 * This method can be directly invoked on a requestPartition object providing that <code>theDefaultPartitionId</code> 308 * is known or through {@link IRequestPartitionHelperSvc.hasDefaultPartitionId} where the implementer of the interface 309 * will provide the default partition id (see {@link IRequestPartitionHelperSvc.getDefaultPartition}). 310 * 311 * @param theDefaultPartitionId is the ID that was given to the default partition. The default partition ID can be 312 * NULL as per default or specifically assigned another value. 313 * See PartitionSettings#setDefaultPartitionId. 314 * @return <code>true</code> if the request partition has the default partition as one of the targeted partition. 315 */ 316 public boolean hasDefaultPartitionId(@Nullable Integer theDefaultPartitionId) { 317 return getPartitionIds().contains(theDefaultPartitionId); 318 } 319 320 public List<Integer> getPartitionIdsWithoutDefault() { 321 return getPartitionIds().stream().filter(Objects::nonNull).collect(Collectors.toList()); 322 } 323 324 @Nullable 325 private static <T> List<T> toListOrNull(@Nullable Collection<T> theList) { 326 if (theList != null) { 327 if (theList.size() == 1) { 328 return Collections.singletonList(theList.iterator().next()); 329 } 330 return Collections.unmodifiableList(new ArrayList<>(theList)); 331 } 332 return null; 333 } 334 335 @Nullable 336 private static <T> List<T> toListOrNull(@Nullable T theObject) { 337 if (theObject != null) { 338 return Collections.singletonList(theObject); 339 } 340 return null; 341 } 342 343 @SafeVarargs 344 @Nullable 345 private static <T> List<T> toListOrNull(@Nullable T... theObject) { 346 if (theObject != null) { 347 return Arrays.asList(theObject); 348 } 349 return null; 350 } 351 352 @Nonnull 353 public static RequestPartitionId allPartitions() { 354 return ALL_PARTITIONS; 355 } 356 357 @Nonnull 358 // TODO GGG: This is a now-bad usage and we should remove it. we cannot assume null means default. 359 public static RequestPartitionId defaultPartition() { 360 return fromPartitionIds(Collections.singletonList(null)); 361 } 362 363 @Nonnull 364 // TODO GGG: This is a now-bad usage and we should remove it. we cannot assume null means default. 365 public static RequestPartitionId defaultPartition(@Nullable LocalDate thePartitionDate) { 366 return fromPartitionIds(Collections.singletonList(null), thePartitionDate); 367 } 368 369 @Nonnull 370 public static RequestPartitionId fromPartitionId(@Nullable Integer thePartitionId) { 371 return fromPartitionIds(Collections.singletonList(thePartitionId)); 372 } 373 374 @Nonnull 375 public static RequestPartitionId fromPartitionId( 376 @Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) { 377 return new RequestPartitionId(null, Collections.singletonList(thePartitionId), thePartitionDate); 378 } 379 380 @Nonnull 381 public static RequestPartitionId fromPartitionIds(@Nonnull Collection<Integer> thePartitionIds) { 382 return fromPartitionIds(thePartitionIds, null); 383 } 384 385 @Nonnull 386 public static RequestPartitionId fromPartitionIds( 387 @Nonnull Collection<Integer> thePartitionIds, @Nullable LocalDate thePartitionDate) { 388 return new RequestPartitionId(null, toListOrNull(thePartitionIds), thePartitionDate); 389 } 390 391 @Nonnull 392 public static RequestPartitionId fromPartitionIds(Integer... thePartitionIds) { 393 return new RequestPartitionId(null, toListOrNull(thePartitionIds), null); 394 } 395 396 @Nonnull 397 public static RequestPartitionId fromPartitionName(@Nullable String thePartitionName) { 398 return fromPartitionName(thePartitionName, null); 399 } 400 401 @Nonnull 402 public static RequestPartitionId fromPartitionName( 403 @Nullable String thePartitionName, @Nullable LocalDate thePartitionDate) { 404 return new RequestPartitionId(thePartitionName, null, thePartitionDate); 405 } 406 407 @Nonnull 408 public static RequestPartitionId fromPartitionNames(@Nullable List<String> thePartitionNames) { 409 return new RequestPartitionId(toListOrNull(thePartitionNames), null, null); 410 } 411 412 @Nonnull 413 public static RequestPartitionId fromPartitionNames(String... thePartitionNames) { 414 return new RequestPartitionId(toListOrNull(thePartitionNames), null, null); 415 } 416 417 @Nonnull 418 public static RequestPartitionId fromPartitionIdAndName( 419 @Nullable Integer thePartitionId, @Nullable String thePartitionName) { 420 return new RequestPartitionId(thePartitionName, thePartitionId, null); 421 } 422 423 @Nonnull 424 public static RequestPartitionId forPartitionIdAndName( 425 @Nullable Integer thePartitionId, @Nullable String thePartitionName, @Nullable LocalDate thePartitionDate) { 426 return new RequestPartitionId(thePartitionName, thePartitionId, thePartitionDate); 427 } 428 429 @Nonnull 430 public static RequestPartitionId forPartitionIdsAndNames( 431 List<String> thePartitionNames, List<Integer> thePartitionIds, LocalDate thePartitionDate) { 432 return new RequestPartitionId(thePartitionNames, thePartitionIds, thePartitionDate); 433 } 434 435 /** 436 * Create a string representation suitable for use as a cache key. Null aware. 437 * <p> 438 * Returns the partition IDs (numeric) as a joined string with a space between, using the string "null" for any null values 439 */ 440 public static String stringifyForKey(@Nonnull RequestPartitionId theRequestPartitionId) { 441 String retVal = "(all)"; 442 if (!theRequestPartitionId.isAllPartitions()) { 443 assert theRequestPartitionId.hasPartitionIds(); 444 retVal = theRequestPartitionId.getPartitionIds().stream() 445 .map(t -> defaultIfNull(t, "null").toString()) 446 .collect(Collectors.joining(" ")); 447 } 448 return retVal; 449 } 450 451 public String asJson() throws JsonProcessingException { 452 return ourObjectMapper.writeValueAsString(this); 453 } 454}