
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.jpa.model.dao.JpaPid; 023import ca.uhn.fhir.jpa.model.util.ResourceLinkUtils; 024import jakarta.persistence.Column; 025import jakarta.persistence.ConstraintMode; 026import jakarta.persistence.Entity; 027import jakarta.persistence.FetchType; 028import jakarta.persistence.ForeignKey; 029import jakarta.persistence.GeneratedValue; 030import jakarta.persistence.GenerationType; 031import jakarta.persistence.Id; 032import jakarta.persistence.IdClass; 033import jakarta.persistence.Index; 034import jakarta.persistence.JoinColumn; 035import jakarta.persistence.JoinColumns; 036import jakarta.persistence.ManyToOne; 037import jakarta.persistence.PostLoad; 038import jakarta.persistence.Table; 039import jakarta.persistence.Temporal; 040import jakarta.persistence.TemporalType; 041import jakarta.persistence.Transient; 042import org.apache.commons.lang3.Validate; 043import org.apache.commons.lang3.builder.EqualsBuilder; 044import org.apache.commons.lang3.builder.HashCodeBuilder; 045import org.hibernate.annotations.GenericGenerator; 046import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; 047import org.hl7.fhir.instance.model.api.IIdType; 048 049import java.time.LocalDate; 050import java.util.Date; 051 052@Entity 053@Table( 054 name = "HFJ_RES_LINK", 055 indexes = { 056 // We need to join both ways, so index from src->tgt and from tgt->src. 057 // From src->tgt, rows are usually written all together as part of ingestion - keep the index small, and 058 // read blocks as needed. 059 @Index(name = "IDX_RL_SRC", columnList = "SRC_RESOURCE_ID"), 060 // But from tgt->src, include all the match columns. Targets will usually be randomly distributed - each row 061 // in separate block. 062 @Index( 063 name = "IDX_RL_TGT_v2", 064 columnList = "TARGET_RESOURCE_ID, SRC_PATH, SRC_RESOURCE_ID, TARGET_RESOURCE_TYPE,PARTITION_ID"), 065 // See https://github.com/hapifhir/hapi-fhir/issues/7223 066 @Index( 067 name = "IDX_RL_SRCPATH_TGTURL", 068 columnList = "SRC_PATH, TARGET_RESOURCE_URL, PARTITION_ID, SRC_RESOURCE_ID") 069 }) 070@IdClass(IdAndPartitionId.class) 071public class ResourceLink extends BaseResourceIndex { 072 073 public static final int SRC_PATH_LENGTH = 500; 074 private static final long serialVersionUID = 1L; 075 public static final String TARGET_RES_PARTITION_ID = "TARGET_RES_PARTITION_ID"; 076 public static final String TARGET_RESOURCE_ID = "TARGET_RESOURCE_ID"; 077 public static final String FK_RESLINK_TARGET = "FK_RESLINK_TARGET"; 078 079 @GenericGenerator(name = "SEQ_RESLINK_ID", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class) 080 @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESLINK_ID") 081 @Id 082 @Column(name = "PID") 083 private Long myId; 084 085 @Column(name = "SRC_PATH", length = SRC_PATH_LENGTH, nullable = false) 086 private String mySourcePath; 087 088 @ManyToOne( 089 optional = false, 090 fetch = FetchType.LAZY, 091 cascade = {}) 092 @JoinColumns( 093 value = { 094 @JoinColumn( 095 name = "SRC_RESOURCE_ID", 096 referencedColumnName = "RES_ID", 097 insertable = false, 098 updatable = false, 099 nullable = false), 100 @JoinColumn( 101 name = "PARTITION_ID", 102 referencedColumnName = "PARTITION_ID", 103 insertable = false, 104 updatable = false, 105 nullable = false) 106 }, 107 foreignKey = @ForeignKey(name = "FK_RESLINK_SOURCE")) 108 private ResourceTable mySourceResource; 109 110 @Column(name = "SRC_RESOURCE_ID", nullable = false) 111 private Long mySourceResourcePid; 112 113 @Column(name = "SOURCE_RESOURCE_TYPE", updatable = false, nullable = false, length = ResourceTable.RESTYPE_LEN) 114 @FullTextField 115 private String mySourceResourceType; 116 117 @ManyToOne(optional = true, fetch = FetchType.EAGER) 118 @JoinColumns( 119 value = { 120 @JoinColumn( 121 name = TARGET_RESOURCE_ID, 122 referencedColumnName = "RES_ID", 123 nullable = true, 124 insertable = false, 125 updatable = false), 126 @JoinColumn( 127 name = TARGET_RES_PARTITION_ID, 128 referencedColumnName = "PARTITION_ID", 129 nullable = true, 130 insertable = false, 131 updatable = false), 132 }, 133 /* 134 * TODO: We need to drop this constraint because it affects performance in pretty 135 * terrible ways on a lot of platforms. But a Hibernate bug present in Hibernate 6.6.4 136 * makes it impossible. 137 * See: https://hibernate.atlassian.net/browse/HHH-19046 138 */ 139 foreignKey = @ForeignKey(name = FK_RESLINK_TARGET)) 140 private ResourceTable myTargetResource; 141 142 @Transient 143 private ResourceTable myTransientTargetResource; 144 145 @Column(name = TARGET_RESOURCE_ID, insertable = true, updatable = true, nullable = true) 146 @FullTextField 147 private Long myTargetResourcePid; 148 149 @Column(name = "TARGET_RESOURCE_TYPE", nullable = false, length = ResourceTable.RESTYPE_LEN) 150 @FullTextField 151 private String myTargetResourceType; 152 153 @Column(name = "TARGET_RESOURCE_URL", length = 200, nullable = true) 154 @FullTextField 155 private String myTargetResourceUrl; 156 157 @Column(name = "TARGET_RESOURCE_VERSION", nullable = true) 158 private Long myTargetResourceVersion; 159 160 @FullTextField 161 @Column(name = "SP_UPDATED", nullable = true) // TODO: make this false after HAPI 2.3 162 @Temporal(TemporalType.TIMESTAMP) 163 private Date myUpdated; 164 165 @Transient 166 private transient String myTargetResourceId; 167 168 @Column(name = TARGET_RES_PARTITION_ID, nullable = true) 169 private Integer myTargetResourcePartitionId; 170 171 @Column(name = "TARGET_RES_PARTITION_DATE", nullable = true) 172 private LocalDate myTargetResourcePartitionDate; 173 174 @ManyToOne(fetch = FetchType.LAZY) 175 @JoinColumn( 176 name = "SRC_RES_TYPE_ID", 177 referencedColumnName = "RES_TYPE_ID", 178 foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), 179 insertable = false, 180 updatable = false, 181 nullable = true) 182 private ResourceTypeEntity mySourceResTypeEntity; 183 184 @Column(name = "SRC_RES_TYPE_ID", nullable = true) 185 private Short mySourceResourceTypeId; 186 187 @ManyToOne(fetch = FetchType.LAZY) 188 @JoinColumn( 189 name = "TARGET_RES_TYPE_ID", 190 referencedColumnName = "RES_TYPE_ID", 191 foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), 192 insertable = false, 193 updatable = false, 194 nullable = true) 195 private ResourceTypeEntity myTargetResTypeEntity; 196 197 @Column(name = "TARGET_RES_TYPE_ID", nullable = true) 198 private Short myTargetResourceTypeId; 199 /** 200 * Constructor 201 */ 202 public ResourceLink() { 203 super(); 204 } 205 206 public Long getTargetResourceVersion() { 207 return myTargetResourceVersion; 208 } 209 210 public void setTargetResourceVersion(Long theTargetResourceVersion) { 211 myTargetResourceVersion = theTargetResourceVersion; 212 } 213 214 public String getTargetResourceId() { 215 if (myTargetResourceId == null && getTargetResource() != null) { 216 myTargetResourceId = getTargetResource().getIdDt().getIdPart(); 217 } 218 return myTargetResourceId; 219 } 220 221 public String getSourceResourceType() { 222 return mySourceResourceType; 223 } 224 225 public String getTargetResourceType() { 226 return myTargetResourceType; 227 } 228 229 @Override 230 public boolean equals(Object theObj) { 231 if (this == theObj) { 232 return true; 233 } 234 if (theObj == null) { 235 return false; 236 } 237 if (!(theObj instanceof ResourceLink)) { 238 return false; 239 } 240 ResourceLink obj = (ResourceLink) theObj; 241 EqualsBuilder b = new EqualsBuilder(); 242 b.append(mySourcePath, obj.mySourcePath); 243 b.append(mySourceResource, obj.mySourceResource); 244 b.append(myTargetResourceUrl, obj.myTargetResourceUrl); 245 b.append(myTargetResourceType, obj.myTargetResourceType); 246 b.append(myTargetResourceVersion, obj.myTargetResourceVersion); 247 // In cases where we are extracting links from a resource that has not yet been persisted, the target resource 248 // pid 249 // will be null so we use the target resource id to differentiate instead 250 if (getTargetResourcePid() == null) { 251 b.append(getTargetResourceId(), obj.getTargetResourceId()); 252 } else { 253 b.append(getTargetResourcePid(), obj.getTargetResourcePid()); 254 } 255 return b.isEquals(); 256 } 257 258 /** 259 * ResourceLink.myTargetResource field is immutable.Transient ResourceLink.myTransientTargetResource property 260 * is used instead, allowing it to be updated via the ResourceLink#copyMutableValuesFrom method 261 * when ResourceLink table row is reused. 262 */ 263 @PostLoad 264 public void postLoad() { 265 myTransientTargetResource = myTargetResource; 266 } 267 268 @Override 269 public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) { 270 ResourceLink source = (ResourceLink) theSource; 271 mySourcePath = source.getSourcePath(); 272 myTransientTargetResource = source.getTargetResource(); 273 myTargetResourceId = source.getTargetResourceId(); 274 myTargetResourcePid = source.getTargetResourcePid(); 275 myTargetResourceType = source.getTargetResourceType(); 276 myTargetResourceTypeId = source.getTargetResourceTypeId(); 277 myTargetResourceVersion = source.getTargetResourceVersion(); 278 myTargetResourceUrl = source.getTargetResourceUrl(); 279 myTargetResourcePartitionId = source.getTargetResourcePartitionId(); 280 myTargetResourcePartitionDate = source.getTargetResourcePartitionDate(); 281 } 282 283 @Override 284 public void setResourceId(Long theResourceId) { 285 mySourceResourcePid = theResourceId; 286 } 287 288 public String getSourcePath() { 289 return mySourcePath; 290 } 291 292 public void setSourcePath(String theSourcePath) { 293 mySourcePath = theSourcePath; 294 } 295 296 public JpaPid getSourceResourcePk() { 297 return JpaPid.fromId(mySourceResourcePid, myPartitionIdValue); 298 } 299 300 public ResourceTable getSourceResource() { 301 return mySourceResource; 302 } 303 304 public void setSourceResource(ResourceTable theSourceResource) { 305 mySourceResource = theSourceResource; 306 mySourceResourcePid = theSourceResource.getId().getId(); 307 mySourceResourceType = theSourceResource.getResourceType(); 308 mySourceResourceTypeId = theSourceResource.getResourceTypeId(); 309 setPartitionId(theSourceResource.getPartitionId()); 310 } 311 312 public void setTargetResource(String theResourceType, Long theResourcePid, String theTargetResourceId) { 313 Validate.notBlank(theResourceType); 314 315 myTargetResourceType = theResourceType; 316 myTargetResourcePid = theResourcePid; 317 myTargetResourceId = theTargetResourceId; 318 } 319 320 public String getTargetResourceUrl() { 321 return myTargetResourceUrl; 322 } 323 324 public void setTargetResourceUrl(IIdType theTargetResourceUrl) { 325 Validate.isTrue(theTargetResourceUrl.hasBaseUrl()); 326 Validate.isTrue(theTargetResourceUrl.hasResourceType()); 327 328 // if (theTargetResourceUrl.hasIdPart()) { 329 // do nothing 330 // } else { 331 // Must have set an url like http://example.org/something 332 // We treat 'something' as the resource type because of fix for #659. Prior to #659 fix, 'something' was 333 // treated as the id and 'example.org' was treated as the resource type 334 // Maybe log a warning? 335 // } 336 337 myTargetResourceType = theTargetResourceUrl.getResourceType(); 338 myTargetResourceUrl = theTargetResourceUrl.getValue(); 339 } 340 341 public Long getTargetResourcePid() { 342 return myTargetResourcePid; 343 } 344 345 public void setTargetResourceUrlCanonical(String theTargetResourceUrl) { 346 Validate.notBlank(theTargetResourceUrl); 347 348 myTargetResourceType = ResourceLinkUtils.UNKNOWN; 349 myTargetResourceUrl = theTargetResourceUrl; 350 } 351 352 public Date getUpdated() { 353 return myUpdated; 354 } 355 356 public void setUpdated(Date theUpdated) { 357 myUpdated = theUpdated; 358 } 359 360 @Override 361 public Long getId() { 362 return myId; 363 } 364 365 @Override 366 public void setId(Long theId) { 367 myId = theId; 368 } 369 370 public LocalDate getTargetResourcePartitionDate() { 371 return myTargetResourcePartitionDate; 372 } 373 374 public Integer getTargetResourcePartitionId() { 375 return myTargetResourcePartitionId; 376 } 377 378 public ResourceLink setTargetResourcePartitionId(PartitionablePartitionId theTargetResourcePartitionId) { 379 myTargetResourcePartitionId = null; 380 myTargetResourcePartitionDate = null; 381 if (theTargetResourcePartitionId != null) { 382 myTargetResourcePartitionId = theTargetResourcePartitionId.getPartitionId(); 383 myTargetResourcePartitionDate = theTargetResourcePartitionId.getPartitionDate(); 384 } 385 return this; 386 } 387 388 public Short getSourceResourceTypeId() { 389 return mySourceResourceTypeId; 390 } 391 392 public ResourceTypeEntity getSourceResTypeEntity() { 393 return mySourceResTypeEntity; 394 } 395 396 public Short getTargetResourceTypeId() { 397 return myTargetResourceTypeId; 398 } 399 400 public void setTargetResourceTypeId(Short theTargetResourceTypeId) { 401 myTargetResourceTypeId = theTargetResourceTypeId; 402 } 403 404 public ResourceTypeEntity getTargetResTypeEntity() { 405 return myTargetResTypeEntity; 406 } 407 408 @Override 409 public void clearHashes() { 410 // nothing right now 411 } 412 413 @Override 414 public void calculateHashes() { 415 // nothing right now 416 } 417 418 @Override 419 public int hashCode() { 420 HashCodeBuilder b = new HashCodeBuilder(); 421 b.append(mySourcePath); 422 b.append(mySourceResource); 423 b.append(myTargetResourceUrl); 424 b.append(myTargetResourceVersion); 425 426 // In cases where we are extracting links from a resource that has not yet been persisted, the target resource 427 // pid 428 // will be null so we use the target resource id to differentiate instead 429 if (getTargetResourcePid() == null) { 430 b.append(getTargetResourceId()); 431 } else { 432 b.append(getTargetResourcePid()); 433 } 434 return b.toHashCode(); 435 } 436 437 @Override 438 public String toString() { 439 StringBuilder b = new StringBuilder(); 440 b.append("ResourceLink["); 441 b.append("path=").append(mySourcePath); 442 b.append(", srcResId=").append(mySourceResourcePid); 443 b.append(", srcPartition=").append(myPartitionIdValue); 444 b.append(", srcResTypeId=").append(getSourceResourceTypeId()); 445 b.append(", targetResId=").append(myTargetResourcePid); 446 b.append(", targetPartition=").append(myTargetResourcePartitionId); 447 b.append(", targetResType=").append(myTargetResourceType); 448 b.append(", targetResTypeId=").append(getTargetResourceTypeId()); 449 b.append(", targetResVersion=").append(myTargetResourceVersion); 450 b.append(", targetResUrl=").append(myTargetResourceUrl); 451 452 b.append("]"); 453 return b.toString(); 454 } 455 456 public ResourceTable getTargetResource() { 457 return myTransientTargetResource; 458 } 459 460 /** 461 * Creates a clone of this resourcelink which doesn't contain the internal PID 462 * of the target resource. 463 */ 464 public ResourceLink cloneWithoutTargetPid() { 465 ResourceLink retVal = new ResourceLink(); 466 retVal.mySourceResource = mySourceResource; 467 retVal.mySourceResourcePid = mySourceResource.getId().getId(); 468 retVal.mySourceResourceType = mySourceResource.getResourceType(); 469 retVal.mySourceResourceTypeId = mySourceResource.getResourceTypeId(); 470 retVal.myPartitionIdValue = mySourceResource.getPartitionId().getPartitionId(); 471 retVal.mySourcePath = mySourcePath; 472 retVal.myUpdated = myUpdated; 473 retVal.myTargetResourceType = myTargetResourceType; 474 retVal.myTargetResourceTypeId = myTargetResourceTypeId; 475 if (myTargetResourceId != null) { 476 retVal.myTargetResourceId = myTargetResourceId; 477 } else if (getTargetResource() != null) { 478 retVal.myTargetResourceId = getTargetResource().getIdDt().getIdPart(); 479 retVal.myTargetResourceTypeId = getTargetResource().getResourceTypeId(); 480 } 481 retVal.myTargetResourceUrl = myTargetResourceUrl; 482 retVal.myTargetResourceVersion = myTargetResourceVersion; 483 return retVal; 484 } 485 486 public static ResourceLink forAbsoluteReference( 487 String theSourcePath, ResourceTable theSourceResource, IIdType theTargetResourceUrl, Date theUpdated) { 488 ResourceLink retVal = new ResourceLink(); 489 retVal.setSourcePath(theSourcePath); 490 retVal.setSourceResource(theSourceResource); 491 retVal.setTargetResourceUrl(theTargetResourceUrl); 492 retVal.setUpdated(theUpdated); 493 return retVal; 494 } 495 496 /** 497 * Factory for canonical URL 498 */ 499 public static ResourceLink forLogicalReference( 500 String theSourcePath, ResourceTable theSourceResource, String theTargetResourceUrl, Date theUpdated) { 501 ResourceLink retVal = new ResourceLink(); 502 retVal.setSourcePath(theSourcePath); 503 retVal.setSourceResource(theSourceResource); 504 retVal.setTargetResourceUrlCanonical(theTargetResourceUrl); 505 retVal.setUpdated(theUpdated); 506 return retVal; 507 } 508 509 public static ResourceLink forLocalReference( 510 ResourceLinkForLocalReferenceParams theResourceLinkForLocalReferenceParams) { 511 512 ResourceLink retVal = new ResourceLink(); 513 retVal.setSourcePath(theResourceLinkForLocalReferenceParams.getSourcePath()); 514 retVal.setSourceResource(theResourceLinkForLocalReferenceParams.getSourceResource()); 515 retVal.setTargetResource( 516 theResourceLinkForLocalReferenceParams.getTargetResourceType(), 517 theResourceLinkForLocalReferenceParams.getTargetResourcePid(), 518 theResourceLinkForLocalReferenceParams.getTargetResourceId()); 519 520 retVal.setTargetResourcePartitionId( 521 theResourceLinkForLocalReferenceParams.getTargetResourcePartitionablePartitionId()); 522 retVal.setTargetResourceVersion(theResourceLinkForLocalReferenceParams.getTargetResourceVersion()); 523 retVal.setUpdated(theResourceLinkForLocalReferenceParams.getUpdated()); 524 525 return retVal; 526 } 527 528 public static class ResourceLinkForLocalReferenceParams { 529 private String mySourcePath; 530 private ResourceTable mySourceResource; 531 private String myTargetResourceType; 532 private Long myTargetResourcePid; 533 private String myTargetResourceId; 534 private Date myUpdated; 535 private Long myTargetResourceVersion; 536 private PartitionablePartitionId myTargetResourcePartitionablePartitionId; 537 538 public static ResourceLinkForLocalReferenceParams instance() { 539 return new ResourceLinkForLocalReferenceParams(); 540 } 541 542 public String getSourcePath() { 543 return mySourcePath; 544 } 545 546 public ResourceLinkForLocalReferenceParams setSourcePath(String theSourcePath) { 547 mySourcePath = theSourcePath; 548 return this; 549 } 550 551 public ResourceTable getSourceResource() { 552 return mySourceResource; 553 } 554 555 public ResourceLinkForLocalReferenceParams setSourceResource(ResourceTable theSourceResource) { 556 mySourceResource = theSourceResource; 557 return this; 558 } 559 560 public String getTargetResourceType() { 561 return myTargetResourceType; 562 } 563 564 public ResourceLinkForLocalReferenceParams setTargetResourceType(String theTargetResourceType) { 565 myTargetResourceType = theTargetResourceType; 566 return this; 567 } 568 569 public Long getTargetResourcePid() { 570 return myTargetResourcePid; 571 } 572 573 public ResourceLinkForLocalReferenceParams setTargetResourcePid(Long theTargetResourcePid) { 574 myTargetResourcePid = theTargetResourcePid; 575 return this; 576 } 577 578 public String getTargetResourceId() { 579 return myTargetResourceId; 580 } 581 582 public ResourceLinkForLocalReferenceParams setTargetResourceId(String theTargetResourceId) { 583 myTargetResourceId = theTargetResourceId; 584 return this; 585 } 586 587 public Date getUpdated() { 588 return myUpdated; 589 } 590 591 public ResourceLinkForLocalReferenceParams setUpdated(Date theUpdated) { 592 myUpdated = theUpdated; 593 return this; 594 } 595 596 public Long getTargetResourceVersion() { 597 return myTargetResourceVersion; 598 } 599 600 /** 601 * @param theTargetResourceVersion This should only be populated if the reference actually had a version 602 */ 603 public ResourceLinkForLocalReferenceParams setTargetResourceVersion(Long theTargetResourceVersion) { 604 myTargetResourceVersion = theTargetResourceVersion; 605 return this; 606 } 607 608 public PartitionablePartitionId getTargetResourcePartitionablePartitionId() { 609 return myTargetResourcePartitionablePartitionId; 610 } 611 612 public ResourceLinkForLocalReferenceParams setTargetResourcePartitionablePartitionId( 613 PartitionablePartitionId theTargetResourcePartitionablePartitionId) { 614 myTargetResourcePartitionablePartitionId = theTargetResourcePartitionablePartitionId; 615 return this; 616 } 617 } 618}