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}