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