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