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.SequenceGenerator; 040import jakarta.persistence.Table; 041import org.apache.commons.lang3.StringUtils; 042import org.apache.commons.lang3.builder.EqualsBuilder; 043import org.apache.commons.lang3.builder.HashCodeBuilder; 044import org.apache.commons.lang3.builder.ToStringBuilder; 045import org.apache.commons.lang3.builder.ToStringStyle; 046import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; 047 048import static org.apache.commons.lang3.StringUtils.defaultString; 049import static org.apache.commons.lang3.StringUtils.trim; 050 051@Embeddable 052@Entity 053@Table( 054 name = "HFJ_SPIDX_TOKEN", 055 indexes = { 056 /* 057 * Note: We previously had indexes with the following names, 058 * do not reuse these names: 059 * IDX_SP_TOKEN 060 * IDX_SP_TOKEN_UNQUAL 061 */ 062 063 @Index(name = "IDX_SP_TOKEN_HASH_V2", columnList = "HASH_IDENTITY,SP_SYSTEM,SP_VALUE,RES_ID,PARTITION_ID"), 064 @Index(name = "IDX_SP_TOKEN_HASH_S_V2", columnList = "HASH_SYS,RES_ID,PARTITION_ID"), 065 @Index(name = "IDX_SP_TOKEN_HASH_SV_V2", columnList = "HASH_SYS_AND_VALUE,RES_ID,PARTITION_ID"), 066 @Index(name = "IDX_SP_TOKEN_HASH_V_V2", columnList = "HASH_VALUE,RES_ID,PARTITION_ID"), 067 @Index( 068 name = "IDX_SP_TOKEN_RESID_V2", 069 columnList = "RES_ID,HASH_SYS_AND_VALUE,HASH_VALUE,HASH_SYS,HASH_IDENTITY,PARTITION_ID") 070 }) 071public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchParam { 072 073 public static final int MAX_LENGTH = 200; 074 075 private static final long serialVersionUID = 1L; 076 077 @FullTextField 078 @Column(name = "SP_SYSTEM", nullable = true, length = MAX_LENGTH) 079 public String mySystem; 080 081 @FullTextField 082 @Column(name = "SP_VALUE", nullable = true, length = MAX_LENGTH) 083 private String myValue; 084 085 @SuppressWarnings("unused") 086 @Id 087 @SequenceGenerator(name = "SEQ_SPIDX_TOKEN", sequenceName = "SEQ_SPIDX_TOKEN") 088 @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_TOKEN") 089 @Column(name = "SP_ID") 090 private Long myId; 091 /** 092 * @since 3.4.0 - At some point this should be made not-null 093 */ 094 @Column(name = "HASH_IDENTITY", nullable = true) 095 private Long myHashIdentity; 096 /** 097 * @since 3.4.0 - At some point this should be made not-null 098 */ 099 @Column(name = "HASH_SYS", nullable = true) 100 private Long myHashSystem; 101 /** 102 * @since 3.4.0 - At some point this should be made not-null 103 */ 104 @Column(name = "HASH_SYS_AND_VALUE", nullable = true) 105 private Long myHashSystemAndValue; 106 /** 107 * @since 3.4.0 - At some point this should be made not-null 108 */ 109 @Column(name = "HASH_VALUE", nullable = true) 110 private Long myHashValue; 111 112 @ManyToOne( 113 optional = false, 114 fetch = FetchType.LAZY, 115 cascade = {}) 116 @JoinColumn( 117 foreignKey = @ForeignKey(name = "FK_SP_TOKEN_RES"), 118 name = "RES_ID", 119 referencedColumnName = "RES_ID", 120 nullable = false) 121 private ResourceTable myResource; 122 123 /** 124 * Constructor 125 */ 126 public ResourceIndexedSearchParamToken() { 127 super(); 128 } 129 130 /** 131 * Constructor 132 */ 133 public ResourceIndexedSearchParamToken( 134 PartitionSettings thePartitionSettings, 135 String theResourceType, 136 String theParamName, 137 String theSystem, 138 String theValue) { 139 super(); 140 setPartitionSettings(thePartitionSettings); 141 setResourceType(theResourceType); 142 setParamName(theParamName); 143 setSystem(theSystem); 144 setValue(theValue); 145 calculateHashes(); 146 } 147 148 /** 149 * Constructor 150 */ 151 public ResourceIndexedSearchParamToken( 152 PartitionSettings thePartitionSettings, String theResourceType, String theParamName, boolean theMissing) { 153 super(); 154 setPartitionSettings(thePartitionSettings); 155 setResourceType(theResourceType); 156 setParamName(theParamName); 157 setMissing(theMissing); 158 calculateHashes(); 159 } 160 161 @Override 162 public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) { 163 super.copyMutableValuesFrom(theSource); 164 ResourceIndexedSearchParamToken source = (ResourceIndexedSearchParamToken) theSource; 165 166 mySystem = source.mySystem; 167 myValue = source.myValue; 168 myHashSystem = source.myHashSystem; 169 myHashSystemAndValue = source.getHashSystemAndValue(); 170 myHashValue = source.myHashValue; 171 myHashIdentity = source.myHashIdentity; 172 } 173 174 @Override 175 public void clearHashes() { 176 myHashIdentity = null; 177 myHashSystem = null; 178 myHashSystemAndValue = null; 179 myHashValue = null; 180 } 181 182 @Override 183 public void calculateHashes() { 184 if (myHashIdentity != null || myHashSystem != null || myHashValue != null || myHashSystemAndValue != null) { 185 return; 186 } 187 188 String resourceType = getResourceType(); 189 String paramName = getParamName(); 190 String system = getSystem(); 191 String value = getValue(); 192 setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); 193 setHashSystemAndValue(calculateHashSystemAndValue( 194 getPartitionSettings(), getPartitionId(), resourceType, paramName, system, value)); 195 196 // Searches using the :of-type modifier can never be partial (system-only or value-only) so don't 197 // bother saving these 198 boolean calculatePartialHashes = !StringUtils.endsWith(paramName, Constants.PARAMQUALIFIER_TOKEN_OF_TYPE); 199 if (calculatePartialHashes) { 200 setHashSystem( 201 calculateHashSystem(getPartitionSettings(), getPartitionId(), resourceType, paramName, system)); 202 setHashValue(calculateHashValue(getPartitionSettings(), getPartitionId(), resourceType, paramName, value)); 203 } 204 } 205 206 @Override 207 public boolean equals(Object theObj) { 208 if (this == theObj) { 209 return true; 210 } 211 if (theObj == null) { 212 return false; 213 } 214 if (!(theObj instanceof ResourceIndexedSearchParamToken)) { 215 return false; 216 } 217 ResourceIndexedSearchParamToken obj = (ResourceIndexedSearchParamToken) theObj; 218 EqualsBuilder b = new EqualsBuilder(); 219 b.append(getHashSystem(), obj.getHashSystem()); 220 b.append(getHashValue(), obj.getHashValue()); 221 b.append(getHashSystemAndValue(), obj.getHashSystemAndValue()); 222 return b.isEquals(); 223 } 224 225 public Long getHashSystem() { 226 return myHashSystem; 227 } 228 229 private void setHashSystem(Long theHashSystem) { 230 myHashSystem = theHashSystem; 231 } 232 233 private void setHashIdentity(Long theHashIdentity) { 234 myHashIdentity = theHashIdentity; 235 } 236 237 public Long getHashSystemAndValue() { 238 return myHashSystemAndValue; 239 } 240 241 private void setHashSystemAndValue(Long theHashSystemAndValue) { 242 myHashSystemAndValue = theHashSystemAndValue; 243 } 244 245 public Long getHashValue() { 246 return myHashValue; 247 } 248 249 private void setHashValue(Long theHashValue) { 250 myHashValue = theHashValue; 251 } 252 253 @Override 254 public Long getId() { 255 return myId; 256 } 257 258 @Override 259 public void setId(Long theId) { 260 myId = theId; 261 } 262 263 public String getSystem() { 264 return mySystem; 265 } 266 267 public void setSystem(String theSystem) { 268 mySystem = StringUtils.defaultIfBlank(theSystem, null); 269 myHashSystemAndValue = null; 270 } 271 272 public String getValue() { 273 return myValue; 274 } 275 276 public ResourceIndexedSearchParamToken setValue(String theValue) { 277 myValue = StringUtils.defaultIfBlank(theValue, null); 278 myHashSystemAndValue = null; 279 return this; 280 } 281 282 @Override 283 public int hashCode() { 284 HashCodeBuilder b = new HashCodeBuilder(); 285 b.append(getResourceType()); 286 b.append(getHashValue()); 287 b.append(getHashSystem()); 288 b.append(getHashSystemAndValue()); 289 290 return b.toHashCode(); 291 } 292 293 @Override 294 public IQueryParameterType toQueryParameterType() { 295 return new TokenParam(getSystem(), getValue()); 296 } 297 298 @Override 299 public String toString() { 300 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 301 b.append("id", getId()); 302 if (getPartitionId() != null) { 303 b.append("partitionId", getPartitionId().getPartitionId()); 304 } 305 b.append("resourceType", getResourceType()); 306 b.append("paramName", getParamName()); 307 if (isMissing()) { 308 b.append("missing", true); 309 } else { 310 b.append("system", getSystem()); 311 b.append("value", getValue()); 312 } 313 b.append("hashIdentity", myHashIdentity); 314 b.append("hashSystem", myHashSystem); 315 b.append("hashValue", myHashValue); 316 b.append("hashSysAndValue", myHashSystemAndValue); 317 b.append("partition", getPartitionId()); 318 return b.build(); 319 } 320 321 @Override 322 public boolean matches(IQueryParameterType theParam) { 323 if (!(theParam instanceof TokenParam)) { 324 return false; 325 } 326 TokenParam token = (TokenParam) theParam; 327 boolean retVal = false; 328 String valueString = defaultString(getValue()); 329 String tokenValueString = defaultString(token.getValue()); 330 331 // Only match on system if it wasn't specified 332 if (token.getSystem() == null || token.getSystem().isEmpty()) { 333 if (valueString.equalsIgnoreCase(tokenValueString)) { 334 retVal = true; 335 } 336 } else if (tokenValueString == null || tokenValueString.isEmpty()) { 337 if (token.getSystem().equalsIgnoreCase(getSystem())) { 338 retVal = true; 339 } 340 } else { 341 if (token.getSystem().equalsIgnoreCase(getSystem()) && valueString.equalsIgnoreCase(tokenValueString)) { 342 retVal = true; 343 } 344 } 345 return retVal; 346 } 347 348 public static long calculateHashSystem( 349 PartitionSettings thePartitionSettings, 350 PartitionablePartitionId theRequestPartitionId, 351 String theResourceType, 352 String theParamName, 353 String theSystem) { 354 RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); 355 return calculateHashSystem(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem); 356 } 357 358 public static long calculateHashSystem( 359 PartitionSettings thePartitionSettings, 360 RequestPartitionId theRequestPartitionId, 361 String theResourceType, 362 String theParamName, 363 String theSystem) { 364 return hash(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 hash( 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 hash(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 public void truncateFieldsForDB() { 433 mySystem = StringUtils.truncate(mySystem, MAX_LENGTH); 434 myValue = StringUtils.truncate(myValue, MAX_LENGTH); 435 } 436}