001/*
002 * #%L
003 * HAPI FHIR JPA Server
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.entity;
021
022import ca.uhn.fhir.interceptor.model.RequestPartitionId;
023import ca.uhn.fhir.jpa.model.dao.JpaPid;
024import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
025import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
026import ca.uhn.fhir.model.api.Include;
027import ca.uhn.fhir.rest.param.DateRangeParam;
028import ca.uhn.fhir.rest.param.HistorySearchStyleEnum;
029import ca.uhn.fhir.rest.server.util.ICachedSearchDetails;
030import ca.uhn.fhir.system.HapiSystemProperties;
031import jakarta.annotation.Nonnull;
032import jakarta.annotation.Nullable;
033import jakarta.persistence.Basic;
034import jakarta.persistence.CascadeType;
035import jakarta.persistence.Column;
036import jakarta.persistence.Entity;
037import jakarta.persistence.EnumType;
038import jakarta.persistence.Enumerated;
039import jakarta.persistence.FetchType;
040import jakarta.persistence.GeneratedValue;
041import jakarta.persistence.GenerationType;
042import jakarta.persistence.Id;
043import jakarta.persistence.Index;
044import jakarta.persistence.Lob;
045import jakarta.persistence.OneToMany;
046import jakarta.persistence.SequenceGenerator;
047import jakarta.persistence.Table;
048import jakarta.persistence.Temporal;
049import jakarta.persistence.TemporalType;
050import jakarta.persistence.Transient;
051import jakarta.persistence.UniqueConstraint;
052import jakarta.persistence.Version;
053import org.apache.commons.lang3.SerializationUtils;
054import org.apache.commons.lang3.builder.ToStringBuilder;
055import org.hibernate.Length;
056import org.hibernate.annotations.JdbcTypeCode;
057import org.hibernate.annotations.OptimisticLock;
058import org.hibernate.type.SqlTypes;
059import org.slf4j.Logger;
060import org.slf4j.LoggerFactory;
061
062import java.io.Serializable;
063import java.util.ArrayList;
064import java.util.Collection;
065import java.util.Collections;
066import java.util.Date;
067import java.util.HashSet;
068import java.util.Optional;
069import java.util.Set;
070import java.util.UUID;
071
072import static org.apache.commons.lang3.StringUtils.left;
073
074@Entity
075@Table(
076                name = Search.HFJ_SEARCH,
077                uniqueConstraints = {@UniqueConstraint(name = "IDX_SEARCH_UUID", columnNames = "SEARCH_UUID")},
078                indexes = {
079                        @Index(name = "IDX_SEARCH_RESTYPE_HASHS", columnList = "RESOURCE_TYPE,SEARCH_QUERY_STRING_HASH,CREATED"),
080                        @Index(name = "IDX_SEARCH_CREATED", columnList = "CREATED")
081                })
082public class Search implements ICachedSearchDetails, Serializable {
083
084        /**
085         * Long enough to accommodate a full UUID (36) with an additional prefix
086         * used by megascale (12)
087         */
088        @SuppressWarnings("WeakerAccess")
089        public static final int SEARCH_UUID_COLUMN_LENGTH = 48;
090
091        public static final String HFJ_SEARCH = "HFJ_SEARCH";
092        public static final String SEARCH_UUID = "SEARCH_UUID";
093        private static final int MAX_SEARCH_QUERY_STRING = 10000;
094        private static final int FAILURE_MESSAGE_LENGTH = 500;
095        private static final long serialVersionUID = 1L;
096        private static final Logger ourLog = LoggerFactory.getLogger(Search.class);
097
098        @Temporal(TemporalType.TIMESTAMP)
099        @Column(name = "CREATED", nullable = false, updatable = false)
100        private Date myCreated;
101
102        @OptimisticLock(excluded = true)
103        @Column(name = "SEARCH_DELETED", nullable = true)
104        private Boolean myDeleted;
105
106        @Column(name = "FAILURE_CODE", nullable = true)
107        private Integer myFailureCode;
108
109        @Column(name = "FAILURE_MESSAGE", length = FAILURE_MESSAGE_LENGTH, nullable = true)
110        private String myFailureMessage;
111
112        @Temporal(TemporalType.TIMESTAMP)
113        @Column(name = "EXPIRY_OR_NULL", nullable = true)
114        private Date myExpiryOrNull;
115
116        @Id
117        @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SEARCH")
118        @SequenceGenerator(name = "SEQ_SEARCH", sequenceName = "SEQ_SEARCH")
119        @Column(name = "PID")
120        private Long myId;
121
122        @OneToMany(mappedBy = "mySearch", cascade = CascadeType.ALL)
123        private Collection<SearchInclude> myIncludes;
124
125        @Temporal(TemporalType.TIMESTAMP)
126        @Column(name = "LAST_UPDATED_HIGH", nullable = true, insertable = true, updatable = false)
127        private Date myLastUpdatedHigh;
128
129        @Temporal(TemporalType.TIMESTAMP)
130        @Column(name = "LAST_UPDATED_LOW", nullable = true, insertable = true, updatable = false)
131        private Date myLastUpdatedLow;
132
133        @Column(name = "NUM_FOUND", nullable = false)
134        private int myNumFound;
135
136        @Column(name = "NUM_BLOCKED", nullable = true)
137        private Integer myNumBlocked;
138
139        @Column(name = "PREFERRED_PAGE_SIZE", nullable = true)
140        private Integer myPreferredPageSize;
141
142        @Column(name = "RESOURCE_ID", nullable = true)
143        private Long myResourceId;
144
145        @Column(name = "PARTITION_ID", nullable = true)
146        private Integer myPartitionId;
147
148        @Column(name = "RESOURCE_TYPE", length = 200, nullable = true)
149        private String myResourceType;
150        /**
151         * Note that this field may have the request partition IDs prepended to it
152         */
153        @Lob // TODO: VC column added in 7.2.0 - Remove non-VC column later
154        @Basic(fetch = FetchType.LAZY)
155        @Column(name = "SEARCH_QUERY_STRING", nullable = true, updatable = false, length = MAX_SEARCH_QUERY_STRING)
156        private String mySearchQueryString;
157        /**
158         * Note that this field may have the request partition IDs prepended to it
159         */
160        @Column(name = "SEARCH_QUERY_STRING_VC", nullable = true, length = Length.LONG32)
161        private String mySearchQueryStringVc;
162
163        @Column(name = "SEARCH_QUERY_STRING_HASH", nullable = true, updatable = false)
164        private Integer mySearchQueryStringHash;
165
166        @Enumerated(EnumType.ORDINAL)
167        @Column(name = "SEARCH_TYPE", nullable = false)
168        @JdbcTypeCode(SqlTypes.INTEGER)
169        private SearchTypeEnum mySearchType;
170
171        @Enumerated(EnumType.STRING)
172        @JdbcTypeCode(SqlTypes.VARCHAR)
173        @Column(name = "SEARCH_STATUS", nullable = false, length = 10)
174        private SearchStatusEnum myStatus;
175
176        @Column(name = "TOTAL_COUNT", nullable = true)
177        private Integer myTotalCount;
178
179        @Column(name = SEARCH_UUID, length = SEARCH_UUID_COLUMN_LENGTH, nullable = false, updatable = false)
180        private String myUuid;
181
182        @SuppressWarnings("unused")
183        @Version
184        @Column(name = "OPTLOCK_VERSION", nullable = true)
185        private Integer myVersion;
186
187        @Lob // TODO: VC column added in 7.2.0 - Remove non-VC column later
188        @Column(name = "SEARCH_PARAM_MAP", nullable = true)
189        private byte[] mySearchParameterMap;
190
191        @Column(name = "SEARCH_PARAM_MAP_BIN", nullable = true, length = Length.LONG32)
192        private byte[] mySearchParameterMapBin;
193
194        @Transient
195        private transient SearchParameterMap mySearchParameterMapTransient;
196        /**
197         * This isn't currently persisted in the DB as it's only used for offset mode. We could
198         * change this if needed in the future.
199         */
200        @Transient
201        private Integer myOffset;
202        /**
203         * This isn't currently persisted in the DB as it's only used for offset mode. We could
204         * change this if needed in the future.
205         */
206        @Transient
207        private Integer mySizeModeSize;
208        /**
209         * This isn't currently persisted in the DB. When there is search criteria defined in the
210         * search parameter, this is used to keep the search criteria type.
211         */
212        @Transient
213        private HistorySearchStyleEnum myHistorySearchStyle;
214
215        /**
216         * Constructor
217         */
218        public Search() {
219                super();
220        }
221
222        public Integer getSizeModeSize() {
223                return mySizeModeSize;
224        }
225
226        @Override
227        public String toString() {
228                return new ToStringBuilder(this)
229                                .append("myLastUpdatedHigh", myLastUpdatedHigh)
230                                .append("myLastUpdatedLow", myLastUpdatedLow)
231                                .append("myNumFound", myNumFound)
232                                .append("myNumBlocked", myNumBlocked)
233                                .append("myStatus", myStatus)
234                                .append("myTotalCount", myTotalCount)
235                                .append("myUuid", myUuid)
236                                .append("myVersion", myVersion)
237                                .toString();
238        }
239
240        public int getNumBlocked() {
241                return myNumBlocked != null ? myNumBlocked : 0;
242        }
243
244        public void setNumBlocked(int theNumBlocked) {
245                myNumBlocked = theNumBlocked;
246        }
247
248        public Date getExpiryOrNull() {
249                return myExpiryOrNull;
250        }
251
252        public void setExpiryOrNull(Date theExpiryOrNull) {
253                myExpiryOrNull = theExpiryOrNull;
254        }
255
256        public Boolean getDeleted() {
257                return myDeleted;
258        }
259
260        public void setDeleted(Boolean theDeleted) {
261                myDeleted = theDeleted;
262        }
263
264        public Date getCreated() {
265                return myCreated;
266        }
267
268        public void setCreated(Date theCreated) {
269                myCreated = theCreated;
270        }
271
272        public Integer getFailureCode() {
273                return myFailureCode;
274        }
275
276        public void setFailureCode(Integer theFailureCode) {
277                myFailureCode = theFailureCode;
278        }
279
280        public String getFailureMessage() {
281                return myFailureMessage;
282        }
283
284        public void setFailureMessage(String theFailureMessage) {
285                myFailureMessage = left(theFailureMessage, FAILURE_MESSAGE_LENGTH);
286                if (HapiSystemProperties.isUnitTestCaptureStackEnabled()) {
287                        myFailureMessage = theFailureMessage;
288                }
289        }
290
291        public Long getId() {
292                return myId;
293        }
294
295        public Collection<SearchInclude> getIncludes() {
296                if (myIncludes == null) {
297                        myIncludes = new ArrayList<>();
298                }
299                return myIncludes;
300        }
301
302        public DateRangeParam getLastUpdated() {
303                if (myLastUpdatedLow == null && myLastUpdatedHigh == null) {
304                        return null;
305                } else {
306                        return new DateRangeParam(myLastUpdatedLow, myLastUpdatedHigh);
307                }
308        }
309
310        public void setLastUpdated(DateRangeParam theLastUpdated) {
311                if (theLastUpdated == null) {
312                        myLastUpdatedLow = null;
313                        myLastUpdatedHigh = null;
314                } else {
315                        myLastUpdatedLow = theLastUpdated.getLowerBoundAsInstant();
316                        myLastUpdatedHigh = theLastUpdated.getUpperBoundAsInstant();
317                }
318        }
319
320        public Date getLastUpdatedHigh() {
321                return myLastUpdatedHigh;
322        }
323
324        public Date getLastUpdatedLow() {
325                return myLastUpdatedLow;
326        }
327
328        public int getNumFound() {
329                ourLog.trace("getNumFound {}", myNumFound);
330                return myNumFound;
331        }
332
333        public void setNumFound(int theNumFound) {
334                ourLog.trace("setNumFound {}", theNumFound);
335                myNumFound = theNumFound;
336        }
337
338        public Integer getPreferredPageSize() {
339                return myPreferredPageSize;
340        }
341
342        public void setPreferredPageSize(Integer thePreferredPageSize) {
343                myPreferredPageSize = thePreferredPageSize;
344        }
345
346        @Nullable
347        public JpaPid getResourceId() {
348                return myResourceId != null ? JpaPid.fromId(myResourceId, myPartitionId) : null;
349        }
350
351        public void setResourceId(@Nullable JpaPid theResourceId) {
352                myResourceId = theResourceId != null ? theResourceId.getId() : null;
353                myPartitionId = theResourceId != null ? theResourceId.getPartitionId() : null;
354        }
355
356        @Override
357        public String getResourceType() {
358                return myResourceType;
359        }
360
361        public void setResourceType(String theResourceType) {
362                myResourceType = theResourceType;
363        }
364
365        /**
366         * Note that this field may have the request partition IDs prepended to it
367         */
368        public String getSearchQueryString() {
369                return mySearchQueryStringVc != null ? mySearchQueryStringVc : mySearchQueryString;
370        }
371
372        public void setSearchQueryString(String theSearchQueryString, RequestPartitionId theRequestPartitionId) {
373                String searchQueryString = null;
374                if (theSearchQueryString != null) {
375                        searchQueryString = createSearchQueryStringForStorage(theSearchQueryString, theRequestPartitionId);
376                }
377                if (searchQueryString == null || searchQueryString.length() > MAX_SEARCH_QUERY_STRING) {
378                        // We want this field to always have a wide distribution of values in order
379                        // to avoid optimizers avoiding using it if it has lots of nulls, so in the
380                        // case of null, just put a value that will never be hit
381                        mySearchQueryStringVc = UUID.randomUUID().toString();
382                } else {
383                        mySearchQueryStringVc = searchQueryString;
384                }
385
386                mySearchQueryString = null;
387                mySearchQueryStringHash = mySearchQueryStringVc.hashCode();
388        }
389
390        public SearchTypeEnum getSearchType() {
391                return mySearchType;
392        }
393
394        public void setSearchType(SearchTypeEnum theSearchType) {
395                mySearchType = theSearchType;
396        }
397
398        public SearchStatusEnum getStatus() {
399                ourLog.trace("getStatus {}", myStatus);
400                return myStatus;
401        }
402
403        public void setStatus(SearchStatusEnum theStatus) {
404                ourLog.trace("setStatus {}", theStatus);
405                myStatus = theStatus;
406        }
407
408        public Integer getTotalCount() {
409                return myTotalCount;
410        }
411
412        public void setTotalCount(Integer theTotalCount) {
413                myTotalCount = theTotalCount;
414        }
415
416        @Override
417        public String getUuid() {
418                return myUuid;
419        }
420
421        @Override
422        public void setUuid(String theUuid) {
423                myUuid = theUuid;
424        }
425
426        public void setLastUpdated(Date theLowerBound, Date theUpperBound) {
427                myLastUpdatedLow = theLowerBound;
428                myLastUpdatedHigh = theUpperBound;
429        }
430
431        private Set<Include> toIncList(boolean theWantReverse, boolean theIncludeAll, boolean theWantIterate) {
432                HashSet<Include> retVal = new HashSet<>();
433                for (SearchInclude next : getIncludes()) {
434                        if (theWantReverse == next.isReverse()) {
435                                if (theIncludeAll) {
436                                        retVal.add(new Include(next.getInclude(), next.isRecurse()));
437                                } else {
438                                        if (theWantIterate == next.isRecurse()) {
439                                                retVal.add(new Include(next.getInclude(), next.isRecurse()));
440                                        }
441                                }
442                        }
443                }
444                return Collections.unmodifiableSet(retVal);
445        }
446
447        private Set<Include> toIncList(boolean theWantReverse) {
448                return toIncList(theWantReverse, true, true);
449        }
450
451        public Set<Include> toIncludesList() {
452                return toIncList(false);
453        }
454
455        public Set<Include> toRevIncludesList() {
456                return toIncList(true);
457        }
458
459        public Set<Include> toIncludesList(boolean iterate) {
460                return toIncList(false, false, iterate);
461        }
462
463        public Set<Include> toRevIncludesList(boolean iterate) {
464                return toIncList(true, false, iterate);
465        }
466
467        public void addInclude(SearchInclude theInclude) {
468                getIncludes().add(theInclude);
469        }
470
471        public Integer getVersion() {
472                return myVersion;
473        }
474
475        /**
476         * Note that this is not always set! We set this if we're storing a
477         * Search in {@link SearchStatusEnum#PASSCMPLET} status since we'll need
478         * the map in order to restart, but otherwise we save space and time by
479         * not storing it.
480         */
481        public Optional<SearchParameterMap> getSearchParameterMap() {
482                if (mySearchParameterMapTransient != null) {
483                        return Optional.of(mySearchParameterMapTransient);
484                }
485                SearchParameterMap searchParameterMap = null;
486                byte[] searchParameterMapSerialized = mySearchParameterMapBin;
487                if (searchParameterMapSerialized == null) {
488                        searchParameterMapSerialized = mySearchParameterMap;
489                }
490                if (searchParameterMapSerialized != null) {
491                        searchParameterMap = SerializationUtils.deserialize(searchParameterMapSerialized);
492                        mySearchParameterMapTransient = searchParameterMap;
493                }
494                return Optional.ofNullable(searchParameterMap);
495        }
496
497        public void setSearchParameterMap(SearchParameterMap theSearchParameterMap) {
498                mySearchParameterMapTransient = theSearchParameterMap;
499                mySearchParameterMapBin = SerializationUtils.serialize(theSearchParameterMap);
500                mySearchParameterMap = null;
501        }
502
503        @Override
504        public void setCannotBeReused() {
505                mySearchQueryStringHash = null;
506        }
507
508        public Integer getOffset() {
509                return myOffset;
510        }
511
512        public void setOffset(Integer theOffset) {
513                myOffset = theOffset;
514        }
515
516        public HistorySearchStyleEnum getHistorySearchStyle() {
517                return myHistorySearchStyle;
518        }
519
520        public void setHistorySearchStyle(HistorySearchStyleEnum theHistorySearchStyle) {
521                this.myHistorySearchStyle = theHistorySearchStyle;
522        }
523
524        @Nonnull
525        public static String createSearchQueryStringForStorage(
526                        @Nonnull String theSearchQueryString, @Nonnull RequestPartitionId theRequestPartitionId) {
527                String searchQueryString = theSearchQueryString;
528                if (!theRequestPartitionId.isAllPartitions()) {
529                        searchQueryString = RequestPartitionId.stringifyForKey(theRequestPartitionId) + " " + searchQueryString;
530                }
531                return searchQueryString;
532        }
533}