001/* 002 * #%L 003 * HAPI FHIR JPA Model 004 * %% 005 * Copyright (C) 2014 - 2024 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 jakarta.annotation.Nullable; 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.Index; 031import jakarta.persistence.JoinColumn; 032import jakarta.persistence.ManyToOne; 033import jakarta.persistence.SequenceGenerator; 034import jakarta.persistence.Table; 035import jakarta.persistence.Temporal; 036import jakarta.persistence.TemporalType; 037import jakarta.persistence.Transient; 038import org.apache.commons.lang3.Validate; 039import org.apache.commons.lang3.builder.EqualsBuilder; 040import org.apache.commons.lang3.builder.HashCodeBuilder; 041import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; 042import org.hl7.fhir.instance.model.api.IIdType; 043 044import java.util.Date; 045 046@Entity 047@Table( 048 name = "HFJ_RES_LINK", 049 indexes = { 050 // We need to join both ways, so index from src->tgt and from tgt->src. 051 // From src->tgt, rows are usually written all together as part of ingestion - keep the index small, and 052 // read blocks as needed. 053 @Index(name = "IDX_RL_SRC", columnList = "SRC_RESOURCE_ID"), 054 // But from tgt->src, include all the match columns. Targets will usually be randomly distributed - each row 055 // in separate block. 056 @Index( 057 name = "IDX_RL_TGT_v2", 058 columnList = "TARGET_RESOURCE_ID, SRC_PATH, SRC_RESOURCE_ID, TARGET_RESOURCE_TYPE,PARTITION_ID") 059 }) 060public class ResourceLink extends BaseResourceIndex { 061 062 public static final int SRC_PATH_LENGTH = 500; 063 private static final long serialVersionUID = 1L; 064 065 @SequenceGenerator(name = "SEQ_RESLINK_ID", sequenceName = "SEQ_RESLINK_ID") 066 @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESLINK_ID") 067 @Id 068 @Column(name = "PID") 069 private Long myId; 070 071 @Column(name = "SRC_PATH", length = SRC_PATH_LENGTH, nullable = false) 072 private String mySourcePath; 073 074 @ManyToOne(optional = false, fetch = FetchType.LAZY) 075 @JoinColumn( 076 name = "SRC_RESOURCE_ID", 077 referencedColumnName = "RES_ID", 078 nullable = false, 079 foreignKey = @ForeignKey(name = "FK_RESLINK_SOURCE")) 080 private ResourceTable mySourceResource; 081 082 @Column(name = "SRC_RESOURCE_ID", insertable = false, updatable = false, nullable = false) 083 private Long mySourceResourcePid; 084 085 @Column(name = "SOURCE_RESOURCE_TYPE", updatable = false, nullable = false, length = ResourceTable.RESTYPE_LEN) 086 @FullTextField 087 private String mySourceResourceType; 088 089 @ManyToOne(optional = true, fetch = FetchType.LAZY) 090 @JoinColumn( 091 name = "TARGET_RESOURCE_ID", 092 referencedColumnName = "RES_ID", 093 nullable = true, 094 insertable = false, 095 updatable = false, 096 foreignKey = @ForeignKey(name = "FK_RESLINK_TARGET")) 097 private ResourceTable myTargetResource; 098 099 @Column(name = "TARGET_RESOURCE_ID", insertable = true, updatable = true, nullable = true) 100 @FullTextField 101 private Long myTargetResourcePid; 102 103 @Column(name = "TARGET_RESOURCE_TYPE", nullable = false, length = ResourceTable.RESTYPE_LEN) 104 @FullTextField 105 private String myTargetResourceType; 106 107 @Column(name = "TARGET_RESOURCE_URL", length = 200, nullable = true) 108 @FullTextField 109 private String myTargetResourceUrl; 110 111 @Column(name = "TARGET_RESOURCE_VERSION", nullable = true) 112 private Long myTargetResourceVersion; 113 114 @FullTextField 115 @Column(name = "SP_UPDATED", nullable = true) // TODO: make this false after HAPI 2.3 116 @Temporal(TemporalType.TIMESTAMP) 117 private Date myUpdated; 118 119 @Transient 120 private transient String myTargetResourceId; 121 122 /** 123 * Constructor 124 */ 125 public ResourceLink() { 126 super(); 127 } 128 129 public Long getTargetResourceVersion() { 130 return myTargetResourceVersion; 131 } 132 133 public void setTargetResourceVersion(Long theTargetResourceVersion) { 134 myTargetResourceVersion = theTargetResourceVersion; 135 } 136 137 public String getTargetResourceId() { 138 if (myTargetResourceId == null && myTargetResource != null) { 139 myTargetResourceId = getTargetResource().getIdDt().getIdPart(); 140 } 141 return myTargetResourceId; 142 } 143 144 public String getSourceResourceType() { 145 return mySourceResourceType; 146 } 147 148 public String getTargetResourceType() { 149 return myTargetResourceType; 150 } 151 152 @Override 153 public boolean equals(Object theObj) { 154 if (this == theObj) { 155 return true; 156 } 157 if (theObj == null) { 158 return false; 159 } 160 if (!(theObj instanceof ResourceLink)) { 161 return false; 162 } 163 ResourceLink obj = (ResourceLink) theObj; 164 EqualsBuilder b = new EqualsBuilder(); 165 b.append(mySourcePath, obj.mySourcePath); 166 b.append(mySourceResource, obj.mySourceResource); 167 b.append(myTargetResourceUrl, obj.myTargetResourceUrl); 168 b.append(myTargetResourceType, obj.myTargetResourceType); 169 b.append(myTargetResourceVersion, obj.myTargetResourceVersion); 170 // In cases where we are extracting links from a resource that has not yet been persisted, the target resource 171 // pid 172 // will be null so we use the target resource id to differentiate instead 173 if (getTargetResourcePid() == null) { 174 b.append(getTargetResourceId(), obj.getTargetResourceId()); 175 } else { 176 b.append(getTargetResourcePid(), obj.getTargetResourcePid()); 177 } 178 return b.isEquals(); 179 } 180 181 @Override 182 public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) { 183 ResourceLink source = (ResourceLink) theSource; 184 mySourcePath = source.getSourcePath(); 185 myTargetResource = source.getTargetResource(); 186 myTargetResourceId = source.getTargetResourceId(); 187 myTargetResourcePid = source.getTargetResourcePid(); 188 myTargetResourceType = source.getTargetResourceType(); 189 myTargetResourceVersion = source.getTargetResourceVersion(); 190 myTargetResourceUrl = source.getTargetResourceUrl(); 191 } 192 193 public String getSourcePath() { 194 return mySourcePath; 195 } 196 197 public void setSourcePath(String theSourcePath) { 198 mySourcePath = theSourcePath; 199 } 200 201 public Long getSourceResourcePid() { 202 return mySourceResourcePid; 203 } 204 205 public ResourceTable getSourceResource() { 206 return mySourceResource; 207 } 208 209 public void setSourceResource(ResourceTable theSourceResource) { 210 mySourceResource = theSourceResource; 211 mySourceResourcePid = theSourceResource.getId(); 212 mySourceResourceType = theSourceResource.getResourceType(); 213 } 214 215 public void setTargetResource(String theResourceType, Long theResourcePid, String theTargetResourceId) { 216 Validate.notBlank(theResourceType); 217 218 myTargetResourceType = theResourceType; 219 myTargetResourcePid = theResourcePid; 220 myTargetResourceId = theTargetResourceId; 221 } 222 223 public String getTargetResourceUrl() { 224 return myTargetResourceUrl; 225 } 226 227 public void setTargetResourceUrl(IIdType theTargetResourceUrl) { 228 Validate.isTrue(theTargetResourceUrl.hasBaseUrl()); 229 Validate.isTrue(theTargetResourceUrl.hasResourceType()); 230 231 // if (theTargetResourceUrl.hasIdPart()) { 232 // do nothing 233 // } else { 234 // Must have set an url like http://example.org/something 235 // We treat 'something' as the resource type because of fix for #659. Prior to #659 fix, 'something' was 236 // treated as the id and 'example.org' was treated as the resource type 237 // Maybe log a warning? 238 // } 239 240 myTargetResourceType = theTargetResourceUrl.getResourceType(); 241 myTargetResourceUrl = theTargetResourceUrl.getValue(); 242 } 243 244 public Long getTargetResourcePid() { 245 return myTargetResourcePid; 246 } 247 248 public void setTargetResourceUrlCanonical(String theTargetResourceUrl) { 249 Validate.notBlank(theTargetResourceUrl); 250 251 myTargetResourceType = "(unknown)"; 252 myTargetResourceUrl = theTargetResourceUrl; 253 } 254 255 public Date getUpdated() { 256 return myUpdated; 257 } 258 259 public void setUpdated(Date theUpdated) { 260 myUpdated = theUpdated; 261 } 262 263 @Override 264 public Long getId() { 265 return myId; 266 } 267 268 @Override 269 public void setId(Long theId) { 270 myId = theId; 271 } 272 273 @Override 274 public void clearHashes() { 275 // nothing right now 276 } 277 278 @Override 279 public void calculateHashes() { 280 // nothing right now 281 } 282 283 @Override 284 public int hashCode() { 285 HashCodeBuilder b = new HashCodeBuilder(); 286 b.append(mySourcePath); 287 b.append(mySourceResource); 288 b.append(myTargetResourceUrl); 289 b.append(myTargetResourceVersion); 290 291 // In cases where we are extracting links from a resource that has not yet been persisted, the target resource 292 // pid 293 // will be null so we use the target resource id to differentiate instead 294 if (getTargetResourcePid() == null) { 295 b.append(getTargetResourceId()); 296 } else { 297 b.append(getTargetResourcePid()); 298 } 299 return b.toHashCode(); 300 } 301 302 @Override 303 public String toString() { 304 StringBuilder b = new StringBuilder(); 305 b.append("ResourceLink["); 306 b.append("path=").append(mySourcePath); 307 b.append(", src=").append(mySourceResourcePid); 308 b.append(", target=").append(myTargetResourcePid); 309 b.append(", targetType=").append(myTargetResourceType); 310 b.append(", targetVersion=").append(myTargetResourceVersion); 311 b.append(", targetUrl=").append(myTargetResourceUrl); 312 313 b.append("]"); 314 return b.toString(); 315 } 316 317 public ResourceTable getTargetResource() { 318 return myTargetResource; 319 } 320 321 /** 322 * Creates a clone of this resourcelink which doesn't contain the internal PID 323 * of the target resource. 324 */ 325 public ResourceLink cloneWithoutTargetPid() { 326 ResourceLink retVal = new ResourceLink(); 327 retVal.mySourceResource = mySourceResource; 328 retVal.mySourceResourcePid = mySourceResource.getId(); 329 retVal.mySourceResourceType = mySourceResource.getResourceType(); 330 retVal.mySourcePath = mySourcePath; 331 retVal.myUpdated = myUpdated; 332 retVal.myTargetResourceType = myTargetResourceType; 333 if (myTargetResourceId != null) { 334 retVal.myTargetResourceId = myTargetResourceId; 335 } else if (myTargetResource != null) { 336 retVal.myTargetResourceId = myTargetResource.getIdDt().getIdPart(); 337 } 338 retVal.myTargetResourceUrl = myTargetResourceUrl; 339 retVal.myTargetResourceVersion = myTargetResourceVersion; 340 return retVal; 341 } 342 343 public static ResourceLink forAbsoluteReference( 344 String theSourcePath, ResourceTable theSourceResource, IIdType theTargetResourceUrl, Date theUpdated) { 345 ResourceLink retVal = new ResourceLink(); 346 retVal.setSourcePath(theSourcePath); 347 retVal.setSourceResource(theSourceResource); 348 retVal.setTargetResourceUrl(theTargetResourceUrl); 349 retVal.setUpdated(theUpdated); 350 return retVal; 351 } 352 353 /** 354 * Factory for canonical URL 355 */ 356 public static ResourceLink forLogicalReference( 357 String theSourcePath, ResourceTable theSourceResource, String theTargetResourceUrl, Date theUpdated) { 358 ResourceLink retVal = new ResourceLink(); 359 retVal.setSourcePath(theSourcePath); 360 retVal.setSourceResource(theSourceResource); 361 retVal.setTargetResourceUrlCanonical(theTargetResourceUrl); 362 retVal.setUpdated(theUpdated); 363 return retVal; 364 } 365 366 /** 367 * @param theTargetResourceVersion This should only be populated if the reference actually had a version 368 */ 369 public static ResourceLink forLocalReference( 370 String theSourcePath, 371 ResourceTable theSourceResource, 372 String theTargetResourceType, 373 Long theTargetResourcePid, 374 String theTargetResourceId, 375 Date theUpdated, 376 @Nullable Long theTargetResourceVersion) { 377 ResourceLink retVal = new ResourceLink(); 378 retVal.setSourcePath(theSourcePath); 379 retVal.setSourceResource(theSourceResource); 380 retVal.setTargetResource(theTargetResourceType, theTargetResourcePid, theTargetResourceId); 381 retVal.setTargetResourceVersion(theTargetResourceVersion); 382 retVal.setUpdated(theUpdated); 383 return retVal; 384 } 385}