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.model.api.IQueryParameterType; 025import ca.uhn.fhir.rest.api.Constants; 026import ca.uhn.fhir.rest.param.TokenParam; 027import jakarta.persistence.Column; 028import jakarta.persistence.Embeddable; 029import jakarta.persistence.Entity; 030import jakarta.persistence.FetchType; 031import jakarta.persistence.ForeignKey; 032import jakarta.persistence.GeneratedValue; 033import jakarta.persistence.GenerationType; 034import jakarta.persistence.Id; 035import jakarta.persistence.Index; 036import jakarta.persistence.JoinColumn; 037import jakarta.persistence.ManyToOne; 038import jakarta.persistence.PrePersist; 039import jakarta.persistence.PreUpdate; 040import jakarta.persistence.SequenceGenerator; 041import jakarta.persistence.Table; 042import org.apache.commons.lang3.StringUtils; 043import org.apache.commons.lang3.builder.EqualsBuilder; 044import org.apache.commons.lang3.builder.HashCodeBuilder; 045import org.apache.commons.lang3.builder.ToStringBuilder; 046import org.apache.commons.lang3.builder.ToStringStyle; 047import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; 048 049import static org.apache.commons.lang3.StringUtils.defaultString; 050import static org.apache.commons.lang3.StringUtils.trim; 051 052@Embeddable 053@Entity 054@Table( 055 name = "HFJ_SPIDX_TOKEN", 056 indexes = { 057 /* 058 * Note: We previously had indexes with the following names, 059 * do not reuse these names: 060 * IDX_SP_TOKEN 061 * IDX_SP_TOKEN_UNQUAL 062 */ 063 064 @Index(name = "IDX_SP_TOKEN_HASH_V2", columnList = "HASH_IDENTITY,SP_SYSTEM,SP_VALUE,RES_ID,PARTITION_ID"), 065 @Index(name = "IDX_SP_TOKEN_HASH_S_V2", columnList = "HASH_SYS,RES_ID,PARTITION_ID"), 066 @Index(name = "IDX_SP_TOKEN_HASH_SV_V2", columnList = "HASH_SYS_AND_VALUE,RES_ID,PARTITION_ID"), 067 @Index(name = "IDX_SP_TOKEN_HASH_V_V2", columnList = "HASH_VALUE,RES_ID,PARTITION_ID"), 068 @Index( 069 name = "IDX_SP_TOKEN_RESID_V2", 070 columnList = "RES_ID,HASH_SYS_AND_VALUE,HASH_VALUE,HASH_SYS,HASH_IDENTITY,PARTITION_ID") 071 }) 072public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchParam { 073 074 public static final int MAX_LENGTH = 200; 075 076 private static final long serialVersionUID = 1L; 077 078 @FullTextField 079 @Column(name = "SP_SYSTEM", nullable = true, length = MAX_LENGTH) 080 public String mySystem; 081 082 @FullTextField 083 @Column(name = "SP_VALUE", nullable = true, length = MAX_LENGTH) 084 private String myValue; 085 086 @SuppressWarnings("unused") 087 @Id 088 @SequenceGenerator(name = "SEQ_SPIDX_TOKEN", sequenceName = "SEQ_SPIDX_TOKEN") 089 @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_TOKEN") 090 @Column(name = "SP_ID") 091 private Long myId; 092 /** 093 * @since 3.4.0 - At some point this should be made not-null 094 */ 095 @Column(name = "HASH_IDENTITY", nullable = true) 096 private Long myHashIdentity; 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(getHashSystem(), obj.getHashSystem()); 221 b.append(getHashValue(), obj.getHashValue()); 222 b.append(getHashSystemAndValue(), obj.getHashSystemAndValue()); 223 return b.isEquals(); 224 } 225 226 public Long getHashSystem() { 227 return myHashSystem; 228 } 229 230 private void setHashSystem(Long theHashSystem) { 231 myHashSystem = theHashSystem; 232 } 233 234 private void setHashIdentity(Long theHashIdentity) { 235 myHashIdentity = theHashIdentity; 236 } 237 238 public Long getHashSystemAndValue() { 239 return myHashSystemAndValue; 240 } 241 242 private void setHashSystemAndValue(Long theHashSystemAndValue) { 243 myHashSystemAndValue = theHashSystemAndValue; 244 } 245 246 public Long getHashValue() { 247 return myHashValue; 248 } 249 250 private void setHashValue(Long theHashValue) { 251 myHashValue = theHashValue; 252 } 253 254 @Override 255 public Long getId() { 256 return myId; 257 } 258 259 @Override 260 public void setId(Long theId) { 261 myId = theId; 262 } 263 264 public String getSystem() { 265 return mySystem; 266 } 267 268 public void setSystem(String theSystem) { 269 mySystem = StringUtils.defaultIfBlank(theSystem, null); 270 myHashSystemAndValue = null; 271 } 272 273 public String getValue() { 274 return myValue; 275 } 276 277 public ResourceIndexedSearchParamToken setValue(String theValue) { 278 myValue = StringUtils.defaultIfBlank(theValue, null); 279 myHashSystemAndValue = null; 280 return this; 281 } 282 283 @Override 284 public int hashCode() { 285 HashCodeBuilder b = new HashCodeBuilder(); 286 b.append(getResourceType()); 287 b.append(getHashValue()); 288 b.append(getHashSystem()); 289 b.append(getHashSystemAndValue()); 290 291 return b.toHashCode(); 292 } 293 294 @Override 295 public IQueryParameterType toQueryParameterType() { 296 return new TokenParam(getSystem(), getValue()); 297 } 298 299 @Override 300 public String toString() { 301 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 302 b.append("id", getId()); 303 if (getPartitionId() != null) { 304 b.append("partitionId", getPartitionId().getPartitionId()); 305 } 306 b.append("resourceType", getResourceType()); 307 b.append("paramName", getParamName()); 308 if (isMissing()) { 309 b.append("missing", true); 310 } else { 311 b.append("system", getSystem()); 312 b.append("value", getValue()); 313 } 314 b.append("hashIdentity", myHashIdentity); 315 b.append("hashSystem", myHashSystem); 316 b.append("hashValue", myHashValue); 317 b.append("hashSysAndValue", myHashSystemAndValue); 318 b.append("partition", getPartitionId()); 319 return b.build(); 320 } 321 322 @Override 323 public boolean matches(IQueryParameterType theParam) { 324 if (!(theParam instanceof TokenParam)) { 325 return false; 326 } 327 TokenParam token = (TokenParam) theParam; 328 boolean retVal = false; 329 String valueString = defaultString(getValue()); 330 String tokenValueString = defaultString(token.getValue()); 331 332 // Only match on system if it wasn't specified 333 if (token.getSystem() == null || token.getSystem().isEmpty()) { 334 if (valueString.equalsIgnoreCase(tokenValueString)) { 335 retVal = true; 336 } 337 } else if (tokenValueString == null || tokenValueString.isEmpty()) { 338 if (token.getSystem().equalsIgnoreCase(getSystem())) { 339 retVal = true; 340 } 341 } else { 342 if (token.getSystem().equalsIgnoreCase(getSystem()) && valueString.equalsIgnoreCase(tokenValueString)) { 343 retVal = true; 344 } 345 } 346 return retVal; 347 } 348 349 public static long calculateHashSystem( 350 PartitionSettings thePartitionSettings, 351 PartitionablePartitionId theRequestPartitionId, 352 String theResourceType, 353 String theParamName, 354 String theSystem) { 355 RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); 356 return calculateHashSystem(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem); 357 } 358 359 public static long calculateHashSystem( 360 PartitionSettings thePartitionSettings, 361 RequestPartitionId theRequestPartitionId, 362 String theResourceType, 363 String theParamName, 364 String theSystem) { 365 return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, trim(theSystem)); 366 } 367 368 public static long calculateHashSystemAndValue( 369 PartitionSettings thePartitionSettings, 370 PartitionablePartitionId theRequestPartitionId, 371 String theResourceType, 372 String theParamName, 373 String theSystem, 374 String theValue) { 375 RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); 376 return calculateHashSystemAndValue( 377 thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem, theValue); 378 } 379 380 public static long calculateHashSystemAndValue( 381 PartitionSettings thePartitionSettings, 382 RequestPartitionId theRequestPartitionId, 383 String theResourceType, 384 String theParamName, 385 String theSystem, 386 String theValue) { 387 return hash( 388 thePartitionSettings, 389 theRequestPartitionId, 390 theResourceType, 391 theParamName, 392 defaultString(trim(theSystem)), 393 trim(theValue)); 394 } 395 396 public static long calculateHashValue( 397 PartitionSettings thePartitionSettings, 398 PartitionablePartitionId theRequestPartitionId, 399 String theResourceType, 400 String theParamName, 401 String theValue) { 402 RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); 403 return calculateHashValue(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theValue); 404 } 405 406 public static long calculateHashValue( 407 PartitionSettings thePartitionSettings, 408 RequestPartitionId theRequestPartitionId, 409 String theResourceType, 410 String theParamName, 411 String theValue) { 412 String value = trim(theValue); 413 return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, value); 414 } 415 416 @Override 417 public ResourceTable getResource() { 418 return myResource; 419 } 420 421 @Override 422 public BaseResourceIndexedSearchParam setResource(ResourceTable theResource) { 423 myResource = theResource; 424 setResourceType(theResource.getResourceType()); 425 return this; 426 } 427 428 /** 429 * We truncate the fields at the last moment because the tables have limited size. 430 * We don't truncate earlier in the flow because the index hashes MUST be calculated on the full string. 431 */ 432 @PrePersist 433 @PreUpdate 434 public void truncateFieldsForDB() { 435 mySystem = StringUtils.truncate(mySystem, MAX_LENGTH); 436 myValue = StringUtils.truncate(myValue, MAX_LENGTH); 437 } 438}