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