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.i18n.Msg; 023import ca.uhn.fhir.interceptor.model.RequestPartitionId; 024import ca.uhn.fhir.jpa.model.config.PartitionSettings; 025import ca.uhn.fhir.jpa.model.listener.IndexStorageOptimizationListener; 026import ca.uhn.fhir.model.api.IQueryParameterType; 027import ca.uhn.fhir.rest.param.StringParam; 028import ca.uhn.fhir.util.StringUtil; 029import jakarta.persistence.Column; 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.IdClass; 038import jakarta.persistence.Index; 039import jakarta.persistence.JoinColumn; 040import jakarta.persistence.JoinColumns; 041import jakarta.persistence.ManyToOne; 042import jakarta.persistence.Table; 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.annotations.GenericGenerator; 048 049import static ca.uhn.fhir.jpa.model.util.SearchParamHash.hashSearchParam; 050import static org.apache.commons.lang3.StringUtils.defaultString; 051 052// @formatter:off 053@EntityListeners(IndexStorageOptimizationListener.class) 054@Entity 055@Table( 056 name = ResourceIndexedSearchParamString.HFJ_SPIDX_STRING, 057 indexes = { 058 /* 059 * Note: We previously had indexes with the following names, 060 * do not reuse these names: 061 * IDX_SP_STRING 062 */ 063 064 // This is used for sorting, and for :contains queries currently 065 @Index(name = "IDX_SP_STRING_HASH_IDENT_V2", columnList = "HASH_IDENTITY,RES_ID,PARTITION_ID"), 066 @Index( 067 name = "IDX_SP_STRING_HASH_NRM_V2", 068 columnList = "HASH_NORM_PREFIX,SP_VALUE_NORMALIZED,RES_ID,PARTITION_ID"), 069 @Index(name = "IDX_SP_STRING_HASH_EXCT_V2", columnList = "HASH_EXACT,RES_ID,PARTITION_ID"), 070 @Index(name = "IDX_SP_STRING_RESID_V2", columnList = "RES_ID,HASH_NORM_PREFIX,PARTITION_ID") 071 }) 072@IdClass(IdAndPartitionId.class) 073public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchParam { 074 075 /* 076 * Note that MYSQL chokes on unique indexes for lengths > 255 so be careful here 077 */ 078 public static final int MAX_LENGTH = 768; 079 public static final int HASH_PREFIX_LENGTH = 1; 080 private static final long serialVersionUID = 1L; 081 public static final String HFJ_SPIDX_STRING = "HFJ_SPIDX_STRING"; 082 083 @Id 084 @GenericGenerator(name = "SEQ_SPIDX_STRING", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class) 085 @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_STRING") 086 @Column(name = "SP_ID") 087 private Long myId; 088 089 @ManyToOne( 090 optional = false, 091 fetch = FetchType.LAZY, 092 cascade = {}) 093 @JoinColumns( 094 value = { 095 @JoinColumn( 096 name = "RES_ID", 097 referencedColumnName = "RES_ID", 098 insertable = false, 099 updatable = false, 100 nullable = false), 101 @JoinColumn( 102 name = "PARTITION_ID", 103 referencedColumnName = "PARTITION_ID", 104 insertable = false, 105 updatable = false, 106 nullable = false) 107 }, 108 foreignKey = @ForeignKey(name = "FK_SPIDXSTR_RESOURCE")) 109 private ResourceTable myResource; 110 111 @Column(name = "RES_ID", nullable = false) 112 private Long myResourceId; 113 114 @Column(name = "SP_VALUE_EXACT", length = MAX_LENGTH, nullable = true) 115 private String myValueExact; 116 117 @Column(name = "SP_VALUE_NORMALIZED", length = MAX_LENGTH, nullable = true) 118 private String myValueNormalized; 119 /** 120 * @since 3.4.0 - At some point this should be made not-null 121 */ 122 @Column(name = "HASH_NORM_PREFIX", nullable = true) 123 private Long myHashNormalizedPrefix; 124 /** 125 * @since 3.4.0 - At some point this should be made not-null 126 */ 127 @Column(name = "HASH_EXACT", nullable = true) 128 private Long myHashExact; 129 130 public ResourceIndexedSearchParamString() { 131 super(); 132 } 133 134 public ResourceIndexedSearchParamString( 135 PartitionSettings thePartitionSettings, 136 StorageSettings theStorageSettings, 137 String theResourceType, 138 String theParamName, 139 String theValueNormalized, 140 String theValueExact) { 141 setPartitionSettings(thePartitionSettings); 142 setStorageSettings(theStorageSettings); 143 setResourceType(theResourceType); 144 setParamName(theParamName); 145 setValueNormalized(theValueNormalized); 146 setValueExact(theValueExact); 147 calculateHashes(); 148 } 149 150 @Override 151 public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) { 152 super.copyMutableValuesFrom(theSource); 153 ResourceIndexedSearchParamString source = (ResourceIndexedSearchParamString) theSource; 154 myValueExact = source.myValueExact; 155 myValueNormalized = source.myValueNormalized; 156 myHashExact = source.myHashExact; 157 myHashIdentity = source.myHashIdentity; 158 myHashNormalizedPrefix = source.myHashNormalizedPrefix; 159 } 160 161 @Override 162 public void setResourceId(Long theResourceId) { 163 myResourceId = theResourceId; 164 } 165 166 @Override 167 public void clearHashes() { 168 myHashIdentity = null; 169 myHashNormalizedPrefix = null; 170 myHashExact = null; 171 } 172 173 @Override 174 public void calculateHashes() { 175 if (myHashIdentity != null || myHashExact != null || myHashNormalizedPrefix != null) { 176 return; 177 } 178 179 String resourceType = getResourceType(); 180 String paramName = getParamName(); 181 String valueNormalized = getValueNormalized(); 182 String valueExact = getValueExact(); 183 setHashNormalizedPrefix(calculateHashNormalized( 184 getPartitionSettings(), 185 getPartitionId(), 186 getStorageSettings(), 187 resourceType, 188 paramName, 189 valueNormalized)); 190 setHashExact(calculateHashExact(getPartitionSettings(), getPartitionId(), resourceType, paramName, valueExact)); 191 setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName)); 192 } 193 194 @Override 195 public boolean equals(Object theObj) { 196 if (this == theObj) { 197 return true; 198 } 199 if (theObj == null) { 200 return false; 201 } 202 if (!(theObj instanceof ResourceIndexedSearchParamString)) { 203 return false; 204 } 205 ResourceIndexedSearchParamString obj = (ResourceIndexedSearchParamString) theObj; 206 EqualsBuilder b = new EqualsBuilder(); 207 b.append(getValueExact(), obj.getValueExact()); 208 b.append(getHashIdentity(), obj.getHashIdentity()); 209 b.append(getHashExact(), obj.getHashExact()); 210 b.append(getHashNormalizedPrefix(), obj.getHashNormalizedPrefix()); 211 b.append(getValueNormalized(), obj.getValueNormalized()); 212 b.append(isMissing(), obj.isMissing()); 213 return b.isEquals(); 214 } 215 216 public Long getHashExact() { 217 return myHashExact; 218 } 219 220 public void setHashExact(Long theHashExact) { 221 myHashExact = theHashExact; 222 } 223 224 public Long getHashNormalizedPrefix() { 225 return myHashNormalizedPrefix; 226 } 227 228 public void setHashNormalizedPrefix(Long theHashNormalizedPrefix) { 229 myHashNormalizedPrefix = theHashNormalizedPrefix; 230 } 231 232 @Override 233 public Long getId() { 234 return myId; 235 } 236 237 @Override 238 public void setId(Long theId) { 239 myId = theId; 240 } 241 242 public String getValueExact() { 243 return myValueExact; 244 } 245 246 public ResourceIndexedSearchParamString setValueExact(String theValueExact) { 247 if (defaultString(theValueExact).length() > MAX_LENGTH) { 248 throw new IllegalArgumentException(Msg.code(1529) + "Value is too long: " + theValueExact.length()); 249 } 250 myValueExact = theValueExact; 251 return this; 252 } 253 254 public String getValueNormalized() { 255 return myValueNormalized; 256 } 257 258 public ResourceIndexedSearchParamString setValueNormalized(String theValueNormalized) { 259 if (defaultString(theValueNormalized).length() > MAX_LENGTH) { 260 throw new IllegalArgumentException(Msg.code(1530) + "Value is too long: " + theValueNormalized.length()); 261 } 262 myValueNormalized = theValueNormalized; 263 return this; 264 } 265 266 @Override 267 public int hashCode() { 268 HashCodeBuilder b = new HashCodeBuilder(); 269 b.append(getValueExact()); 270 b.append(getHashIdentity()); 271 b.append(getHashExact()); 272 b.append(getHashNormalizedPrefix()); 273 b.append(getValueNormalized()); 274 b.append(isMissing()); 275 return b.toHashCode(); 276 } 277 278 @Override 279 public IQueryParameterType toQueryParameterType() { 280 return new StringParam(getValueExact()); 281 } 282 283 @Override 284 public String toString() { 285 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 286 b.append("resourceType", getResourceType()); 287 b.append("paramName", getParamName()); 288 b.append("resourceId", getResourcePid()); 289 b.append("hashIdentity", getHashIdentity()); 290 b.append("hashNormalizedPrefix", getHashNormalizedPrefix()); 291 b.append("valueNormalized", getValueNormalized()); 292 b.append("partitionId", getPartitionId()); 293 return b.build(); 294 } 295 296 @Override 297 public boolean matches(IQueryParameterType theParam) { 298 if (!(theParam instanceof StringParam)) { 299 return false; 300 } 301 StringParam string = (StringParam) theParam; 302 String normalizedString = StringUtil.normalizeStringForSearchIndexing(defaultString(string.getValue())); 303 return defaultString(getValueNormalized()).startsWith(normalizedString); 304 } 305 306 public static long calculateHashExact( 307 PartitionSettings thePartitionSettings, 308 PartitionablePartitionId theRequestPartitionId, 309 String theResourceType, 310 String theParamName, 311 String theValueExact) { 312 RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); 313 return calculateHashExact( 314 thePartitionSettings, requestPartitionId, theResourceType, theParamName, theValueExact); 315 } 316 317 public static long calculateHashExact( 318 PartitionSettings thePartitionSettings, 319 RequestPartitionId theRequestPartitionId, 320 String theResourceType, 321 String theParamName, 322 String theValueExact) { 323 return hashSearchParam( 324 thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theValueExact); 325 } 326 327 public static long calculateHashNormalized( 328 PartitionSettings thePartitionSettings, 329 PartitionablePartitionId theRequestPartitionId, 330 StorageSettings theStorageSettings, 331 String theResourceType, 332 String theParamName, 333 String theValueNormalized) { 334 RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); 335 return calculateHashNormalized( 336 thePartitionSettings, 337 requestPartitionId, 338 theStorageSettings, 339 theResourceType, 340 theParamName, 341 theValueNormalized); 342 } 343 344 public static long calculateHashNormalized( 345 PartitionSettings thePartitionSettings, 346 RequestPartitionId theRequestPartitionId, 347 StorageSettings theStorageSettings, 348 String theResourceType, 349 String theParamName, 350 String theValueNormalized) { 351 /* 352 * If we're not allowing contained searches, we'll add the first 353 * bit of the normalized value to the hash. This helps to 354 * make the hash even more unique, which will be good for 355 * performance. 356 */ 357 int hashPrefixLength = HASH_PREFIX_LENGTH; 358 if (theStorageSettings.isAllowContainsSearches()) { 359 hashPrefixLength = 0; 360 } 361 362 String value = StringUtil.left(theValueNormalized, hashPrefixLength); 363 return hashSearchParam(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, value); 364 } 365 366 @Override 367 public ResourceTable getResource() { 368 return myResource; 369 } 370 371 @Override 372 public BaseResourceIndexedSearchParam setResource(ResourceTable theResource) { 373 setResourceType(theResource.getResourceType()); 374 return this; 375 } 376}