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