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.i18n.Msg;
023import ca.uhn.fhir.interceptor.model.RequestPartitionId;
024import ca.uhn.fhir.jpa.model.config.PartitionSettings;
025import ca.uhn.fhir.jpa.model.listener.IndexStorageOptimizationListener;
026import ca.uhn.fhir.model.api.IQueryParameterType;
027import ca.uhn.fhir.rest.param.StringParam;
028import ca.uhn.fhir.util.StringUtil;
029import jakarta.persistence.Column;
030import jakarta.persistence.Entity;
031import jakarta.persistence.EntityListeners;
032import jakarta.persistence.FetchType;
033import jakarta.persistence.ForeignKey;
034import jakarta.persistence.GeneratedValue;
035import jakarta.persistence.GenerationType;
036import jakarta.persistence.Id;
037import jakarta.persistence.IdClass;
038import jakarta.persistence.Index;
039import jakarta.persistence.JoinColumn;
040import jakarta.persistence.JoinColumns;
041import jakarta.persistence.ManyToOne;
042import jakarta.persistence.Table;
043import org.apache.commons.lang3.builder.EqualsBuilder;
044import org.apache.commons.lang3.builder.HashCodeBuilder;
045import org.apache.commons.lang3.builder.ToStringBuilder;
046import org.apache.commons.lang3.builder.ToStringStyle;
047import org.hibernate.annotations.GenericGenerator;
048
049import static ca.uhn.fhir.jpa.model.util.SearchParamHash.hashSearchParam;
050import static org.apache.commons.lang3.StringUtils.defaultString;
051
052// @formatter:off
053@EntityListeners(IndexStorageOptimizationListener.class)
054@Entity
055@Table(
056                name = ResourceIndexedSearchParamString.HFJ_SPIDX_STRING,
057                indexes = {
058                        /*
059                         * Note: We previously had indexes with the following names,
060                         * do not reuse these names:
061                         * IDX_SP_STRING
062                         */
063
064                        // This is used for sorting, and for :contains queries currently
065                        @Index(name = "IDX_SP_STRING_HASH_IDENT_V2", columnList = "HASH_IDENTITY,RES_ID,PARTITION_ID"),
066                        @Index(
067                                        name = "IDX_SP_STRING_HASH_NRM_V2",
068                                        columnList = "HASH_NORM_PREFIX,SP_VALUE_NORMALIZED,RES_ID,PARTITION_ID"),
069                        @Index(name = "IDX_SP_STRING_HASH_EXCT_V2", columnList = "HASH_EXACT,RES_ID,PARTITION_ID"),
070                        @Index(name = "IDX_SP_STRING_RESID_V2", columnList = "RES_ID,HASH_NORM_PREFIX,PARTITION_ID")
071                })
072@IdClass(IdAndPartitionId.class)
073public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchParam {
074
075        /*
076         * Note that MYSQL chokes on unique indexes for lengths > 255 so be careful here
077         */
078        public static final int MAX_LENGTH = 768;
079        public static final int HASH_PREFIX_LENGTH = 1;
080        private static final long serialVersionUID = 1L;
081        public static final String HFJ_SPIDX_STRING = "HFJ_SPIDX_STRING";
082
083        @Id
084        @GenericGenerator(name = "SEQ_SPIDX_STRING", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class)
085        @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_STRING")
086        @Column(name = "SP_ID")
087        private Long myId;
088
089        @ManyToOne(
090                        optional = false,
091                        fetch = FetchType.LAZY,
092                        cascade = {})
093        @JoinColumns(
094                        value = {
095                                @JoinColumn(
096                                                name = "RES_ID",
097                                                referencedColumnName = "RES_ID",
098                                                insertable = false,
099                                                updatable = false,
100                                                nullable = false),
101                                @JoinColumn(
102                                                name = "PARTITION_ID",
103                                                referencedColumnName = "PARTITION_ID",
104                                                insertable = false,
105                                                updatable = false,
106                                                nullable = false)
107                        },
108                        foreignKey = @ForeignKey(name = "FK_SPIDXSTR_RESOURCE"))
109        private ResourceTable myResource;
110
111        @Column(name = "RES_ID", nullable = false)
112        private Long myResourceId;
113
114        @Column(name = "SP_VALUE_EXACT", length = MAX_LENGTH, nullable = true)
115        private String myValueExact;
116
117        @Column(name = "SP_VALUE_NORMALIZED", length = MAX_LENGTH, nullable = true)
118        private String myValueNormalized;
119        /**
120         * @since 3.4.0 - At some point this should be made not-null
121         */
122        @Column(name = "HASH_NORM_PREFIX", nullable = true)
123        private Long myHashNormalizedPrefix;
124        /**
125         * @since 3.4.0 - At some point this should be made not-null
126         */
127        @Column(name = "HASH_EXACT", nullable = true)
128        private Long myHashExact;
129
130        public ResourceIndexedSearchParamString() {
131                super();
132        }
133
134        public ResourceIndexedSearchParamString(
135                        PartitionSettings thePartitionSettings,
136                        StorageSettings theStorageSettings,
137                        String theResourceType,
138                        String theParamName,
139                        String theValueNormalized,
140                        String theValueExact) {
141                setPartitionSettings(thePartitionSettings);
142                setStorageSettings(theStorageSettings);
143                setResourceType(theResourceType);
144                setParamName(theParamName);
145                setValueNormalized(theValueNormalized);
146                setValueExact(theValueExact);
147                calculateHashes();
148        }
149
150        @Override
151        public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) {
152                super.copyMutableValuesFrom(theSource);
153                ResourceIndexedSearchParamString source = (ResourceIndexedSearchParamString) theSource;
154                myValueExact = source.myValueExact;
155                myValueNormalized = source.myValueNormalized;
156                myHashExact = source.myHashExact;
157                myHashIdentity = source.myHashIdentity;
158                myHashNormalizedPrefix = source.myHashNormalizedPrefix;
159        }
160
161        @Override
162        public void setResourceId(Long theResourceId) {
163                myResourceId = theResourceId;
164        }
165
166        @Override
167        public void clearHashes() {
168                myHashIdentity = null;
169                myHashNormalizedPrefix = null;
170                myHashExact = null;
171        }
172
173        @Override
174        public void calculateHashes() {
175                if (myHashIdentity != null || myHashExact != null || myHashNormalizedPrefix != null) {
176                        return;
177                }
178
179                String resourceType = getResourceType();
180                String paramName = getParamName();
181                String valueNormalized = getValueNormalized();
182                String valueExact = getValueExact();
183                setHashNormalizedPrefix(calculateHashNormalized(
184                                getPartitionSettings(),
185                                getPartitionId(),
186                                getStorageSettings(),
187                                resourceType,
188                                paramName,
189                                valueNormalized));
190                setHashExact(calculateHashExact(getPartitionSettings(), getPartitionId(), resourceType, paramName, valueExact));
191                setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName));
192        }
193
194        @Override
195        public boolean equals(Object theObj) {
196                if (this == theObj) {
197                        return true;
198                }
199                if (theObj == null) {
200                        return false;
201                }
202                if (!(theObj instanceof ResourceIndexedSearchParamString)) {
203                        return false;
204                }
205                ResourceIndexedSearchParamString obj = (ResourceIndexedSearchParamString) theObj;
206                EqualsBuilder b = new EqualsBuilder();
207                b.append(getValueExact(), obj.getValueExact());
208                b.append(getHashIdentity(), obj.getHashIdentity());
209                b.append(getHashExact(), obj.getHashExact());
210                b.append(getHashNormalizedPrefix(), obj.getHashNormalizedPrefix());
211                b.append(getValueNormalized(), obj.getValueNormalized());
212                b.append(isMissing(), obj.isMissing());
213                return b.isEquals();
214        }
215
216        public Long getHashExact() {
217                return myHashExact;
218        }
219
220        public void setHashExact(Long theHashExact) {
221                myHashExact = theHashExact;
222        }
223
224        public Long getHashNormalizedPrefix() {
225                return myHashNormalizedPrefix;
226        }
227
228        public void setHashNormalizedPrefix(Long theHashNormalizedPrefix) {
229                myHashNormalizedPrefix = theHashNormalizedPrefix;
230        }
231
232        @Override
233        public Long getId() {
234                return myId;
235        }
236
237        @Override
238        public void setId(Long theId) {
239                myId = theId;
240        }
241
242        public String getValueExact() {
243                return myValueExact;
244        }
245
246        public ResourceIndexedSearchParamString setValueExact(String theValueExact) {
247                if (defaultString(theValueExact).length() > MAX_LENGTH) {
248                        throw new IllegalArgumentException(Msg.code(1529) + "Value is too long: " + theValueExact.length());
249                }
250                myValueExact = theValueExact;
251                return this;
252        }
253
254        public String getValueNormalized() {
255                return myValueNormalized;
256        }
257
258        public ResourceIndexedSearchParamString setValueNormalized(String theValueNormalized) {
259                if (defaultString(theValueNormalized).length() > MAX_LENGTH) {
260                        throw new IllegalArgumentException(Msg.code(1530) + "Value is too long: " + theValueNormalized.length());
261                }
262                myValueNormalized = theValueNormalized;
263                return this;
264        }
265
266        @Override
267        public int hashCode() {
268                HashCodeBuilder b = new HashCodeBuilder();
269                b.append(getValueExact());
270                b.append(getHashIdentity());
271                b.append(getHashExact());
272                b.append(getHashNormalizedPrefix());
273                b.append(getValueNormalized());
274                b.append(isMissing());
275                return b.toHashCode();
276        }
277
278        @Override
279        public IQueryParameterType toQueryParameterType() {
280                return new StringParam(getValueExact());
281        }
282
283        @Override
284        public String toString() {
285                ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
286                b.append("resourceType", getResourceType());
287                b.append("paramName", getParamName());
288                b.append("resourceId", getResourcePid());
289                b.append("hashIdentity", getHashIdentity());
290                b.append("hashNormalizedPrefix", getHashNormalizedPrefix());
291                b.append("valueNormalized", getValueNormalized());
292                b.append("partitionId", getPartitionId());
293                return b.build();
294        }
295
296        @Override
297        public boolean matches(IQueryParameterType theParam) {
298                if (!(theParam instanceof StringParam)) {
299                        return false;
300                }
301                StringParam string = (StringParam) theParam;
302                String normalizedString = StringUtil.normalizeStringForSearchIndexing(defaultString(string.getValue()));
303                return defaultString(getValueNormalized()).startsWith(normalizedString);
304        }
305
306        public static long calculateHashExact(
307                        PartitionSettings thePartitionSettings,
308                        PartitionablePartitionId theRequestPartitionId,
309                        String theResourceType,
310                        String theParamName,
311                        String theValueExact) {
312                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
313                return calculateHashExact(
314                                thePartitionSettings, requestPartitionId, theResourceType, theParamName, theValueExact);
315        }
316
317        public static long calculateHashExact(
318                        PartitionSettings thePartitionSettings,
319                        RequestPartitionId theRequestPartitionId,
320                        String theResourceType,
321                        String theParamName,
322                        String theValueExact) {
323                return hashSearchParam(
324                                thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theValueExact);
325        }
326
327        public static long calculateHashNormalized(
328                        PartitionSettings thePartitionSettings,
329                        PartitionablePartitionId theRequestPartitionId,
330                        StorageSettings theStorageSettings,
331                        String theResourceType,
332                        String theParamName,
333                        String theValueNormalized) {
334                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
335                return calculateHashNormalized(
336                                thePartitionSettings,
337                                requestPartitionId,
338                                theStorageSettings,
339                                theResourceType,
340                                theParamName,
341                                theValueNormalized);
342        }
343
344        public static long calculateHashNormalized(
345                        PartitionSettings thePartitionSettings,
346                        RequestPartitionId theRequestPartitionId,
347                        StorageSettings theStorageSettings,
348                        String theResourceType,
349                        String theParamName,
350                        String theValueNormalized) {
351                /*
352                 * If we're not allowing contained searches, we'll add the first
353                 * bit of the normalized value to the hash. This helps to
354                 * make the hash even more unique, which will be good for
355                 * performance.
356                 */
357                int hashPrefixLength = HASH_PREFIX_LENGTH;
358                if (theStorageSettings.isAllowContainsSearches()) {
359                        hashPrefixLength = 0;
360                }
361
362                String value = StringUtil.left(theValueNormalized, hashPrefixLength);
363                return hashSearchParam(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, value);
364        }
365
366        @Override
367        public ResourceTable getResource() {
368                return myResource;
369        }
370
371        @Override
372        public BaseResourceIndexedSearchParam setResource(ResourceTable theResource) {
373                setResourceType(theResource.getResourceType());
374                return this;
375        }
376}