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.interceptor.model.RequestPartitionId;
023import ca.uhn.fhir.jpa.model.config.PartitionSettings;
024import ca.uhn.fhir.jpa.model.listener.IndexStorageOptimizationListener;
025import ca.uhn.fhir.model.api.IQueryParameterType;
026import ca.uhn.fhir.rest.api.Constants;
027import ca.uhn.fhir.rest.param.TokenParam;
028import jakarta.persistence.Column;
029import jakarta.persistence.Entity;
030import jakarta.persistence.EntityListeners;
031import jakarta.persistence.FetchType;
032import jakarta.persistence.ForeignKey;
033import jakarta.persistence.GeneratedValue;
034import jakarta.persistence.GenerationType;
035import jakarta.persistence.Id;
036import jakarta.persistence.IdClass;
037import jakarta.persistence.Index;
038import jakarta.persistence.JoinColumn;
039import jakarta.persistence.JoinColumns;
040import jakarta.persistence.ManyToOne;
041import jakarta.persistence.PrePersist;
042import jakarta.persistence.PreUpdate;
043import jakarta.persistence.Table;
044import org.apache.commons.lang3.StringUtils;
045import org.apache.commons.lang3.builder.EqualsBuilder;
046import org.apache.commons.lang3.builder.HashCodeBuilder;
047import org.apache.commons.lang3.builder.ToStringBuilder;
048import org.apache.commons.lang3.builder.ToStringStyle;
049import org.hibernate.annotations.GenericGenerator;
050import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
051
052import static ca.uhn.fhir.jpa.model.util.SearchParamHash.hashSearchParam;
053import static org.apache.commons.lang3.StringUtils.defaultString;
054import static org.apache.commons.lang3.StringUtils.trim;
055
056@EntityListeners(IndexStorageOptimizationListener.class)
057@Entity
058@Table(
059                name = ResourceIndexedSearchParamToken.HFJ_SPIDX_TOKEN,
060                indexes = {
061                        /*
062                         * Note: We previously had indexes with the following names,
063                         * do not reuse these names:
064                         * IDX_SP_TOKEN
065                         * IDX_SP_TOKEN_UNQUAL
066                         */
067
068                        @Index(name = "IDX_SP_TOKEN_HASH_V2", columnList = "HASH_IDENTITY,SP_SYSTEM,SP_VALUE,RES_ID,PARTITION_ID"),
069                        @Index(name = "IDX_SP_TOKEN_HASH_S_V2", columnList = "HASH_SYS,RES_ID,PARTITION_ID"),
070                        @Index(name = "IDX_SP_TOKEN_HASH_SV_V2", columnList = "HASH_SYS_AND_VALUE,RES_ID,PARTITION_ID"),
071                        @Index(name = "IDX_SP_TOKEN_HASH_V_V2", columnList = "HASH_VALUE,RES_ID,PARTITION_ID"),
072                        @Index(
073                                        name = "IDX_SP_TOKEN_RESID_V2",
074                                        columnList = "RES_ID,HASH_SYS_AND_VALUE,HASH_VALUE,HASH_SYS,HASH_IDENTITY,PARTITION_ID")
075                })
076@IdClass(IdAndPartitionId.class)
077public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchParam {
078
079        public static final int MAX_LENGTH = 200;
080
081        private static final long serialVersionUID = 1L;
082        public static final String HFJ_SPIDX_TOKEN = "HFJ_SPIDX_TOKEN";
083
084        @FullTextField
085        @Column(name = "SP_SYSTEM", nullable = true, length = MAX_LENGTH)
086        public String mySystem;
087
088        @FullTextField
089        @Column(name = "SP_VALUE", nullable = true, length = MAX_LENGTH)
090        private String myValue;
091
092        @SuppressWarnings("unused")
093        @Id
094        @GenericGenerator(name = "SEQ_SPIDX_TOKEN", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class)
095        @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_TOKEN")
096        @Column(name = "SP_ID")
097        private Long myId;
098
099        /**
100         * @since 3.4.0 - At some point this should be made not-null
101         */
102        @Column(name = "HASH_SYS", nullable = true)
103        private Long myHashSystem;
104        /**
105         * @since 3.4.0 - At some point this should be made not-null
106         */
107        @Column(name = "HASH_SYS_AND_VALUE", nullable = true)
108        private Long myHashSystemAndValue;
109        /**
110         * @since 3.4.0 - At some point this should be made not-null
111         */
112        @Column(name = "HASH_VALUE", nullable = true)
113        private Long myHashValue;
114
115        @ManyToOne(
116                        optional = false,
117                        fetch = FetchType.LAZY,
118                        cascade = {})
119        @JoinColumns(
120                        value = {
121                                @JoinColumn(
122                                                name = "RES_ID",
123                                                referencedColumnName = "RES_ID",
124                                                insertable = false,
125                                                updatable = false,
126                                                nullable = false),
127                                @JoinColumn(
128                                                name = "PARTITION_ID",
129                                                referencedColumnName = "PARTITION_ID",
130                                                insertable = false,
131                                                updatable = false,
132                                                nullable = false)
133                        },
134                        foreignKey = @ForeignKey(name = "FK_SP_TOKEN_RES"))
135        private ResourceTable myResource;
136
137        @Column(name = "RES_ID", nullable = false)
138        private Long myResourceId;
139
140        /**
141         * Constructor
142         */
143        public ResourceIndexedSearchParamToken() {
144                super();
145        }
146
147        /**
148         * Constructor
149         */
150        public ResourceIndexedSearchParamToken(
151                        PartitionSettings thePartitionSettings,
152                        String theResourceType,
153                        String theParamName,
154                        String theSystem,
155                        String theValue) {
156                super();
157                setPartitionSettings(thePartitionSettings);
158                setResourceType(theResourceType);
159                setParamName(theParamName);
160                setSystem(theSystem);
161                setValue(theValue);
162                calculateHashes();
163        }
164
165        /**
166         * Constructor
167         */
168        public ResourceIndexedSearchParamToken(
169                        PartitionSettings thePartitionSettings, String theResourceType, String theParamName, boolean theMissing) {
170                super();
171                setPartitionSettings(thePartitionSettings);
172                setResourceType(theResourceType);
173                setParamName(theParamName);
174                setMissing(theMissing);
175                calculateHashes();
176        }
177
178        @Override
179        public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) {
180                super.copyMutableValuesFrom(theSource);
181                ResourceIndexedSearchParamToken source = (ResourceIndexedSearchParamToken) theSource;
182
183                mySystem = source.mySystem;
184                myValue = source.myValue;
185                myHashSystem = source.myHashSystem;
186                myHashSystemAndValue = source.getHashSystemAndValue();
187                myHashValue = source.myHashValue;
188                myHashIdentity = source.myHashIdentity;
189        }
190
191        @Override
192        public void setResourceId(Long theResourceId) {
193                myResourceId = theResourceId;
194        }
195
196        @Override
197        public void clearHashes() {
198                myHashIdentity = null;
199                myHashSystem = null;
200                myHashSystemAndValue = null;
201                myHashValue = null;
202        }
203
204        @Override
205        public void calculateHashes() {
206                if (myHashIdentity != null || myHashSystem != null || myHashValue != null || myHashSystemAndValue != null) {
207                        return;
208                }
209
210                String resourceType = getResourceType();
211                String paramName = getParamName();
212                String system = getSystem();
213                String value = getValue();
214                setHashIdentity(calculateHashIdentity(getPartitionSettings(), getPartitionId(), resourceType, paramName));
215                setHashSystemAndValue(calculateHashSystemAndValue(
216                                getPartitionSettings(), getPartitionId(), resourceType, paramName, system, value));
217
218                // Searches using the :of-type modifier can never be partial (system-only or value-only) so don't
219                // bother saving these
220                boolean calculatePartialHashes = !StringUtils.endsWith(paramName, Constants.PARAMQUALIFIER_TOKEN_OF_TYPE);
221                if (calculatePartialHashes) {
222                        setHashSystem(
223                                        calculateHashSystem(getPartitionSettings(), getPartitionId(), resourceType, paramName, system));
224                        setHashValue(calculateHashValue(getPartitionSettings(), getPartitionId(), resourceType, paramName, value));
225                }
226        }
227
228        @Override
229        public boolean equals(Object theObj) {
230                if (this == theObj) {
231                        return true;
232                }
233                if (theObj == null) {
234                        return false;
235                }
236                if (!(theObj instanceof ResourceIndexedSearchParamToken)) {
237                        return false;
238                }
239                ResourceIndexedSearchParamToken obj = (ResourceIndexedSearchParamToken) theObj;
240                EqualsBuilder b = new EqualsBuilder();
241                b.append(getHashIdentity(), obj.getHashIdentity());
242                b.append(getHashSystem(), obj.getHashSystem());
243                b.append(getHashValue(), obj.getHashValue());
244                b.append(getHashSystemAndValue(), obj.getHashSystemAndValue());
245                b.append(isMissing(), obj.isMissing());
246                return b.isEquals();
247        }
248
249        public Long getHashSystem() {
250                return myHashSystem;
251        }
252
253        private void setHashSystem(Long theHashSystem) {
254                myHashSystem = theHashSystem;
255        }
256
257        public Long getHashSystemAndValue() {
258                return myHashSystemAndValue;
259        }
260
261        private void setHashSystemAndValue(Long theHashSystemAndValue) {
262                myHashSystemAndValue = theHashSystemAndValue;
263        }
264
265        public Long getHashValue() {
266                return myHashValue;
267        }
268
269        private void setHashValue(Long theHashValue) {
270                myHashValue = theHashValue;
271        }
272
273        @Override
274        public Long getId() {
275                return myId;
276        }
277
278        @Override
279        public void setId(Long theId) {
280                myId = theId;
281        }
282
283        public String getSystem() {
284                return mySystem;
285        }
286
287        public void setSystem(String theSystem) {
288                mySystem = StringUtils.defaultIfBlank(theSystem, null);
289                myHashSystemAndValue = null;
290        }
291
292        public String getValue() {
293                return myValue;
294        }
295
296        public ResourceIndexedSearchParamToken setValue(String theValue) {
297                myValue = StringUtils.defaultIfBlank(theValue, null);
298                myHashSystemAndValue = null;
299                return this;
300        }
301
302        @Override
303        public int hashCode() {
304                HashCodeBuilder b = new HashCodeBuilder();
305                b.append(getHashIdentity());
306                b.append(getHashValue());
307                b.append(getHashSystem());
308                b.append(getHashSystemAndValue());
309                b.append(isMissing());
310                return b.toHashCode();
311        }
312
313        @Override
314        public IQueryParameterType toQueryParameterType() {
315                return new TokenParam(getSystem(), getValue());
316        }
317
318        @Override
319        public String toString() {
320                ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
321                b.append("id", getId());
322                if (getPartitionId() != null) {
323                        b.append("partitionId", getPartitionId().getPartitionId());
324                }
325                b.append("resourceType", getResourceType());
326                b.append("paramName", getParamName());
327                if (isMissing()) {
328                        b.append("missing", true);
329                } else {
330                        b.append("system", getSystem());
331                        b.append("value", getValue());
332                }
333                b.append("hashIdentity", myHashIdentity);
334                b.append("hashSystem", myHashSystem);
335                b.append("hashValue", myHashValue);
336                b.append("hashSysAndValue", myHashSystemAndValue);
337                b.append("partition", getPartitionId());
338                return b.build();
339        }
340
341        @Override
342        public boolean matches(IQueryParameterType theParam) {
343                if (!(theParam instanceof TokenParam)) {
344                        return false;
345                }
346                TokenParam token = (TokenParam) theParam;
347                boolean retVal = false;
348                String valueString = defaultString(getValue());
349                String tokenValueString = defaultString(token.getValue());
350
351                // Only match on system if it wasn't specified
352                if (token.getSystem() == null || token.getSystem().isEmpty()) {
353                        if (valueString.equalsIgnoreCase(tokenValueString)) {
354                                retVal = true;
355                        }
356                } else if (tokenValueString == null || tokenValueString.isEmpty()) {
357                        if (token.getSystem().equalsIgnoreCase(getSystem())) {
358                                retVal = true;
359                        }
360                } else {
361                        if (token.getSystem().equalsIgnoreCase(getSystem()) && valueString.equalsIgnoreCase(tokenValueString)) {
362                                retVal = true;
363                        }
364                }
365                return retVal;
366        }
367
368        public static long calculateHashSystem(
369                        PartitionSettings thePartitionSettings,
370                        PartitionablePartitionId theRequestPartitionId,
371                        String theResourceType,
372                        String theParamName,
373                        String theSystem) {
374                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
375                return calculateHashSystem(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem);
376        }
377
378        public static long calculateHashSystem(
379                        PartitionSettings thePartitionSettings,
380                        RequestPartitionId theRequestPartitionId,
381                        String theResourceType,
382                        String theParamName,
383                        String theSystem) {
384                return hashSearchParam(
385                                thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, trim(theSystem));
386        }
387
388        public static long calculateHashSystemAndValue(
389                        PartitionSettings thePartitionSettings,
390                        PartitionablePartitionId theRequestPartitionId,
391                        String theResourceType,
392                        String theParamName,
393                        String theSystem,
394                        String theValue) {
395                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
396                return calculateHashSystemAndValue(
397                                thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem, theValue);
398        }
399
400        public static long calculateHashSystemAndValue(
401                        PartitionSettings thePartitionSettings,
402                        RequestPartitionId theRequestPartitionId,
403                        String theResourceType,
404                        String theParamName,
405                        String theSystem,
406                        String theValue) {
407                return hashSearchParam(
408                                thePartitionSettings,
409                                theRequestPartitionId,
410                                theResourceType,
411                                theParamName,
412                                defaultString(trim(theSystem)),
413                                trim(theValue));
414        }
415
416        public static long calculateHashValue(
417                        PartitionSettings thePartitionSettings,
418                        PartitionablePartitionId theRequestPartitionId,
419                        String theResourceType,
420                        String theParamName,
421                        String theValue) {
422                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
423                return calculateHashValue(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theValue);
424        }
425
426        public static long calculateHashValue(
427                        PartitionSettings thePartitionSettings,
428                        RequestPartitionId theRequestPartitionId,
429                        String theResourceType,
430                        String theParamName,
431                        String theValue) {
432                String value = trim(theValue);
433                return hashSearchParam(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, value);
434        }
435
436        @Override
437        public ResourceTable getResource() {
438                return myResource;
439        }
440
441        @Override
442        public BaseResourceIndexedSearchParam setResource(ResourceTable theResource) {
443                setResourceType(theResource.getResourceType());
444                return this;
445        }
446
447        /**
448         * We truncate the fields at the last moment because the tables have limited size.
449         * We don't truncate earlier in the flow because the index hashes MUST be calculated on the full string.
450         */
451        @PrePersist
452        @PreUpdate
453        public void truncateFieldsForDB() {
454                mySystem = StringUtils.truncate(mySystem, MAX_LENGTH);
455                myValue = StringUtils.truncate(myValue, MAX_LENGTH);
456        }
457}