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