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}