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