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.jpa.model.util.SearchParamHash; 023import ca.uhn.hapi.fhir.sql.hibernatesvc.PartitionedIndex; 024import ca.uhn.hapi.fhir.sql.hibernatesvc.PartitionedIndexes; 025import jakarta.persistence.Column; 026import jakarta.persistence.Entity; 027import jakarta.persistence.ForeignKey; 028import jakarta.persistence.GeneratedValue; 029import jakarta.persistence.GenerationType; 030import jakarta.persistence.Id; 031import jakarta.persistence.IdClass; 032import jakarta.persistence.Index; 033import jakarta.persistence.JoinColumn; 034import jakarta.persistence.JoinColumns; 035import jakarta.persistence.ManyToOne; 036import jakarta.persistence.Table; 037import org.apache.commons.lang3.Validate; 038import org.apache.commons.lang3.builder.CompareToBuilder; 039import org.apache.commons.lang3.builder.EqualsBuilder; 040import org.apache.commons.lang3.builder.HashCodeBuilder; 041import org.apache.commons.lang3.builder.ToStringBuilder; 042import org.apache.commons.lang3.builder.ToStringStyle; 043import org.hibernate.annotations.GenericGenerator; 044import org.hl7.fhir.instance.model.api.IIdType; 045 046/** 047 * NOTE ON LIMITATIONS HERE 048 * <p> 049 * This table does not include the partition ID in the uniqueness check. This was the case 050 * when this table was originally created. In other words, the uniqueness constraint does not 051 * include the partition column, and would therefore not be able to guarantee uniqueness 052 * local to a partition. 053 * </p> 054 * <p> 055 * TODO: HAPI FHIR 7.4.0 introduced hashes to this table - In a future release we should 056 * move the uniqueness constraint over to using them instead of the long string. At that 057 * time we could probably decide whether it makes sense to include the partition ID in 058 * the uniqueness check. Null values will be an issue there, we may need to introduce 059 * a rule that if you want to enforce uniqueness on a partitioned system you need a 060 * non-null default partition ID? 061 * </p> 062 */ 063@Entity() 064@Table( 065 name = ResourceIndexedComboStringUnique.HFJ_IDX_CMP_STRING_UNIQ, 066 indexes = { 067 @Index( 068 name = ResourceIndexedComboStringUnique.IDX_IDXCMPSTRUNIQ_STRING, 069 columnList = "PARTITION_ID,IDX_STRING", 070 unique = true), 071 @Index( 072 name = ResourceIndexedComboStringUnique.IDX_IDXCMPSTRUNIQ_RESOURCE, 073 columnList = "PARTITION_ID,RES_ID", 074 unique = false) 075 }) 076@PartitionedIndexes({ 077 @PartitionedIndex( 078 name = ResourceIndexedComboStringUnique.IDX_IDXCMPSTRUNIQ_RESOURCE, 079 columns = {"RES_ID"}) 080}) 081@IdClass(IdAndPartitionId.class) 082public class ResourceIndexedComboStringUnique extends BaseResourceIndexedCombo 083 implements Comparable<ResourceIndexedComboStringUnique>, IResourceIndexComboSearchParameter { 084 085 public static final int MAX_STRING_LENGTH = 500; 086 public static final String IDX_IDXCMPSTRUNIQ_STRING = "IDX_IDXCMPSTRUNIQ_STRING"; 087 public static final String IDX_IDXCMPSTRUNIQ_RESOURCE = "IDX_IDXCMPSTRUNIQ_RESOURCE"; 088 public static final String HFJ_IDX_CMP_STRING_UNIQ = "HFJ_IDX_CMP_STRING_UNIQ"; 089 090 @GenericGenerator( 091 name = "SEQ_IDXCMPSTRUNIQ_ID", 092 type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class) 093 @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_IDXCMPSTRUNIQ_ID") 094 @Id 095 @Column(name = "PID") 096 private Long myId; 097 098 @ManyToOne( 099 optional = false, 100 cascade = {}) 101 @JoinColumns( 102 value = { 103 @JoinColumn( 104 name = "RES_ID", 105 referencedColumnName = "RES_ID", 106 insertable = false, 107 updatable = false, 108 nullable = true), 109 @JoinColumn( 110 name = "PARTITION_ID", 111 referencedColumnName = "PARTITION_ID", 112 insertable = false, 113 updatable = false, 114 nullable = true) 115 }, 116 foreignKey = @ForeignKey(name = "FK_IDXCMPSTRUNIQ_RES_ID")) 117 private ResourceTable myResource; 118 119 @Column(name = "RES_ID", updatable = false, nullable = true) 120 private Long myResourceId; 121 122 // TODO: These hashes were added in 7.4.0 - They aren't used or indexed yet, but 123 // eventually we should replace the string index with a hash index in order to 124 // reduce the space usage. 125 @Column(name = "HASH_COMPLETE") 126 private Long myHashComplete; 127 128 /** 129 * Because we'll be using these hashes to enforce uniqueness, the risk of 130 * collisions is bad, since it would be plain impossible to insert a row 131 * with a false collision here. So in order to reduce that risk, we 132 * double the number of bits we hash by having two hashes, effectively 133 * making the hash a 128-bit hash instead of just 64. 134 * <p> 135 * The idea is that having two of them widens the hash from 64 bits to 128 136 * bits 137 * </p><p> 138 * If we have a value we want to guarantee uniqueness on of 139 * <code>Observation?code=A</code>, say it hashes to <code>12345</code>. 140 * And suppose we have another value of <code>Observation?code=B</code> which 141 * also hashes to <code>12345</code>. This is unlikely but not impossible. 142 * And if this happens, it will be impossible to add a resource with 143 * code B if there is already a resource with code A. 144 * </p><p> 145 * Adding a second, salted hash reduces the likelihood of this happening, 146 * since it's unlikely the second hash would also collide. Not impossible 147 * of course, but orders of magnitude less likely still. 148 * </p> 149 * 150 * @see #calculateHashComplete2(String) to see how this is calculated 151 */ 152 @Column(name = "HASH_COMPLETE_2") 153 private Long myHashComplete2; 154 155 @Column(name = "IDX_STRING", nullable = false, length = MAX_STRING_LENGTH) 156 private String myIndexString; 157 158 /** 159 * Constructor 160 */ 161 public ResourceIndexedComboStringUnique() { 162 super(); 163 } 164 165 /** 166 * Constructor 167 */ 168 public ResourceIndexedComboStringUnique( 169 ResourceTable theResource, String theIndexString, IIdType theSearchParameterId) { 170 setResource(theResource); 171 setIndexString(theIndexString); 172 setPartitionId(theResource.getPartitionId()); 173 setSearchParameterId(theSearchParameterId); 174 } 175 176 @Override 177 public int compareTo(ResourceIndexedComboStringUnique theO) { 178 CompareToBuilder b = new CompareToBuilder(); 179 b.append(myIndexString, theO.getIndexString()); 180 return b.toComparison(); 181 } 182 183 @Override 184 public boolean equals(Object theO) { 185 if (this == theO) return true; 186 187 if (!(theO instanceof ResourceIndexedComboStringUnique)) { 188 return false; 189 } 190 191 calculateHashes(); 192 193 ResourceIndexedComboStringUnique that = (ResourceIndexedComboStringUnique) theO; 194 195 EqualsBuilder b = new EqualsBuilder(); 196 b.append(myHashComplete, that.myHashComplete); 197 b.append(myHashComplete2, that.myHashComplete2); 198 return b.isEquals(); 199 } 200 201 @Override 202 public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) { 203 ResourceIndexedComboStringUnique source = (ResourceIndexedComboStringUnique) theSource; 204 myIndexString = source.myIndexString; 205 myHashComplete = source.myHashComplete; 206 myHashComplete2 = source.myHashComplete2; 207 } 208 209 @Override 210 public void setResourceId(Long theResourceId) { 211 myResourceId = theResourceId; 212 } 213 214 @Override 215 public String getIndexString() { 216 return myIndexString; 217 } 218 219 public void setIndexString(String theIndexString) { 220 myIndexString = theIndexString; 221 } 222 223 @Override 224 public ResourceTable getResource() { 225 return myResource; 226 } 227 228 @Override 229 public void setResource(ResourceTable theResource) { 230 Validate.notNull(theResource, "theResource must not be null"); 231 myResource = theResource; 232 } 233 234 @Override 235 public Long getId() { 236 return myId; 237 } 238 239 @Override 240 public void setId(Long theId) { 241 myId = theId; 242 } 243 244 public Long getHashComplete() { 245 return myHashComplete; 246 } 247 248 public void setHashComplete(Long theHashComplete) { 249 myHashComplete = theHashComplete; 250 } 251 252 public Long getHashComplete2() { 253 return myHashComplete2; 254 } 255 256 public void setHashComplete2(Long theHashComplete2) { 257 myHashComplete2 = theHashComplete2; 258 } 259 260 @Override 261 public void setPlaceholderHashesIfMissing() { 262 super.setPlaceholderHashesIfMissing(); 263 if (myHashComplete == null) { 264 myHashComplete = 0L; 265 } 266 if (myHashComplete2 == null) { 267 myHashComplete2 = 0L; 268 } 269 } 270 271 @Override 272 public void calculateHashes() { 273 if (myHashComplete == null) { 274 setHashComplete(calculateHashComplete(myIndexString)); 275 setHashComplete2(calculateHashComplete2(myIndexString)); 276 } 277 } 278 279 public static long calculateHashComplete(String theQueryString) { 280 return SearchParamHash.hashSearchParam(theQueryString); 281 } 282 283 public static long calculateHashComplete2(String theQueryString) { 284 // Just add a constant salt to the query string in order to hopefully 285 // further avoid collisions 286 String newQueryString = theQueryString + "ABC123"; 287 return calculateHashComplete(newQueryString); 288 } 289 290 @Override 291 public void clearHashes() { 292 myHashComplete = null; 293 myHashComplete2 = null; 294 } 295 296 @Override 297 public int hashCode() { 298 calculateHashes(); 299 300 HashCodeBuilder b = new HashCodeBuilder(17, 37); 301 b.append(myHashComplete); 302 b.append(myHashComplete2); 303 return b.toHashCode(); 304 } 305 306 @Override 307 public String toString() { 308 return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 309 .append("id", myId) 310 .append("resourceId", myResourceId) 311 .append("indexString", myIndexString) 312 .append("hashComplete", myHashComplete) 313 .append("hashComplete2", myHashComplete2) 314 .append("partition", getPartitionId()) 315 .toString(); 316 } 317}