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