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