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}