net.doubledoordev.backend.server.Server.java Source code

Java tutorial

Introduction

Here is the source code for net.doubledoordev.backend.server.Server.java

Source

/*
 *     D3Backend
 *     Copyright (C) 2015  Dries007 & Double Door Development
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 *     You should have received a copy of the GNU Affero General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.doubledoordev.backend.server;

import com.google.gson.annotations.Expose;
import net.doubledoordev.backend.permissions.User;
import net.doubledoordev.backend.server.query.MCQuery;
import net.doubledoordev.backend.server.query.QueryResponse;
import net.doubledoordev.backend.util.*;
import net.doubledoordev.backend.util.exceptions.AuthenticationException;
import net.doubledoordev.backend.util.exceptions.ServerOfflineException;
import net.doubledoordev.backend.util.exceptions.ServerOnlineException;
import net.doubledoordev.backend.util.methodCaller.IMethodCaller;
import net.doubledoordev.backend.web.socket.ServerconsoleSocketApplication;
import net.lingala.zip4j.core.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.progress.ProgressMonitor;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.Strings;

import java.io.*;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.util.*;

import static net.doubledoordev.backend.util.Constants.*;

/**
 * Class that holds methods related to Server instances.
 * The json based data is in a separate class for easy GSON integration.
 *
 * @author Dries007
 */
@SuppressWarnings("UnusedDeclaration")
public class Server {
    public static final String SERVER_PROPERTIES = "server.properties";
    public static final String SERVER_PORT = "server-port";
    public static final String QUERY_PORT = "query.port";
    public static final String QUERY_ENABLE = "enable-query";
    public static final String SERVER_IP = "server-ip";
    @Expose
    private final Map<String, Dimension> dimensionMap = new HashMap<>();
    /**
     * Diskspace var + timer to avoid long page load times.
     */
    public int[] size = new int[3];
    public QueryResponse cachedResponse;
    /*
     * START exposed Json data
     */
    @Expose
    private String ID;
    @Expose
    private Integer serverPort = 25565;
    @Expose
    private String ip = "";
    @Expose
    private String owner = "";
    @Expose
    private List<String> admins = new ArrayList<>();
    @Expose
    private List<String> coOwners = new ArrayList<>();
    /*
     * END exposed Json data
     */
    @Expose
    private RestartingInfo restartingInfo = new RestartingInfo();
    @Expose
    private JvmData jvmData = new JvmData();
    /**
     * Used to reroute server output to our console.
     * NOT LOGGED TO FILE!
     */
    private Logger logger;
    private File folder;
    private File propertiesFile;
    private long propertiesFileLastModified = 0L;
    private Properties properties = new Properties();
    /**
     * MCQuery and QueryResponse instances + timer to avoid long page load times.
     */
    private MCQuery query;
    /**
     * The process the server will be running in
     */
    private Process process;
    private boolean starting = false;
    private File backupFolder;
    private WorldManager worldManager = new WorldManager(this);
    private User ownerObject;
    private boolean downloading = false;

    public Server(String ID, String owner) {
        this.ID = ID;
        this.owner = owner;
    }

    private Server() {
    }

    /*
     * ========================================================================================
     * ========================================================================================
     * PUBLIC METHODS
     */

    public void init() {
        if (this.logger != null)
            return; // don't do this twice.
        this.logger = LogManager.getLogger(ID);
        this.folder = new File(SERVERS, ID);
        this.backupFolder = new File(BACKUPS, ID);
        this.propertiesFile = new File(folder, SERVER_PROPERTIES);

        if (!backupFolder.exists())
            backupFolder.mkdirs();
        if (!folder.exists())
            folder.mkdir();

        try {
            SizeCounter sizeCounter = new SizeCounter();
            Files.walkFileTree(getFolder().toPath(), sizeCounter);
            size[0] = sizeCounter.getSizeInMB();
            sizeCounter = new SizeCounter();
            if (getBackupFolder().exists())
                Files.walkFileTree(getBackupFolder().toPath(), sizeCounter);
            size[1] = sizeCounter.getSizeInMB();
            size[2] = size[0] + size[1];
        } catch (IOException ignored) {
        }
        try {
            getProperties();
            saveProperties();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /*
     * ========================================================================================
     * GETTERS
     */

    public String getIp() {
        return ip;
    }

    public boolean isDownloading() {
        return downloading;
    }

    public boolean isStarting() {
        return starting;
    }

    public MCQuery getQuery() {
        if (query == null)
            query = new MCQuery(LOCALHOST, serverPort);
        return query;
    }

    public Process getProcess() {
        return process;
    }

    /**
     * @return server.properties file
     */
    public Properties getProperties() {
        if (propertiesFile.exists() && propertiesFile.lastModified() > propertiesFileLastModified) {
            try {
                //properties.load(new StringReader(FileUtils.readFileToString(propertiesFile, "latin1")));
                properties.load(new StringReader(FileUtils.readFileToString(propertiesFile)));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        normalizeProperties();
        return properties;
    }

    /**
     * Get all server.properties keys
     *
     * @return the value
     */
    public Enumeration<Object> getPropertyKeys() {
        return properties.keys();
    }

    public String getIP() {
        return ip;
    }

    /**
     * Get a server.properties
     *
     * @param key the key
     * @return the value
     */
    public String getProperty(String key) {
        return properties.getProperty(key);
    }

    /**
     * Check server online status.
     * Ony detects when the process is started by us.
     * Bypass this limitation with RCon
     */
    public boolean getOnline() {
        try {
            if (process == null)
                return false;
            process.exitValue();
            return false;
        } catch (IllegalThreadStateException e) {
            return true;
        }
    }

    /**
     * @return Human readable server address
     */
    public String getDisplayAddress() {
        StringBuilder builder = new StringBuilder(25);
        if (ip != null && ip.trim().length() != 0)
            builder.append(ip);
        else if (Strings.isNotBlank(Settings.SETTINGS.hostname))
            builder.append(Settings.SETTINGS.hostname);
        builder.append(':').append(serverPort);
        return builder.toString();
    }

    public int getServerPort() {
        return serverPort;
    }

    public int getOnlinePlayers() {
        return cachedResponse == null ? 0 : cachedResponse.getOnlinePlayers();
    }

    public int getSlots() {
        return cachedResponse == null ? -1 : cachedResponse.getMaxPlayers();
    }

    public String getMotd() {
        return cachedResponse == null ? "?" : cachedResponse.getMotd();
    }

    public String getGameMode() {
        return cachedResponse == null ? "?" : cachedResponse.getGameMode();
    }

    public String getMapName() {
        return cachedResponse == null ? "?" : cachedResponse.getMapName();
    }

    public ArrayList<String> getPlayerList() {
        return cachedResponse == null ? new ArrayList<String>() : cachedResponse.getPlayerList();
    }

    public String getPlugins() {
        return cachedResponse == null ? "?" : cachedResponse.getPlugins();
    }

    public String getVersion() {
        return cachedResponse == null ? "?" : cachedResponse.getVersion();
    }

    public String getGameID() {
        return cachedResponse == null ? "?" : cachedResponse.getGameID();
    }

    public String getID() {
        return ID;
    }

    public String getOwner() {
        return owner;
    }

    public List<String> getAdmins() {
        return admins;
    }

    public User getOwnerObject() {
        if (ownerObject == null)
            ownerObject = Settings.getUserByName(getOwner());
        if (ownerObject == null) {
            for (User user : Settings.SETTINGS.users.values()) {
                if (user.isAdmin()) {
                    ownerObject = user;
                    break;
                }
            }
        }
        return ownerObject;
    }

    public List<String> getCoOwners() {
        return coOwners;
    }

    public File getBackupFolder() {
        return backupFolder;
    }

    public int[] getDiskspaceUse() {
        return size;
    }

    public WorldManager getWorldManager() {
        return worldManager;
    }

    public Map<String, Dimension> getDimensionMap() {
        return dimensionMap;
    }

    public File getFolder() {
        return folder;
    }

    @Override
    public String toString() {
        return getID();
    }

    public RestartingInfo getRestartingInfo() {
        if (restartingInfo == null)
            restartingInfo = new RestartingInfo();
        return restartingInfo;
    }

    public JvmData getJvmData() {
        if (jvmData == null)
            jvmData = new JvmData();
        return jvmData;
    }

    public Collection<String> getPossibleJarnames() {
        LinkedHashSet<String> names = new LinkedHashSet<>();
        names.addAll(Arrays.asList(folder.list(ACCEPT_FORGE_FILTER)));
        names.addAll(Arrays.asList(folder.list(ACCEPT_MINECRAFT_SERVER_FILTER)));
        names.addAll(Arrays.asList(folder.list(ACCEPT_ALL_JAR_FILTER)));
        return names;
    }

    /*
     * ========================================================================================
     * SETTERS
     */

    /**
     * Set a server.properties property and save the file.
     *
     * @param key   the key
     * @param value the value
     * @throws ServerOnlineException when the server is online
     */
    public void setProperty(IMethodCaller caller, String key, String value) throws IOException {
        if (!isCoOwner(caller.getUser()))
            throw new AuthenticationException();
        properties.put(key, value);
        normalizeProperties();
        saveProperties();
    }

    public void setOwner(IMethodCaller methodCaller, String username) {
        if (!isCoOwner(methodCaller.getUser()))
            throw new AuthenticationException();
        ownerObject = null;
        owner = username;
        update();
    }

    public void setServerPort(IMethodCaller caller, int serverPort) throws IOException {
        if (!isCoOwner(caller.getUser()))
            throw new AuthenticationException();
        this.serverPort = serverPort;
        normalizeProperties();
    }

    public void setIP(IMethodCaller caller, String IP) throws IOException {
        if (!isCoOwner(caller.getUser()))
            throw new AuthenticationException();
        this.ip = IP;
        normalizeProperties();
    }

    /*
     * ========================================================================================
     * SETTERS VIA DOWNLOADERS
     */

    /**
     * Remove the old and download the new server jar file
     */
    public void setVersion(final IMethodCaller methodCaller, final String version) {
        if (getOnline())
            throw new ServerOnlineException();
        if (downloading)
            throw new IllegalStateException("Already downloading something.");
        if (!isCoOwner(methodCaller.getUser()))
            throw new AuthenticationException();
        final Server instance = this;
        new Thread(new Runnable() {
            @Override
            public void run() {
                downloading = true;
                try {
                    // delete old files
                    for (File file : folder.listFiles(ACCEPT_MINECRAFT_SERVER_FILTER))
                        file.delete();
                    for (File file : folder.listFiles(ACCEPT_FORGE_FILTER))
                        file.delete();

                    File jarfile = new File(folder, getJvmData().jarName);
                    if (jarfile.exists())
                        jarfile.delete();
                    File tempFile = new File(folder, getJvmData().jarName + ".tmp");

                    // Downloading new file

                    Download download = new Download(new URL(Constants.MC_SERVER_JAR_URL.replace("%ID%", version)),
                            tempFile);

                    long lastTime = System.currentTimeMillis();
                    int lastInfo = 0;

                    while (download.getStatus() == Download.Status.Downloading) {
                        if (download.getSize() != -1) {
                            methodCaller.sendMessage(
                                    String.format("Download is %dMB", (download.getSize() / (1024 * 1024))));
                            printLine(String.format("Download is %dMB", (download.getSize() / (1024 * 1024))));
                            break;
                        }
                        Thread.sleep(10);
                    }

                    methodCaller.sendProgress(0);

                    while (download.getStatus() == Download.Status.Downloading) {
                        if ((download.getProgress() - lastInfo >= 5)
                                || (System.currentTimeMillis() - lastTime > 1000 * 10)) {
                            lastInfo = (int) download.getProgress();
                            lastTime = System.currentTimeMillis();

                            methodCaller.sendProgress(download.getProgress());
                            printLine(String.format("Downloaded %2.0f%% (%dMB / %dMB)", download.getProgress(),
                                    (download.getDownloaded() / (1024 * 1024)),
                                    (download.getSize() / (1024 * 1024))));
                        }

                        Thread.sleep(10);
                    }

                    if (download.getStatus() == Download.Status.Error) {
                        throw new Exception(download.getMessage());
                    }
                    methodCaller.sendDone();

                    tempFile.renameTo(jarfile);
                    instance.update();
                } catch (Exception e) {
                    error(e);
                }
                downloading = false;
            }
        }, getID() + "-jar-downloader").start();
    }

    /**
     * Downloads and uses specific forge installer
     */
    public void installForge(final IMethodCaller methodCaller, final String name) {
        if (getOnline())
            throw new ServerOnlineException();
        final String version = Helper.getForgeVersionForName(name);
        if (version == null)
            throw new IllegalArgumentException("Forge with ID " + name + " not found.");
        if (downloading)
            throw new IllegalStateException("Already downloading something.");
        if (!isCoOwner(methodCaller.getUser()))
            throw new AuthenticationException();
        final Server instance = this;
        new Thread(new Runnable() {
            @Override
            public void run() {
                downloading = true;
                try {
                    // delete old files
                    for (File file : folder.listFiles(ACCEPT_MINECRAFT_SERVER_FILTER))
                        file.delete();
                    for (File file : folder.listFiles(ACCEPT_FORGE_FILTER))
                        file.delete();

                    // download new files
                    String url = Constants.FORGE_INSTALLER_URL.replace("%ID%", version);
                    String forgeName = url.substring(url.lastIndexOf('/'));
                    File forge = new File(folder, forgeName);
                    FileUtils.copyURLToFile(new URL(url), forge);

                    // run installer
                    List<String> arguments = new ArrayList<>();

                    arguments.add(Constants.getJavaPath());
                    arguments.add("-Xmx1G");

                    arguments.add("-jar");
                    arguments.add(forge.getName());

                    arguments.add("--installServer");

                    ProcessBuilder builder = new ProcessBuilder(arguments);
                    builder.directory(folder);
                    builder.redirectErrorStream(true);
                    final Process process = builder.start();
                    printLine(arguments.toString());
                    BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                    String line;
                    while ((line = reader.readLine()) != null) {
                        methodCaller.sendMessage(line);
                        printLine(line);
                    }

                    try {
                        process.waitFor();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    for (String name : folder.list(ACCEPT_FORGE_FILTER))
                        getJvmData().jarName = name;

                    forge.delete();

                    methodCaller.sendDone();
                    printLine("Forge installer done.");

                    instance.update();
                } catch (IOException e) {
                    printLine("##################################################################");
                    printLine("Error installing a new forge version (version " + version + ")");
                    printLine(e.toString());
                    printLine("##################################################################");
                    e.printStackTrace();
                }
                downloading = false;
            }
        }, getID() + "-forge-installer").start();
    }

    public void downloadModpack(final IMethodCaller methodCaller, final String zipURL, final boolean purge)
            throws IOException, ZipException {
        if (!isCoOwner(methodCaller.getUser()))
            throw new AuthenticationException();
        if (downloading)
            throw new IllegalStateException("Already downloading something.");
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    downloading = true;
                    if (purge)
                        for (File file : folder.listFiles(Constants.ACCEPT_ALL_FILTER))
                            if (file.isFile())
                                file.delete();
                            else
                                FileUtils.deleteDirectory(file);
                    if (!folder.exists())
                        folder.mkdirs();

                    final File zip = new File(folder, "modpack.zip");

                    if (zip.exists())
                        zip.delete();
                    zip.createNewFile();

                    printLine("Downloading zip...");

                    Download download = new Download(new URL(URLDecoder.decode(zipURL, "UTF-8")), zip);

                    long lastTime = System.currentTimeMillis();
                    int lastInfo = 0;

                    while (download.getStatus() == Download.Status.Downloading) {
                        if (download.getSize() != -1) {
                            methodCaller.sendMessage(
                                    String.format("Download is %dMB", (download.getSize() / (1024 * 1024))));
                            printLine(String.format("Download is %dMB", (download.getSize() / (1024 * 1024))));
                            break;
                        }
                        Thread.sleep(10);
                    }

                    methodCaller.sendProgress(0);

                    while (download.getStatus() == Download.Status.Downloading) {
                        if ((download.getProgress() - lastInfo >= 5)
                                || (System.currentTimeMillis() - lastTime > 1000 * 10)) {
                            lastInfo = (int) download.getProgress();
                            lastTime = System.currentTimeMillis();

                            methodCaller.sendProgress(download.getProgress());

                            printLine(String.format("Downloaded %2.0f%% (%dMB / %dMB)", download.getProgress(),
                                    (download.getDownloaded() / (1024 * 1024)),
                                    (download.getSize() / (1024 * 1024))));
                        }

                        Thread.sleep(10);
                    }

                    if (download.getStatus() == Download.Status.Error) {
                        throw new Exception(download.getMessage());
                    }

                    printLine("Downloading zip done, extracting...");

                    ZipFile zipFile = new ZipFile(zip);
                    zipFile.setRunInThread(true);
                    zipFile.extractAll(folder.getCanonicalPath());
                    lastTime = System.currentTimeMillis();
                    lastInfo = 0;
                    while (zipFile.getProgressMonitor().getState() == ProgressMonitor.STATE_BUSY) {
                        if (zipFile.getProgressMonitor().getPercentDone() - lastInfo >= 10
                                || System.currentTimeMillis() - lastTime > 1000 * 10) {
                            lastInfo = zipFile.getProgressMonitor().getPercentDone();
                            lastTime = System.currentTimeMillis();

                            printLine(String.format("Extracting %d%%",
                                    zipFile.getProgressMonitor().getPercentDone()));
                        }

                        Thread.sleep(10);
                    }

                    methodCaller.sendProgress(100);
                    methodCaller.sendDone();

                    zip.delete();

                    printLine("Done extracting zip.");
                } catch (Exception e) {
                    printLine("##################################################################");
                    printLine("Error installing the modpack");
                    printLine(e.toString());
                    printLine("##################################################################");
                    e.printStackTrace();
                }
                downloading = false;
            }
        }, getID() + "-modpack-installer").start();
    }

    /*
     * ========================================================================================
     * REMOVE and ADD methods
     */

    public void removeAdmin(IMethodCaller methodCaller, String name) {
        if (!isCoOwner(methodCaller.getUser()))
            throw new AuthenticationException();
        Iterator<String> i = admins.iterator();
        while (i.hasNext()) {
            if (i.next().equalsIgnoreCase(name))
                i.remove();
        }
        update();
    }

    public void addAdmin(IMethodCaller methodCaller, String name) {
        if (!isCoOwner(methodCaller.getUser()))
            throw new AuthenticationException();
        admins.add(name);
        update();
    }

    public void addCoowner(IMethodCaller methodCaller, String name) {
        if (!isCoOwner(methodCaller.getUser()))
            throw new AuthenticationException();
        coOwners.add(name);
        update();
    }

    public void removeCoowner(IMethodCaller methodCaller, String name) {
        if (!isCoOwner(methodCaller.getUser()))
            throw new AuthenticationException();
        Iterator<String> i = coOwners.iterator();
        while (i.hasNext()) {
            if (i.next().equalsIgnoreCase(name))
                i.remove();
        }
        update();
    }

    /*
     * ========================================================================================
     * MAKE or RENEW methods
     */
    public void renewQuery() {
        cachedResponse = getQuery().fullStat();
    }

    private void saveProperties() throws IOException {
        if (!propertiesFile.exists())
            propertiesFile.createNewFile();

        FileOutputStream outputStream = new FileOutputStream(propertiesFile);
        Properties properties = getProperties();
        properties.store(outputStream, "Modified by the backend");
        propertiesFileLastModified = propertiesFile.lastModified();
        outputStream.close();
    }

    /**
     * Start the server in a process controlled by us.
     * Threaded to avoid haning.
     *
     * @throws ServerOnlineException
     */
    public void startServer() throws Exception {
        if (getOnline() || starting)
            throw new ServerOnlineException();
        if (downloading)
            throw new Exception("Still downloading something. You can see the progress in the server console.");
        if (new File(folder, getJvmData().jarName + ".tmp").exists())
            throw new Exception("Minecraft server jar still downloading...");
        if (!new File(folder, getJvmData().jarName).exists())
            throw new FileNotFoundException(getJvmData().jarName + " not found.");
        User user = Settings.getUserByName(getOwner());
        if (user == null)
            throw new Exception("No owner set??");
        if (user.getMaxRamLeft() != -1 && getJvmData().ramMax > user.getMaxRamLeft())
            throw new Exception("Out of usable RAM. Lower your max RAM.");
        saveProperties();
        starting = true;
        final Server instance = this;
        for (String blocked : SERVER_START_ARGS_BLACKLIST_PATTERNS)
            if (getJvmData().extraJavaParameters.contains(blocked))
                throw new Exception("JVM options contain a blocked option: " + blocked);

        File eula = new File(getFolder(), "eula.txt");
        if (!eula.exists()) {
            try {
                FileUtils.writeStringToFile(eula,
                        "#The server owner indicated to agree with the EULA when submitting the from that produced this server instance.\n"
                                + "#That means that there is no need for extra halting of the server startup sequence with this stupid file.\n"
                                + "#" + new Date().toString() + "\n" + "eula=true\n");
            } catch (IOException e) {
                printLine("Error making the eula file....");
                e.printStackTrace();
            }
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                printLine("Starting server ................");
                try {
                    /**
                     * Build arguments list.
                     */
                    List<String> arguments = new ArrayList<>();
                    arguments.add(Constants.getJavaPath());
                    arguments.add("-server");
                    {
                        int amount = getJvmData().ramMin;
                        if (amount > 0)
                            arguments.add(String.format("-Xms%dM", amount));
                        amount = getJvmData().ramMax;
                        if (amount > 0)
                            arguments.add(String.format("-Xmx%dM", amount));
                        amount = getJvmData().permGen;
                        if (amount > 0)
                            arguments.add(String.format("-XX:MaxPermSize=%dm", amount));
                    }
                    if (getJvmData().extraJavaParameters.trim().length() != 0)
                        arguments.add(getJvmData().extraJavaParameters.trim());
                    arguments.add("-jar");
                    arguments.add(getJvmData().jarName);
                    arguments.add("nogui");
                    if (getJvmData().extraMCParameters.trim().length() != 0)
                        arguments.add(getJvmData().extraMCParameters.trim());

                    // Debug printout
                    printLine("Arguments: " + arguments.toString());

                    /**
                     * Make ProcessBuilder, set rundir, and make sure the io gets redirected
                     */
                    ProcessBuilder pb = new ProcessBuilder(arguments);
                    pb.directory(folder);
                    pb.redirectErrorStream(true);
                    if (!new File(folder, getJvmData().jarName).exists())
                        return; // for reasons of WTF?
                    process = pb.start();
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                printLine("----=====##### STARTING SERVER #####=====-----");
                                BufferedReader reader = new BufferedReader(
                                        new InputStreamReader(process.getInputStream()));
                                String line;
                                while ((line = reader.readLine()) != null) {
                                    printLine(line);
                                }
                                printLine("----=====##### SERVER PROCESS HAS ENDED #####=====-----");
                                instance.update();
                            } catch (IOException e) {
                                error(e);
                            }
                        }
                    }, ID.concat("-streamEater")).start();
                    instance.update();
                } catch (IOException e) {
                    error(e);
                }
                starting = false;
            }
        }, "ServerStarter-" + getID()).start(); // <-- Very important call.
    }

    public void printLine(String line) {
        logger.info(line);
        ServerconsoleSocketApplication.sendLine(this, line);
    }

    public void error(Throwable e) {
        logger.error(e);
        StringWriter error = new StringWriter();
        e.printStackTrace(new PrintWriter(error));
        ServerconsoleSocketApplication.sendLine(this, error.toString());
    }

    /**
     * Stop the server gracefully
     */
    public boolean stopServer(String message) {
        if (!getOnline())
            return false;
        try {
            renewQuery();
            printLine("----=====##### STOPPING SERVER WITH WITH KICK #####=====-----");
            for (String user : getPlayerList())
                sendCmd(String.format("kick %s %s", user, message));
            sendCmd("stop");
            return true;
        } catch (Exception e) {
            printLine("----=====##### STOPPING SERVER #####=====-----");
            PrintWriter printWriter = new PrintWriter(process.getOutputStream());
            printWriter.println("stop");
            printWriter.flush();
            return false;
        }
    }

    public boolean forceStopServer() throws Exception {
        if (!getOnline())
            throw new ServerOfflineException();
        printLine("----=====##### KILLING SERVER #####=====-----");
        process.destroy();
        try {
            process.getOutputStream().close();
        } catch (IOException ignored) {
        }
        try {
            process.getErrorStream().close();
        } catch (IOException ignored) {
        }
        try {
            process.getInputStream().close();
        } catch (IOException ignored) {
        }
        return true;
    }

    public boolean canUserControl(User user) {
        if (user == null)
            return false;
        if (user.isAdmin() || user.getUsername().equalsIgnoreCase(getOwner()))
            return true;
        for (String admin : getAdmins())
            if (admin.equalsIgnoreCase(user.getUsername()))
                return true;
        for (String admin : getCoOwners())
            if (admin.equalsIgnoreCase(user.getUsername()))
                return true;
        return false;
    }

    public boolean isCoOwner(User user) {
        if (user == null)
            return false;
        if (user.isAdmin() || user.getUsername().equalsIgnoreCase(getOwner()))
            return true;
        for (String admin : getCoOwners())
            if (admin.equalsIgnoreCase(user.getUsername()))
                return true;
        return false;
    }

    public void delete(final IMethodCaller methodCaller) throws IOException {
        try {
            if (getOnline())
                throw new ServerOnlineException();
            if (!methodCaller.getUser().isAdmin() && methodCaller.getUser() != getOwnerObject())
                throw new AuthenticationException();
            Settings.SETTINGS.servers.remove(getID()); // Needs to happen first because
            FileUtils.deleteDirectory(folder);
            FileUtils.deleteDirectory(backupFolder);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void sendCmd(String s) {
        PrintWriter printWriter = new PrintWriter(process.getOutputStream());
        printWriter.println(s);
        printWriter.flush();
    }

    /*
     * ========================================================================================
     * ========================================================================================
     * PRIVATE METHODS
     */

    private void normalizeProperties() {
        if (!properties.containsKey(SERVER_IP))
            properties.setProperty(SERVER_IP, ip);
        if (!properties.containsKey(SERVER_PORT))
            properties.setProperty(SERVER_PORT, String.valueOf(serverPort));
        if (!properties.containsKey(QUERY_PORT))
            properties.setProperty(QUERY_PORT, String.valueOf(serverPort));

        if (Settings.SETTINGS.fixedPorts) {
            properties.setProperty(SERVER_PORT, String.valueOf(serverPort));
            properties.setProperty(QUERY_PORT, String.valueOf(serverPort));
        } else {
            serverPort = Integer.parseInt(properties.getProperty(SERVER_PORT, String.valueOf(serverPort)));
            properties.setProperty(QUERY_PORT, String.valueOf(serverPort));
        }

        if (Settings.SETTINGS.fixedIP)
            properties.setProperty(SERVER_IP, ip);
        else
            ip = properties.getProperty(SERVER_IP, ip);

        properties.put(QUERY_ENABLE, "true");
        query = null;
        renewQuery();
    }

    private void update() {
        WebSocketHelper.sendServerUpdate(this);
        Settings.save();
    }
}