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}