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.Table;
043import org.apache.commons.lang3.StringUtils;
044import org.apache.commons.lang3.builder.EqualsBuilder;
045import org.apache.commons.lang3.builder.HashCodeBuilder;
046import org.apache.commons.lang3.builder.ToStringBuilder;
047import org.apache.commons.lang3.builder.ToStringStyle;
048import org.hibernate.annotations.GenericGenerator;
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 = 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                })
076public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchParam {
077
078        public static final int MAX_LENGTH = 200;
079
080        private static final long serialVersionUID = 1L;
081        public static final String HFJ_SPIDX_TOKEN = "HFJ_SPIDX_TOKEN";
082
083        @FullTextField
084        @Column(name = "SP_SYSTEM", nullable = true, length = MAX_LENGTH)
085        public String mySystem;
086
087        @FullTextField
088        @Column(name = "SP_VALUE", nullable = true, length = MAX_LENGTH)
089        private String myValue;
090
091        @SuppressWarnings("unused")
092        @Id
093        @GenericGenerator(name = "SEQ_SPIDX_TOKEN", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class)
094        @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SPIDX_TOKEN")
095        @Column(name = "SP_ID")
096        private Long myId;
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(getHashIdentity(), obj.getHashIdentity());
221                b.append(getHashSystem(), obj.getHashSystem());
222                b.append(getHashValue(), obj.getHashValue());
223                b.append(getHashSystemAndValue(), obj.getHashSystemAndValue());
224                b.append(isMissing(), obj.isMissing());
225                return b.isEquals();
226        }
227
228        public Long getHashSystem() {
229                return myHashSystem;
230        }
231
232        private void setHashSystem(Long theHashSystem) {
233                myHashSystem = theHashSystem;
234        }
235
236        public Long getHashSystemAndValue() {
237                return myHashSystemAndValue;
238        }
239
240        private void setHashSystemAndValue(Long theHashSystemAndValue) {
241                myHashSystemAndValue = theHashSystemAndValue;
242        }
243
244        public Long getHashValue() {
245                return myHashValue;
246        }
247
248        private void setHashValue(Long theHashValue) {
249                myHashValue = theHashValue;
250        }
251
252        @Override
253        public Long getId() {
254                return myId;
255        }
256
257        @Override
258        public void setId(Long theId) {
259                myId = theId;
260        }
261
262        public String getSystem() {
263                return mySystem;
264        }
265
266        public void setSystem(String theSystem) {
267                mySystem = StringUtils.defaultIfBlank(theSystem, null);
268                myHashSystemAndValue = null;
269        }
270
271        public String getValue() {
272                return myValue;
273        }
274
275        public ResourceIndexedSearchParamToken setValue(String theValue) {
276                myValue = StringUtils.defaultIfBlank(theValue, null);
277                myHashSystemAndValue = null;
278                return this;
279        }
280
281        @Override
282        public int hashCode() {
283                HashCodeBuilder b = new HashCodeBuilder();
284                b.append(getHashIdentity());
285                b.append(getHashValue());
286                b.append(getHashSystem());
287                b.append(getHashSystemAndValue());
288                b.append(isMissing());
289                return b.toHashCode();
290        }
291
292        @Override
293        public IQueryParameterType toQueryParameterType() {
294                return new TokenParam(getSystem(), getValue());
295        }
296
297        @Override
298        public String toString() {
299                ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
300                b.append("id", getId());
301                if (getPartitionId() != null) {
302                        b.append("partitionId", getPartitionId().getPartitionId());
303                }
304                b.append("resourceType", getResourceType());
305                b.append("paramName", getParamName());
306                if (isMissing()) {
307                        b.append("missing", true);
308                } else {
309                        b.append("system", getSystem());
310                        b.append("value", getValue());
311                }
312                b.append("hashIdentity", myHashIdentity);
313                b.append("hashSystem", myHashSystem);
314                b.append("hashValue", myHashValue);
315                b.append("hashSysAndValue", myHashSystemAndValue);
316                b.append("partition", getPartitionId());
317                return b.build();
318        }
319
320        @Override
321        public boolean matches(IQueryParameterType theParam) {
322                if (!(theParam instanceof TokenParam)) {
323                        return false;
324                }
325                TokenParam token = (TokenParam) theParam;
326                boolean retVal = false;
327                String valueString = defaultString(getValue());
328                String tokenValueString = defaultString(token.getValue());
329
330                // Only match on system if it wasn't specified
331                if (token.getSystem() == null || token.getSystem().isEmpty()) {
332                        if (valueString.equalsIgnoreCase(tokenValueString)) {
333                                retVal = true;
334                        }
335                } else if (tokenValueString == null || tokenValueString.isEmpty()) {
336                        if (token.getSystem().equalsIgnoreCase(getSystem())) {
337                                retVal = true;
338                        }
339                } else {
340                        if (token.getSystem().equalsIgnoreCase(getSystem()) && valueString.equalsIgnoreCase(tokenValueString)) {
341                                retVal = true;
342                        }
343                }
344                return retVal;
345        }
346
347        public static long calculateHashSystem(
348                        PartitionSettings thePartitionSettings,
349                        PartitionablePartitionId theRequestPartitionId,
350                        String theResourceType,
351                        String theParamName,
352                        String theSystem) {
353                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
354                return calculateHashSystem(thePartitionSettings, requestPartitionId, theResourceType, theParamName, theSystem);
355        }
356
357        public static long calculateHashSystem(
358                        PartitionSettings thePartitionSettings,
359                        RequestPartitionId theRequestPartitionId,
360                        String theResourceType,
361                        String theParamName,
362                        String theSystem) {
363                return hashSearchParam(
364                                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 hashSearchParam(
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 hashSearchParam(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        @PreUpdate
433        public void truncateFieldsForDB() {
434                mySystem = StringUtils.truncate(mySystem, MAX_LENGTH);
435                myValue = StringUtils.truncate(myValue, MAX_LENGTH);
436        }
437}