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