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