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