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