001package ca.uhn.fhir.jpa.model.entity;
002
003/*
004 * #%L
005 * HAPI FHIR JPA Model
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import java.text.ParseException;
024import java.text.SimpleDateFormat;
025import java.util.Date;
026
027import javax.persistence.Column;
028import javax.persistence.Embeddable;
029import javax.persistence.Entity;
030import javax.persistence.GeneratedValue;
031import javax.persistence.GenerationType;
032import javax.persistence.Id;
033import javax.persistence.Index;
034import javax.persistence.SequenceGenerator;
035import javax.persistence.Table;
036import javax.persistence.Temporal;
037import javax.persistence.TemporalType;
038import javax.persistence.Transient;
039
040import org.apache.commons.lang3.StringUtils;
041import org.apache.commons.lang3.builder.EqualsBuilder;
042import org.apache.commons.lang3.builder.HashCodeBuilder;
043import org.apache.commons.lang3.builder.ToStringBuilder;
044import org.apache.commons.lang3.builder.ToStringStyle;
045import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
046import org.hl7.fhir.r4.model.DateTimeType;
047
048import ca.uhn.fhir.jpa.model.config.PartitionSettings;
049import ca.uhn.fhir.model.api.IQueryParameterType;
050import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
051import ca.uhn.fhir.model.primitive.InstantDt;
052import ca.uhn.fhir.rest.param.DateParam;
053import ca.uhn.fhir.rest.param.DateRangeParam;
054import ca.uhn.fhir.util.DateUtils;
055
056@Embeddable
057@Entity
058@Table(name = "HFJ_SPIDX_DATE", indexes = {
059        // We previously had an index called IDX_SP_DATE - Dont reuse
060        @Index(name = "IDX_SP_DATE_HASH", columnList = "HASH_IDENTITY,SP_VALUE_LOW,SP_VALUE_HIGH"),
061        @Index(name = "IDX_SP_DATE_HASH_LOW", columnList = "HASH_IDENTITY,SP_VALUE_LOW"),
062        @Index(name = "IDX_SP_DATE_HASH_HIGH", columnList = "HASH_IDENTITY,SP_VALUE_HIGH"),
063        @Index(name = "IDX_SP_DATE_ORD_HASH", columnList = "HASH_IDENTITY,SP_VALUE_LOW_DATE_ORDINAL,SP_VALUE_HIGH_DATE_ORDINAL"),
064        @Index(name = "IDX_SP_DATE_ORD_HASH_LOW", columnList = "HASH_IDENTITY,SP_VALUE_LOW_DATE_ORDINAL"),
065        @Index(name = "IDX_SP_DATE_RESID", columnList = "RES_ID"),
066        @Index(name = "IDX_SP_DATE_UPDATED", columnList = "SP_UPDATED"),
067})
068public class ResourceIndexedSearchParamDate extends BaseResourceIndexedSearchParam {
069
070        private static final long serialVersionUID = 1L;
071
072        @Column(name = "SP_VALUE_HIGH", nullable = true)
073        @Temporal(TemporalType.TIMESTAMP)
074        @FullTextField
075        public Date myValueHigh;
076
077        @Column(name = "SP_VALUE_LOW", nullable = true)
078        @Temporal(TemporalType.TIMESTAMP)
079        @FullTextField
080        public Date myValueLow;
081
082        /**
083         * Field which stores an integer representation of YYYYMDD as calculated by Calendar
084         * e.g. 2019-01-20 -> 20190120
085         */
086        @Column(name = "SP_VALUE_LOW_DATE_ORDINAL")
087        public Integer myValueLowDateOrdinal;
088        @Column(name = "SP_VALUE_HIGH_DATE_ORDINAL")
089        public Integer myValueHighDateOrdinal;
090
091        @Transient
092        private transient String myOriginalValue;
093        @Id
094        @SequenceGenerator(name = "SEQ_SPIDX_DATE", sequenceName = "SEQ_SPIDX_DATE")
095        @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_DATE")
096        @Column(name = "SP_ID")
097        private Long myId;
098        /**
099         * @since 3.5.0 - At some point this should be made not-null
100         */
101        @Column(name = "HASH_IDENTITY", nullable = true)
102        private Long myHashIdentity;
103
104        /**
105         * Constructor
106         */
107        public ResourceIndexedSearchParamDate() {
108                super();
109        }
110
111        /**
112         * Constructor
113         */
114        public ResourceIndexedSearchParamDate(PartitionSettings thePartitionSettings, String theResourceType, String theParamName, Date theLow, String theLowString, Date theHigh, String theHighString, String theOriginalValue) {
115                setPartitionSettings(thePartitionSettings);
116                setResourceType(theResourceType);
117                setParamName(theParamName);
118                setValueLow(theLow);
119                setValueHigh(theHigh);
120                if (theHigh != null && theHighString == null) {
121                        theHighString = DateUtils.convertDateToIso8601String(theHigh);
122                }
123                if (theLow != null && theLowString == null) {
124                        theLowString = DateUtils.convertDateToIso8601String(theLow);
125                }
126                computeValueHighDateOrdinal(theHighString);
127                computeValueLowDateOrdinal(theLowString);
128                reComputeValueHighDate(theHigh, theHighString);
129                myOriginalValue = theOriginalValue;
130                calculateHashes();
131        }
132
133        private void computeValueHighDateOrdinal(String theHigh) {
134                if (!StringUtils.isBlank(theHigh)) {
135                        this.myValueHighDateOrdinal = generateHighOrdinalDateInteger(theHigh);
136                }
137        }
138
139        private void reComputeValueHighDate(Date theHigh, String theHighString) {
140                if (StringUtils.isBlank(theHighString) || theHigh == null)
141                        return;
142                // FT : 2021-09-10 not very comfortable to set the high value to the last second
143                // Timezone? existing data?
144                // if YYYY or YYYY-MM or YYYY-MM-DD add the last second
145                if (theHighString.length() == 4 || theHighString.length() == 7 || theHighString.length() == 10) {
146                        
147                        String theCompleteDateStr =  DateUtils.getCompletedDate(theHighString).getRight();
148                        try {
149                                Date complateDate = new SimpleDateFormat("yyyy-MM-dd").parse(theCompleteDateStr);  
150                            this.myValueHigh = DateUtils.getEndOfDay(complateDate);
151                        } catch (ParseException e) {
152                                // do nothing; 
153                        }
154                }
155                
156        }
157        private int generateLowOrdinalDateInteger(String theDateString) {
158                if (theDateString.contains("T")) {
159                        theDateString = theDateString.substring(0, theDateString.indexOf("T"));
160                }
161                
162                theDateString = DateUtils.getCompletedDate(theDateString).getLeft();
163                theDateString = theDateString.replace("-", "");
164                return Integer.valueOf(theDateString);
165        }
166
167        private int generateHighOrdinalDateInteger(String theDateString) {
168                
169                if (theDateString.contains("T")) {
170                        theDateString = theDateString.substring(0, theDateString.indexOf("T"));
171                }
172                
173                theDateString = DateUtils.getCompletedDate(theDateString).getRight();
174                theDateString = theDateString.replace("-", "");
175                return Integer.valueOf(theDateString);
176        }
177        
178        private void computeValueLowDateOrdinal(String theLow) {
179                if (StringUtils.isNotBlank(theLow)) {
180                        this.myValueLowDateOrdinal = generateLowOrdinalDateInteger(theLow);
181                }
182        }
183
184        public Integer getValueLowDateOrdinal() {
185                return myValueLowDateOrdinal;
186        }
187
188        public Integer getValueHighDateOrdinal() {
189                return myValueHighDateOrdinal;
190        }
191
192        @Override
193        public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) {
194                super.copyMutableValuesFrom(theSource);
195                ResourceIndexedSearchParamDate source = (ResourceIndexedSearchParamDate) theSource;
196                myValueHigh = source.myValueHigh;
197                myValueLow = source.myValueLow;
198                myValueHighDateOrdinal = source.myValueHighDateOrdinal;
199                myValueLowDateOrdinal = source.myValueLowDateOrdinal;
200                myHashIdentity = source.myHashIdentity;
201        }
202
203        @Override
204        public void clearHashes() {
205                myHashIdentity = null;
206        }
207
208        @Override
209        public void calculateHashes() {
210                if (myHashIdentity != null) {
211                        return;
212                }
213
214                String resourceType = getResourceType();
215                String paramName = getParamName();
216                setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName));
217        }
218
219        @Override
220        public boolean equals(Object theObj) {
221                if (this == theObj) {
222                        return true;
223                }
224                if (theObj == null) {
225                        return false;
226                }
227                if (!(theObj instanceof ResourceIndexedSearchParamDate)) {
228                        return false;
229                }
230                ResourceIndexedSearchParamDate obj = (ResourceIndexedSearchParamDate) theObj;
231                EqualsBuilder b = new EqualsBuilder();
232                b.append(getResourceType(), obj.getResourceType());
233                b.append(getParamName(), obj.getParamName());
234                b.append(getTimeFromDate(getValueHigh()), getTimeFromDate(obj.getValueHigh()));
235                b.append(getTimeFromDate(getValueLow()), getTimeFromDate(obj.getValueLow()));
236                b.append(getValueLowDateOrdinal(), obj.getValueLowDateOrdinal());
237                b.append(getValueHighDateOrdinal(), obj.getValueHighDateOrdinal());
238                b.append(isMissing(), obj.isMissing());
239                return b.isEquals();
240        }
241
242        public void setHashIdentity(Long theHashIdentity) {
243                myHashIdentity = theHashIdentity;
244        }
245
246        @Override
247        public Long getId() {
248                return myId;
249        }
250
251        @Override
252        public void setId(Long theId) {
253                myId = theId;
254        }
255
256        protected Long getTimeFromDate(Date date) {
257                if (date != null) {
258                        return date.getTime();
259                }
260                return null;
261        }
262
263        public Date getValueHigh() {
264                return myValueHigh;
265        }
266
267        public ResourceIndexedSearchParamDate setValueHigh(Date theValueHigh) {
268                myValueHigh = theValueHigh;
269                return this;
270        }
271
272        public Date getValueLow() {
273                return myValueLow;
274        }
275
276        public ResourceIndexedSearchParamDate setValueLow(Date theValueLow) {
277                myValueLow = theValueLow;
278                return this;
279        }
280
281        @Override
282        public int hashCode() {
283                HashCodeBuilder b = new HashCodeBuilder();
284                b.append(getResourceType());
285                b.append(getParamName());
286                b.append(getTimeFromDate(getValueHigh()));
287                b.append(getTimeFromDate(getValueLow()));
288                return b.toHashCode();
289        }
290
291        @Override
292        public IQueryParameterType toQueryParameterType() {
293                DateTimeType value = new DateTimeType(myOriginalValue);
294                if (value.getPrecision().ordinal() > TemporalPrecisionEnum.DAY.ordinal()) {
295                        value.setTimeZoneZulu(true);
296                }
297                return new DateParam(value.getValueAsString());
298        }
299
300        @Override
301        public String toString() {
302                ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
303                b.append("partitionId", getPartitionId());
304                b.append("paramName", getParamName());
305                b.append("resourceId", getResourcePid());
306                b.append("valueLow", new InstantDt(getValueLow()));
307                b.append("valueHigh", new InstantDt(getValueHigh()));
308                b.append("ordLow", myValueLowDateOrdinal);
309                b.append("ordHigh", myValueHighDateOrdinal);
310                b.append("hashIdentity", myHashIdentity);
311                b.append("missing", isMissing());
312                return b.build();
313        }
314
315        @SuppressWarnings("ConstantConditions")
316        @Override
317        public boolean matches(IQueryParameterType theParam) {
318                if (!(theParam instanceof DateParam)) {
319                        return false;
320                }
321                DateParam dateParam = (DateParam) theParam;
322                DateRangeParam range = new DateRangeParam(dateParam);
323
324
325                boolean result;
326                if (dateParam.getPrecision().ordinal() <= TemporalPrecisionEnum.DAY.ordinal()) {
327                        result = matchesOrdinalDateBounds(range);
328                } else {
329                        result = matchesDateBounds(range);
330                }
331
332                return result;
333        }
334
335        private boolean matchesDateBounds(DateRangeParam range) {
336                Date lowerBound = range.getLowerBoundAsInstant();
337                Date upperBound = range.getUpperBoundAsInstant();
338                if (lowerBound == null && upperBound == null) {
339                        // should never happen
340                        return false;
341                }
342                boolean result = true;
343                if (lowerBound != null) {
344                        result &= (myValueLow.after(lowerBound) || myValueLow.equals(lowerBound));
345                        result &= (myValueHigh.after(lowerBound) || myValueHigh.equals(lowerBound));
346                }
347                if (upperBound != null) {
348                        result &= (myValueLow.before(upperBound) || myValueLow.equals(upperBound));
349                        result &= (myValueHigh.before(upperBound) || myValueHigh.equals(upperBound));
350                }
351                return result;
352        }
353
354        private boolean matchesOrdinalDateBounds(DateRangeParam range) {
355                boolean result = true;
356                Integer lowerBoundAsDateInteger = range.getLowerBoundAsDateInteger();
357                Integer upperBoundAsDateInteger = range.getUpperBoundAsDateInteger();
358                if (upperBoundAsDateInteger == null && lowerBoundAsDateInteger == null) {
359                        return false;
360                }
361                if (lowerBoundAsDateInteger != null) {
362                        //TODO as we run into equality issues
363                        result &= (myValueLowDateOrdinal.equals(lowerBoundAsDateInteger) || myValueLowDateOrdinal > lowerBoundAsDateInteger);
364                        result &= (myValueHighDateOrdinal.equals(lowerBoundAsDateInteger) || myValueHighDateOrdinal > lowerBoundAsDateInteger);
365                }
366                if (upperBoundAsDateInteger != null) {
367                        result &= (myValueHighDateOrdinal.equals(upperBoundAsDateInteger) || myValueHighDateOrdinal < upperBoundAsDateInteger);
368                        result &= (myValueLowDateOrdinal.equals(upperBoundAsDateInteger) || myValueLowDateOrdinal < upperBoundAsDateInteger);
369                }
370                return result;
371        }
372
373
374        public static Long calculateOrdinalValue(Date theDate) {
375                if (theDate == null) {
376                        return null;
377                }
378                return (long) DateUtils.convertDateToDayInteger(theDate);
379        }
380
381}