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}