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}