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