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