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