001/*-
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2024 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        public boolean isDefaultPartition() {
225                if (isAllPartitions()) {
226                        return false;
227                }
228                return hasPartitionIds()
229                                && getPartitionIds().size() == 1
230                                && getPartitionIds().get(0) == null;
231        }
232
233        public boolean hasPartitionId(Integer thePartitionId) {
234                Validate.notNull(myPartitionIds, "Partition IDs not set");
235                return myPartitionIds.contains(thePartitionId);
236        }
237
238        public boolean hasPartitionIds() {
239                return myPartitionIds != null;
240        }
241
242        public boolean hasPartitionNames() {
243                return myPartitionNames != null;
244        }
245
246        public boolean hasDefaultPartitionId() {
247                return getPartitionIds().contains(null);
248        }
249
250        public List<Integer> getPartitionIdsWithoutDefault() {
251                return getPartitionIds().stream().filter(Objects::nonNull).collect(Collectors.toList());
252        }
253
254        @Nullable
255        private static <T> List<T> toListOrNull(@Nullable Collection<T> theList) {
256                if (theList != null) {
257                        if (theList.size() == 1) {
258                                return Collections.singletonList(theList.iterator().next());
259                        }
260                        return Collections.unmodifiableList(new ArrayList<>(theList));
261                }
262                return null;
263        }
264
265        @Nullable
266        private static <T> List<T> toListOrNull(@Nullable T theObject) {
267                if (theObject != null) {
268                        return Collections.singletonList(theObject);
269                }
270                return null;
271        }
272
273        @SafeVarargs
274        @Nullable
275        private static <T> List<T> toListOrNull(@Nullable T... theObject) {
276                if (theObject != null) {
277                        return Arrays.asList(theObject);
278                }
279                return null;
280        }
281
282        @Nonnull
283        public static RequestPartitionId allPartitions() {
284                return ALL_PARTITIONS;
285        }
286
287        @Nonnull
288        public static RequestPartitionId defaultPartition() {
289                return fromPartitionIds(Collections.singletonList(null));
290        }
291
292        @Nonnull
293        public static RequestPartitionId defaultPartition(@Nullable LocalDate thePartitionDate) {
294                return fromPartitionIds(Collections.singletonList(null), thePartitionDate);
295        }
296
297        @Nonnull
298        public static RequestPartitionId fromPartitionId(@Nullable Integer thePartitionId) {
299                return fromPartitionIds(Collections.singletonList(thePartitionId));
300        }
301
302        @Nonnull
303        public static RequestPartitionId fromPartitionId(
304                        @Nullable Integer thePartitionId, @Nullable LocalDate thePartitionDate) {
305                return new RequestPartitionId(null, Collections.singletonList(thePartitionId), thePartitionDate);
306        }
307
308        @Nonnull
309        public static RequestPartitionId fromPartitionIds(@Nonnull Collection<Integer> thePartitionIds) {
310                return fromPartitionIds(thePartitionIds, null);
311        }
312
313        @Nonnull
314        public static RequestPartitionId fromPartitionIds(
315                        @Nonnull Collection<Integer> thePartitionIds, @Nullable LocalDate thePartitionDate) {
316                return new RequestPartitionId(null, toListOrNull(thePartitionIds), thePartitionDate);
317        }
318
319        @Nonnull
320        public static RequestPartitionId fromPartitionIds(Integer... thePartitionIds) {
321                return new RequestPartitionId(null, toListOrNull(thePartitionIds), null);
322        }
323
324        @Nonnull
325        public static RequestPartitionId fromPartitionName(@Nullable String thePartitionName) {
326                return fromPartitionName(thePartitionName, null);
327        }
328
329        @Nonnull
330        public static RequestPartitionId fromPartitionName(
331                        @Nullable String thePartitionName, @Nullable LocalDate thePartitionDate) {
332                return new RequestPartitionId(thePartitionName, null, thePartitionDate);
333        }
334
335        @Nonnull
336        public static RequestPartitionId fromPartitionNames(@Nullable List<String> thePartitionNames) {
337                return new RequestPartitionId(toListOrNull(thePartitionNames), null, null);
338        }
339
340        @Nonnull
341        public static RequestPartitionId fromPartitionNames(String... thePartitionNames) {
342                return new RequestPartitionId(toListOrNull(thePartitionNames), null, null);
343        }
344
345        @Nonnull
346        public static RequestPartitionId fromPartitionIdAndName(
347                        @Nullable Integer thePartitionId, @Nullable String thePartitionName) {
348                return new RequestPartitionId(thePartitionName, thePartitionId, null);
349        }
350
351        @Nonnull
352        public static RequestPartitionId forPartitionIdAndName(
353                        @Nullable Integer thePartitionId, @Nullable String thePartitionName, @Nullable LocalDate thePartitionDate) {
354                return new RequestPartitionId(thePartitionName, thePartitionId, thePartitionDate);
355        }
356
357        @Nonnull
358        public static RequestPartitionId forPartitionIdsAndNames(
359                        List<String> thePartitionNames, List<Integer> thePartitionIds, LocalDate thePartitionDate) {
360                return new RequestPartitionId(thePartitionNames, thePartitionIds, thePartitionDate);
361        }
362
363        public static boolean isDefaultPartition(@Nullable RequestPartitionId thePartitionId) {
364                if (thePartitionId == null) {
365                        return false;
366                }
367
368                return thePartitionId.isDefaultPartition();
369        }
370
371        /**
372         * Create a string representation suitable for use as a cache key. Null aware.
373         * <p>
374         * Returns the partition IDs (numeric) as a joined string with a space between, using the string "null" for any null values
375         */
376        public static String stringifyForKey(@Nonnull RequestPartitionId theRequestPartitionId) {
377                String retVal = "(all)";
378                if (!theRequestPartitionId.isAllPartitions()) {
379                        assert theRequestPartitionId.hasPartitionIds();
380                        retVal = theRequestPartitionId.getPartitionIds().stream()
381                                        .map(t -> defaultIfNull(t, "null").toString())
382                                        .collect(Collectors.joining(" "));
383                }
384                return retVal;
385        }
386
387        public String asJson() throws JsonProcessingException {
388                return ourObjectMapper.writeValueAsString(this);
389        }
390}