/*
* Copyright 2011 Tyler Blair. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and contributors and should not be interpreted as representing official policies,
* either expressed or implied, of anybody else.
*/
package com.griefcraft.modules.admin;
import com.griefcraft.lwc.LWC;
import com.griefcraft.model.Protection;
import com.griefcraft.scripting.JavaModule;
import com.griefcraft.scripting.event.LWCCommandEvent;
import com.griefcraft.sql.Database;
import com.griefcraft.sql.PhysDB;
import com.griefcraft.util.Colors;
import org.bukkit.Bukkit;
import org.bukkit.block.Block;
import org.bukkit.command.CommandSender;
import org.bukkit.scheduler.BukkitScheduler;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
public class AdminCleanup extends JavaModule {
/**
* The amount of protection block gets to batch at once
*/
private static int BATCH_SIZE = 250;
@Override
public void onCommand(LWCCommandEvent event) {
if (event.isCancelled()) {
return;
}
if (!event.hasFlag("a", "admin")) {
return;
}
LWC lwc = event.getLWC();
CommandSender sender = event.getSender();
String[] args = event.getArgs();
if (!args[0].equals("cleanup")) {
return;
}
// we have the right command
event.setCancelled(true);
// if we shouldn't output
boolean silent = false;
if (args.length > 1 && args[1].equalsIgnoreCase("silent")) {
silent = true;
}
lwc.sendLocale(sender, "protection.admin.cleanup.start", "count", lwc.getPhysicalDatabase().getProtectionCount());
// do the work in a separate thread so we don't fully lock the server
// new Thread(new Admin_Cleanup_Thread(lwc, sender)).start();
Bukkit.getScheduler().scheduleAsyncDelayedTask(lwc.getPlugin(), new Admin_Cleanup_Thread(lwc, sender, silent));
}
/**
* Class that handles cleaning up the LWC database usage: /lwc admin cleanup
*/
private static class Admin_Cleanup_Thread implements Runnable {
private LWC lwc;
private CommandSender sender;
private boolean silent;
public Admin_Cleanup_Thread(LWC lwc, CommandSender sender, boolean silent) {
this.lwc = lwc;
this.sender = sender;
this.silent = silent;
}
/**
* Push removal changes to the database
*
* @param toRemove
*/
public void push(List<Integer> toRemove) throws SQLException {
final StringBuilder builder = new StringBuilder();
final int total = toRemove.size();
int count = 0;
// iterate over the items to remove
Iterator<Integer> iter = toRemove.iterator();
// the database prefix
String prefix = lwc.getPhysicalDatabase().getPrefix();
// create the statement to use
Statement statement = lwc.getPhysicalDatabase().getConnection().createStatement();
while (iter.hasNext()) {
int protectionId = iter.next();
if (count % 100000 == 0) {
builder.append("DELETE FROM ").append(prefix).append("protections WHERE id IN (").append(protectionId);
} else {
builder.append(",").append(protectionId);
}
if (count % 100000 == 99999 || count == (total - 1)) {
builder.append(")");
statement.executeUpdate(builder.toString());
builder.setLength(0);
sender.sendMessage(Colors.Green + "REMOVED " + (count + 1) + " / " + total);
}
count++;
}
statement.close();
}
public void run() {
List<Integer> toRemove = new LinkedList<Integer>();
int removed = 0;
int percentChecked = 0;
// the bukkit scheduler
BukkitScheduler scheduler = Bukkit.getScheduler();
try {
sender.sendMessage(Colors.Red + "Processing cleanup request now in a separate thread");
// the list of protections work off of. We batch updates to the world
// so we can more than 20 results/second.
final List<Protection> protections = new ArrayList<Protection>(BATCH_SIZE);
// amount of protections
int totalProtections = lwc.getPhysicalDatabase().getProtectionCount();
// TODO separate stream logic to somewhere else :)
// Create a new database connection, we are just reading
PhysDB database = new PhysDB();
database.connect();
database.load();
Statement resultStatement = database.getConnection().createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
if (lwc.getPhysicalDatabase().getType() == Database.Type.MySQL) {
resultStatement.setFetchSize(Integer.MIN_VALUE);
}
String prefix = lwc.getPhysicalDatabase().getPrefix();
ResultSet result = resultStatement.executeQuery("SELECT id, owner, type, x, y, z, data, blockId, world, password, date, last_accessed FROM " + prefix + "protections");
int checked = 0;
while (result.next()) {
final Protection tprotection = database.resolveProtection(result);
if (protections.size() != BATCH_SIZE) {
// Wait until we have BATCH_SIZE protections
protections.add(tprotection);
if (protections.size() != totalProtections) {
continue;
}
}
// Get all of the blocks in the world
Future<Void> getBlocks = scheduler.callSyncMethod(lwc.getPlugin(), new Callable<Void>() {
public Void call() throws Exception {
for (Protection protection : protections) {
protection.getBlock(); // this will cache it also :D
}
return null;
}
});
// Get all of the blocks
getBlocks.get();
for (Protection protection : protections) {
Block block = protection.getBlock();
// remove protections not found in the world
if (block == null || !lwc.isProtectable(block)) {
toRemove.add(protection.getId());
removed ++;
if (!silent) {
lwc.sendLocale(sender, "protection.admin.cleanup.removednoexist", "protection", protection.toString());
}
}
checked ++;
}
// percentage dump
int percent = (int) ((((double) checked) / totalProtections) * 100);
if (percent % 5 == 0 && percentChecked != percent) {
percentChecked = percent;
sender.sendMessage(Colors.Red + "Cleanup @ " + percent + "% [ " + checked + "/" + totalProtections + " protections ] [ removed " + removed + " protections ]");
}
// Clear the protection set, we are done with them
protections.clear();
}
// close the sql statements
result.close();
resultStatement.close();
// flush all of the queries
push(toRemove);
sender.sendMessage("Cleanup completed. Removed " + removed + " protections out of " + checked + " checked protections.");
} catch (Exception e) { // database.connect() throws Exception
System.out.println("Exception caught during cleanup: " + e.getMessage());
}
}
}
}