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