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