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}