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