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 jakarta.persistence.Column;
024import jakarta.persistence.EmbeddedId;
025import jakarta.persistence.Entity;
026import jakarta.persistence.Index;
027import jakarta.persistence.Table;
028import jakarta.persistence.Temporal;
029import jakarta.persistence.TemporalType;
030import org.apache.commons.lang3.builder.ToStringBuilder;
031import org.apache.commons.lang3.builder.ToStringStyle;
032
033import java.time.LocalDate;
034import java.util.Date;
035import java.util.Optional;
036
037/**
038 * This entity is used to enforce uniqueness on a given search URL being
039 * used as a conditional operation URL, e.g. a conditional create or a
040 * conditional update. When we perform a conditional operation that is
041 * creating a new resource, we store an entity with the conditional URL
042 * in this table. The URL is the PK of the table, so the database
043 * ensures that two concurrent threads don't accidentally create two
044 * resources with the same conditional URL.
045 * <p>
046 * Note that this entity is partitioned, but does not always respect the
047 * partition ID of the parent resource entity (it may just use a
048 * hardcoded partition ID of -1 depending on configuration). As a result
049 * we don't have a FK relationship. This table only contains short-lived
050 * rows that get cleaned up and purged periodically, so it should never
051 * grow terribly large anyhow.
052 */
053@Entity
054@Table(
055                name = ResourceSearchUrlEntity.TABLE_NAME,
056                indexes = {
057                        @Index(name = "IDX_RESSEARCHURL_RES", columnList = "RES_ID"),
058                        @Index(name = "IDX_RESSEARCHURL_TIME", columnList = "CREATED_TIME")
059                })
060public class ResourceSearchUrlEntity {
061
062        public static final String RES_SEARCH_URL_COLUMN_NAME = "RES_SEARCH_URL";
063        public static final String PARTITION_ID = "PARTITION_ID";
064        public static final String TABLE_NAME = "HFJ_RES_SEARCH_URL";
065
066        @EmbeddedId
067        private ResourceSearchUrlEntityPK myPk;
068
069        /*
070         * Note: We previously had a foreign key here, but it's just not possible for this to still
071         * work with partition IDs in the PKs since non-partitioned mode currently depends on the
072         * partition ID being a part of the PK and it necessarily has to be stripped out if we're
073         * stripping out others. So we'll leave this without a FK relationship, which does increase
074         * the possibility of dangling records in this table but that's probably an ok compromise.
075         *
076         * Ultimately records in this table get cleaned up based on their CREATED_TIME anyhow, so
077         * it's really not a big deal to not have a FK relationship here.
078         */
079
080        @Column(name = "RES_ID", updatable = false, nullable = false, insertable = true)
081        private Long myResourcePid;
082
083        @Column(name = "PARTITION_ID", updatable = false, nullable = false, insertable = false)
084        private Integer myPartitionIdValue;
085
086        @Column(name = "PARTITION_DATE", nullable = true, insertable = true, updatable = false)
087        private LocalDate myPartitionDate;
088
089        @Column(name = "CREATED_TIME", nullable = false)
090        @Temporal(TemporalType.TIMESTAMP)
091        private Date myCreatedTime;
092
093        public static ResourceSearchUrlEntity from(
094                        String theUrl, ResourceTable theResourceTable, boolean theSearchUrlDuplicateAcrossPartitionsEnabled) {
095
096                return new ResourceSearchUrlEntity()
097                                .setPk(ResourceSearchUrlEntityPK.from(
098                                                theUrl, theResourceTable, theSearchUrlDuplicateAcrossPartitionsEnabled))
099                                .setPartitionDate(Optional.ofNullable(theResourceTable.getPartitionId())
100                                                .map(PartitionablePartitionId::getPartitionDate)
101                                                .orElse(null))
102                                .setResourceTable(theResourceTable)
103                                .setCreatedTime(new Date());
104        }
105
106        public ResourceSearchUrlEntityPK getPk() {
107                return myPk;
108        }
109
110        public ResourceSearchUrlEntity setPk(ResourceSearchUrlEntityPK thePk) {
111                myPk = thePk;
112                return this;
113        }
114
115        public JpaPid getResourcePid() {
116                return JpaPid.fromId(myResourcePid, myPartitionIdValue);
117        }
118
119        public ResourceSearchUrlEntity setResourcePid(Long theResourcePid) {
120                myResourcePid = theResourcePid;
121                return this;
122        }
123
124        public ResourceSearchUrlEntity setResourceTable(ResourceTable theResourceTable) {
125                this.myResourcePid = theResourceTable.getId().getId();
126                this.myPartitionIdValue = theResourceTable.getPartitionId().getPartitionId();
127                return this;
128        }
129
130        public Date getCreatedTime() {
131                return myCreatedTime;
132        }
133
134        public ResourceSearchUrlEntity setCreatedTime(Date theCreatedTime) {
135                myCreatedTime = theCreatedTime;
136                return this;
137        }
138
139        public String getSearchUrl() {
140                return myPk.getSearchUrl();
141        }
142
143        public Integer getPartitionId() {
144                return myPk.getPartitionId();
145        }
146
147        public LocalDate getPartitionDate() {
148                return myPartitionDate;
149        }
150
151        public ResourceSearchUrlEntity setPartitionDate(LocalDate thePartitionDate) {
152                myPartitionDate = thePartitionDate;
153                return this;
154        }
155
156        @Override
157        public String toString() {
158                return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
159                                .append("searchUrl", getPk().getSearchUrl())
160                                .append("partitionId", myPartitionIdValue)
161                                .append("resourcePid", myResourcePid)
162                                .toString();
163        }
164}