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}