/**
* Copyright (C) 2013-2014 Project-Vethrfolnir
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.vethrfolnir.game.services;
import java.sql.*;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.concurrent.locks.ReentrantLock;
import com.vethrfolnir.database.DatabaseFactory;
import com.vethrfolnir.logging.MuLogger;
import corvus.corax.Corax;
import corvus.corax.inject.Inject;
/**
* This class is Thread-Safe.<br>
* This class is designed to be very strict with id usage.
*
* @author Vlad
* @author SoulTaker @ aion-emu
*/
public class IdFactory {
private static final MuLogger log = MuLogger.getLogger(IdFactory.class);
private final BitSet idList = new BitSet();
/**
* Synchronization of bitset
*/
private final ReentrantLock lock = new ReentrantLock();
/**
* Id that will be used as minimal on next id request
*/
private int nextMinId = 0;
@Inject
private void load() {
DatabaseFactory factory = Corax.fetch(DatabaseFactory.class);
try (Connection con = factory.getConnection()) {
ArrayList<Integer> ids = new ArrayList<Integer>();
loadCharacterIds(con, ids);
loadItemIds(con, ids);
lockIds(ids);
log.info("Locked "+ids.size()+" id(s). Limit "+Integer.MAX_VALUE);
ids.clear();
}
catch (Exception e) {
log.fatal("Failed reading database!", e);
}
}
private void loadItemIds(Connection con, ArrayList<Integer> ids) throws SQLException {
try (PreparedStatement st = con.prepareStatement("select objectId from character_items")) {
ResultSet rs = st.executeQuery();
while(rs.next())
ids.add(rs.getInt(1));
}
}
private void loadCharacterIds(Connection con, ArrayList<Integer> ids) throws SQLException {
try (PreparedStatement st = con.prepareStatement("select charId from characters")) {
ResultSet rs = st.executeQuery();
while(rs.next())
ids.add(rs.getInt(1));
}
}
/**
* Returns next free id.
*
* @return next free id
* @throws IDFactoryError
* if there is no free id's
*/
public int obtain() {
try {
lock.lock();
int id;
if (nextMinId == Integer.MIN_VALUE) {
// Error will be thrown few lines later, we have no more free
// id's.
// BitSet will throw IllegalArgumentException if nextMinId is
// negative
id = Integer.MIN_VALUE;
}
else {
id = idList.nextClearBit(nextMinId);
}
// If BitSet reached Integer.MAX_VALUE size and returned last free
// id before - it will return
// Intger.MIN_VALUE as the next id, so we must catch such case and
// throw error (no free id's left)
if (id == Integer.MIN_VALUE) {
log.error("All id's are used, please clear your database");
}
idList.set(id);
// It ok to have Integer OverFlow here, on next ID request IDFactory
// will throw error
nextMinId = id + 1;
return id;
}
finally {
lock.unlock();
}
}
/**
* Locks given ids.
*
* @param ids
* ids to lock
* @throws IDFactoryError
* if some of the id's were locked before
*/
public void lockIds(int... ids) {
try {
lock.lock();
for (int id : ids) {
boolean status = idList.get(id);
if (status) {
log.error("ID " + id + " is already taken, fatal error!!!");
}
idList.set(id);
}
}
finally {
lock.unlock();
}
}
/**
* Locks given ids.
*
* @param ids
* ids to lock
* @throws IDFactoryError
* if some of the id's were locked before
*/
public void lockIds(Iterable<Integer> ids) {
try {
lock.lock();
for (int id : ids) {
boolean status = idList.get(id);
if (status) {
log.error("ID " + id + " is already taken, fatal error!!!");
}
idList.set(id);
}
}
finally {
lock.unlock();
}
}
public void free(int id) {
try {
lock.lock();
boolean status = idList.get(id);
if (!status) {
log.warn("ID " + id + " is not taken, can't release it.", new RuntimeException("Id not taken!"));
}
idList.clear(id);
if (id < nextMinId || nextMinId == Integer.MIN_VALUE) {
nextMinId = id;
}
}
finally {
lock.unlock();
}
}
public int getUsedCount() {
try {
lock.lock();
return idList.cardinality();
}
finally {
lock.unlock();
}
}
}