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