001/* 002 * #%L 003 * HAPI FHIR JPA Model 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.model.entity; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 024import ca.uhn.fhir.jpa.model.dao.JpaPid; 025import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData; 026import ca.uhn.fhir.jpa.model.search.ResourceTableRoutingBinder; 027import ca.uhn.fhir.jpa.model.search.SearchParamTextPropertyBinder; 028import ca.uhn.fhir.model.primitive.IdDt; 029import ca.uhn.fhir.rest.api.Constants; 030import com.google.common.annotations.VisibleForTesting; 031import jakarta.persistence.CascadeType; 032import jakarta.persistence.Column; 033import jakarta.persistence.Entity; 034import jakarta.persistence.FetchType; 035import jakarta.persistence.GeneratedValue; 036import jakarta.persistence.GenerationType; 037import jakarta.persistence.Id; 038import jakarta.persistence.Index; 039import jakarta.persistence.NamedEntityGraph; 040import jakarta.persistence.OneToMany; 041import jakarta.persistence.PostPersist; 042import jakarta.persistence.PrePersist; 043import jakarta.persistence.PreUpdate; 044import jakarta.persistence.Table; 045import jakarta.persistence.Transient; 046import jakarta.persistence.UniqueConstraint; 047import jakarta.persistence.Version; 048import org.apache.commons.lang3.builder.ToStringBuilder; 049import org.apache.commons.lang3.builder.ToStringStyle; 050import org.hibernate.Session; 051import org.hibernate.annotations.GenerationTime; 052import org.hibernate.annotations.GeneratorType; 053import org.hibernate.annotations.GenericGenerator; 054import org.hibernate.annotations.OptimisticLock; 055import org.hibernate.search.engine.backend.types.Projectable; 056import org.hibernate.search.engine.backend.types.Searchable; 057import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.PropertyBinderRef; 058import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.RoutingBinderRef; 059import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; 060import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField; 061import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; 062import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency; 063import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ObjectPath; 064import org.hibernate.search.mapper.pojo.mapping.definition.annotation.PropertyBinding; 065import org.hibernate.search.mapper.pojo.mapping.definition.annotation.PropertyValue; 066import org.hibernate.tuple.ValueGenerator; 067import org.hl7.fhir.instance.model.api.IIdType; 068import org.hl7.fhir.r4.model.InstantType; 069 070import java.io.Serializable; 071import java.util.ArrayList; 072import java.util.Collection; 073import java.util.HashSet; 074import java.util.Objects; 075import java.util.Set; 076import java.util.stream.Collectors; 077 078import static ca.uhn.fhir.jpa.model.entity.ResourceTable.IDX_RES_TYPE_FHIR_ID; 079 080@Indexed(routingBinder = @RoutingBinderRef(type = ResourceTableRoutingBinder.class)) 081@Entity 082@Table( 083 name = ResourceTable.HFJ_RESOURCE, 084 uniqueConstraints = { 085 @UniqueConstraint( 086 name = IDX_RES_TYPE_FHIR_ID, 087 columnNames = {"RES_TYPE", "FHIR_ID"}) 088 }, 089 indexes = { 090 // Do not reuse previously used index name: IDX_INDEXSTATUS, IDX_RES_TYPE 091 @Index(name = "IDX_RES_DATE", columnList = BaseHasResource.RES_UPDATED), 092 @Index(name = "IDX_RES_FHIR_ID", columnList = "FHIR_ID"), 093 @Index( 094 name = "IDX_RES_TYPE_DEL_UPDATED", 095 columnList = "RES_TYPE,RES_DELETED_AT,RES_UPDATED,PARTITION_ID,RES_ID"), 096 @Index(name = "IDX_RES_RESID_UPDATED", columnList = "RES_ID, RES_UPDATED, PARTITION_ID") 097 }) 098@NamedEntityGraph(name = "Resource.noJoins") 099public class ResourceTable extends BaseHasResource implements Serializable, IBasePersistedResource<JpaPid> { 100 public static final int RESTYPE_LEN = 40; 101 public static final String HFJ_RESOURCE = "HFJ_RESOURCE"; 102 public static final String RES_TYPE = "RES_TYPE"; 103 public static final String FHIR_ID = "FHIR_ID"; 104 private static final int MAX_LANGUAGE_LENGTH = 20; 105 private static final long serialVersionUID = 1L; 106 public static final int MAX_FORCED_ID_LENGTH = 100; 107 public static final String IDX_RES_TYPE_FHIR_ID = "IDX_RES_TYPE_FHIR_ID"; 108 109 /** 110 * Holds the narrative text only - Used for Fulltext searching but not directly stored in the DB 111 * Note the extra config needed in HS6 for indexing transient props: 112 * https://docs.jboss.org/hibernate/search/6.0/migration/html_single/#indexed-transient-requires-configuration 113 * <p> 114 * Note that we depend on `myVersion` updated for this field to be indexed. 115 */ 116 @Transient 117 @FullTextField( 118 name = "myContentText", 119 searchable = Searchable.YES, 120 projectable = Projectable.YES, 121 analyzer = "standardAnalyzer") 122 @FullTextField( 123 name = "myContentTextEdgeNGram", 124 searchable = Searchable.YES, 125 projectable = Projectable.NO, 126 analyzer = "autocompleteEdgeAnalyzer") 127 @FullTextField( 128 name = "myContentTextNGram", 129 searchable = Searchable.YES, 130 projectable = Projectable.NO, 131 analyzer = "autocompleteNGramAnalyzer") 132 @FullTextField( 133 name = "myContentTextPhonetic", 134 searchable = Searchable.YES, 135 projectable = Projectable.NO, 136 analyzer = "autocompletePhoneticAnalyzer") 137 @OptimisticLock(excluded = true) 138 @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "myVersion"))) 139 private String myContentText; 140 141 @Column(name = "HASH_SHA256", length = 64, nullable = true) 142 @OptimisticLock(excluded = true) 143 private String myHashSha256; 144 145 @Column(name = "SP_HAS_LINKS") 146 @OptimisticLock(excluded = true) 147 private boolean myHasLinks; 148 149 @Id 150 @GenericGenerator(name = "SEQ_RESOURCE_ID", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class) 151 @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESOURCE_ID") 152 @Column(name = "RES_ID") 153 @GenericField(projectable = Projectable.YES) 154 private Long myId; 155 156 @Column(name = "SP_INDEX_STATUS", nullable = true) 157 @OptimisticLock(excluded = true) 158 private Long myIndexStatus; 159 160 // TODO: Removed in 5.5.0. Drop in a future release. 161 @Column(name = "RES_LANGUAGE", length = MAX_LANGUAGE_LENGTH, nullable = true) 162 @OptimisticLock(excluded = true) 163 private String myLanguage; 164 165 /** 166 * Holds the narrative text only - Used for Fulltext searching but not directly stored in the DB 167 */ 168 @Transient() 169 @FullTextField( 170 name = "myNarrativeText", 171 searchable = Searchable.YES, 172 projectable = Projectable.YES, 173 analyzer = "standardAnalyzer") 174 @FullTextField( 175 name = "myNarrativeTextEdgeNGram", 176 searchable = Searchable.YES, 177 projectable = Projectable.NO, 178 analyzer = "autocompleteEdgeAnalyzer") 179 @FullTextField( 180 name = "myNarrativeTextNGram", 181 searchable = Searchable.YES, 182 projectable = Projectable.NO, 183 analyzer = "autocompleteNGramAnalyzer") 184 @FullTextField( 185 name = "myNarrativeTextPhonetic", 186 searchable = Searchable.YES, 187 projectable = Projectable.NO, 188 analyzer = "autocompletePhoneticAnalyzer") 189 @OptimisticLock(excluded = true) 190 @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "myVersion"))) 191 private String myNarrativeText; 192 193 @Transient 194 @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "myVersion"))) 195 @PropertyBinding(binder = @PropertyBinderRef(type = SearchParamTextPropertyBinder.class)) 196 private ExtendedHSearchIndexData myLuceneIndexData; 197 198 @OneToMany( 199 mappedBy = "myResource", 200 cascade = {}, 201 fetch = FetchType.LAZY, 202 orphanRemoval = false) 203 @OptimisticLock(excluded = true) 204 private Collection<ResourceIndexedSearchParamCoords> myParamsCoords; 205 206 @Column(name = "SP_COORDS_PRESENT") 207 @OptimisticLock(excluded = true) 208 private boolean myParamsCoordsPopulated; 209 210 @OneToMany( 211 mappedBy = "myResource", 212 cascade = {}, 213 fetch = FetchType.LAZY, 214 orphanRemoval = false) 215 @OptimisticLock(excluded = true) 216 private Collection<ResourceIndexedSearchParamDate> myParamsDate; 217 218 @Column(name = "SP_DATE_PRESENT") 219 @OptimisticLock(excluded = true) 220 private boolean myParamsDatePopulated; 221 222 @OptimisticLock(excluded = true) 223 @OneToMany( 224 mappedBy = "myResource", 225 cascade = {}, 226 fetch = FetchType.LAZY, 227 orphanRemoval = false) 228 private Collection<ResourceIndexedSearchParamNumber> myParamsNumber; 229 230 @Column(name = "SP_NUMBER_PRESENT") 231 @OptimisticLock(excluded = true) 232 private boolean myParamsNumberPopulated; 233 234 @OneToMany( 235 mappedBy = "myResource", 236 cascade = {}, 237 fetch = FetchType.LAZY, 238 orphanRemoval = false) 239 @OptimisticLock(excluded = true) 240 private Collection<ResourceIndexedSearchParamQuantity> myParamsQuantity; 241 242 @Column(name = "SP_QUANTITY_PRESENT") 243 @OptimisticLock(excluded = true) 244 private boolean myParamsQuantityPopulated; 245 246 /** 247 * Added to support UCUM conversion 248 * since 5.3.0 249 */ 250 @OneToMany( 251 mappedBy = "myResource", 252 cascade = {}, 253 fetch = FetchType.LAZY, 254 orphanRemoval = false) 255 @OptimisticLock(excluded = true) 256 private Collection<ResourceIndexedSearchParamQuantityNormalized> myParamsQuantityNormalized; 257 258 /** 259 * Added to support UCUM conversion, 260 * NOTE : use Boolean class instead of boolean primitive, in order to set the existing rows to null 261 * since 5.3.0 262 */ 263 @Column(name = "SP_QUANTITY_NRML_PRESENT") 264 @OptimisticLock(excluded = true) 265 private Boolean myParamsQuantityNormalizedPopulated = Boolean.FALSE; 266 267 @OneToMany( 268 mappedBy = "myResource", 269 cascade = {}, 270 fetch = FetchType.LAZY, 271 orphanRemoval = false) 272 @OptimisticLock(excluded = true) 273 private Collection<ResourceIndexedSearchParamString> myParamsString; 274 275 @Column(name = "SP_STRING_PRESENT") 276 @OptimisticLock(excluded = true) 277 private boolean myParamsStringPopulated; 278 279 @OneToMany( 280 mappedBy = "myResource", 281 cascade = {}, 282 fetch = FetchType.LAZY, 283 orphanRemoval = false) 284 @OptimisticLock(excluded = true) 285 private Collection<ResourceIndexedSearchParamToken> myParamsToken; 286 287 @Column(name = "SP_TOKEN_PRESENT") 288 @OptimisticLock(excluded = true) 289 private boolean myParamsTokenPopulated; 290 291 @OneToMany( 292 mappedBy = "myResource", 293 cascade = {}, 294 fetch = FetchType.LAZY, 295 orphanRemoval = false) 296 @OptimisticLock(excluded = true) 297 private Collection<ResourceIndexedSearchParamUri> myParamsUri; 298 299 @Column(name = "SP_URI_PRESENT") 300 @OptimisticLock(excluded = true) 301 private boolean myParamsUriPopulated; 302 303 // Added in 3.0.0 - Should make this a primitive Boolean at some point 304 @OptimisticLock(excluded = true) 305 @Column(name = "SP_CMPSTR_UNIQ_PRESENT") 306 private Boolean myParamsComboStringUniquePresent = false; 307 308 @OneToMany( 309 mappedBy = "myResource", 310 cascade = {}, 311 fetch = FetchType.LAZY, 312 orphanRemoval = false) 313 @OptimisticLock(excluded = true) 314 private Collection<ResourceIndexedComboStringUnique> myParamsComboStringUnique; 315 316 // Added in 5.5.0 - Should make this a primitive Boolean at some point 317 @OptimisticLock(excluded = true) 318 @Column(name = "SP_CMPTOKS_PRESENT") 319 private Boolean myParamsComboTokensNonUniquePresent = false; 320 321 @OneToMany( 322 mappedBy = "myResource", 323 cascade = {}, 324 fetch = FetchType.LAZY, 325 orphanRemoval = false) 326 @OptimisticLock(excluded = true) 327 private Collection<ResourceIndexedComboTokenNonUnique> myParamsComboTokensNonUnique; 328 329 @OneToMany( 330 mappedBy = "mySourceResource", 331 cascade = {}, 332 fetch = FetchType.LAZY, 333 orphanRemoval = false) 334 @OptimisticLock(excluded = true) 335 private Collection<ResourceLink> myResourceLinks; 336 337 /** 338 * This is a clone of {@link #myResourceLinks} but without the hibernate annotations. 339 * Before we persist we copy the contents of {@link #myResourceLinks} into this field. We 340 * have this separate because that way we can only populate this field if 341 * {@link #myHasLinks} is true, meaning that there are actually resource links present 342 * right now. This avoids Hibernate Search triggering a select on the resource link 343 * table. 344 * <p> 345 * This field is used by FulltextSearchSvcImpl 346 * <p> 347 * You can test that any changes don't cause extra queries by running 348 * FhirResourceDaoR4QueryCountTest 349 */ 350 @FullTextField 351 @Transient 352 @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "myResourceLinks"))) 353 private String myResourceLinksField; 354 355 @OneToMany( 356 mappedBy = "myTargetResource", 357 cascade = {}, 358 fetch = FetchType.LAZY, 359 orphanRemoval = false) 360 @OptimisticLock(excluded = true) 361 private Collection<ResourceLink> myResourceLinksAsTarget; 362 363 @Column(name = RES_TYPE, length = RESTYPE_LEN, nullable = false) 364 @FullTextField 365 @OptimisticLock(excluded = true) 366 private String myResourceType; 367 368 @OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) 369 @OptimisticLock(excluded = true) 370 private Collection<SearchParamPresentEntity> mySearchParamPresents; 371 372 @OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) 373 @OptimisticLock(excluded = true) 374 private Set<ResourceTag> myTags; 375 376 @Transient 377 private transient boolean myUnchangedInCurrentOperation; 378 379 /** 380 * The id of the Resource. 381 * Will contain either the client-assigned id, or the sequence value. 382 * Will be null during insert time until the first read. 383 */ 384 @Column( 385 name = FHIR_ID, 386 // [A-Za-z0-9\-\.]{1,64} - https://www.hl7.org/fhir/datatypes.html#id 387 length = 64, 388 // we never update this after insert, and the Generator will otherwise "dirty" the object. 389 updatable = false) 390 391 // inject the pk for server-assigned sequence ids. 392 @GeneratorType(when = GenerationTime.INSERT, type = FhirIdGenerator.class) 393 // Make sure the generator doesn't bump the history version. 394 @OptimisticLock(excluded = true) 395 private String myFhirId; 396 397 /** 398 * Is there a corresponding row in {@link ResourceSearchUrlEntity} for 399 * this row. 400 * TODO: Added in 6.6.0 - Should make this a primitive boolean at some point 401 */ 402 @OptimisticLock(excluded = true) 403 @Column(name = "SEARCH_URL_PRESENT", nullable = true) 404 private Boolean mySearchUrlPresent = false; 405 406 @Version 407 @Column(name = "RES_VER") 408 private long myVersion; 409 410 @OneToMany(mappedBy = "myResourceTable", fetch = FetchType.LAZY) 411 private Collection<ResourceHistoryProvenanceEntity> myProvenance; 412 413 @Transient 414 private transient ResourceHistoryTable myCurrentVersionEntity; 415 416 @Transient 417 private transient ResourceHistoryTable myNewVersionEntity; 418 419 @Transient 420 private transient boolean myVersionUpdatedInCurrentTransaction; 421 422 @Transient 423 private volatile String myCreatedByMatchUrl; 424 425 @Transient 426 private volatile String myUpdatedByMatchUrl; 427 428 /** 429 * Constructor 430 */ 431 public ResourceTable() { 432 super(); 433 } 434 435 /** 436 * Setting this flag is an indication that we're making changes and the version number will 437 * be incremented in the current transaction. When this is set, calls to {@link #getVersion()} 438 * will be incremented by one. 439 * This flag is cleared in {@link #postPersist()} since at that time the new version number 440 * should be reflected. 441 */ 442 public void markVersionUpdatedInCurrentTransaction() { 443 if (!myVersionUpdatedInCurrentTransaction) { 444 /* 445 * Note that modifying this number doesn't actually directly affect what 446 * gets stored in the database since this is a @Version field and the 447 * value is therefore managed by Hibernate. So in other words, if the 448 * row in the database is updated, it doesn't matter what we set 449 * this field to, hibernate will increment it by one. However, we still 450 * increment it for two reasons: 451 * 1. The value gets used for the version attribute in the ResourceHistoryTable 452 * entity we create for each new version. 453 * 2. For updates to existing resources, there may actually not be any other 454 * changes to this entity so incrementing this is a signal to 455 * Hibernate that something changed and we need to force an entity 456 * update. 457 */ 458 myVersion++; 459 this.myVersionUpdatedInCurrentTransaction = true; 460 } 461 } 462 463 @PostPersist 464 public void postPersist() { 465 myVersionUpdatedInCurrentTransaction = false; 466 } 467 468 @Override 469 public ResourceTag addTag(TagDefinition theTag) { 470 for (ResourceTag next : getTags()) { 471 if (next.getTag().equals(theTag)) { 472 return next; 473 } 474 } 475 ResourceTag tag = new ResourceTag(this, theTag, getPartitionId()); 476 getTags().add(tag); 477 return tag; 478 } 479 480 public String getHashSha256() { 481 return myHashSha256; 482 } 483 484 public void setHashSha256(String theHashSha256) { 485 myHashSha256 = theHashSha256; 486 } 487 488 @Override 489 public Long getId() { 490 return myId; 491 } 492 493 public void setId(Long theId) { 494 myId = theId; 495 } 496 497 public Long getIndexStatus() { 498 return myIndexStatus; 499 } 500 501 public void setIndexStatus(Long theIndexStatus) { 502 myIndexStatus = theIndexStatus; 503 } 504 505 public Collection<ResourceIndexedComboStringUnique> getParamsComboStringUnique() { 506 if (myParamsComboStringUnique == null) { 507 myParamsComboStringUnique = new ArrayList<>(); 508 } 509 return myParamsComboStringUnique; 510 } 511 512 public Collection<ResourceIndexedComboTokenNonUnique> getmyParamsComboTokensNonUnique() { 513 if (myParamsComboTokensNonUnique == null) { 514 myParamsComboTokensNonUnique = new ArrayList<>(); 515 } 516 return myParamsComboTokensNonUnique; 517 } 518 519 public Collection<ResourceIndexedSearchParamCoords> getParamsCoords() { 520 if (myParamsCoords == null) { 521 myParamsCoords = new ArrayList<>(); 522 } 523 return myParamsCoords; 524 } 525 526 public void setParamsCoords(Collection<ResourceIndexedSearchParamCoords> theParamsCoords) { 527 if (!isParamsTokenPopulated() && theParamsCoords.isEmpty()) { 528 return; 529 } 530 getParamsCoords().clear(); 531 getParamsCoords().addAll(theParamsCoords); 532 } 533 534 public Collection<ResourceIndexedSearchParamDate> getParamsDate() { 535 if (myParamsDate == null) { 536 myParamsDate = new ArrayList<>(); 537 } 538 return myParamsDate; 539 } 540 541 public void setParamsDate(Collection<ResourceIndexedSearchParamDate> theParamsDate) { 542 if (!isParamsDatePopulated() && theParamsDate.isEmpty()) { 543 return; 544 } 545 getParamsDate().clear(); 546 getParamsDate().addAll(theParamsDate); 547 } 548 549 public Collection<ResourceIndexedSearchParamNumber> getParamsNumber() { 550 if (myParamsNumber == null) { 551 myParamsNumber = new ArrayList<>(); 552 } 553 return myParamsNumber; 554 } 555 556 public void setParamsNumber(Collection<ResourceIndexedSearchParamNumber> theNumberParams) { 557 if (!isParamsNumberPopulated() && theNumberParams.isEmpty()) { 558 return; 559 } 560 getParamsNumber().clear(); 561 getParamsNumber().addAll(theNumberParams); 562 } 563 564 public Collection<ResourceIndexedSearchParamQuantity> getParamsQuantity() { 565 if (myParamsQuantity == null) { 566 myParamsQuantity = new ArrayList<>(); 567 } 568 return myParamsQuantity; 569 } 570 571 public void setParamsQuantity(Collection<ResourceIndexedSearchParamQuantity> theQuantityParams) { 572 if (!isParamsQuantityPopulated() && theQuantityParams.isEmpty()) { 573 return; 574 } 575 getParamsQuantity().clear(); 576 getParamsQuantity().addAll(theQuantityParams); 577 } 578 579 public Collection<ResourceIndexedSearchParamQuantityNormalized> getParamsQuantityNormalized() { 580 if (myParamsQuantityNormalized == null) { 581 myParamsQuantityNormalized = new ArrayList<>(); 582 } 583 return myParamsQuantityNormalized; 584 } 585 586 public void setParamsQuantityNormalized( 587 Collection<ResourceIndexedSearchParamQuantityNormalized> theQuantityNormalizedParams) { 588 if (!isParamsQuantityNormalizedPopulated() && theQuantityNormalizedParams.isEmpty()) { 589 return; 590 } 591 getParamsQuantityNormalized().clear(); 592 getParamsQuantityNormalized().addAll(theQuantityNormalizedParams); 593 } 594 595 public Collection<ResourceIndexedSearchParamString> getParamsString() { 596 if (myParamsString == null) { 597 myParamsString = new ArrayList<>(); 598 } 599 return myParamsString; 600 } 601 602 public void setParamsString(Collection<ResourceIndexedSearchParamString> theParamsString) { 603 if (!isParamsStringPopulated() && theParamsString.isEmpty()) { 604 return; 605 } 606 getParamsString().clear(); 607 getParamsString().addAll(theParamsString); 608 } 609 610 public Collection<ResourceIndexedSearchParamToken> getParamsToken() { 611 if (myParamsToken == null) { 612 myParamsToken = new ArrayList<>(); 613 } 614 return myParamsToken; 615 } 616 617 public void setParamsToken(Collection<ResourceIndexedSearchParamToken> theParamsToken) { 618 if (!isParamsTokenPopulated() && theParamsToken.isEmpty()) { 619 return; 620 } 621 getParamsToken().clear(); 622 getParamsToken().addAll(theParamsToken); 623 } 624 625 public Collection<ResourceIndexedSearchParamUri> getParamsUri() { 626 if (myParamsUri == null) { 627 myParamsUri = new ArrayList<>(); 628 } 629 return myParamsUri; 630 } 631 632 public void setParamsUri(Collection<ResourceIndexedSearchParamUri> theParamsUri) { 633 if (!isParamsTokenPopulated() && theParamsUri.isEmpty()) { 634 return; 635 } 636 getParamsUri().clear(); 637 getParamsUri().addAll(theParamsUri); 638 } 639 640 @Override 641 public Long getResourceId() { 642 return getId(); 643 } 644 645 public Collection<ResourceLink> getResourceLinks() { 646 if (myResourceLinks == null) { 647 myResourceLinks = new ArrayList<>(); 648 } 649 return myResourceLinks; 650 } 651 652 public void setResourceLinks(Collection<ResourceLink> theLinks) { 653 if (!isHasLinks() && theLinks.isEmpty()) { 654 return; 655 } 656 getResourceLinks().clear(); 657 getResourceLinks().addAll(theLinks); 658 } 659 660 @Override 661 public String getResourceType() { 662 return myResourceType; 663 } 664 665 public ResourceTable setResourceType(String theResourceType) { 666 myResourceType = theResourceType; 667 return this; 668 } 669 670 @Override 671 public Collection<ResourceTag> getTags() { 672 if (myTags == null) { 673 myTags = new HashSet<>(); 674 } 675 return myTags; 676 } 677 678 @Override 679 public long getVersion() { 680 return myVersion; 681 } 682 683 /** 684 * Sets the version on this entity to {@literal 1}. This should only be called 685 * on resources that are not yet persisted. After that time the version number 686 * is managed by hibernate. 687 */ 688 public void initializeVersion() { 689 assert myId == null; 690 myVersion = 1; 691 } 692 693 /** 694 * Don't call this in any JPA environments, the version will be ignored 695 * since this field is managed by hibernate 696 */ 697 @VisibleForTesting 698 public void setVersionForUnitTest(long theVersion) { 699 myVersion = theVersion; 700 } 701 702 @Override 703 public boolean isDeleted() { 704 return getDeleted() != null; 705 } 706 707 @Override 708 public void setNotDeleted() { 709 setDeleted(null); 710 } 711 712 public boolean isHasLinks() { 713 return myHasLinks; 714 } 715 716 public void setHasLinks(boolean theHasLinks) { 717 myHasLinks = theHasLinks; 718 } 719 720 /** 721 * Clears all the index population flags, e.g. {@link #isParamsStringPopulated()} 722 * 723 * @since 6.8.0 724 */ 725 public void clearAllParamsPopulated() { 726 myParamsTokenPopulated = false; 727 myParamsCoordsPopulated = false; 728 myParamsDatePopulated = false; 729 myParamsNumberPopulated = false; 730 myParamsStringPopulated = false; 731 myParamsQuantityPopulated = false; 732 myParamsQuantityNormalizedPopulated = false; 733 myParamsUriPopulated = false; 734 myHasLinks = false; 735 } 736 737 public boolean isParamsComboStringUniquePresent() { 738 if (myParamsComboStringUniquePresent == null) { 739 return false; 740 } 741 return myParamsComboStringUniquePresent; 742 } 743 744 public void setParamsComboStringUniquePresent(boolean theParamsComboStringUniquePresent) { 745 myParamsComboStringUniquePresent = theParamsComboStringUniquePresent; 746 } 747 748 public boolean isParamsComboTokensNonUniquePresent() { 749 if (myParamsComboTokensNonUniquePresent == null) { 750 return false; 751 } 752 return myParamsComboTokensNonUniquePresent; 753 } 754 755 public void setParamsComboTokensNonUniquePresent(boolean theParamsComboTokensNonUniquePresent) { 756 myParamsComboTokensNonUniquePresent = theParamsComboTokensNonUniquePresent; 757 } 758 759 public boolean isParamsCoordsPopulated() { 760 return myParamsCoordsPopulated; 761 } 762 763 public void setParamsCoordsPopulated(boolean theParamsCoordsPopulated) { 764 myParamsCoordsPopulated = theParamsCoordsPopulated; 765 } 766 767 public boolean isParamsDatePopulated() { 768 return myParamsDatePopulated; 769 } 770 771 public void setParamsDatePopulated(boolean theParamsDatePopulated) { 772 myParamsDatePopulated = theParamsDatePopulated; 773 } 774 775 public boolean isParamsNumberPopulated() { 776 return myParamsNumberPopulated; 777 } 778 779 public void setParamsNumberPopulated(boolean theParamsNumberPopulated) { 780 myParamsNumberPopulated = theParamsNumberPopulated; 781 } 782 783 public boolean isParamsQuantityPopulated() { 784 return myParamsQuantityPopulated; 785 } 786 787 public void setParamsQuantityPopulated(boolean theParamsQuantityPopulated) { 788 myParamsQuantityPopulated = theParamsQuantityPopulated; 789 } 790 791 public Boolean isParamsQuantityNormalizedPopulated() { 792 if (myParamsQuantityNormalizedPopulated == null) return Boolean.FALSE; 793 else return myParamsQuantityNormalizedPopulated; 794 } 795 796 public void setParamsQuantityNormalizedPopulated(Boolean theParamsQuantityNormalizedPopulated) { 797 if (theParamsQuantityNormalizedPopulated == null) myParamsQuantityNormalizedPopulated = Boolean.FALSE; 798 else myParamsQuantityNormalizedPopulated = theParamsQuantityNormalizedPopulated; 799 } 800 801 public boolean isParamsStringPopulated() { 802 return myParamsStringPopulated; 803 } 804 805 public void setParamsStringPopulated(boolean theParamsStringPopulated) { 806 myParamsStringPopulated = theParamsStringPopulated; 807 } 808 809 public boolean isParamsTokenPopulated() { 810 return myParamsTokenPopulated; 811 } 812 813 public void setParamsTokenPopulated(boolean theParamsTokenPopulated) { 814 myParamsTokenPopulated = theParamsTokenPopulated; 815 } 816 817 public boolean isParamsUriPopulated() { 818 return myParamsUriPopulated; 819 } 820 821 public void setParamsUriPopulated(boolean theParamsUriPopulated) { 822 myParamsUriPopulated = theParamsUriPopulated; 823 } 824 825 /** 826 * Transient (not saved in DB) flag indicating that this resource was found to be unchanged by the current operation 827 * and was not re-saved in the database 828 */ 829 public boolean isUnchangedInCurrentOperation() { 830 return myUnchangedInCurrentOperation; 831 } 832 833 /** 834 * Transient (not saved in DB) flag indicating that this resource was found to be unchanged by the current operation 835 * and was not re-saved in the database 836 */ 837 public void setUnchangedInCurrentOperation(boolean theUnchangedInCurrentOperation) { 838 839 myUnchangedInCurrentOperation = theUnchangedInCurrentOperation; 840 } 841 842 public String getContentText() { 843 return myContentText; 844 } 845 846 public void setContentText(String theContentText) { 847 myContentText = theContentText; 848 } 849 850 public void setNarrativeText(String theNarrativeText) { 851 myNarrativeText = theNarrativeText; 852 } 853 854 public boolean isSearchUrlPresent() { 855 return Boolean.TRUE.equals(mySearchUrlPresent); 856 } 857 858 public void setSearchUrlPresent(boolean theSearchUrlPresent) { 859 mySearchUrlPresent = theSearchUrlPresent; 860 } 861 862 /** 863 * This method creates a new history entity, or might reuse the current one if we've 864 * already created one in the current transaction. This is because we can only increment 865 * the version once in a DB transaction (since hibernate manages that number) so creating 866 * multiple {@link ResourceHistoryTable} entities will result in a constraint error. 867 */ 868 public ResourceHistoryTable toHistory(boolean theCreateVersionTags) { 869 boolean createVersionTags = theCreateVersionTags; 870 871 ResourceHistoryTable retVal = myNewVersionEntity; 872 if (retVal == null) { 873 retVal = new ResourceHistoryTable(); 874 myNewVersionEntity = retVal; 875 } else { 876 // Tags should already be set 877 createVersionTags = false; 878 } 879 880 retVal.setResourceId(myId); 881 retVal.setResourceType(myResourceType); 882 retVal.setTransientForcedId(getFhirId()); 883 retVal.setFhirVersion(getFhirVersion()); 884 retVal.setResourceTable(this); 885 retVal.setPartitionId(getPartitionId()); 886 887 retVal.setHasTags(isHasTags()); 888 if (isHasTags() && createVersionTags) { 889 for (ResourceTag next : getTags()) { 890 retVal.addTag(next); 891 } 892 } 893 894 populateHistoryEntityVersionAndDates(retVal); 895 896 return retVal; 897 } 898 899 /** 900 * Updates several temporal values in a {@link ResourceHistoryTable} entity which 901 * are pulled from this entity, including the resource version, and the 902 * creation, update, and deletion dates. 903 */ 904 public void populateHistoryEntityVersionAndDates(ResourceHistoryTable theResourceHistoryTable) { 905 theResourceHistoryTable.setVersion(getVersion()); 906 theResourceHistoryTable.setPublished(getPublishedDate()); 907 theResourceHistoryTable.setUpdated(getUpdatedDate()); 908 theResourceHistoryTable.setDeleted(getDeleted()); 909 } 910 911 @Override 912 public String toString() { 913 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 914 b.append("pid", myId); 915 b.append("fhirId", myFhirId); 916 b.append("resourceType", myResourceType); 917 b.append("version", myVersion); 918 if (getPartitionId() != null) { 919 b.append("partitionId", getPartitionId().getPartitionId()); 920 } 921 b.append("lastUpdated", getUpdated().getValueAsString()); 922 if (getDeleted() != null) { 923 b.append("deleted", new InstantType(getDeleted()).getValueAsString()); 924 } 925 return b.build(); 926 } 927 928 @PrePersist 929 @PreUpdate 930 public void preSave() { 931 if (myHasLinks && myResourceLinks != null) { 932 myResourceLinksField = getResourceLinks().stream() 933 .map(ResourceLink::getTargetResourcePid) 934 .filter(Objects::nonNull) 935 .map(Object::toString) 936 .collect(Collectors.joining(" ")); 937 } else { 938 myResourceLinksField = null; 939 } 940 } 941 942 /** 943 * This is a convenience to avoid loading the version a second time within a single transaction. It is 944 * not persisted. 945 */ 946 public ResourceHistoryTable getCurrentVersionEntity() { 947 return myCurrentVersionEntity; 948 } 949 950 /** 951 * This is a convenience to avoid loading the version a second time within a single transaction. It is 952 * not persisted. 953 */ 954 public void setCurrentVersionEntity(ResourceHistoryTable theCurrentVersionEntity) { 955 myCurrentVersionEntity = theCurrentVersionEntity; 956 } 957 958 @Override 959 public JpaPid getPersistentId() { 960 return JpaPid.fromId(getId()); 961 } 962 963 @Override 964 public IdDt getIdDt() { 965 IdDt retVal = new IdDt(); 966 populateId(retVal); 967 return retVal; 968 } 969 970 public IIdType getIdType(FhirContext theContext) { 971 IIdType retVal = theContext.getVersion().newIdType(); 972 populateId(retVal); 973 return retVal; 974 } 975 976 private void populateId(IIdType retVal) { 977 String resourceId; 978 if (myFhirId != null && !myFhirId.isEmpty()) { 979 resourceId = myFhirId; 980 } else { 981 Long id = this.getResourceId(); 982 resourceId = Long.toString(id); 983 } 984 retVal.setValue(getResourceType() + '/' + resourceId + '/' + Constants.PARAM_HISTORY + '/' + getVersion()); 985 } 986 987 public String getCreatedByMatchUrl() { 988 return myCreatedByMatchUrl; 989 } 990 991 public void setCreatedByMatchUrl(String theCreatedByMatchUrl) { 992 myCreatedByMatchUrl = theCreatedByMatchUrl; 993 } 994 995 public String getUpdatedByMatchUrl() { 996 return myUpdatedByMatchUrl; 997 } 998 999 public void setUpdatedByMatchUrl(String theUpdatedByMatchUrl) { 1000 myUpdatedByMatchUrl = theUpdatedByMatchUrl; 1001 } 1002 1003 public boolean isVersionUpdatedInCurrentTransaction() { 1004 return myVersionUpdatedInCurrentTransaction; 1005 } 1006 1007 public void setLuceneIndexData(ExtendedHSearchIndexData theLuceneIndexData) { 1008 myLuceneIndexData = theLuceneIndexData; 1009 } 1010 1011 public Collection<SearchParamPresentEntity> getSearchParamPresents() { 1012 if (mySearchParamPresents == null) { 1013 mySearchParamPresents = new ArrayList<>(); 1014 } 1015 return mySearchParamPresents; 1016 } 1017 1018 /** 1019 * Get the FHIR resource id. 1020 * 1021 * @return the resource id, or null if the resource doesn't have a client-assigned id, 1022 * and hasn't been saved to the db to get a server-assigned id yet. 1023 */ 1024 public String getFhirId() { 1025 return myFhirId; 1026 } 1027 1028 public void setFhirId(String theFhirId) { 1029 myFhirId = theFhirId; 1030 } 1031 1032 public String asTypedFhirResourceId() { 1033 return getResourceType() + "/" + getFhirId(); 1034 } 1035 1036 /** 1037 * Populate myFhirId with server-assigned sequence id when no client-id provided. 1038 * We eat this complexity during insert to simplify query time with a uniform column. 1039 * Server-assigned sequence ids aren't available until just before insertion. 1040 * Hibernate calls insert Generators after the pk has been assigned, so we can use myId safely here. 1041 */ 1042 public static final class FhirIdGenerator implements ValueGenerator<String> { 1043 @Override 1044 public String generateValue(Session session, Object owner) { 1045 ResourceTable that = (ResourceTable) owner; 1046 return that.myFhirId != null ? that.myFhirId : that.myId.toString(); 1047 } 1048 } 1049}