/* * This file is part of Libelula Minecraft Edition Project. * * Libelula Minecraft Edition 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. * * Libelula Minecraft Edition 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 Libelula Minecraft Edition. * If not, see <http://www.gnu.org/licenses/>. * */ package me.libelula.meode; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.RandomAccessFile; import java.io.SyncFailedException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.BitSet; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.logging.Level; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import static me.libelula.meode.Store.getRowName; import static me.libelula.meode.Store.getTableName; import static me.libelula.meode.Store.rowSize; import orestes.bloomfilter.BloomFilter; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.plugin.Plugin; /** * Class HDStore of the plugin. * * @author Diego Lucio D'Onofrio <ddonofrio@member.fsf.org> * @version 1.0 */ public class HDStore extends Store { private final File database; private final Plugin plugin; public boolean debug; private File supertable; private File playersTable; private TreeMap<String, Integer> playerIDs; private FilenameFilter supertableNameFilter; private BloomFilter<String> dbFilter; private String lastQueryCacheString; RandomAccessFile lastQueryCacheAccessFile; /** * * @param dbPath Path to DB directory. * @param maxDbSizeMB Maximum amount of MB to store in disk before rotating * storage information. * @param plugin main plugin. */ public HDStore(String dbPath, int maxDbSizeMB, Plugin plugin) throws Exception { database = new File(dbPath); this.plugin = plugin; playerIDs = null; debug = false; supertable = null; playersTable = null; lastQueryCacheAccessFile = null; lastQueryCacheString = null; if (!database.mkdirs() && !database.isDirectory() || !database.canWrite() || !database.canRead()) { throw new Exception("Unable to access, create or write database directory: " .concat(dbPath)); } supertableNameFilter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.matches("\\d{4}-\\d{2}-\\d{2}"); } }; Object objects[] = {this}; new AsyncTask(AsyncTask.TaskType.LOAD_FILTER, objects, plugin).runTaskAsynchronously(plugin); } private void persistPlayersTable() throws IOException { if (playersTable != null && playerIDs != null) { if (debug) { plugin.getLogger().info("DEBUG: Saving players to ".concat(playersTable.getAbsolutePath())); } try (ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream(playersTable.getCanonicalPath(), false))) { oos.writeObject(playerIDs); oos.close(); } } else { throw new SyncFailedException("Unable to persist player database."); } } @SuppressWarnings("unchecked") public void setSupertable(String supertableName) throws IOException, ClassNotFoundException, Exception { if (debug) { plugin.getLogger().info("DEBUG: setting table to ".concat(supertableName)); } supertable = new File(database.getCanonicalPath() .concat("/").concat(supertableName)); if (!supertable.mkdirs() && !supertable.isDirectory() || !supertable.canWrite() || !supertable.canRead()) { throw new Exception("Unable to access, create or write table directory: " .concat(supertableName)); } playersTable = new File(supertable.getCanonicalPath().concat("/players.dat")); if (debug) { plugin.getLogger().info("DEBUG: Looking for players table: ".concat(playersTable.getCanonicalPath())); } if (playersTable.exists()) { try (ObjectInputStream ois = new ObjectInputStream( new FileInputStream(playersTable.getCanonicalPath()))) { playerIDs = (TreeMap<String, Integer>) ois.readObject(); if (debug && playerIDs != null) { plugin.getLogger().log(Level.INFO, "DEBUG: {0} players loaded from table.", playerIDs.size()); } ois.close(); } if (playerIDs == null) { plugin.getLogger().severe("Players table is corrupted. Saved user information were lost."); File corruptedFile = new File(supertable.getCanonicalPath().concat("/") .concat(new SimpleDateFormat("mmss").format(new Date()) .concat("corrupted-players.dat"))); playersTable.renameTo(corruptedFile); playersTable.deleteOnExit(); } } else { if (debug) { plugin.getLogger().info("DEBUG: Players table not found, creating a new one on memory."); } playerIDs = new TreeMap<>(); } } private int getPlayerID(String playerName) { if (playerIDs != null) { if (!playerIDs.containsKey(playerName)) { playerIDs.put(playerName, playerIDs.size()); } return playerIDs.get(playerName); } return -1; } public void loadBloomFilter() { try { this.dbFilter = getBloomFilter(); } catch (Exception ex) { plugin.getLogger().severe(ex.toString()); plugin.getLogger().severe("Database will work with low performance."); dbFilter = null; } } private BloomFilter<String> getBloomFilter() throws FileNotFoundException, IOException { dbFilter = new BloomFilter<>(10_000_000, 0.01); dbFilter.add("MEODE 1.0"); File bloomFilterFile = new File(database.getAbsolutePath() .concat("/filter.dat")); if (bloomFilterFile.exists()) { if (debug) { plugin.getLogger().log(Level.INFO, "DEBUG: {0} file found.", bloomFilterFile); } FileInputStream fis = new FileInputStream(bloomFilterFile); try (GZIPInputStream gzipIn = new GZIPInputStream(fis)) { try (ObjectInputStream objectIn = new ObjectInputStream(gzipIn)) { dbFilter.setBitSet((BitSet) objectIn.readObject()); } } catch (ClassNotFoundException ex) { plugin.getLogger().severe("filter.dat corrupted. Reindexing requiered."); dbFilter = null; if (debug) { plugin.getLogger().info("DEBUG: ".concat(ex.toString())); } } fis.close(); } if (dbFilter != null) { if (!dbFilter.contains("MEODE 1.0")) { plugin.getLogger().severe("filter.dat incompatible with this engine version."); dbFilter = null; } } return dbFilter; } public void peristRamAsynchronously(RAMStore rams) { Object objects[] = {rams, this}; new AsyncTask(AsyncTask.TaskType.SAVE_EVENTS, objects, plugin).runTaskAsynchronously(plugin); } /** * * @param rams RAMStore Object */ public void peristRamSynchronously(RAMStore rams) throws IOException, ClassNotFoundException, Exception { TreeSet<BlockEvent> blockEvents = rams.getBlockEventsAndRotate(); File table = null; File row = null; boolean pathChanged = false; DataOutputStream rowStream = null; for (Iterator<BlockEvent> it = blockEvents.iterator(); it.hasNext();) { BlockEvent be = it.next(); if (supertable == null || !supertable.getPath().endsWith(getSuperTableName(be.eventTime))) { if (supertable != null) { persistPlayersTable(); } this.setSupertable(getSuperTableName(be.eventTime)); pathChanged = true; } if (table == null || !table.getPath().endsWith(getTableName(be.location.getWorld().getName(), be.location.getBlockZ()))) { table = new File(supertable.getAbsolutePath().concat("/") .concat(getTableName(be.location.getWorld().getName(), be.location.getBlockZ()))); table.mkdir(); pathChanged = true; } if (pathChanged || row == null || !row.getPath().endsWith(getRowName(be.location.getBlockY(), be.placed))) { pathChanged = false; if (rowStream != null) { if (debug && row != null) { plugin.getLogger().info("DEBUG: Closing file ".concat(row.getAbsolutePath())); } rowStream.close(); } row = new File(table.getAbsolutePath().concat("/") .concat(getRowName(be.location.getBlockY(), be.placed))); if (debug) { plugin.getLogger().info("DEBUG: Opening file ".concat(row.getAbsolutePath())); } rowStream = new DataOutputStream(new FileOutputStream(row, true)); } if (rowStream != null) { rowStream.writeInt(be.location.getBlockX()); rowStream.writeInt(be.location.getBlockZ()); rowStream.writeShort(Auxiliary.minutesFromMidnight(be.eventTime)); rowStream.writeInt(be.blockTypeID); rowStream.writeByte(be.blockData); rowStream.writeShort(getPlayerID(be.playerName)); addModifiedBlockToFilter(be.location); if (be.blockTypeID == 63 || be.blockTypeID == 68) { saveSigns(supertable, be.location, be.aditionalData); } } } // for (Iterator... if (rowStream != null) { if (debug && row != null) { plugin.getLogger().info("DEBUG: Closing file ".concat(row.getAbsolutePath())); } rowStream.close(); } if (supertable != null) { persistPlayersTable(); } saveDBFilterToDisk(); } private static void saveSigns(File supertable, Location loc, String aditionalData) throws FileNotFoundException, IOException { if (aditionalData == null) { return; } File signDB = new File(supertable, loc.getWorld().getName().concat("-signs.db")); DataOutputStream signStream = new DataOutputStream(new FileOutputStream(signDB, true)); signStream.writeInt(loc.getBlockX()); signStream.writeInt(loc.getBlockY()); signStream.writeInt(loc.getBlockZ()); signStream.writeBytes(aditionalData.concat("\n")); signStream.close(); } private String getSignsText(String supertableName, Location loc, BlockEvent be) throws FileNotFoundException, IOException { File signDB = new File(database.getAbsolutePath().concat("/").concat(supertableName), loc.getWorld().getName().concat("-signs.db")); if (!signDB.exists()) { return null; } RandomAccessFile signAccess = new RandomAccessFile(signDB, "r"); String result = null; int registerSize = 61 + (3 * 4); int cursor = 0; while (true) { signAccess.seek(cursor); int X = signAccess.readInt(); int Y = signAccess.readInt(); int Z = signAccess.readInt(); if (loc.getBlockX() == X && loc.getBlockY() == Y && loc.getBlockZ() == Z) { result = signAccess.readLine(); break; } cursor = cursor + registerSize; if (cursor > signAccess.length() - registerSize) { break; } } signAccess.close(); return result; } void saveDBFilterToDisk() throws FileNotFoundException, IOException { if (debug) { plugin.getLogger().info("DEBUG: Saving DB Filter to disk..."); } File bloomFilterFile; if (dbFilter != null) { bloomFilterFile = new File(database.getAbsolutePath(), "/filter.dat.tmp"); bloomFilterFile.delete(); FileOutputStream fos = new FileOutputStream(bloomFilterFile); try (GZIPOutputStream gzipOut = new GZIPOutputStream(fos)) { try (ObjectOutputStream objectOut = new ObjectOutputStream(gzipOut)) { objectOut.writeObject(dbFilter.getBitSet()); } } fos.close(); if (debug) { plugin.getLogger().info("DB Filter saved."); } File oldBloomFilterFile = new File(database.getAbsolutePath(), "/filter.dat"); oldBloomFilterFile.delete(); bloomFilterFile.renameTo(oldBloomFilterFile); } else { if (debug) { plugin.getLogger().info("DB Filter not actived, nothing to save."); } } } public String query(Location loc, boolean placed) throws IOException, ClassNotFoundException, ParseException { BlockEvent be = getLastBlockEvent(loc, placed); String result = null; if (be != null) { result = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(be.eventTime) .concat(" ").concat(be.playerName) .concat(be.placed ? " placed " : " removed ") .concat(Material.getMaterial(be.blockTypeID).toString()); } return result; } public BlockEvent getLastBlockEvent(Location loc, boolean placed) throws IOException, ClassNotFoundException, ParseException { BlockEvent result = null; TreeSet<String> tables = new TreeSet<>(Collections.reverseOrder()); tables.addAll(Arrays.asList(database.list(supertableNameFilter))); for (Iterator<String> it = tables.iterator(); it.hasNext();) { String superTableName = it.next(); File table = new File(database.getCanonicalPath() .concat("/") .concat(superTableName) .concat("/") .concat(getTableName(loc.getWorld().getUID().getLeastSignificantBits(), loc.getBlockZ()))); if (!table.exists()) { table = new File(database.getCanonicalPath() .concat("/") .concat(superTableName) .concat("/") .concat(getTableName(loc.getWorld().getName(), loc.getBlockZ()))); if (!table.exists()) continue; } File row = new File(table.getAbsolutePath().concat("/").concat(getRowName(loc.getBlockY(), placed))); if (!row.exists()) { continue; } RandomAccessFile rowAccess; if (lastQueryCacheString == null) { lastQueryCacheString = row.getAbsolutePath(); rowAccess = new RandomAccessFile(row, "r"); lastQueryCacheAccessFile = rowAccess; } else { if (lastQueryCacheString.equals(row.getAbsolutePath())) { try { lastQueryCacheAccessFile.read(); rowAccess = lastQueryCacheAccessFile; } catch (IOException ex) { rowAccess = new RandomAccessFile(row, "r"); } } else { lastQueryCacheAccessFile.close(); rowAccess = new RandomAccessFile(row, "r"); lastQueryCacheAccessFile = rowAccess; } } for (long i = rowAccess.length() - rowSize; i >= 0; i -= rowSize) { rowAccess.seek(i); int blockPosX = rowAccess.readInt(); int blockPosZ = rowAccess.readInt(); if (blockPosX == loc.getBlockX() && blockPosZ == loc.getBlockZ()) { result = new BlockEvent(); short minutesFromMidNight = rowAccess.readShort(); result.blockTypeID = rowAccess.readInt(); result.blockData = rowAccess.readByte(); short playerId = rowAccess.readShort(); result.playerName = getPlayerFromSupertable(superTableName, playerId); SimpleDateFormat superTableFormat = new SimpleDateFormat("yyyy-MM-dd"); result.eventTime = new Date((60 + minutesFromMidNight) * 60 * 1000).getTime() + superTableFormat.parse(superTableName).getTime(); // Time in Disk has not defintion, the comparator founds blocks equals so... result.eventTime = result.eventTime + i; result.placed = placed; result.location = loc; if (result.blockTypeID == 63 || result.blockTypeID == 68) { result.aditionalData = getSignsText(superTableName, loc, result); } return result; } } rowAccess.close(); } return result; } @SuppressWarnings("unchecked") private String getPlayerFromSupertable(String superTableName, short playerId) throws IOException, ClassNotFoundException { TreeMap<String, Integer> playerIDsLocal; File playerTable = new File(database.getCanonicalPath() .concat("/").concat(superTableName) .concat("/players.dat")); if (!playerTable.exists()) { return "(Unknown:" + playerId + ")"; } try (ObjectInputStream ois = new ObjectInputStream( new FileInputStream(playerTable.getCanonicalPath()))) { playerIDsLocal = (TreeMap<String, Integer>) ois.readObject(); ois.close(); } Set<String> playerName = Auxiliary.getKeysByValue(playerIDsLocal, (int) playerId); if (playerName != null && !playerName.isEmpty()) { return playerName.iterator().next(); } return "(Unknown:" + playerId + ")"; } public TreeSet<BlockEvent> getBlockEvents(Location minLoc, Location maxLoc, TreeSet<BlockEvent> ignore, String playerName) throws IOException, ClassNotFoundException, ParseException { TreeSet<BlockEvent> resultSet = new TreeSet<>(new BlockEventsTimeComparator()); TreeSet<BlockEvent> querySet = new TreeSet<>(); if (minLoc.getBlockY() < 0) { minLoc.setY(0); } if (maxLoc.getY() > 255) { maxLoc.setY(255); } World world = minLoc.getWorld(); int id = 0; for (int x = minLoc.getBlockX(); x <= maxLoc.getBlockX(); x++) { for (int y = minLoc.getBlockY(); y <= maxLoc.getBlockY(); y++) { for (int z = minLoc.getBlockZ(); z <= maxLoc.getBlockZ(); z++) { if (!filterContains(world.getName(), x, y, z)) { continue; } Location loc = new Location(world, x, y, z); BlockEvent be = new BlockEvent(); be.location = loc; be.eventTime = id; querySet.add(be); id++; } } } BlockEvent result; for (BlockEvent be : querySet) { for (int placed = 0; placed <= 1; placed++) { if (placed == 0) { result = getLastBlockEvent(be.location, true); } else { result = getLastBlockEvent(be.location, false); } if (result == null) { continue; } if (playerName != null) { if (!playerName.equalsIgnoreCase(result.playerName)) { continue; } } resultSet.add(result); } } return resultSet; } protected void addModifiedBlockToFilter(Block block) { if (dbFilter != null) { addModifiedBlockToFilter(block.getLocation()); } } protected void addModifiedBlockToFilter(Location loc) { if (dbFilter != null) { dbFilter.add(loc.getWorld().getName() + loc.getBlockX() + "X" + loc.getBlockY() + "Y" + loc.getBlockZ() + "Z"); } } public boolean filterContains(Block block) { return filterContains(block.getLocation()); } public boolean filterContains(Location loc) { return filterContains(loc.getWorld().getName(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); } public boolean filterContains(String worldName, int X, int Y, int Z) { if (dbFilter != null) { return dbFilter.contains(worldName + X + "X" + Y + "Y" + Z + "Z"); } else { return true; } } }