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}