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}