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.interceptor.model.RequestPartitionId; 023import ca.uhn.fhir.jpa.model.config.PartitionSettings; 024import ca.uhn.fhir.jpa.model.listener.IndexStorageOptimizationListener; 025import ca.uhn.fhir.model.api.IQueryParameterType; 026import ca.uhn.fhir.rest.api.Constants; 027import ca.uhn.fhir.rest.param.TokenParam; 028import jakarta.persistence.Column; 029import jakarta.persistence.Entity; 030import jakarta.persistence.EntityListeners; 031import jakarta.persistence.FetchType; 032import jakarta.persistence.ForeignKey; 033import jakarta.persistence.GeneratedValue; 034import jakarta.persistence.GenerationType; 035import jakarta.persistence.Id; 036import jakarta.persistence.IdClass; 037import jakarta.persistence.Index; 038import jakarta.persistence.JoinColumn; 039import jakarta.persistence.JoinColumns; 040import jakarta.persistence.ManyToOne; 041import jakarta.persistence.PrePersist; 042import jakarta.persistence.PreUpdate; 043import jakarta.persistence.Table; 044import org.apache.commons.lang3.StringUtils; 045import org.apache.commons.lang3.builder.EqualsBuilder; 046import org.apache.commons.lang3.builder.HashCodeBuilder; 047import org.apache.commons.lang3.builder.ToStringBuilder; 048import org.apache.commons.lang3.builder.ToStringStyle; 049import org.hibernate.annotations.GenericGenerator; 050import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; 051 052import static ca.uhn.fhir.jpa.model.util.SearchParamHash.hashSearchParam; 053import static org.apache.commons.lang3.StringUtils.defaultString; 054import static org.apache.commons.lang3.StringUtils.trim; 055 056@EntityListeners(IndexStorageOptimizationListener.class) 057@Entity 058@Table( 059 name = ResourceIndexedSearchParamToken.HFJ_SPIDX_TOKEN, 060 indexes = { 061 /* 062 * Note: We previously had indexes with the following names, 063 * do not reuse these names: 064 * IDX_SP_TOKEN 065 * IDX_SP_TOKEN_UNQUAL 066 */ 067 068 @Index(name = "IDX_SP_TOKEN_HASH_V2", columnList = "HASH_IDENTITY,SP_SYSTEM,SP_VALUE,RES_ID,PARTITION_ID"), 069 @Index(name = "IDX_SP_TOKEN_HASH_S_V2", columnList = "HASH_SYS,RES_ID,PARTITION_ID"), 070 @Index(name = "IDX_SP_TOKEN_HASH_SV_V2", columnList = "HASH_SYS_AND_VALUE,RES_ID,PARTITION_ID"), 071 @Index(name = "IDX_SP_TOKEN_HASH_V_V2", columnList = "HASH_VALUE,RES_ID,PARTITION_ID"), 072 @Index( 073 name = "IDX_SP_TOKEN_RESID_V2", 074 columnList = "RES_ID,HASH_SYS_AND_VALUE,HASH_VALUE,HASH_SYS,HASH_IDENTITY,PARTITION_ID") 075 }) 076@IdClass(IdAndPartitionId.class) 077public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchParam { 078 079 public static final int MAX_LENGTH = 200; 080 081 private static final long serialVersionUID = 1L; 082 public static final String HFJ_SPIDX_TOKEN = "HFJ_SPIDX_TOKEN"; 083 084 @FullTextField 085 @Column(name = "SP_SYSTEM", nullable = true, length = MAX_LENGTH) 086 public String mySystem; 087 088 @FullTextField 089 @Column(name = "SP_VALUE", nullable = true, length = MAX_LENGTH) 090 private String myValue; 091 092 @SuppressWarnings("unused") 093 @Id 094 @GenericGenerator(name = "SEQ_SPIDX_TOKEN", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class) 095 @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_TOKEN") 096 @Column(name = "SP_ID") 097 private Long myId; 098 099 /** 100 * @since 3.4.0 - At some point this should be made not-null 101 */ 102 @Column(name = "HASH_SYS", nullable = true) 103 private Long myHashSystem; 104 /** 105 * @since 3.4.0 - At some point this should be made not-null 106 */ 107 @Column(name = "HASH_SYS_AND_VALUE", nullable = true) 108 private Long myHashSystemAndValue; 109 /** 110 * @since 3.4.0 - At some point this should be made not-null 111 */ 112 @Column(name = "HASH_VALUE", nullable = true) 113 private Long myHashValue; 114 115 @ManyToOne( 116 optional = false, 117 fetch = FetchType.LAZY, 118 cascade = {}) 119 @JoinColumns( 120 value = { 121 @JoinColumn( 122 name = "RES_ID", 123 referencedColumnName = "RES_ID", 124 insertable = false, 125 updatable = false, 126 nullable = false), 127 @JoinColumn( 128 name = "PARTITION_ID", 129 referencedColumnName = "PARTITION_ID", 130 insertable = false, 131 updatable = false, 132 nullable = false) 133 }, 134 foreignKey = @ForeignKey(name = "FK_SP_TOKEN_RES")) 135 private ResourceTable myResource; 136 137 @Column(name = "RES_ID", nullable = false) 138 private Long myResourceId; 139 140 /** 141 * Constructor 142 */ 143 public ResourceIndexedSearchParamToken() { 144 super(); 145 } 146 147 /** 148 * Constructor 149 */ 150 public ResourceIndexedSearchParamToken( 151 PartitionSettings thePartitionSettings, 152 String theResourceType, 153 String theParamName, 154 String theSystem, 155 String theValue) { 156 super(); 157 setPartitionSettings(thePartitionSettings); 158 setResourceType(theResourceType); 159 setParamName(theParamName); 160 setSystem(theSystem); 161 setValue(theValue); 162 calculateHashes(); 163 } 164 165 /** 166 * Constructor 167 */ 168 public ResourceIndexedSearchParamToken( 169 PartitionSettings thePartitionSettings, String theResourceType, String theParamName, boolean theMissing) { 170 super(); 171 setPartitionSettings(thePartitionSettings); 172 setResourceType(theResourceType); 173 setParamName(theParamName); 174 setMissing(theMissing); 175 calculateHashes(); 176 } 177 178 @Override 179 public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) { 180 super.copyMutableValuesFrom(theSource); 181 ResourceIndexedSearchParamToken source = (ResourceIndexedSearchParamToken) theSource; 182 183 mySystem = source.mySystem; 184 myValue = source.myValue; 185 myHashSystem = source.myHashSystem; 186 myHashSystemAndValue = source.getHashSystemAndValue(); 187 myHashValue = source.myHashValue; 188 myHashIdentity = source.myHashIdentity; 189 } 190 191 @Override 192 public void setResourceId(Long theResourceId) { 193 myResourceId = theResourceId; 194 } 195 196 @Override 197 public void clearHashes() { 198 myHashIdentity = null; 199 myHashSystem = null; 200 myHashSystemAndValue = null; 201 myHashValue = null; 202 } 203 204 @Override 205 public void calculateHashes() { 206 if (myHashIdentity != null || myHashSystem != null || myHashValue != null || myHashSystemAndValue != null) { 207 return; 208 } 209 210 String resourceType = getResourceType(); 211 String paramName = getParamName(); 212 String system = getSystem(); 213 String value = getValue(); 214 setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); 215 setHashSystemAndValue(calculateHashSystemAndValue( 216 getPartitionSettings(), getPartitionId(), resourceType, paramName, system, value)); 217 218 // Searches using the :of-type modifier can never be partial (system-only or value-only) so don't 219 // bother saving these 220 boolean calculatePartialHashes = !StringUtils.endsWith(paramName, Constants.PARAMQUALIFIER_TOKEN_OF_TYPE); 221 if (calculatePartialHashes) { 222 setHashSystem( 223 calculateHashSystem(getPartitionSettings(), getPartitionId(), resourceType, paramName, system)); 224 setHashValue(calculateHashValue(getPartitionSettings(), getPartitionId(), resourceType, paramName, value)); 225 } 226 } 227 228 @Override 229 public boolean equals(Object theObj) { 230 if (this == theObj) { 231 return true; 232 } 233 if (theObj == null) { 234 return false; 235 } 236 if (!(theObj instanceof ResourceIndexedSearchParamToken)) { 237 return false; 238 } 239 ResourceIndexedSearchParamToken obj = (ResourceIndexedSearchParamToken) theObj; 240 EqualsBuilder b = new EqualsBuilder(); 241 b.append(getHashIdentity(), obj.getHashIdentity()); 242 b.append(getHashSystem(), obj.getHashSystem()); 243 b.append(getHashValue(), obj.getHashValue()); 244 b.append(getHashSystemAndValue(), obj.getHashSystemAndValue()); 245 b.append(isMissing(), obj.isMissing()); 246 return b.isEquals(); 247 } 248 249 public Long getHashSystem() { 250 return myHashSystem; 251 } 252 253 private void setHashSystem(Long theHashSystem) { 254 myHashSystem = theHashSystem; 255 } 256 257 public Long getHashSystemAndValue() { 258 return myHashSystemAndValue; 259 } 260 261 private void setHashSystemAndValue(Long theHashSystemAndValue) { 262 myHashSystemAndValue = theHashSystemAndValue; 263 } 264 265 public Long getHashValue() { 266 return myHashValue; 267 } 268 269 private void setHashValue(Long theHashValue) { 270 myHashValue = theHashValue; 271 } 272 273 @Override 274 public Long getId() { 275 return myId; 276 } 277 278 @Override 279 public void setId(Long theId) { 280 myId = theId; 281 } 282 283 public String getSystem() { 284 return mySystem; 285 } 286 287 public void setSystem(String theSystem) { 288 mySystem = StringUtils.defaultIfBlank(theSystem, null); 289 myHashSystemAndValue = null; 290 } 291 292 public String getValue() { 293 return myValue; 294 } 295 296 public ResourceIndexedSearchParamToken setValue(String theValue) { 297 myValue = StringUtils.defaultIfBlank(theValue, null); 298 myHashSystemAndValue = null; 299 return this; 300 } 301 302 @Override 303 public int hashCode() { 304 HashCodeBuilder b = new HashCodeBuilder(); 305 b.append(getHashIdentity()); 306 b.append(getHashValue()); 307 b.append(getHashSystem()); 308 b.append(getHashSystemAndValue()); 309 b.append(isMissing()); 310 return b.toHashCode(); 311 } 312 313 @Override 314 public IQueryParameterType toQueryParameterType() { 315 return new TokenParam(getSystem(), getValue()); 316 } 317 318 @Override 319 public String toString() { 320 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 321 b.append("id", getId()); 322 if (getPartitionId() != null) { 323 b.append("partitionId", getPartitionId().getPartitionId()); 324 } 325 b.append("resourceType", getResourceType()); 326 b.append("paramName", getParamName()); 327 if (isMissing()) { 328 b.append("missing", true); 329 } else { 330 b.append("system", getSystem()); 331 b.append("value", getValue()); 332 } 333 b.append("hashIdentity", myHashIdentity); 334 b.append("hashSystem", myHashSystem); 335 b.append("hashValue", myHashValue); 336 b.append("hashSysAndValue", myHashSystemAndValue); 337 b.append("partition", getPartitionId()); 338 return b.build(); 339 } 340 341 @Override 342 public boolean matches(IQueryParameterType theParam) { 343 if (!(theParam instanceof TokenParam)) { 344 return false; 345 } 346 TokenParam token = (TokenParam) theParam; 347 boolean retVal = false; 348 String valueString = defaultString(getValue()); 349 String tokenValueString = defaultString(token.getValue()); 350 351 // Only match on system if it wasn't specified 352 if (token.getSystem() == null || token.getSystem().isEmpty()) { 353 if (valueString.equalsIgnoreCase(tokenValueString)) { 354 retVal = true; 355 } 356 } else if (tokenValueString == null || tokenValueString.isEmpty()) { 357 if (token.getSystem().equalsIgnoreCase(getSystem())) { 358 retVal = true; 359 } 360 } else { 361 if (token.getSystem().equalsIgnoreCase(getSystem()) && valueString.equalsIgnoreCase(tokenValueString)) { 362 retVal = true; 363 } 364 } 365 return retVal; 366 } 367 368 public static long calculateHashSystem( 369 PartitionSettings thePartitionSettings, 370 PartitionablePartitionId theRequestPartitionId, 371 String theResourceType, 372 String theParamName, 373 String theSystem) { 374 RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); 375 return calculateHashSystem(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem); 376 } 377 378 public static long calculateHashSystem( 379 PartitionSettings thePartitionSettings, 380 RequestPartitionId theRequestPartitionId, 381 String theResourceType, 382 String theParamName, 383 String theSystem) { 384 return hashSearchParam( 385 thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, trim(theSystem)); 386 } 387 388 public static long calculateHashSystemAndValue( 389 PartitionSettings thePartitionSettings, 390 PartitionablePartitionId theRequestPartitionId, 391 String theResourceType, 392 String theParamName, 393 String theSystem, 394 String theValue) { 395 RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); 396 return calculateHashSystemAndValue( 397 thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem, theValue); 398 } 399 400 public static long calculateHashSystemAndValue( 401 PartitionSettings thePartitionSettings, 402 RequestPartitionId theRequestPartitionId, 403 String theResourceType, 404 String theParamName, 405 String theSystem, 406 String theValue) { 407 return hashSearchParam( 408 thePartitionSettings, 409 theRequestPartitionId, 410 theResourceType, 411 theParamName, 412 defaultString(trim(theSystem)), 413 trim(theValue)); 414 } 415 416 public static long calculateHashValue( 417 PartitionSettings thePartitionSettings, 418 PartitionablePartitionId theRequestPartitionId, 419 String theResourceType, 420 String theParamName, 421 String theValue) { 422 RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); 423 return calculateHashValue(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theValue); 424 } 425 426 public static long calculateHashValue( 427 PartitionSettings thePartitionSettings, 428 RequestPartitionId theRequestPartitionId, 429 String theResourceType, 430 String theParamName, 431 String theValue) { 432 String value = trim(theValue); 433 return hashSearchParam(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, value); 434 } 435 436 @Override 437 public ResourceTable getResource() { 438 return myResource; 439 } 440 441 @Override 442 public BaseResourceIndexedSearchParam setResource(ResourceTable theResource) { 443 setResourceType(theResource.getResourceType()); 444 return this; 445 } 446 447 /** 448 * We truncate the fields at the last moment because the tables have limited size. 449 * We don't truncate earlier in the flow because the index hashes MUST be calculated on the full string. 450 */ 451 @PrePersist 452 @PreUpdate 453 public void truncateFieldsForDB() { 454 mySystem = StringUtils.truncate(mySystem, MAX_LENGTH); 455 myValue = StringUtils.truncate(myValue, MAX_LENGTH); 456 } 457}