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