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}