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