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.SequenceGenerator;
040import jakarta.persistence.Table;
041import org.apache.commons.lang3.StringUtils;
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;
046import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
047
048import static org.apache.commons.lang3.StringUtils.defaultString;
049import static org.apache.commons.lang3.StringUtils.trim;
050
051@Embeddable
052@Entity
053@Table(
054                name = "HFJ_SPIDX_TOKEN",
055                indexes = {
056                        /*
057                         * Note: We previously had indexes with the following names,
058                         * do not reuse these names:
059                         * IDX_SP_TOKEN
060                         * IDX_SP_TOKEN_UNQUAL
061                         */
062
063                        @Index(name = "IDX_SP_TOKEN_HASH_V2", columnList = "HASH_IDENTITY,SP_SYSTEM,SP_VALUE,RES_ID,PARTITION_ID"),
064                        @Index(name = "IDX_SP_TOKEN_HASH_S_V2", columnList = "HASH_SYS,RES_ID,PARTITION_ID"),
065                        @Index(name = "IDX_SP_TOKEN_HASH_SV_V2", columnList = "HASH_SYS_AND_VALUE,RES_ID,PARTITION_ID"),
066                        @Index(name = "IDX_SP_TOKEN_HASH_V_V2", columnList = "HASH_VALUE,RES_ID,PARTITION_ID"),
067                        @Index(
068                                        name = "IDX_SP_TOKEN_RESID_V2",
069                                        columnList = "RES_ID,HASH_SYS_AND_VALUE,HASH_VALUE,HASH_SYS,HASH_IDENTITY,PARTITION_ID")
070                })
071public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchParam {
072
073        public static final int MAX_LENGTH = 200;
074
075        private static final long serialVersionUID = 1L;
076
077        @FullTextField
078        @Column(name = "SP_SYSTEM", nullable = true, length = MAX_LENGTH)
079        public String mySystem;
080
081        @FullTextField
082        @Column(name = "SP_VALUE", nullable = true, length = MAX_LENGTH)
083        private String myValue;
084
085        @SuppressWarnings("unused")
086        @Id
087        @SequenceGenerator(name = "SEQ_SPIDX_TOKEN", sequenceName = "SEQ_SPIDX_TOKEN")
088        @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_TOKEN")
089        @Column(name = "SP_ID")
090        private Long myId;
091        /**
092         * @since 3.4.0 - At some point this should be made not-null
093         */
094        @Column(name = "HASH_IDENTITY", nullable = true)
095        private Long myHashIdentity;
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(getHashSystem(), obj.getHashSystem());
220                b.append(getHashValue(), obj.getHashValue());
221                b.append(getHashSystemAndValue(), obj.getHashSystemAndValue());
222                return b.isEquals();
223        }
224
225        public Long getHashSystem() {
226                return myHashSystem;
227        }
228
229        private void setHashSystem(Long theHashSystem) {
230                myHashSystem = theHashSystem;
231        }
232
233        private void setHashIdentity(Long theHashIdentity) {
234                myHashIdentity = theHashIdentity;
235        }
236
237        public Long getHashSystemAndValue() {
238                return myHashSystemAndValue;
239        }
240
241        private void setHashSystemAndValue(Long theHashSystemAndValue) {
242                myHashSystemAndValue = theHashSystemAndValue;
243        }
244
245        public Long getHashValue() {
246                return myHashValue;
247        }
248
249        private void setHashValue(Long theHashValue) {
250                myHashValue = theHashValue;
251        }
252
253        @Override
254        public Long getId() {
255                return myId;
256        }
257
258        @Override
259        public void setId(Long theId) {
260                myId = theId;
261        }
262
263        public String getSystem() {
264                return mySystem;
265        }
266
267        public void setSystem(String theSystem) {
268                mySystem = StringUtils.defaultIfBlank(theSystem, null);
269                myHashSystemAndValue = null;
270        }
271
272        public String getValue() {
273                return myValue;
274        }
275
276        public ResourceIndexedSearchParamToken setValue(String theValue) {
277                myValue = StringUtils.defaultIfBlank(theValue, null);
278                myHashSystemAndValue = null;
279                return this;
280        }
281
282        @Override
283        public int hashCode() {
284                HashCodeBuilder b = new HashCodeBuilder();
285                b.append(getResourceType());
286                b.append(getHashValue());
287                b.append(getHashSystem());
288                b.append(getHashSystemAndValue());
289
290                return b.toHashCode();
291        }
292
293        @Override
294        public IQueryParameterType toQueryParameterType() {
295                return new TokenParam(getSystem(), getValue());
296        }
297
298        @Override
299        public String toString() {
300                ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
301                b.append("id", getId());
302                if (getPartitionId() != null) {
303                        b.append("partitionId", getPartitionId().getPartitionId());
304                }
305                b.append("resourceType", getResourceType());
306                b.append("paramName", getParamName());
307                if (isMissing()) {
308                        b.append("missing", true);
309                } else {
310                        b.append("system", getSystem());
311                        b.append("value", getValue());
312                }
313                b.append("hashIdentity", myHashIdentity);
314                b.append("hashSystem", myHashSystem);
315                b.append("hashValue", myHashValue);
316                b.append("hashSysAndValue", myHashSystemAndValue);
317                b.append("partition", getPartitionId());
318                return b.build();
319        }
320
321        @Override
322        public boolean matches(IQueryParameterType theParam) {
323                if (!(theParam instanceof TokenParam)) {
324                        return false;
325                }
326                TokenParam token = (TokenParam) theParam;
327                boolean retVal = false;
328                String valueString = defaultString(getValue());
329                String tokenValueString = defaultString(token.getValue());
330
331                // Only match on system if it wasn't specified
332                if (token.getSystem() == null || token.getSystem().isEmpty()) {
333                        if (valueString.equalsIgnoreCase(tokenValueString)) {
334                                retVal = true;
335                        }
336                } else if (tokenValueString == null || tokenValueString.isEmpty()) {
337                        if (token.getSystem().equalsIgnoreCase(getSystem())) {
338                                retVal = true;
339                        }
340                } else {
341                        if (token.getSystem().equalsIgnoreCase(getSystem()) && valueString.equalsIgnoreCase(tokenValueString)) {
342                                retVal = true;
343                        }
344                }
345                return retVal;
346        }
347
348        public static long calculateHashSystem(
349                        PartitionSettings thePartitionSettings,
350                        PartitionablePartitionId theRequestPartitionId,
351                        String theResourceType,
352                        String theParamName,
353                        String theSystem) {
354                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
355                return calculateHashSystem(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem);
356        }
357
358        public static long calculateHashSystem(
359                        PartitionSettings thePartitionSettings,
360                        RequestPartitionId theRequestPartitionId,
361                        String theResourceType,
362                        String theParamName,
363                        String theSystem) {
364                return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, trim(theSystem));
365        }
366
367        public static long calculateHashSystemAndValue(
368                        PartitionSettings thePartitionSettings,
369                        PartitionablePartitionId theRequestPartitionId,
370                        String theResourceType,
371                        String theParamName,
372                        String theSystem,
373                        String theValue) {
374                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
375                return calculateHashSystemAndValue(
376                                thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem, theValue);
377        }
378
379        public static long calculateHashSystemAndValue(
380                        PartitionSettings thePartitionSettings,
381                        RequestPartitionId theRequestPartitionId,
382                        String theResourceType,
383                        String theParamName,
384                        String theSystem,
385                        String theValue) {
386                return hash(
387                                thePartitionSettings,
388                                theRequestPartitionId,
389                                theResourceType,
390                                theParamName,
391                                defaultString(trim(theSystem)),
392                                trim(theValue));
393        }
394
395        public static long calculateHashValue(
396                        PartitionSettings thePartitionSettings,
397                        PartitionablePartitionId theRequestPartitionId,
398                        String theResourceType,
399                        String theParamName,
400                        String theValue) {
401                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
402                return calculateHashValue(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theValue);
403        }
404
405        public static long calculateHashValue(
406                        PartitionSettings thePartitionSettings,
407                        RequestPartitionId theRequestPartitionId,
408                        String theResourceType,
409                        String theParamName,
410                        String theValue) {
411                String value = trim(theValue);
412                return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName, value);
413        }
414
415        @Override
416        public ResourceTable getResource() {
417                return myResource;
418        }
419
420        @Override
421        public BaseResourceIndexedSearchParam setResource(ResourceTable theResource) {
422                myResource = theResource;
423                setResourceType(theResource.getResourceType());
424                return this;
425        }
426
427        /**
428         * We truncate the fields at the last moment because the tables have limited size.
429         * We don't truncate earlier in the flow because the index hashes MUST be calculated on the full string.
430         */
431        @PrePersist
432        public void truncateFieldsForDB() {
433                mySystem = StringUtils.truncate(mySystem, MAX_LENGTH);
434                myValue = StringUtils.truncate(myValue, MAX_LENGTH);
435        }
436}