001/*
002 * #%L
003 * HAPI FHIR JPA Model
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.jpa.model.entity;
021
022import ca.uhn.fhir.jpa.model.config.PartitionSettings;
023import ca.uhn.fhir.jpa.model.listener.IndexStorageOptimizationListener;
024import ca.uhn.fhir.model.api.IQueryParameterType;
025import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
026import ca.uhn.fhir.model.primitive.InstantDt;
027import ca.uhn.fhir.rest.param.DateParam;
028import ca.uhn.fhir.rest.param.DateRangeParam;
029import ca.uhn.fhir.util.DateUtils;
030import jakarta.persistence.Column;
031import jakarta.persistence.Entity;
032import jakarta.persistence.EntityListeners;
033import jakarta.persistence.FetchType;
034import jakarta.persistence.ForeignKey;
035import jakarta.persistence.GeneratedValue;
036import jakarta.persistence.GenerationType;
037import jakarta.persistence.Id;
038import jakarta.persistence.IdClass;
039import jakarta.persistence.Index;
040import jakarta.persistence.JoinColumn;
041import jakarta.persistence.JoinColumns;
042import jakarta.persistence.ManyToOne;
043import jakarta.persistence.Table;
044import jakarta.persistence.Temporal;
045import jakarta.persistence.TemporalType;
046import jakarta.persistence.Transient;
047import org.apache.commons.lang3.StringUtils;
048import org.apache.commons.lang3.builder.EqualsBuilder;
049import org.apache.commons.lang3.builder.HashCodeBuilder;
050import org.apache.commons.lang3.builder.ToStringBuilder;
051import org.apache.commons.lang3.builder.ToStringStyle;
052import org.hibernate.annotations.GenericGenerator;
053import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
054import org.hl7.fhir.r4.model.DateTimeType;
055
056import java.text.ParseException;
057import java.text.SimpleDateFormat;
058import java.util.Date;
059
060@EntityListeners(IndexStorageOptimizationListener.class)
061@Entity
062@Table(
063                name = "HFJ_SPIDX_DATE",
064                indexes = {
065                        // We previously had an index called IDX_SP_DATE - Dont reuse
066                        @Index(
067                                        name = "IDX_SP_DATE_HASH_V2",
068                                        columnList = "HASH_IDENTITY,SP_VALUE_LOW,SP_VALUE_HIGH,RES_ID,PARTITION_ID"),
069                        @Index(name = "IDX_SP_DATE_HASH_HIGH_V2", columnList = "HASH_IDENTITY,SP_VALUE_HIGH,RES_ID,PARTITION_ID"),
070                        @Index(
071                                        name = "IDX_SP_DATE_ORD_HASH_V2",
072                                        columnList =
073                                                        "HASH_IDENTITY,SP_VALUE_LOW_DATE_ORDINAL,SP_VALUE_HIGH_DATE_ORDINAL,RES_ID,PARTITION_ID"),
074                        @Index(
075                                        name = "IDX_SP_DATE_ORD_HASH_HIGH_V2",
076                                        columnList = "HASH_IDENTITY,SP_VALUE_HIGH_DATE_ORDINAL,RES_ID,PARTITION_ID"),
077                        @Index(
078                                        name = "IDX_SP_DATE_RESID_V2",
079                                        columnList =
080                                                        "RES_ID,HASH_IDENTITY,SP_VALUE_LOW,SP_VALUE_HIGH,SP_VALUE_LOW_DATE_ORDINAL,SP_VALUE_HIGH_DATE_ORDINAL,PARTITION_ID"),
081                })
082@IdClass(IdAndPartitionId.class)
083public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchParam {
084
085        private static final long serialVersionUID = 1L;
086
087        @Column(name = "SP_VALUE_HIGH", nullable = true)
088        @Temporal(TemporalType.TIMESTAMP)
089        @FullTextField
090        public Date myValueHigh;
091
092        @Column(name = "SP_VALUE_LOW", nullable = true)
093        @Temporal(TemporalType.TIMESTAMP)
094        @FullTextField
095        public Date myValueLow;
096
097        /**
098         * Field which stores an integer representation of YYYYMDD as calculated by Calendar
099         * e.g. 2019-01-20 -> 20190120
100         */
101        @Column(name = "SP_VALUE_LOW_DATE_ORDINAL")
102        public Integer myValueLowDateOrdinal;
103
104        @Column(name = "SP_VALUE_HIGH_DATE_ORDINAL")
105        public Integer myValueHighDateOrdinal;
106
107        @Transient
108        private transient String myOriginalValue;
109
110        @Id
111        @GenericGenerator(name = "SEQ_SPIDX_DATE", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class)
112        @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_DATE")
113        @Column(name = "SP_ID")
114        private Long myId;
115
116        @ManyToOne(
117                        optional = false,
118                        fetch = FetchType.LAZY,
119                        cascade = {})
120        @JoinColumns(
121                        value = {
122                                @JoinColumn(
123                                                name = "RES_ID",
124                                                referencedColumnName = "RES_ID",
125                                                insertable = false,
126                                                updatable = false,
127                                                nullable = false),
128                                @JoinColumn(
129                                                name = "PARTITION_ID",
130                                                referencedColumnName = "PARTITION_ID",
131                                                insertable = false,
132                                                updatable = false,
133                                                nullable = false)
134                        },
135                        foreignKey = @ForeignKey(name = "FK_SP_DATE_RES"))
136        private ResourceTable myResource;
137
138        @Column(name = "RES_ID", nullable = false)
139        private Long myResourceId;
140
141        /**
142         * Constructor
143         */
144        public ResourceIndexedSearchParamDate() {
145                super();
146        }
147
148        /**
149         * Constructor
150         */
151        public ResourceIndexedSearchParamDate(
152                        PartitionSettings thePartitionSettings,
153                        String theResourceType,
154                        String theParamName,
155                        Date theLow,
156                        String theLowString,
157                        Date theHigh,
158                        String theHighString,
159                        String theOriginalValue) {
160                setPartitionSettings(thePartitionSettings);
161                setResourceType(theResourceType);
162                setParamName(theParamName);
163                setValueLow(theLow);
164                setValueHigh(theHigh);
165                if (theHigh != null && theHighString == null) {
166                        theHighString = DateUtils.convertDateToIso8601String(theHigh);
167                }
168                if (theLow != null && theLowString == null) {
169                        theLowString = DateUtils.convertDateToIso8601String(theLow);
170                }
171                computeValueHighDateOrdinal(theHighString);
172                computeValueLowDateOrdinal(theLowString);
173                reComputeValueHighDate(theHigh, theHighString);
174                myOriginalValue = theOriginalValue;
175                calculateHashes();
176        }
177
178        private void computeValueHighDateOrdinal(String theHigh) {
179                if (!StringUtils.isBlank(theHigh)) {
180                        this.myValueHighDateOrdinal = generateHighOrdinalDateInteger(theHigh);
181                }
182        }
183
184        private void reComputeValueHighDate(Date theHigh, String theHighString) {
185                if (StringUtils.isBlank(theHighString) || theHigh == null) return;
186                // FT : 2021-09-10 not very comfortable to set the high value to the last second
187                // Timezone? existing data?
188                // if YYYY or YYYY-MM or YYYY-MM-DD add the last second
189                if (theHighString.length() == 4 || theHighString.length() == 7 || theHighString.length() == 10) {
190
191                        String theCompleteDateStr =
192                                        DateUtils.getCompletedDate(theHighString).getRight();
193                        try {
194                                Date complateDate = new SimpleDateFormat("yyyy-MM-dd").parse(theCompleteDateStr);
195                                this.myValueHigh = DateUtils.getEndOfDay(complateDate);
196                        } catch (ParseException e) {
197                                // do nothing;
198                        }
199                }
200        }
201
202        private int generateLowOrdinalDateInteger(String theDateString) {
203                if (theDateString.contains("T")) {
204                        theDateString = theDateString.substring(0, theDateString.indexOf("T"));
205                }
206
207                theDateString = DateUtils.getCompletedDate(theDateString).getLeft();
208                theDateString = theDateString.replace("-", "");
209                return Integer.valueOf(theDateString);
210        }
211
212        private int generateHighOrdinalDateInteger(String theDateString) {
213
214                if (theDateString.contains("T")) {
215                        theDateString = theDateString.substring(0, theDateString.indexOf("T"));
216                }
217
218                theDateString = DateUtils.getCompletedDate(theDateString).getRight();
219                theDateString = theDateString.replace("-", "");
220                return Integer.valueOf(theDateString);
221        }
222
223        private void computeValueLowDateOrdinal(String theLow) {
224                if (StringUtils.isNotBlank(theLow)) {
225                        this.myValueLowDateOrdinal = generateLowOrdinalDateInteger(theLow);
226                }
227        }
228
229        public Integer getValueLowDateOrdinal() {
230                return myValueLowDateOrdinal;
231        }
232
233        public Integer getValueHighDateOrdinal() {
234                return myValueHighDateOrdinal;
235        }
236
237        @Override
238        public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) {
239                super.copyMutableValuesFrom(theSource);
240                ResourceIndexedSearchParamDate source = (ResourceIndexedSearchParamDate) theSource;
241                myValueHigh = source.myValueHigh;
242                myValueLow = source.myValueLow;
243                myValueHighDateOrdinal = source.myValueHighDateOrdinal;
244                myValueLowDateOrdinal = source.myValueLowDateOrdinal;
245                myHashIdentity = source.myHashIdentity;
246        }
247
248        @Override
249        public void setResourceId(Long theResourceId) {
250                myResourceId = theResourceId;
251        }
252
253        @Override
254        public void clearHashes() {
255                myHashIdentity = null;
256        }
257
258        @Override
259        public void calculateHashes() {
260                if (myHashIdentity != null) {
261                        return;
262                }
263
264                String resourceType = getResourceType();
265                String paramName = getParamName();
266                setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName));
267        }
268
269        @Override
270        public boolean equals(Object theObj) {
271                if (this == theObj) {
272                        return true;
273                }
274                if (theObj == null) {
275                        return false;
276                }
277                if (!(theObj instanceof ResourceIndexedSearchParamDate)) {
278                        return false;
279                }
280                ResourceIndexedSearchParamDate obj = (ResourceIndexedSearchParamDate) theObj;
281                EqualsBuilder b = new EqualsBuilder();
282                b.append(getHashIdentity(), obj.getHashIdentity());
283                b.append(getTimeFromDate(getValueHigh()), getTimeFromDate(obj.getValueHigh()));
284                b.append(getTimeFromDate(getValueLow()), getTimeFromDate(obj.getValueLow()));
285                b.append(getValueLowDateOrdinal(), obj.getValueLowDateOrdinal());
286                b.append(getValueHighDateOrdinal(), obj.getValueHighDateOrdinal());
287                b.append(isMissing(), obj.isMissing());
288                return b.isEquals();
289        }
290
291        @Override
292        public Long getId() {
293                return myId;
294        }
295
296        @Override
297        public void setId(Long theId) {
298                myId = theId;
299        }
300
301        protected Long getTimeFromDate(Date date) {
302                if (date != null) {
303                        return date.getTime();
304                }
305                return null;
306        }
307
308        public Date getValueHigh() {
309                return myValueHigh;
310        }
311
312        public ResourceIndexedSearchParamDate setValueHigh(Date theValueHigh) {
313                myValueHigh = theValueHigh;
314                return this;
315        }
316
317        public Date getValueLow() {
318                return myValueLow;
319        }
320
321        public ResourceIndexedSearchParamDate setValueLow(Date theValueLow) {
322                myValueLow = theValueLow;
323                return this;
324        }
325
326        @Override
327        public int hashCode() {
328                HashCodeBuilder b = new HashCodeBuilder();
329                b.append(getHashIdentity());
330                b.append(getTimeFromDate(getValueHigh()));
331                b.append(getTimeFromDate(getValueLow()));
332                b.append(getValueHighDateOrdinal());
333                b.append(getValueLowDateOrdinal());
334                b.append(isMissing());
335                return b.toHashCode();
336        }
337
338        @Override
339        public IQueryParameterType toQueryParameterType() {
340                DateTimeType value = new DateTimeType(myOriginalValue);
341                if (value.getPrecision().ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
342                        value.setTimeZoneZulu(true);
343                }
344                return new DateParam(value.getValueAsString());
345        }
346
347        @Override
348        public String toString() {
349                ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
350                b.append("partitionId", getPartitionId());
351                b.append("paramName", getParamName());
352                b.append("resourceId", getResourcePid());
353                b.append("valueLow", new InstantDt(getValueLow()));
354                b.append("valueHigh", new InstantDt(getValueHigh()));
355                b.append("ordLow", myValueLowDateOrdinal);
356                b.append("ordHigh", myValueHighDateOrdinal);
357                b.append("hashIdentity", myHashIdentity);
358                b.append("missing", isMissing());
359                return b.build();
360        }
361
362        @SuppressWarnings("ConstantConditions")
363        @Override
364        public boolean matches(IQueryParameterType theParam) {
365                if (!(theParam instanceof DateParam)) {
366                        return false;
367                }
368                DateParam dateParam = (DateParam) theParam;
369                DateRangeParam range = new DateRangeParam(dateParam);
370
371                boolean result;
372                if (dateParam.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) {
373                        result = matchesOrdinalDateBounds(range);
374                } else {
375                        result = matchesDateBounds(range);
376                }
377
378                return result;
379        }
380
381        private boolean matchesDateBounds(DateRangeParam range) {
382                Date lowerBound = range.getLowerBoundAsInstant();
383                Date upperBound = range.getUpperBoundAsInstant();
384                if (lowerBound == null && upperBound == null) {
385                        // should never happen
386                        return false;
387                }
388                boolean result = true;
389                if (lowerBound != null) {
390                        result &= (myValueLow.after(lowerBound) || myValueLow.equals(lowerBound));
391                        result &= (myValueHigh.after(lowerBound) || myValueHigh.equals(lowerBound));
392                }
393                if (upperBound != null) {
394                        result &= (myValueLow.before(upperBound) || myValueLow.equals(upperBound));
395                        result &= (myValueHigh.before(upperBound) || myValueHigh.equals(upperBound));
396                }
397                return result;
398        }
399
400        private boolean matchesOrdinalDateBounds(DateRangeParam range) {
401                boolean result = true;
402                Integer lowerBoundAsDateInteger = range.getLowerBoundAsDateInteger();
403                Integer upperBoundAsDateInteger = range.getUpperBoundAsDateInteger();
404                if (upperBoundAsDateInteger == null && lowerBoundAsDateInteger == null) {
405                        return false;
406                }
407                if (lowerBoundAsDateInteger != null) {
408                        // TODO as we run into equality issues
409                        result &= (myValueLowDateOrdinal.equals(lowerBoundAsDateInteger)
410                                        || myValueLowDateOrdinal > lowerBoundAsDateInteger);
411                        result &= (myValueHighDateOrdinal.equals(lowerBoundAsDateInteger)
412                                        || myValueHighDateOrdinal > lowerBoundAsDateInteger);
413                }
414                if (upperBoundAsDateInteger != null) {
415                        result &= (myValueHighDateOrdinal.equals(upperBoundAsDateInteger)
416                                        || myValueHighDateOrdinal < upperBoundAsDateInteger);
417                        result &= (myValueLowDateOrdinal.equals(upperBoundAsDateInteger)
418                                        || myValueLowDateOrdinal < upperBoundAsDateInteger);
419                }
420                return result;
421        }
422
423        public static Long calculateOrdinalValue(Date theDate) {
424                if (theDate == null) {
425                        return null;
426                }
427                return (long) DateUtils.convertDateToDayInteger(theDate);
428        }
429
430        @Override
431        public ResourceTable getResource() {
432                return myResource;
433        }
434
435        @Override
436        public BaseResourceIndexedSearchParam setResource(ResourceTable theResource) {
437                myResource = theResource;
438                setResourceType(theResource.getResourceType());
439                return this;
440        }
441}