001// --------------------------------------------------------------------------------
002// Copyright 2002-2024 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;
025
026public class EntityIdGenerator {
027    
028    static private final String selectQuery = "SELECT eid_lastentityid FROM entityids WHERE eid_componentvendorname = ? AND eid_entitytypename = ?";
029    static private final String updateQuery = "UPDATE entityids SET eid_lastentityid = ? WHERE eid_componentvendorname = ? AND eid_entitytypename = ? AND eid_lastentityid = ?";
030    static private final String insertQuery = "INSERT INTO entityids (eid_componentvendorname, eid_entitytypename, eid_lastentityid) VALUES (?, ?, ?)";
031    
032    static private final int defaultChunkSize = 100;
033    static private final int retryCount = 5;
034
035    private final ReentrantLock lock = new ReentrantLock(true);
036
037    private String componentVendorName;
038    private String entityTypeName;
039    private int chunkSize;
040    private long guardedValue = 0;
041    private long currentValue = 0;
042    
043    private void init(String componentVendorName, String entityTypeName, int chunkSize) {
044        this.componentVendorName = componentVendorName;
045        this.entityTypeName = entityTypeName;
046        this.chunkSize = chunkSize;
047    }
048    
049    /** Creates a new instance of EntityIdGenerator */
050    public EntityIdGenerator(String componentVendorName, String entityTypeName, int chunkSize) {
051        init(componentVendorName, entityTypeName, chunkSize);
052    }
053    
054    /** Creates a new instance of EntityIdGenerator */
055    public EntityIdGenerator(String componentVendorName, String entityTypeName) {
056        init(componentVendorName, entityTypeName, defaultChunkSize);
057    }
058    
059    public Connection getConnection() {
060        return DslContextFactory.getInstance().getNTDslContext().parsingConnection();
061    }
062    
063    private boolean getNewChunk()
064            throws SQLException {
065        PreparedStatement ps = null;
066        ResultSet rs = null;
067        long currentMax;
068
069        try(var conn = getConnection();) {
070            try {
071                ps = conn.prepareStatement(selectQuery);
072                ps.setString(1, componentVendorName);
073                ps.setString(2, entityTypeName);
074                rs = ps.executeQuery();
075
076                if(rs.next()) {
077                    currentMax = rs.getLong(1);
078
079                    rs.close();
080                    rs = null;
081
082                    ps.close();
083                    ps = null;
084
085                    ps = conn.prepareStatement(updateQuery);
086                    ps.setLong(1, currentMax + chunkSize);
087                    ps.setString(2, componentVendorName);
088                    ps.setString(3, entityTypeName);
089                    ps.setLong(4, currentMax);
090
091                    if(ps.executeUpdate() != 1)
092                        return false;
093                    else {
094                        guardedValue = currentMax + chunkSize;
095                        currentValue = currentMax;
096                        return true;
097                    }
098                } else {
099                    rs.close();
100                    rs = null;
101
102                    ps.close();
103                    ps = null;
104
105                    ps = conn.prepareStatement(insertQuery);
106                    ps.setString(1, componentVendorName);
107                    ps.setString(2, entityTypeName);
108                    ps.setLong(3, chunkSize);
109
110                    if(ps.executeUpdate() != 1) {
111                        return false;
112                    } else {
113                        guardedValue = chunkSize;
114                        currentValue = 0;
115                        return true;
116                    }
117                }
118            } finally {
119                if(rs != null) {
120                    rs.close();
121                }
122
123                if(ps != null) {
124                    ps.close();
125                }
126
127                if(conn != null) {
128                    conn.close();
129                }
130            }
131        }
132    }
133    
134    public long getNextEntityId() {
135        lock.lock();
136
137        try {
138            if(currentValue == guardedValue) {
139                try {
140                    for(var iter = 0; iter <= retryCount; iter++) {
141                        if(getNewChunk())
142                            break;
143                    }
144                } catch (SQLException se) {
145                    throw new PersistenceDatabaseException(se);
146                }
147            }
148
149            if(currentValue == guardedValue) {
150                throw new PersistenceDatabaseException("Could not get next increment value");
151            }
152
153            return ++currentValue;
154        } finally {
155            lock.unlock();
156        }
157    }
158    
159}