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.model.api.IQueryParameterType; 026import ca.uhn.fhir.rest.param.StringParam; 027import ca.uhn.fhir.util.StringUtil; 028import jakarta.persistence.Column; 029import jakarta.persistence.Embeddable; 030import jakarta.persistence.Entity; 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.SequenceGenerator; 039import jakarta.persistence.Table; 040import org.apache.commons.lang3.builder.EqualsBuilder; 041import org.apache.commons.lang3.builder.HashCodeBuilder; 042import org.apache.commons.lang3.builder.ToStringBuilder; 043import org.apache.commons.lang3.builder.ToStringStyle; 044 045import static org.apache.commons.lang3.StringUtils.defaultString; 046 047// @formatter:off 048@Embeddable 049@Entity 050@Table( 051 name = "HFJ_SPIDX_STRING", 052 indexes = { 053 /* 054 * Note: We previously had indexes with the following names, 055 * do not reuse these names: 056 * IDX_SP_STRING 057 */ 058 059 // This is used for sorting, and for :contains queries currently 060 @Index(name = "IDX_SP_STRING_HASH_IDENT_V2", columnList = "HASH_IDENTITY,RES_ID,PARTITION_ID"), 061 @Index( 062 name = "IDX_SP_STRING_HASH_NRM_V2", 063 columnList = "HASH_NORM_PREFIX,SP_VALUE_NORMALIZED,RES_ID,PARTITION_ID"), 064 @Index(name = "IDX_SP_STRING_HASH_EXCT_V2", columnList = "HASH_EXACT,RES_ID,PARTITION_ID"), 065 @Index(name = "IDX_SP_STRING_RESID_V2", columnList = "RES_ID,HASH_NORM_PREFIX,PARTITION_ID") 066 }) 067public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchParam { 068 069 /* 070 * Note that MYSQL chokes on unique indexes for lengths > 255 so be careful here 071 */ 072 public static final int MAX_LENGTH = 200; 073 public static final int HASH_PREFIX_LENGTH = 1; 074 private static final long serialVersionUID = 1L; 075 076 @Id 077 @SequenceGenerator(name = "SEQ_SPIDX_STRING", sequenceName = "SEQ_SPIDX_STRING") 078 @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_STRING") 079 @Column(name = "SP_ID") 080 private Long myId; 081 082 @ManyToOne(optional = false) 083 @JoinColumn( 084 name = "RES_ID", 085 referencedColumnName = "RES_ID", 086 nullable = false, 087 foreignKey = @ForeignKey(name = "FK_SPIDXSTR_RESOURCE")) 088 private ResourceTable myResource; 089 090 @Column(name = "SP_VALUE_EXACT", length = MAX_LENGTH, nullable = true) 091 private String myValueExact; 092 093 @Column(name = "SP_VALUE_NORMALIZED", length = MAX_LENGTH, nullable = true) 094 private String myValueNormalized; 095 /** 096 * @since 3.4.0 - At some point this should be made not-null 097 */ 098 @Column(name = "HASH_NORM_PREFIX", nullable = true) 099 private Long myHashNormalizedPrefix; 100 /** 101 * @since 3.6.0 - At some point this should be made not-null 102 */ 103 @Column(name = "HASH_IDENTITY", nullable = true) 104 private Long myHashIdentity; 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(getResourceType(), obj.getResourceType()); 184 b.append(getParamName(), obj.getParamName()); 185 b.append(getValueExact(), obj.getValueExact()); 186 b.append(getHashIdentity(), obj.getHashIdentity()); 187 b.append(getHashExact(), obj.getHashExact()); 188 b.append(getHashNormalizedPrefix(), obj.getHashNormalizedPrefix()); 189 b.append(getValueNormalized(), obj.getValueNormalized()); 190 return b.isEquals(); 191 } 192 193 private Long getHashIdentity() { 194 return myHashIdentity; 195 } 196 197 public void setHashIdentity(Long theHashIdentity) { 198 myHashIdentity = theHashIdentity; 199 } 200 201 public Long getHashExact() { 202 return myHashExact; 203 } 204 205 public void setHashExact(Long theHashExact) { 206 myHashExact = theHashExact; 207 } 208 209 public Long getHashNormalizedPrefix() { 210 return myHashNormalizedPrefix; 211 } 212 213 public void setHashNormalizedPrefix(Long theHashNormalizedPrefix) { 214 myHashNormalizedPrefix = theHashNormalizedPrefix; 215 } 216 217 @Override 218 public Long getId() { 219 return myId; 220 } 221 222 @Override 223 public void setId(Long theId) { 224 myId = theId; 225 } 226 227 public String getValueExact() { 228 return myValueExact; 229 } 230 231 public ResourceIndexedSearchParamString setValueExact(String theValueExact) { 232 if (defaultString(theValueExact).length() > MAX_LENGTH) { 233 throw new IllegalArgumentException(Msg.code(1529) + "Value is too long: " + theValueExact.length()); 234 } 235 myValueExact = theValueExact; 236 return this; 237 } 238 239 public String getValueNormalized() { 240 return myValueNormalized; 241 } 242 243 public ResourceIndexedSearchParamString setValueNormalized(String theValueNormalized) { 244 if (defaultString(theValueNormalized).length() > MAX_LENGTH) { 245 throw new IllegalArgumentException(Msg.code(1530) + "Value is too long: " + theValueNormalized.length()); 246 } 247 myValueNormalized = theValueNormalized; 248 return this; 249 } 250 251 @Override 252 public int hashCode() { 253 HashCodeBuilder b = new HashCodeBuilder(); 254 b.append(getResourceType()); 255 b.append(getParamName()); 256 b.append(getValueExact()); 257 b.append(getHashIdentity()); 258 b.append(getHashExact()); 259 b.append(getHashNormalizedPrefix()); 260 b.append(getValueNormalized()); 261 return b.toHashCode(); 262 } 263 264 @Override 265 public IQueryParameterType toQueryParameterType() { 266 return new StringParam(getValueExact()); 267 } 268 269 @Override 270 public String toString() { 271 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 272 b.append("resourceType", getResourceType()); 273 b.append("paramName", getParamName()); 274 b.append("resourceId", getResourcePid()); 275 b.append("hashIdentity", getHashIdentity()); 276 b.append("hashNormalizedPrefix", getHashNormalizedPrefix()); 277 b.append("valueNormalized", getValueNormalized()); 278 b.append("partitionId", getPartitionId()); 279 return b.build(); 280 } 281 282 @Override 283 public boolean matches(IQueryParameterType theParam) { 284 if (!(theParam instanceof StringParam)) { 285 return false; 286 } 287 StringParam string = (StringParam) theParam; 288 String normalizedString = StringUtil.normalizeStringForSearchIndexing(defaultString(string.getValue())); 289 return defaultString(getValueNormalized()).startsWith(normalizedString); 290 } 291 292 public static long calculateHashExact( 293 PartitionSettings thePartitionSettings, 294 PartitionablePartitionId theRequestPartitionId, 295 String theResourceType, 296 String theParamName, 297 String theValueExact) { 298 RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); 299 return calculateHashExact( 300 thePartitionSettings, requestPartitionId, theResourceType, theParamName, theValueExact); 301 } 302 303 public static long calculateHashExact( 304 PartitionSettings thePartitionSettings, 305 RequestPartitionId theRequestPartitionId, 306 String theResourceType, 307 String theParamName, 308 String theValueExact) { 309 return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theValueExact); 310 } 311 312 public static long calculateHashNormalized( 313 PartitionSettings thePartitionSettings, 314 PartitionablePartitionId theRequestPartitionId, 315 StorageSettings theStorageSettings, 316 String theResourceType, 317 String theParamName, 318 String theValueNormalized) { 319 RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId); 320 return calculateHashNormalized( 321 thePartitionSettings, 322 requestPartitionId, 323 theStorageSettings, 324 theResourceType, 325 theParamName, 326 theValueNormalized); 327 } 328 329 public static long calculateHashNormalized( 330 PartitionSettings thePartitionSettings, 331 RequestPartitionId theRequestPartitionId, 332 StorageSettings theStorageSettings, 333 String theResourceType, 334 String theParamName, 335 String theValueNormalized) { 336 /* 337 * If we're not allowing contained searches, we'll add the first 338 * bit of the normalized value to the hash. This helps to 339 * make the hash even more unique, which will be good for 340 * performance. 341 */ 342 int hashPrefixLength = HASH_PREFIX_LENGTH; 343 if (theStorageSettings.isAllowContainsSearches()) { 344 hashPrefixLength = 0; 345 } 346 347 String value = StringUtil.left(theValueNormalized, hashPrefixLength); 348 return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, value); 349 } 350 351 @Override 352 public ResourceTable getResource() { 353 return myResource; 354 } 355 356 @Override 357 public BaseResourceIndexedSearchParam setResource(ResourceTable theResource) { 358 myResource = theResource; 359 setResourceType(theResource.getResourceType()); 360 return this; 361 } 362}