/* GriefPrevention Server Plugin for Minecraft Copyright (C) 2012 Ryan Hamshire 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 . */ package me.ryanhamshire.GriefPrevention; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.text.SimpleDateFormat; import java.util.*; import org.bukkit.*; //manages data stored in the file system public class DatabaseDataStore extends DataStore { private Connection databaseConnection = null; private String databaseUrl; private String userName; private String password; DatabaseDataStore(String url, String userName, String password) throws Exception { this.databaseUrl = url; this.userName = userName; this.password = password; this.initialize(); } @Override void initialize() throws Exception { try { //load the java driver for mySQL Class.forName("com.mysql.jdbc.Driver"); } catch(Exception e) { GriefPrevention.AddLogEntry("ERROR: Unable to load Java's mySQL database driver. Check to make sure you've installed it properly."); throw e; } try { this.refreshDataConnection(); } catch(Exception e2) { GriefPrevention.AddLogEntry("ERROR: Unable to connect to database. Check your config file settings."); throw e2; } try { //ensure the data tables exist Statement statement = databaseConnection.createStatement(); statement.execute("CREATE TABLE IF NOT EXISTS griefprevention_nextclaimid (nextid INT(15));"); statement.execute("CREATE TABLE IF NOT EXISTS griefprevention_claimdata (id INT(15), owner VARCHAR(50), lessercorner VARCHAR(100), greatercorner VARCHAR(100), builders VARCHAR(1000), containers VARCHAR(1000), accessors VARCHAR(1000), managers VARCHAR(1000), parentid INT(15));"); statement.execute("CREATE TABLE IF NOT EXISTS griefprevention_playerdata (name VARCHAR(50), lastlogin DATETIME, accruedblocks INT(15), bonusblocks INT(15));"); statement.execute("CREATE TABLE IF NOT EXISTS griefprevention_schemaversion (version INT(15));"); //if the next claim id table is empty, this is a brand new database which will write using the latest schema //otherwise, schema version is determined by schemaversion table (or =0 if table is empty, see getSchemaVersion()) ResultSet results = statement.executeQuery("SELECT * FROM griefprevention_nextclaimid;"); if(!results.next()) { this.setSchemaVersion(latestSchemaVersion); } } catch(Exception e3) { GriefPrevention.AddLogEntry("ERROR: Unable to create the necessary database table. Details:"); GriefPrevention.AddLogEntry(e3.getMessage()); throw e3; } //load group data into memory Statement statement = databaseConnection.createStatement(); ResultSet results = statement.executeQuery("SELECT * FROM griefprevention_playerdata;"); while(results.next()) { String name = results.getString("name"); //ignore non-groups. all group names start with a dollar sign. if(!name.startsWith("$")) continue; String groupName = name.substring(1); if(groupName == null || groupName.isEmpty()) continue; //defensive coding, avoid unlikely cases int groupBonusBlocks = results.getInt("bonusblocks"); this.permissionToBonusBlocksMap.put(groupName, groupBonusBlocks); } //load next claim number into memory results = statement.executeQuery("SELECT * FROM griefprevention_nextclaimid;"); //if there's nothing yet, add it if(!results.next()) { statement.execute("INSERT INTO griefprevention_nextclaimid VALUES(0);"); this.nextClaimID = (long)0; } //otherwise load it else { this.nextClaimID = results.getLong("nextid"); } //load claims data into memory results = statement.executeQuery("SELECT * FROM griefprevention_claimdata;"); ArrayList claimsToRemove = new ArrayList(); while(results.next()) { try { //skip subdivisions long parentId = results.getLong("parentid"); if(parentId != -1) continue; long claimID = results.getLong("id"); String lesserCornerString = results.getString("lessercorner"); Location lesserBoundaryCorner = this.locationFromString(lesserCornerString); String greaterCornerString = results.getString("greatercorner"); Location greaterBoundaryCorner = this.locationFromString(greaterCornerString); String ownerName = results.getString("owner"); UUID ownerID = null; if(ownerName.isEmpty()) { ownerID = null; //administrative land claim } else if(this.getSchemaVersion() < 0) { try { ownerID = UUIDFetcher.getUUIDOf(ownerName); } catch(Exception ex){ } //if UUID not found, use NULL } else { try { ownerID = UUID.fromString(ownerName); } catch(Exception ex) { GriefPrevention.AddLogEntry("Failed to look up UUID for player " + ownerName + "."); GriefPrevention.AddLogEntry(" Converted land claim to administrative @ " + lesserBoundaryCorner.toString()); } } String buildersString = results.getString("builders"); String [] builderNames = buildersString.split(";"); builderNames = this.convertNameListToUUIDList(builderNames); String containersString = results.getString("containers"); String [] containerNames = containersString.split(";"); containerNames = this.convertNameListToUUIDList(containerNames); String accessorsString = results.getString("accessors"); String [] accessorNames = accessorsString.split(";"); accessorNames = this.convertNameListToUUIDList(accessorNames); String managersString = results.getString("managers"); String [] managerNames = managersString.split(";"); managerNames = this.convertNameListToUUIDList(managerNames); Claim topLevelClaim = new Claim(lesserBoundaryCorner, greaterBoundaryCorner, ownerID, builderNames, containerNames, accessorNames, managerNames, claimID); //search for another claim overlapping this one Claim conflictClaim = this.getClaimAt(topLevelClaim.lesserBoundaryCorner, true, null); //if there is such a claim, mark it for later removal if(conflictClaim != null) { claimsToRemove.add(conflictClaim); continue; } //otherwise, add this claim to the claims collection else { int j = 0; while(j < this.claims.size() && !this.claims.get(j).greaterThan(topLevelClaim)) j++; if(j < this.claims.size()) this.claims.add(j, topLevelClaim); else this.claims.add(this.claims.size(), topLevelClaim); topLevelClaim.inDataStore = true; } //look for any subdivisions for this claim Statement statement2 = this.databaseConnection.createStatement(); ResultSet childResults = statement2.executeQuery("SELECT * FROM griefprevention_claimdata WHERE parentid=" + topLevelClaim.id + ";"); while(childResults.next()) { lesserCornerString = childResults.getString("lessercorner"); lesserBoundaryCorner = this.locationFromString(lesserCornerString); greaterCornerString = childResults.getString("greatercorner"); greaterBoundaryCorner = this.locationFromString(greaterCornerString); buildersString = childResults.getString("builders"); builderNames = buildersString.split(";"); builderNames = this.convertNameListToUUIDList(builderNames); containersString = childResults.getString("containers"); containerNames = containersString.split(";"); containerNames = this.convertNameListToUUIDList(containerNames); accessorsString = childResults.getString("accessors"); accessorNames = accessorsString.split(";"); accessorNames = this.convertNameListToUUIDList(accessorNames); managersString = childResults.getString("managers"); managerNames = managersString.split(";"); managerNames = this.convertNameListToUUIDList(managerNames); Claim childClaim = new Claim(lesserBoundaryCorner, greaterBoundaryCorner, null, builderNames, containerNames, accessorNames, managerNames, null); //add this claim to the list of children of the current top level claim childClaim.parent = topLevelClaim; topLevelClaim.children.add(childClaim); childClaim.inDataStore = true; } } catch(SQLException e) { GriefPrevention.AddLogEntry("Unable to load a claim. Details: " + e.getMessage() + " ... " + results.toString()); e.printStackTrace(); } } for(int i = 0; i < claimsToRemove.size(); i++) { this.deleteClaimFromSecondaryStorage(claimsToRemove.get(i)); } if(this.getSchemaVersion() == 0) { try { this.refreshDataConnection(); //pull ALL player data from the database statement = this.databaseConnection.createStatement(); results = statement.executeQuery("SELECT * FROM griefprevention_playerdata;"); //make a list of changes to be made HashMap changes = new HashMap(); //for each result while(results.next()) { //get the id String playerName = results.getString("name"); //ignore groups if(playerName.startsWith("$")) continue; //try to convert player name to UUID try { UUID playerID = UUIDFetcher.getUUIDOf(playerName); //if successful, update the playerdata row by replacing the player's name with the player's UUID if(playerID != null) { changes.put(playerName, playerID); } } //otherwise leave it as-is. no harm done - it won't be requested by name, and this update only happens once. catch(Exception ex){ } } for(String name : changes.keySet()) { statement = this.databaseConnection.createStatement(); statement.execute("UPDATE griefprevention_playerdata SET name = '" + changes.get(name).toString() + "' WHERE name = '" + name + "';"); } } catch(SQLException e) { GriefPrevention.AddLogEntry("Unable to convert player data. Details:"); GriefPrevention.AddLogEntry(e.getMessage()); e.printStackTrace(); } } super.initialize(); } @Override synchronized void writeClaimToStorage(Claim claim) //see datastore.cs. this will ALWAYS be a top level claim { try { this.refreshDataConnection(); //wipe out any existing data about this claim this.deleteClaimFromSecondaryStorage(claim); //write top level claim data to the database this.writeClaimData(claim); //for each subdivision for(int i = 0; i < claim.children.size(); i++) { //write the subdivision's data to the database this.writeClaimData(claim.children.get(i)); } } catch(SQLException e) { GriefPrevention.AddLogEntry("Unable to save data for claim at " + this.locationToString(claim.lesserBoundaryCorner) + ". Details:"); GriefPrevention.AddLogEntry(e.getMessage()); } } //actually writes claim data to the database synchronized private void writeClaimData(Claim claim) throws SQLException { String lesserCornerString = this.locationToString(claim.getLesserBoundaryCorner()); String greaterCornerString = this.locationToString(claim.getGreaterBoundaryCorner()); String owner = ""; if(claim.ownerID != null) owner = claim.ownerID.toString(); ArrayList builders = new ArrayList(); ArrayList containers = new ArrayList(); ArrayList accessors = new ArrayList(); ArrayList managers = new ArrayList(); claim.getPermissions(builders, containers, accessors, managers); String buildersString = ""; for(int i = 0; i < builders.size(); i++) { buildersString += builders.get(i) + ";"; } String containersString = ""; for(int i = 0; i < containers.size(); i++) { containersString += containers.get(i) + ";"; } String accessorsString = ""; for(int i = 0; i < accessors.size(); i++) { accessorsString += accessors.get(i) + ";"; } String managersString = ""; for(int i = 0; i < managers.size(); i++) { managersString += managers.get(i) + ";"; } long parentId; if(claim.parent == null) { parentId = -1; } else { parentId = claim.parent.id; } long id; if(claim.id == null) { id = -1; } else { id = claim.id; } try { this.refreshDataConnection(); Statement statement = databaseConnection.createStatement(); statement.execute("INSERT INTO griefprevention_claimdata VALUES(" + id + ", '" + owner + "', '" + lesserCornerString + "', '" + greaterCornerString + "', '" + buildersString + "', '" + containersString + "', '" + accessorsString + "', '" + managersString + "', " + parentId + ");"); } catch(SQLException e) { GriefPrevention.AddLogEntry("Unable to save data for claim at " + this.locationToString(claim.lesserBoundaryCorner) + ". Details:"); GriefPrevention.AddLogEntry(e.getMessage()); } } //deletes a top level claim from the database @Override synchronized void deleteClaimFromSecondaryStorage(Claim claim) { try { this.refreshDataConnection(); Statement statement = this.databaseConnection.createStatement(); statement.execute("DELETE FROM griefprevention_claimdata WHERE id=" + claim.id + ";"); statement.execute("DELETE FROM griefprevention_claimdata WHERE parentid=" + claim.id + ";"); } catch(SQLException e) { GriefPrevention.AddLogEntry("Unable to delete data for claim at " + this.locationToString(claim.lesserBoundaryCorner) + ". Details:"); GriefPrevention.AddLogEntry(e.getMessage()); } } @Override synchronized PlayerData getPlayerDataFromStorage(UUID playerID) { PlayerData playerData = new PlayerData(); playerData.playerID = playerID; try { this.refreshDataConnection(); Statement statement = this.databaseConnection.createStatement(); ResultSet results = statement.executeQuery("SELECT * FROM griefprevention_playerdata WHERE name='" + playerID.toString() + "';"); //if there's no data for this player, create it with defaults if(!results.next()) { this.savePlayerData(playerID, playerData); } //otherwise, just read from the database else { playerData.lastLogin = results.getTimestamp("lastlogin"); playerData.accruedClaimBlocks = results.getInt("accruedblocks"); playerData.bonusClaimBlocks = results.getInt("bonusblocks"); } } catch(SQLException e) { GriefPrevention.AddLogEntry("Unable to retrieve data for player " + playerID.toString() + ". Details:"); GriefPrevention.AddLogEntry(e.getMessage()); } return playerData; } //saves changes to player data. MUST be called after you're done making changes, otherwise a reload will lose them @Override synchronized public void savePlayerData(UUID playerID, PlayerData playerData) { //never save data for the "administrative" account. an empty string for player name indicates administrative account if(playerID == null) return; this.savePlayerData(playerID.toString(), playerData); } private void savePlayerData(String playerID, PlayerData playerData) { try { this.refreshDataConnection(); SimpleDateFormat sqlFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String dateString = sqlFormat.format(playerData.lastLogin); Statement statement = databaseConnection.createStatement(); statement.execute("DELETE FROM griefprevention_playerdata WHERE name='" + playerID.toString() + "';"); statement.execute("INSERT INTO griefprevention_playerdata VALUES ('" + playerID.toString() + "', '" + dateString + "', " + playerData.accruedClaimBlocks + ", " + playerData.bonusClaimBlocks + ");"); } catch(SQLException e) { GriefPrevention.AddLogEntry("Unable to save data for player " + playerID.toString() + ". Details:"); GriefPrevention.AddLogEntry(e.getMessage()); } } @Override synchronized void incrementNextClaimID() { this.setNextClaimID(this.nextClaimID + 1); } //sets the next claim ID. used by incrementNextClaimID() above, and also while migrating data from a flat file data store synchronized void setNextClaimID(long nextID) { this.nextClaimID = nextID; try { this.refreshDataConnection(); Statement statement = databaseConnection.createStatement(); statement.execute("DELETE FROM griefprevention_nextclaimid;"); statement.execute("INSERT INTO griefprevention_nextclaimid VALUES (" + nextID + ");"); } catch(SQLException e) { GriefPrevention.AddLogEntry("Unable to set next claim ID to " + nextID + ". Details:"); GriefPrevention.AddLogEntry(e.getMessage()); } } //updates the database with a group's bonus blocks @Override synchronized void saveGroupBonusBlocks(String groupName, int currentValue) { //group bonus blocks are stored in the player data table, with player name = $groupName String playerName = "$" + groupName; PlayerData playerData = new PlayerData(); playerData.bonusClaimBlocks = currentValue; this.savePlayerData(playerName, playerData); } @Override synchronized void close() { if(this.databaseConnection != null) { try { if(!this.databaseConnection.isClosed()) { this.databaseConnection.close(); } } catch(SQLException e){}; } this.databaseConnection = null; } private void refreshDataConnection() throws SQLException { if(this.databaseConnection == null || this.databaseConnection.isClosed()) { //set username/pass properties Properties connectionProps = new Properties(); connectionProps.put("user", this.userName); connectionProps.put("password", this.password); //establish connection this.databaseConnection = DriverManager.getConnection(this.databaseUrl, connectionProps); } } @Override protected int getSchemaVersionFromStorage() { try { this.refreshDataConnection(); Statement statement = this.databaseConnection.createStatement(); ResultSet results = statement.executeQuery("SELECT * FROM griefprevention_schemaversion;"); //if there's nothing yet, assume 0 and add it if(!results.next()) { this.setSchemaVersion(0); return 0; } //otherwise return the value that's in the table else { return results.getInt(0); } } catch(SQLException e) { GriefPrevention.AddLogEntry("Unable to retrieve schema version from database. Details:"); GriefPrevention.AddLogEntry(e.getMessage()); return 0; } } @Override protected void updateSchemaVersionInStorage(int versionToSet) { try { this.refreshDataConnection(); Statement statement = databaseConnection.createStatement(); statement.execute("DELETE FROM griefprevention_schemaversion;"); statement.execute("INSERT INTO griefprevention_schemaversion VALUES (" + versionToSet + ");"); } catch(SQLException e) { GriefPrevention.AddLogEntry("Unable to set next schema version to " + versionToSet + ". Details:"); GriefPrevention.AddLogEntry(e.getMessage()); } } }