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