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}