385 lines
15 KiB
Java
385 lines
15 KiB
Java
package com.boydti.fawe.database;
|
|
|
|
import com.boydti.fawe.Fawe;
|
|
import com.boydti.fawe.config.Settings;
|
|
import com.boydti.fawe.logging.rollback.RollbackOptimizedHistory;
|
|
import com.boydti.fawe.object.collection.YieldIterable;
|
|
import com.boydti.fawe.object.task.AsyncNotifyQueue;
|
|
import com.boydti.fawe.util.MainUtil;
|
|
import com.sk89q.worldedit.internal.util.LogManagerCompat;
|
|
import com.sk89q.worldedit.math.BlockVector3;
|
|
import com.sk89q.worldedit.regions.CuboidRegion;
|
|
import com.sk89q.worldedit.world.World;
|
|
import org.apache.logging.log4j.Logger;
|
|
import org.intellij.lang.annotations.Language;
|
|
import org.jetbrains.annotations.NotNull;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.sql.Connection;
|
|
import java.sql.DriverManager;
|
|
import java.sql.PreparedStatement;
|
|
import java.sql.ResultSet;
|
|
import java.sql.SQLException;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.Future;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.function.Supplier;
|
|
import java.util.stream.IntStream;
|
|
|
|
public class RollbackDatabase extends AsyncNotifyQueue {
|
|
|
|
private static final Logger LOGGER = LogManagerCompat.getLogger();
|
|
|
|
private final String prefix;
|
|
private final File dbLocation;
|
|
private final World world;
|
|
private Connection connection;
|
|
|
|
@Language("SQLite")
|
|
private String createTable = "CREATE TABLE IF NOT EXISTS `{0}edits` (`player` BLOB(16) NOT NULL,`id` INT NOT NULL, `time` INT NOT NULL,`x1` INT NOT NULL,`x2` INT NOT NULL,`z1` INT NOT NULL,`z2` INT NOT NULL,`y1` INT NOT NULL, `y2` INT NOT NULL, `size` INT NOT NULL, `command` VARCHAR, PRIMARY KEY (player, id))";
|
|
@Language("SQLite")
|
|
private String updateTable1 = "ALTER TABLE `{0}edits` ADD COLUMN `command` VARCHAR";
|
|
@Language("SQLite")
|
|
private String updateTable2 = "alter table `{0}edits` add size int default 0 not null";
|
|
@Language("SQLite")
|
|
private String insertEdit = "INSERT OR REPLACE INTO `{0}edits` (`player`,`id`,`time`,`x1`,`x2`,`z1`,`z2`,`y1`,`y2`,`command`,`size`) VALUES(?,?,?,?,?,?,?,?,?,?,?)";
|
|
@Language("SQLite")
|
|
private String purge = "DELETE FROM `{0}edits` WHERE `time`<?";
|
|
@Language("SQLite")
|
|
private String getEditsUser = "SELECT * FROM `{0}edits` WHERE `time`>? AND `x2`>=? AND `x1`<=? AND `z2`>=? AND `z1`<=? AND `y2`>=? AND `y1`<=? AND `player`=? ORDER BY `time` DESC, `id` DESC";
|
|
@Language("SQLite")
|
|
private String getEditsUserAsc = "SELECT * FROM `{0}edits` WHERE `time`>? AND `x2`>=? AND `x1`<=? AND `z2`>=? AND `z1`<=? AND `y2`>=? AND `y1`<=? AND `player`=? ORDER BY `time` ASC, `id` ASC";
|
|
@Language("SQLite")
|
|
private String getEdits = "SELECT * FROM `{0}edits` WHERE `time`>? AND `x2`>=? AND `x1`<=? AND `z2`>=? AND `z1`<=? AND `y2`>=? AND `y1`<=? ORDER BY `time` DESC, `id` DESC";
|
|
@Language("SQLite")
|
|
private String getEditsAsc = "SELECT * FROM `{0}edits` WHERE `time`>? AND `x2`>=? AND `x1`<=? AND `z2`>=? AND `z1`<=? AND `y2`>=? AND `y1`<=? ORDER BY `time` , `id` ";
|
|
@Language("SQLite")
|
|
private String getEditUser = "SELECT * FROM `{0}edits` WHERE `player`=? AND `id`=?";
|
|
|
|
@Language("SQLite")
|
|
private String deleteEditsUser = "DELETE FROM `{0}edits` WHERE `player`=? AND `time`>? AND `x2`>=? AND `x1`<=? AND `y2`>=? AND `y1`<=? AND `z2`>=? AND `z1`<=?";
|
|
@Language("SQLite")
|
|
private String deleteEditUser = "DELETE FROM `{0}edits` WHERE `player`=? AND `id`=?";
|
|
|
|
private final ConcurrentLinkedQueue<RollbackOptimizedHistory> historyChanges = new ConcurrentLinkedQueue<>();
|
|
|
|
RollbackDatabase(World world) throws SQLException, ClassNotFoundException {
|
|
super((t, e) -> e.printStackTrace());
|
|
this.prefix = "";
|
|
this.world = world;
|
|
this.dbLocation = MainUtil.getFile(Fawe.imp().getDirectory(), Settings.IMP.PATHS.HISTORY + File.separator + world.getName() + File.separator + "summary.db");
|
|
connection = openConnection();
|
|
|
|
// update vars
|
|
createTable = createTable.replace("{0}", prefix);
|
|
updateTable1 = updateTable1.replace("{0}", prefix);
|
|
updateTable2 = updateTable2.replace("{0}", prefix);
|
|
insertEdit = insertEdit.replace("{0}", prefix);
|
|
purge = purge.replace("{0}", prefix);
|
|
getEditsUser = getEditsUser.replace("{0}", prefix);
|
|
getEditsUserAsc = getEditsUserAsc.replace("{0}", prefix);
|
|
getEdits = getEdits.replace("{0}", prefix);
|
|
getEditsAsc = getEditsAsc.replace("{0}", prefix);
|
|
getEditUser = getEditUser.replace("{0}", prefix);
|
|
deleteEditsUser = deleteEditsUser.replace("{0}", prefix);
|
|
deleteEditUser = deleteEditUser.replace("{0}", prefix);
|
|
|
|
try {
|
|
init().get();
|
|
purge((int) TimeUnit.DAYS.toSeconds(Settings.IMP.HISTORY.DELETE_AFTER_DAYS));
|
|
} catch (InterruptedException | ExecutionException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
private byte[] toBytes(UUID uuid) {
|
|
return ByteBuffer.allocate(16).putLong(uuid.getMostSignificantBits()).putLong(uuid.getLeastSignificantBits()).array();
|
|
}
|
|
|
|
public Future<Boolean> init() {
|
|
return call(() -> {
|
|
try (PreparedStatement stmt = connection.prepareStatement(createTable)) {
|
|
stmt.executeUpdate();
|
|
}
|
|
try (PreparedStatement stmt = connection.prepareStatement(updateTable1)) {
|
|
stmt.executeUpdate();
|
|
} catch (SQLException ignored) {
|
|
} // Already updated
|
|
try (PreparedStatement stmt = connection.prepareStatement(updateTable2)) {
|
|
stmt.executeUpdate();
|
|
} catch (SQLException ignored) {
|
|
} // Already updated
|
|
return true;
|
|
});
|
|
}
|
|
|
|
public Future<Integer> delete(UUID uuid, int id) {
|
|
return call(() -> {
|
|
try (PreparedStatement stmt = connection.prepareStatement(deleteEditUser)) {
|
|
stmt.setBytes(1, toBytes(uuid));
|
|
stmt.setInt(2, id);
|
|
return stmt.executeUpdate();
|
|
}
|
|
});
|
|
}
|
|
|
|
public Future<RollbackOptimizedHistory> getEdit(@NotNull UUID uuid, int id) {
|
|
return call(() -> {
|
|
try (PreparedStatement stmt = connection.prepareStatement(getEditUser)) {
|
|
stmt.setBytes(1, toBytes(uuid));
|
|
stmt.setInt(2, id);
|
|
ResultSet result = stmt.executeQuery();
|
|
if (!result.next()) {
|
|
return null;
|
|
}
|
|
return create(result).get();
|
|
}
|
|
});
|
|
}
|
|
|
|
private Supplier<RollbackOptimizedHistory> create(ResultSet result) throws SQLException {
|
|
byte[] uuidBytes = result.getBytes("player");
|
|
int index = result.getInt("id");
|
|
int x1 = result.getInt("x1");
|
|
int x2 = result.getInt("x2");
|
|
int y1 = result.getByte("y1") + 128;
|
|
int y2 = result.getByte("y2") + 128;
|
|
int z1 = result.getInt("z1");
|
|
int z2 = result.getInt("z2");
|
|
CuboidRegion region = new CuboidRegion(BlockVector3.at(x1, y1, z1), BlockVector3.at(x2, y2, z2));
|
|
|
|
long time = result.getInt("time") * 1000L;
|
|
long size = result.getInt("size");
|
|
|
|
String command = result.getString("command");
|
|
|
|
ByteBuffer bb = ByteBuffer.wrap(uuidBytes);
|
|
long high = bb.getLong();
|
|
long low = bb.getLong();
|
|
UUID uuid = new UUID(high, low);
|
|
|
|
return () -> new RollbackOptimizedHistory(world, uuid, index, time, size, region, command);
|
|
}
|
|
|
|
public Future<Integer> purge(int diff) {
|
|
long now = System.currentTimeMillis() / 1000;
|
|
final int then = (int) (now - diff);
|
|
return call(() -> {
|
|
try (PreparedStatement stmt = connection.prepareStatement(purge)) {
|
|
stmt.setInt(1, then);
|
|
return stmt.executeUpdate();
|
|
}
|
|
});
|
|
}
|
|
|
|
public Iterable<Supplier<RollbackOptimizedHistory>> getEdits(BlockVector3 pos, boolean ascending) {
|
|
return getEdits(null, 0, pos, pos, false, ascending);
|
|
}
|
|
|
|
public Iterable<Supplier<RollbackOptimizedHistory>> getEdits(UUID uuid, long minTime, BlockVector3 pos1, BlockVector3 pos2, boolean delete, boolean ascending) {
|
|
YieldIterable<Supplier<RollbackOptimizedHistory>> yieldIterable = new YieldIterable<>();
|
|
|
|
Future<Integer> future = call(() -> {
|
|
try {
|
|
int count = 0;
|
|
String stmtStr = ascending ? uuid == null ? getEditsAsc : getEditsUserAsc :
|
|
uuid == null ? getEdits : getEditsUser;
|
|
try (PreparedStatement stmt = connection.prepareStatement(stmtStr)) {
|
|
stmt.setInt(1, (int) (minTime / 1000));
|
|
stmt.setInt(2, pos1.getBlockX());
|
|
stmt.setInt(3, pos2.getBlockX());
|
|
stmt.setInt(4, pos1.getBlockZ());
|
|
stmt.setInt(5, pos2.getBlockZ());
|
|
stmt.setByte(6, (byte) (pos1.getBlockY() - 128));
|
|
stmt.setByte(7, (byte) (pos2.getBlockY() - 128));
|
|
if (uuid != null) {
|
|
byte[] uuidBytes = toBytes(uuid);
|
|
stmt.setBytes(8, uuidBytes);
|
|
}
|
|
ResultSet result = stmt.executeQuery();
|
|
if (!result.next()) {
|
|
return 0;
|
|
}
|
|
do {
|
|
count++;
|
|
Supplier<RollbackOptimizedHistory> history = create(result);
|
|
yieldIterable.accept(history);
|
|
} while (result.next());
|
|
}
|
|
if (delete && uuid != null) {
|
|
try (PreparedStatement stmt = connection.prepareStatement(deleteEditsUser)) {
|
|
stmt.setInt(1, (int) (minTime / 1000));
|
|
stmt.setInt(2, pos1.getBlockX());
|
|
stmt.setInt(3, pos2.getBlockX());
|
|
stmt.setInt(4, pos1.getBlockZ());
|
|
stmt.setInt(5, pos2.getBlockZ());
|
|
stmt.setByte(6, (byte) (pos1.getBlockY() - 128));
|
|
stmt.setByte(7, (byte) (pos2.getBlockY() - 128));
|
|
byte[] uuidBytes = ByteBuffer.allocate(16).putLong(uuid.getMostSignificantBits()).putLong(uuid.getLeastSignificantBits()).array();
|
|
stmt.setBytes(8, uuidBytes);
|
|
}
|
|
}
|
|
return count;
|
|
} finally {
|
|
yieldIterable.close();
|
|
}
|
|
});
|
|
yieldIterable.setFuture(future);
|
|
|
|
return yieldIterable;
|
|
}
|
|
|
|
public Future<?> logEdit(RollbackOptimizedHistory history) {
|
|
historyChanges.add(history);
|
|
return call(this::sendBatch);
|
|
}
|
|
|
|
private boolean sendBatch() throws SQLException {
|
|
int size = Math.min(1048572, historyChanges.size());
|
|
|
|
if (size == 0) {
|
|
return false;
|
|
}
|
|
|
|
commit();
|
|
if (connection.getAutoCommit()) {
|
|
connection.setAutoCommit(false);
|
|
}
|
|
|
|
RollbackOptimizedHistory[] copy = IntStream.range(0, size)
|
|
.mapToObj(i -> historyChanges.poll()).toArray(RollbackOptimizedHistory[]::new);
|
|
|
|
try (PreparedStatement stmt = connection.prepareStatement(insertEdit)) {
|
|
// `player`,`id`,`time`,`x1`,`x2`,`z1`,`z2`,`y1`,`y2`,`command`,`size`) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
|
|
for (RollbackOptimizedHistory change : copy) {
|
|
UUID uuid = change.getUUID();
|
|
byte[] uuidBytes = toBytes(uuid);
|
|
stmt.setBytes(1, uuidBytes);
|
|
stmt.setInt(2, change.getIndex());
|
|
stmt.setInt(3, (int) (change.getTime() / 1000));
|
|
|
|
BlockVector3 pos1 = change.getMinimumPoint();
|
|
BlockVector3 pos2 = change.getMaximumPoint();
|
|
|
|
stmt.setInt(4, pos1.getX());
|
|
stmt.setInt(5, pos2.getX());
|
|
stmt.setInt(6, pos1.getZ());
|
|
stmt.setInt(7, pos2.getZ());
|
|
stmt.setByte(8, (byte) (pos1.getY() - 128));
|
|
stmt.setByte(9, (byte) (pos2.getY() - 128));
|
|
stmt.setString(10, change.getCommand());
|
|
stmt.setInt(11, change.size());
|
|
stmt.executeUpdate();
|
|
stmt.clearParameters();
|
|
}
|
|
} finally {
|
|
commit();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void commit() {
|
|
try {
|
|
if (connection == null) {
|
|
return;
|
|
}
|
|
if (!connection.getAutoCommit()) {
|
|
connection.commit();
|
|
connection.setAutoCommit(true);
|
|
}
|
|
} catch (SQLException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
private Connection openConnection() throws SQLException, ClassNotFoundException {
|
|
if (checkConnection()) {
|
|
return connection;
|
|
}
|
|
if (!Fawe.imp().getDirectory().exists()) {
|
|
Fawe.imp().getDirectory().mkdirs();
|
|
}
|
|
if (!dbLocation.exists()) {
|
|
try {
|
|
dbLocation.getParentFile().mkdirs();
|
|
dbLocation.createNewFile();
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
LOGGER.error("Unable to create the database!");
|
|
}
|
|
}
|
|
Class.forName("org.sqlite.JDBC");
|
|
connection = DriverManager.getConnection("jdbc:sqlite:" + dbLocation);
|
|
return connection;
|
|
}
|
|
|
|
private Connection forceConnection() throws SQLException, ClassNotFoundException {
|
|
Class.forName("org.sqlite.JDBC");
|
|
connection = DriverManager.getConnection("jdbc:sqlite:" + dbLocation);
|
|
return connection;
|
|
}
|
|
|
|
/**
|
|
* Gets the connection with the database.
|
|
*
|
|
* @return Connection with the database, null if none
|
|
*/
|
|
public Connection getConnection() {
|
|
if (connection == null) {
|
|
try {
|
|
forceConnection();
|
|
} catch (ClassNotFoundException | SQLException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
return connection;
|
|
}
|
|
|
|
/**
|
|
* Closes the connection with the database.
|
|
*
|
|
* @return true if successful
|
|
* @throws SQLException if the connection cannot be closed
|
|
*/
|
|
public boolean closeConnection() throws SQLException {
|
|
if (connection == null) {
|
|
return false;
|
|
}
|
|
synchronized (this) {
|
|
if (connection == null) {
|
|
return false;
|
|
}
|
|
connection.close();
|
|
connection = null;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a connection is open with the database.
|
|
*
|
|
* @return true if the connection is open
|
|
*/
|
|
public boolean checkConnection() {
|
|
try {
|
|
return connection != null && !connection.isClosed();
|
|
} catch (SQLException e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
try {
|
|
closeConnection();
|
|
} catch (SQLException e) {
|
|
e.printStackTrace();
|
|
}
|
|
super.close();
|
|
}
|
|
}
|