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}