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}