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.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.Embeddable;
031import jakarta.persistence.Entity;
032import jakarta.persistence.EntityListeners;
033import jakarta.persistence.ForeignKey;
034import jakarta.persistence.GeneratedValue;
035import jakarta.persistence.GenerationType;
036import jakarta.persistence.Id;
037import jakarta.persistence.Index;
038import jakarta.persistence.JoinColumn;
039import jakarta.persistence.ManyToOne;
040import jakarta.persistence.Table;
041import org.apache.commons.lang3.builder.EqualsBuilder;
042import org.apache.commons.lang3.builder.HashCodeBuilder;
043import org.apache.commons.lang3.builder.ToStringBuilder;
044import org.apache.commons.lang3.builder.ToStringStyle;
045import org.hibernate.annotations.GenericGenerator;
046
047import static ca.uhn.fhir.jpa.model.util.SearchParamHash.hashSearchParam;
048import static org.apache.commons.lang3.StringUtils.defaultString;
049
050// @formatter:off
051@Embeddable
052@EntityListeners(IndexStorageOptimizationListener.class)
053@Entity
054@Table(
055                name = ResourceIndexedSearchParamString.HFJ_SPIDX_STRING,
056                indexes = {
057                        /*
058                         * Note: We previously had indexes with the following names,
059                         * do not reuse these names:
060                         * IDX_SP_STRING
061                         */
062
063                        // This is used for sorting, and for :contains queries currently
064                        @Index(name = "IDX_SP_STRING_HASH_IDENT_V2", columnList = "HASH_IDENTITY,RES_ID,PARTITION_ID"),
065                        @Index(
066                                        name = "IDX_SP_STRING_HASH_NRM_V2",
067                                        columnList = "HASH_NORM_PREFIX,SP_VALUE_NORMALIZED,RES_ID,PARTITION_ID"),
068                        @Index(name = "IDX_SP_STRING_HASH_EXCT_V2", columnList = "HASH_EXACT,RES_ID,PARTITION_ID"),
069                        @Index(name = "IDX_SP_STRING_RESID_V2", columnList = "RES_ID,HASH_NORM_PREFIX,PARTITION_ID")
070                })
071public class ResourceIndexedSearchParamString extends BaseResourceIndexedSearchParam {
072
073        /*
074         * Note that MYSQL chokes on unique indexes for lengths > 255 so be careful here
075         */
076        public static final int MAX_LENGTH = 768;
077        public static final int HASH_PREFIX_LENGTH = 1;
078        private static final long serialVersionUID = 1L;
079        public static final String HFJ_SPIDX_STRING = "HFJ_SPIDX_STRING";
080
081        @Id
082        @GenericGenerator(name = "SEQ_SPIDX_STRING", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class)
083        @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_STRING")
084        @Column(name = "SP_ID")
085        private Long myId;
086
087        @ManyToOne(optional = false)
088        @JoinColumn(
089                        name = "RES_ID",
090                        referencedColumnName = "RES_ID",
091                        nullable = false,
092                        foreignKey = @ForeignKey(name = "FK_SPIDXSTR_RESOURCE"))
093        private ResourceTable myResource;
094
095        @Column(name = "SP_VALUE_EXACT", length = MAX_LENGTH, nullable = true)
096        private String myValueExact;
097
098        @Column(name = "SP_VALUE_NORMALIZED", length = MAX_LENGTH, nullable = true)
099        private String myValueNormalized;
100        /**
101         * @since 3.4.0 - At some point this should be made not-null
102         */
103        @Column(name = "HASH_NORM_PREFIX", nullable = true)
104        private Long myHashNormalizedPrefix;
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(getValueExact(), obj.getValueExact());
184                b.append(getHashIdentity(), obj.getHashIdentity());
185                b.append(getHashExact(), obj.getHashExact());
186                b.append(getHashNormalizedPrefix(), obj.getHashNormalizedPrefix());
187                b.append(getValueNormalized(), obj.getValueNormalized());
188                b.append(isMissing(), obj.isMissing());
189                return b.isEquals();
190        }
191
192        public Long getHashExact() {
193                return myHashExact;
194        }
195
196        public void setHashExact(Long theHashExact) {
197                myHashExact = theHashExact;
198        }
199
200        public Long getHashNormalizedPrefix() {
201                return myHashNormalizedPrefix;
202        }
203
204        public void setHashNormalizedPrefix(Long theHashNormalizedPrefix) {
205                myHashNormalizedPrefix = theHashNormalizedPrefix;
206        }
207
208        @Override
209        public Long getId() {
210                return myId;
211        }
212
213        @Override
214        public void setId(Long theId) {
215                myId = theId;
216        }
217
218        public String getValueExact() {
219                return myValueExact;
220        }
221
222        public ResourceIndexedSearchParamString setValueExact(String theValueExact) {
223                if (defaultString(theValueExact).length() > MAX_LENGTH) {
224                        throw new IllegalArgumentException(Msg.code(1529) + "Value is too long: " + theValueExact.length());
225                }
226                myValueExact = theValueExact;
227                return this;
228        }
229
230        public String getValueNormalized() {
231                return myValueNormalized;
232        }
233
234        public ResourceIndexedSearchParamString setValueNormalized(String theValueNormalized) {
235                if (defaultString(theValueNormalized).length() > MAX_LENGTH) {
236                        throw new IllegalArgumentException(Msg.code(1530) + "Value is too long: " + theValueNormalized.length());
237                }
238                myValueNormalized = theValueNormalized;
239                return this;
240        }
241
242        @Override
243        public int hashCode() {
244                HashCodeBuilder b = new HashCodeBuilder();
245                b.append(getValueExact());
246                b.append(getHashIdentity());
247                b.append(getHashExact());
248                b.append(getHashNormalizedPrefix());
249                b.append(getValueNormalized());
250                b.append(isMissing());
251                return b.toHashCode();
252        }
253
254        @Override
255        public IQueryParameterType toQueryParameterType() {
256                return new StringParam(getValueExact());
257        }
258
259        @Override
260        public String toString() {
261                ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
262                b.append("resourceType", getResourceType());
263                b.append("paramName", getParamName());
264                b.append("resourceId", getResourcePid());
265                b.append("hashIdentity", getHashIdentity());
266                b.append("hashNormalizedPrefix", getHashNormalizedPrefix());
267                b.append("valueNormalized", getValueNormalized());
268                b.append("partitionId", getPartitionId());
269                return b.build();
270        }
271
272        @Override
273        public boolean matches(IQueryParameterType theParam) {
274                if (!(theParam instanceof StringParam)) {
275                        return false;
276                }
277                StringParam string = (StringParam) theParam;
278                String normalizedString = StringUtil.normalizeStringForSearchIndexing(defaultString(string.getValue()));
279                return defaultString(getValueNormalized()).startsWith(normalizedString);
280        }
281
282        public static long calculateHashExact(
283                        PartitionSettings thePartitionSettings,
284                        PartitionablePartitionId theRequestPartitionId,
285                        String theResourceType,
286                        String theParamName,
287                        String theValueExact) {
288                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
289                return calculateHashExact(
290                                thePartitionSettings, requestPartitionId, theResourceType, theParamName, theValueExact);
291        }
292
293        public static long calculateHashExact(
294                        PartitionSettings thePartitionSettings,
295                        RequestPartitionId theRequestPartitionId,
296                        String theResourceType,
297                        String theParamName,
298                        String theValueExact) {
299                return hashSearchParam(
300                                thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, theValueExact);
301        }
302
303        public static long calculateHashNormalized(
304                        PartitionSettings thePartitionSettings,
305                        PartitionablePartitionId theRequestPartitionId,
306                        StorageSettings theStorageSettings,
307                        String theResourceType,
308                        String theParamName,
309                        String theValueNormalized) {
310                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
311                return calculateHashNormalized(
312                                thePartitionSettings,
313                                requestPartitionId,
314                                theStorageSettings,
315                                theResourceType,
316                                theParamName,
317                                theValueNormalized);
318        }
319
320        public static long calculateHashNormalized(
321                        PartitionSettings thePartitionSettings,
322                        RequestPartitionId theRequestPartitionId,
323                        StorageSettings theStorageSettings,
324                        String theResourceType,
325                        String theParamName,
326                        String theValueNormalized) {
327                /*
328                 * If we're not allowing contained searches, we'll add the first
329                 * bit of the normalized value to the hash. This helps to
330                 * make the hash even more unique, which will be good for
331                 * performance.
332                 */
333                int hashPrefixLength = HASH_PREFIX_LENGTH;
334                if (theStorageSettings.isAllowContainsSearches()) {
335                        hashPrefixLength = 0;
336                }
337
338                String value = StringUtil.left(theValueNormalized, hashPrefixLength);
339                return hashSearchParam(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, value);
340        }
341
342        @Override
343        public ResourceTable getResource() {
344                return myResource;
345        }
346
347        @Override
348        public BaseResourceIndexedSearchParam setResource(ResourceTable theResource) {
349                myResource = theResource;
350                setResourceType(theResource.getResourceType());
351                return this;
352        }
353}