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}