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}