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}