001// --------------------------------------------------------------------------------
002// Copyright 2002-2026 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.util.server.persistence;
018
019import com.echothree.util.common.exception.PersistenceDatabaseException;
020import java.sql.Connection;
021import java.sql.PreparedStatement;
022import java.sql.ResultSet;
023import java.sql.SQLException;
024import java.util.concurrent.locks.ReentrantLock;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028public class EntityIdGenerator {
029
030    private static final Logger log = LoggerFactory.getLogger(EntityIdGenerator.class);
031
032    static private final String selectQuery = "SELECT eid_lastentityid FROM entityids WHERE eid_componentvendorname = ? AND eid_entitytypename = ?";
033    static private final String updateQuery = "UPDATE entityids SET eid_lastentityid = ? WHERE eid_componentvendorname = ? AND eid_entitytypename = ? AND eid_lastentityid = ?";
034    static private final String insertQuery = "INSERT INTO entityids (eid_componentvendorname, eid_entitytypename, eid_lastentityid) VALUES (?, ?, ?)";
035    
036    static private final int defaultChunkSize = 100;
037    static private final int retryCount = 5;
038
039    private final ReentrantLock lock = new ReentrantLock(true);
040
041    private String componentVendorName;
042    private String entityTypeName;
043    private int chunkSize;
044    private long guardedValue = 0;
045    private long currentValue = 0;
046    
047    private void init(String componentVendorName, String entityTypeName, int chunkSize) {
048        log.debug("EntityIdGenerator(componentVendorName = {}, entityTypeName = {}, chunkSize = {})",
049                componentVendorName, entityTypeName, chunkSize);
050
051        this.componentVendorName = componentVendorName;
052        this.entityTypeName = entityTypeName;
053        this.chunkSize = chunkSize;
054    }
055    
056    /** Creates a new instance of EntityIdGenerator */
057    public EntityIdGenerator(String componentVendorName, String entityTypeName, int chunkSize) {
058        init(componentVendorName, entityTypeName, chunkSize);
059    }
060    
061    /** Creates a new instance of EntityIdGenerator */
062    public EntityIdGenerator(String componentVendorName, String entityTypeName) {
063        init(componentVendorName, entityTypeName, defaultChunkSize);
064    }
065    
066    public Connection getConnection() {
067        return DslContextFactory.getInstance().getNTDslContext().parsingConnection();
068    }
069    
070    private boolean getNewChunk()
071            throws SQLException {
072        log.debug("getNewChunk(), componentVendorName = {}, entityTypeName = {}",
073                componentVendorName, entityTypeName);
074
075        PreparedStatement ps = null;
076        ResultSet rs = null;
077        long currentMax;
078
079        try(var conn = getConnection();) {
080            try {
081                ps = conn.prepareStatement(selectQuery);
082                ps.setString(1, componentVendorName);
083                ps.setString(2, entityTypeName);
084                rs = ps.executeQuery();
085
086                if(rs.next()) {
087                    currentMax = rs.getLong(1);
088
089                    rs.close();
090                    rs = null;
091
092                    ps.close();
093                    ps = null;
094
095                    ps = conn.prepareStatement(updateQuery);
096                    ps.setLong(1, currentMax + chunkSize);
097                    ps.setString(2, componentVendorName);
098                    ps.setString(3, entityTypeName);
099                    ps.setLong(4, currentMax);
100
101                    if(ps.executeUpdate() != 1)
102                        return false;
103                    else {
104                        guardedValue = currentMax + chunkSize;
105                        currentValue = currentMax;
106                        return true;
107                    }
108                } else {
109                    rs.close();
110                    rs = null;
111
112                    ps.close();
113                    ps = null;
114
115                    ps = conn.prepareStatement(insertQuery);
116                    ps.setString(1, componentVendorName);
117                    ps.setString(2, entityTypeName);
118                    ps.setLong(3, chunkSize);
119
120                    if(ps.executeUpdate() != 1) {
121                        return false;
122                    } else {
123                        guardedValue = chunkSize;
124                        currentValue = 0;
125                        return true;
126                    }
127                }
128            } finally {
129                if(rs != null) {
130                    rs.close();
131                }
132
133                if(ps != null) {
134                    ps.close();
135                }
136
137                if(conn != null) {
138                    conn.close();
139                }
140            }
141        }
142    }
143    
144    public long getNextEntityId() {
145        long nextEntityId;
146
147        lock.lock();
148        try {
149            if(currentValue == guardedValue) {
150                try {
151                    for(var iter = 0; iter <= retryCount; iter++) {
152                        if(getNewChunk())
153                            break;
154                    }
155                } catch (SQLException se) {
156                    throw new PersistenceDatabaseException(se);
157                }
158            }
159
160            if(currentValue == guardedValue) {
161                throw new PersistenceDatabaseException("Could not get next increment value");
162            }
163
164            nextEntityId = ++currentValue;
165        } finally {
166            lock.unlock();
167        }
168
169        log.debug("getNextEntityId(), nextEntityId = {}, componentVendorName = {}, entityTypeName = {}",
170                nextEntityId, componentVendorName, entityTypeName);
171
172        return nextEntityId;
173    }
174    
175}