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