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