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 = "HFJ_RES_SEARCH_URL",
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
065        @EmbeddedId
066        private ResourceSearchUrlEntityPK myPk;
067
068        /*
069         * Note: We previously had a foreign key here, but it's just not possible for this to still
070         * work with partition IDs in the PKs since non-partitioned mode currently depends on the
071         * partition ID being a part of the PK and it necessarily has to be stripped out if we're
072         * stripping out others. So we'll leave this without a FK relationship, which does increase
073         * the possibility of dangling records in this table but that's probably an ok compromise.
074         *
075         * Ultimately records in this table get cleaned up based on their CREATED_TIME anyhow, so
076         * it's really not a big deal to not have a FK relationship here.
077         */
078
079        @Column(name = "RES_ID", updatable = false, nullable = false, insertable = true)
080        private Long myResourcePid;
081
082        @Column(name = "PARTITION_ID", updatable = false, nullable = false, insertable = false)
083        private Integer myPartitionIdValue;
084
085        @Column(name = "PARTITION_DATE", nullable = true, insertable = true, updatable = false)
086        private LocalDate myPartitionDate;
087
088        @Column(name = "CREATED_TIME", nullable = false)
089        @Temporal(TemporalType.TIMESTAMP)
090        private Date myCreatedTime;
091
092        public static ResourceSearchUrlEntity from(
093                        String theUrl, ResourceTable theResourceTable, boolean theSearchUrlDuplicateAcrossPartitionsEnabled) {
094
095                return new ResourceSearchUrlEntity()
096                                .setPk(ResourceSearchUrlEntityPK.from(
097                                                theUrl, theResourceTable, theSearchUrlDuplicateAcrossPartitionsEnabled))
098                                .setPartitionDate(Optional.ofNullable(theResourceTable.getPartitionId())
099                                                .map(PartitionablePartitionId::getPartitionDate)
100                                                .orElse(null))
101                                .setResourceTable(theResourceTable)
102                                .setCreatedTime(new Date());
103        }
104
105        public ResourceSearchUrlEntityPK getPk() {
106                return myPk;
107        }
108
109        public ResourceSearchUrlEntity setPk(ResourceSearchUrlEntityPK thePk) {
110                myPk = thePk;
111                return this;
112        }
113
114        public JpaPid getResourcePid() {
115                return JpaPid.fromId(myResourcePid, myPartitionIdValue);
116        }
117
118        public ResourceSearchUrlEntity setResourcePid(Long theResourcePid) {
119                myResourcePid = theResourcePid;
120                return this;
121        }
122
123        public ResourceSearchUrlEntity setResourceTable(ResourceTable theResourceTable) {
124                this.myResourcePid = theResourceTable.getId().getId();
125                this.myPartitionIdValue = theResourceTable.getPartitionId().getPartitionId();
126                return this;
127        }
128
129        public Date getCreatedTime() {
130                return myCreatedTime;
131        }
132
133        public ResourceSearchUrlEntity setCreatedTime(Date theCreatedTime) {
134                myCreatedTime = theCreatedTime;
135                return this;
136        }
137
138        public String getSearchUrl() {
139                return myPk.getSearchUrl();
140        }
141
142        public Integer getPartitionId() {
143                return myPk.getPartitionId();
144        }
145
146        public LocalDate getPartitionDate() {
147                return myPartitionDate;
148        }
149
150        public ResourceSearchUrlEntity setPartitionDate(LocalDate thePartitionDate) {
151                myPartitionDate = thePartitionDate;
152                return this;
153        }
154
155        @Override
156        public String toString() {
157                return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
158                                .append("searchUrl", getPk().getSearchUrl())
159                                .append("partitionId", myPartitionIdValue)
160                                .append("resourcePid", myResourcePid)
161                                .toString();
162        }
163}