
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.dao.JpaPidFk; 024import ca.uhn.fhir.model.primitive.IdDt; 025import ca.uhn.fhir.rest.api.Constants; 026import jakarta.annotation.Nonnull; 027import jakarta.annotation.Nullable; 028import jakarta.persistence.AttributeOverride; 029import jakarta.persistence.CascadeType; 030import jakarta.persistence.Column; 031import jakarta.persistence.ConstraintMode; 032import jakarta.persistence.Embedded; 033import jakarta.persistence.EmbeddedId; 034import jakarta.persistence.Entity; 035import jakarta.persistence.EnumType; 036import jakarta.persistence.Enumerated; 037import jakarta.persistence.FetchType; 038import jakarta.persistence.ForeignKey; 039import jakarta.persistence.Index; 040import jakarta.persistence.JoinColumn; 041import jakarta.persistence.JoinColumns; 042import jakarta.persistence.Lob; 043import jakarta.persistence.ManyToOne; 044import jakarta.persistence.OneToMany; 045import jakarta.persistence.Table; 046import jakarta.persistence.Transient; 047import jakarta.persistence.UniqueConstraint; 048import org.apache.commons.lang3.builder.ToStringBuilder; 049import org.apache.commons.lang3.builder.ToStringStyle; 050import org.hibernate.Length; 051import org.hibernate.annotations.JdbcTypeCode; 052import org.hibernate.annotations.OptimisticLock; 053import org.hibernate.type.SqlTypes; 054 055import java.io.Serializable; 056import java.time.LocalDate; 057import java.util.ArrayList; 058import java.util.Collection; 059 060@Entity 061@Table( 062 name = ResourceHistoryTable.HFJ_RES_VER, 063 uniqueConstraints = { 064 @UniqueConstraint( 065 name = ResourceHistoryTable.IDX_RESVER_ID_VER, 066 columnNames = {"PARTITION_ID", "RES_ID", "RES_VER"}) 067 }, 068 indexes = { 069 @Index(name = "IDX_RESVER_TYPE_DATE", columnList = "RES_TYPE,RES_UPDATED,RES_ID"), 070 @Index(name = "IDX_RESVER_ID_DATE", columnList = "RES_ID,RES_UPDATED"), 071 @Index(name = "IDX_RESVER_DATE", columnList = "RES_UPDATED,RES_ID"), 072 @Index(name = "IDX_RESVER_ID_SRC_URI", columnList = "SOURCE_URI,RES_ID,PARTITION_ID") 073 }) 074public class ResourceHistoryTable extends BaseHasResource<ResourceHistoryTablePk> implements Serializable { 075 public static final String IDX_RESVER_ID_VER = "IDX_RESVER_ID_VER"; 076 public static final int SOURCE_URI_LENGTH = ResourceIndexedSearchParamString.MAX_LENGTH; 077 /** 078 * @see ResourceEncodingEnum 079 */ 080 // Don't reduce the visibility here, we reference this from Smile 081 @SuppressWarnings("WeakerAccess") 082 public static final int ENCODING_COL_LENGTH = 5; 083 084 public static final String HFJ_RES_VER = "HFJ_RES_VER"; 085 private static final long serialVersionUID = 1L; 086 087 @EmbeddedId 088 private ResourceHistoryTablePk myId; 089 090 @Column(name = PartitionablePartitionId.PARTITION_ID, nullable = true, insertable = false, updatable = false) 091 private Integer myPartitionIdValue; 092 093 @SuppressWarnings("unused") 094 @Column(name = PartitionablePartitionId.PARTITION_DATE, updatable = false, nullable = true) 095 private LocalDate myPartitionDateValue; 096 097 @Override 098 @Nullable 099 public PartitionablePartitionId getPartitionId() { 100 return PartitionablePartitionId.with(getResourceId().getPartitionId(), myPartitionDateValue); 101 } 102 103 @ManyToOne(fetch = FetchType.LAZY) 104 @JoinColumns( 105 value = { 106 @JoinColumn(name = "RES_ID", nullable = false, insertable = false, updatable = false), 107 @JoinColumn(name = "PARTITION_ID", nullable = false, insertable = false, updatable = false), 108 }, 109 foreignKey = @ForeignKey(name = "FK_RESOURCE_HISTORY_RESOURCE")) 110 private ResourceTable myResourceTable; 111 112 @Embedded 113 @AttributeOverride(name = "myId", column = @Column(name = "RES_ID", insertable = true, updatable = false)) 114 @AttributeOverride( 115 name = "myPartitionIdValue", 116 column = @Column(name = "PARTITION_ID", insertable = false, updatable = false)) 117 private JpaPidFk myResourcePid; 118 119 /** 120 * This is here for sorting only, don't get or set this value 121 */ 122 @SuppressWarnings("unused") 123 @Column(name = "RES_ID", insertable = false, nullable = false, updatable = false) 124 private Long myResourceId; 125 126 @Column(name = "RES_TYPE", length = ResourceTable.RESTYPE_LEN, nullable = false) 127 private String myResourceType; 128 129 @Column(name = "RES_VER", nullable = false) 130 private Long myResourceVersion; 131 132 @OneToMany(mappedBy = "myResourceHistory", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) 133 private Collection<ResourceHistoryTag> myTags; 134 135 @Column(name = "RES_TEXT", length = Integer.MAX_VALUE - 1, nullable = true) 136 @Lob() 137 @OptimisticLock(excluded = true) 138 private byte[] myResource; 139 140 @Column(name = "RES_TEXT_VC", length = Length.LONG32, nullable = true) 141 @OptimisticLock(excluded = true) 142 private String myResourceTextVc; 143 144 @Column(name = "RES_ENCODING", nullable = false, length = ENCODING_COL_LENGTH) 145 @Enumerated(EnumType.STRING) 146 @JdbcTypeCode(SqlTypes.VARCHAR) 147 @OptimisticLock(excluded = true) 148 private ResourceEncodingEnum myEncoding; 149 150 // TODO: This was added in 6.8.0 - In the future we should drop ResourceHistoryProvenanceEntity 151 @Column(name = "SOURCE_URI", length = SOURCE_URI_LENGTH, nullable = true) 152 private String mySourceUri; 153 // TODO: This was added in 6.8.0 - In the future we should drop ResourceHistoryProvenanceEntity 154 @Column(name = "REQUEST_ID", length = Constants.REQUEST_ID_LENGTH, nullable = true) 155 private String myRequestId; 156 157 @ManyToOne(fetch = FetchType.LAZY) 158 @JoinColumn( 159 name = "RES_TYPE_ID", 160 referencedColumnName = "RES_TYPE_ID", 161 foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), 162 insertable = false, 163 updatable = false, 164 nullable = true) 165 private ResourceTypeEntity myResourceTypeEntity; 166 167 @Column(name = "RES_TYPE_ID", nullable = true) 168 private Short myResourceTypeId; 169 170 @Transient 171 private transient ResourceHistoryProvenanceEntity myNewHistoryProvenanceEntity; 172 /** 173 * This is stored as an optimization to avoid needing to fetch ResourceTable 174 * to access the resource id. 175 */ 176 @Transient 177 private transient String myTransientForcedId; 178 179 /** 180 * Constructor 181 */ 182 public ResourceHistoryTable() { 183 super(); 184 } 185 186 public String getSourceUri() { 187 return mySourceUri; 188 } 189 190 public void setSourceUri(String theSourceUri) { 191 mySourceUri = theSourceUri; 192 } 193 194 public String getRequestId() { 195 return myRequestId; 196 } 197 198 public void setRequestId(String theRequestId) { 199 myRequestId = theRequestId; 200 } 201 202 @Override 203 public String toString() { 204 JpaPid resourceId = getResourceId(); 205 return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 206 .append("resourceId", resourceId.getId()) 207 .append("partitionId", resourceId.getPartitionId()) 208 .append("resourceType", myResourceType) 209 .append("resourceTypeId", getResourceTypeId()) 210 .append("resourceVersion", myResourceVersion) 211 .append("pid", myId) 212 .append("updated", getPublished()) 213 .toString(); 214 } 215 216 public String getResourceTextVc() { 217 return myResourceTextVc; 218 } 219 220 public void setResourceTextVc(String theResourceTextVc) { 221 myResourceTextVc = theResourceTextVc; 222 } 223 224 public void addTag(ResourceTag theTag) { 225 ResourceHistoryTag tag = new ResourceHistoryTag(this, theTag.getTag(), getPartitionId()); 226 tag.setResourceType(theTag.getResourceType()); 227 tag.setResourceTypeId(theTag.getResourceTypeId()); 228 getTags().add(tag); 229 } 230 231 @Override 232 public ResourceHistoryTag addTag(TagDefinition theTag) { 233 for (ResourceHistoryTag next : getTags()) { 234 if (next.getTag().equals(theTag)) { 235 return next; 236 } 237 } 238 ResourceHistoryTag historyTag = new ResourceHistoryTag(this, theTag, getPartitionId()); 239 getTags().add(historyTag); 240 return historyTag; 241 } 242 243 public ResourceEncodingEnum getEncoding() { 244 return myEncoding; 245 } 246 247 public void setEncoding(ResourceEncodingEnum theEncoding) { 248 myEncoding = theEncoding; 249 } 250 251 @Nonnull 252 @Override 253 public ResourceHistoryTablePk getId() { 254 if (myId == null) { 255 myId = new ResourceHistoryTablePk(); 256 } 257 return myId; 258 } 259 260 /** 261 * Do not delete, required for java bean introspection 262 */ 263 public ResourceHistoryTablePk getMyId() { 264 return getId(); 265 } 266 267 /** 268 * Do not delete, required for java bean introspection 269 */ 270 public void setMyId(ResourceHistoryTablePk theId) { 271 myId = theId; 272 } 273 274 public byte[] getResource() { 275 return myResource; 276 } 277 278 public void setResource(byte[] theResource) { 279 myResource = theResource; 280 } 281 282 @Override 283 public JpaPid getResourceId() { 284 initializeResourceId(); 285 JpaPid retVal = myResourcePid.toJpaPid(); 286 retVal.setVersion(myResourceVersion); 287 retVal.setResourceType(myResourceType); 288 if (retVal.getPartitionId() == null) { 289 retVal.setPartitionId(myPartitionIdValue); 290 } 291 return retVal; 292 } 293 294 private void initializeResourceId() { 295 if (myResourcePid == null) { 296 myResourcePid = new JpaPidFk(); 297 } 298 } 299 300 public void setResourceId(Long theResourceId) { 301 initializeResourceId(); 302 myResourcePid.setId(theResourceId); 303 } 304 305 @Override 306 public String getResourceType() { 307 return myResourceType; 308 } 309 310 @Override 311 public String getFhirId() { 312 return getIdDt().getIdPart(); 313 } 314 315 public void setResourceType(String theResourceType) { 316 myResourceType = theResourceType; 317 } 318 319 @Override 320 public Short getResourceTypeId() { 321 return myResourceTypeId; 322 } 323 324 public void setResourceTypeId(Short theResourceTypeId) { 325 myResourceTypeId = theResourceTypeId; 326 } 327 328 public ResourceTypeEntity getResourceTypeEntity() { 329 return myResourceTypeEntity; 330 } 331 332 @Override 333 public Collection<ResourceHistoryTag> getTags() { 334 if (myTags == null) { 335 myTags = new ArrayList<>(); 336 } 337 return myTags; 338 } 339 340 @Override 341 public long getVersion() { 342 return myResourceVersion; 343 } 344 345 public void setVersion(long theVersion) { 346 myResourceVersion = theVersion; 347 } 348 349 @Override 350 public boolean isDeleted() { 351 return getDeleted() != null; 352 } 353 354 @Override 355 public void setNotDeleted() { 356 setDeleted(null); 357 } 358 359 @Override 360 public JpaPid getPersistentId() { 361 return getResourceId(); 362 } 363 364 public ResourceTable getResourceTable() { 365 return myResourceTable; 366 } 367 368 public void setResourceTable(ResourceTable theResourceTable) { 369 myResourceTable = theResourceTable; 370 } 371 372 @Override 373 public IdDt getIdDt() { 374 // Avoid a join query if possible 375 String resourceIdPart; 376 if (getTransientForcedId() != null) { 377 resourceIdPart = getTransientForcedId(); 378 } else { 379 resourceIdPart = getResourceTable().getFhirId(); 380 } 381 return new IdDt(getResourceType() + '/' + resourceIdPart + '/' + Constants.PARAM_HISTORY + '/' + getVersion()); 382 } 383 384 /** 385 * Returns <code>true</code> if there is a populated resource text (i.e. 386 * either {@link #getResource()} or {@link #getResourceTextVc()} return a non null 387 * value. 388 */ 389 public boolean hasResource() { 390 return myResource != null || myResourceTextVc != null; 391 } 392 393 /** 394 * This method creates a new HistoryProvenance entity, or might reuse the current one if we've 395 * already created one in the current transaction. This is because we can only increment 396 * the version once in a DB transaction (since hibernate manages that number) so creating 397 * multiple {@link ResourceHistoryProvenanceEntity} entities will result in a constraint error. 398 */ 399 public ResourceHistoryProvenanceEntity toProvenance() { 400 if (myNewHistoryProvenanceEntity == null) { 401 myNewHistoryProvenanceEntity = new ResourceHistoryProvenanceEntity(); 402 } 403 return myNewHistoryProvenanceEntity; 404 } 405 406 public String getTransientForcedId() { 407 return myTransientForcedId; 408 } 409 410 public void setTransientForcedId(String theTransientForcedId) { 411 assert theTransientForcedId == null || !theTransientForcedId.contains("/") 412 : "Invalid FHIR ID: " + theTransientForcedId; 413 myTransientForcedId = theTransientForcedId; 414 } 415 416 public void setPartitionId(PartitionablePartitionId thePartitionablePartitionId) { 417 if (thePartitionablePartitionId != null) { 418 getId().setPartitionIdValue(thePartitionablePartitionId.getPartitionId()); 419 420 initializeResourceId(); 421 myResourcePid.setPartitionId(thePartitionablePartitionId.getPartitionId()); 422 423 myPartitionIdValue = thePartitionablePartitionId.getPartitionId(); 424 myPartitionDateValue = thePartitionablePartitionId.getPartitionDate(); 425 } else { 426 getId().setPartitionIdValue(null); 427 428 initializeResourceId(); 429 myResourcePid.setPartitionId(null); 430 431 myPartitionIdValue = null; 432 myPartitionDateValue = null; 433 } 434 } 435}