io.github.lucaseasedup.logit.account.AccountManager.java Source code

Java tutorial

Introduction

Here is the source code for io.github.lucaseasedup.logit.account.AccountManager.java

Source

/*
 * AccountManager.java
 *
 * Copyright (C) 2012-2014 LucasEasedUp
 *
 * 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 <http://www.gnu.org/licenses/>.
 */
package io.github.lucaseasedup.logit.account;

import static io.github.lucaseasedup.logit.message.MessageHelper.t;
import io.github.lucaseasedup.logit.CancelledState;
import io.github.lucaseasedup.logit.LogItCoreObject;
import io.github.lucaseasedup.logit.common.QueuedMap;
import io.github.lucaseasedup.logit.common.ReportedException;
import io.github.lucaseasedup.logit.config.TimeUnit;
import io.github.lucaseasedup.logit.logging.CustomLevel;
import io.github.lucaseasedup.logit.session.SessionManager;
import io.github.lucaseasedup.logit.storage.Infix;
import io.github.lucaseasedup.logit.storage.Selector;
import io.github.lucaseasedup.logit.storage.SelectorCondition;
import io.github.lucaseasedup.logit.storage.SelectorConstant;
import io.github.lucaseasedup.logit.storage.Storage;
import io.github.lucaseasedup.logit.storage.Storage.Entry.Datum;
import io.github.lucaseasedup.logit.storage.StorageObserver;
import io.github.lucaseasedup.logit.storage.WrapperStorage;
import io.github.lucaseasedup.logit.util.CollectionUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import org.apache.commons.lang.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;

public final class AccountManager extends LogItCoreObject implements Runnable {
    /**
     * Constructs a new {@code AccountManager}.
     * 
     * @param storage the storage that this {@code AccountManager} will operate on.
     * @param unit    the name of a unit eligible for account storage.
     * @param keys    the account keys present in the specified unit.
     */
    public AccountManager(WrapperStorage storage, String unit, AccountKeys keys) {
        if (storage == null || unit == null || keys == null)
            throw new IllegalArgumentException();

        try {
            if (!storage.isConnected()) {
                throw new IllegalStateException("isConnected() returned false");
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }

        storage.addObserver(new StorageObserver() {
            @Override
            public void beforeClose() {
                flushBuffer();
            }
        });

        this.storage = storage;
        this.unit = unit;
        this.keys = keys;
    }

    @Override
    public void dispose() {
        storage = null;
        unit = null;
        keys = null;

        if (pingerTask != null) {
            pingerTask.cancel();
            pingerTask = null;
        }

        if (buffer != null) {
            buffer.clear();
            buffer = null;
        }
    }

    /**
     * Internal method. Do not call directly.
     */
    @Override
    public void run() {
        if (pingerTask == null) {
            pingerTask = new BukkitRunnable() {
                @Override
                public void run() {
                    try {
                        storage.ping();
                    } catch (IOException ex) {
                        log(Level.WARNING, "Could not ping the database.", ex);
                    }
                }
            }.runTaskTimer(getPlugin(), 20L, TimeUnit.MINUTES.convert(5, TimeUnit.TICKS));
        }

        flushBuffer();
    }

    /**
     * Selects an account with the given username from the underlying storage unit.
     * 
     * @param username  the username of an account to be selected.
     * @param queryKeys the account keys to be returned by this query.
     * 
     * @return an {@code Account} object, or {@code null}
     *         if there was no account with the given username
     *         or an I/O error occurred.
     * 
     * @throws IllegalArgumentException if {@code username} or
     *                                  {@code queryKeys} is {@code null}.
     * 
     * @throws ReportedException        if an I/O error occurred,
     *                                  and it was reported to the logger.
     */
    public Account selectAccount(String username, List<String> queryKeys) {
        if (username == null || queryKeys == null)
            throw new IllegalArgumentException();

        if (!queryKeys.contains(keys.username()))
            throw new IllegalArgumentException("Missing query key: username");

        username = username.toLowerCase();

        Account cachedAccount = null;

        // If the buffer contains some information about this account.
        if (buffer.containsKey(username)) {
            cachedAccount = buffer.get(username);

            // The account is known not to exist.
            if (cachedAccount == null) {
                return null;
            }
            // The account exists in the buffer.
            else {
                // All the query keys can be found in the cached entry.
                if (CollectionUtils.isSubset(queryKeys, cachedAccount.getEntry().getKeys())) {
                    return cachedAccount;
                }
                // Some keys need to be fetched from the storage
                // in order to fulfill the selection request.
                else {
                    // Remove the keys that have already been fetched;
                    // we only need those that hasn't been.
                    queryKeys = new ArrayList<>(queryKeys);
                    queryKeys.removeAll(cachedAccount.getEntry().getKeys());

                    // If the username key has been removed
                    // (actually, it is always the case),
                    // then put it back into the key list.
                    if (!queryKeys.contains(keys.username())) {
                        queryKeys.add(keys.username());
                    }
                }
            }
        }

        List<Storage.Entry> entries = null;

        try {
            entries = storage.selectEntries(unit, queryKeys,
                    new SelectorCondition(keys.username(), Infix.EQUALS, username));
        } catch (IOException ex) {
            log(Level.WARNING, ex);

            ReportedException.throwNew(ex);
        }

        // If an I/O error occurred, return null.
        if (entries == null)
            return null;

        // If no such account exists in the storage,
        // mark it in the buffer as non-existing and return null.
        if (entries.isEmpty()) {
            buffer.put(username, null);

            return null;
        }

        // If the account is just partially cached,
        // fill the missing keys with the values fetched from the storage.
        if (cachedAccount != null) {
            for (Datum datum : entries.get(0)) {
                if (!cachedAccount.getEntry().containsKey(datum.getKey())) {
                    cachedAccount.getEntry().put(datum.getKey(), datum.getValue());
                    cachedAccount.getEntry().clearKeyDirty(datum.getKey());
                }
            }
        }

        // If there was no cached account in the buffer,
        // create a new Account object for it and put it into the buffer.
        if (!buffer.containsKey(username)) {
            cachedAccount = new Account(entries.get(0), false);

            buffer.put(username, cachedAccount);
        }

        return cachedAccount;
    }

    public boolean isRegistered(String username) {
        if (StringUtils.isBlank(username))
            throw new IllegalArgumentException();

        return selectAccount(username, Arrays.asList(keys.username())) != null;
    }

    public List<Account> selectAccounts(List<String> queryKeys, Selector selector) {
        if (queryKeys == null || selector == null)
            throw new IllegalArgumentException();

        if (!queryKeys.contains(keys.username()))
            throw new IllegalArgumentException("Missing query key: username");

        List<Storage.Entry> entries = null;

        try {
            entries = storage.selectEntries(unit, queryKeys, selector);
        } catch (IOException ex) {
            log(Level.WARNING, ex);

            ReportedException.throwNew(ex);
        }

        if (entries == null)
            return null;

        List<Account> accounts = new ArrayList<>(entries.size());

        for (Storage.Entry entry : entries) {
            String username = entry.get(keys().username());

            if (buffer.get(username) != null) {
                for (Datum datum : buffer.get(username).getEntry()) {
                    entry.put(datum.getKey(), datum.getValue());
                }
            }

            Account account = new Account(entry, false);

            if (buffer.get(username) == null) {
                buffer.put(username, account);
            }

            accounts.add(account);
        }

        return accounts;
    }

    /**
     * Returns all registered usernames in this {@code AccountManager}.
     *
     * @return A {@code Set} containing all registered usernames lowercase, or
     *         {@code null} if an I/O error occurred.
     *
     * @throws ReportedException
     *        If an I/O error occurred, and it was reported to the logger.
     */
    public Set<String> getRegisteredUsernames() {
        List<Account> accounts = getAccountManager().selectAccounts(Arrays.asList(keys().username()),
                new SelectorConstant(true));

        if (accounts == null)
            return null;

        Set<String> usernames = new LinkedHashSet<>();

        for (Account account : accounts) {
            usernames.add(account.getUsername());
        }

        return usernames;
    }

    public CancelledState insertAccount(Account account) {
        if (account == null)
            throw new IllegalArgumentException();

        AccountEvent event = new AccountInsertEvent(account.getEntry());

        Bukkit.getPluginManager().callEvent(event);

        if (event.isCancelled())
            return CancelledState.CANCELLED;

        try {
            Storage.Entry entry = account.getEntry();

            storage.addEntry(unit, entry);

            for (Datum datum : entry) {
                entry.clearKeyDirty(datum.getKey());
            }

            buffer.remove(account.getUsername());
            buffer.put(account.getUsername(), account);

            log(Level.WARNING, t("createAccount.success.log").replace("{0}", account.getUsername()));

            event.executeSuccessTasks();
        } catch (IOException ex) {
            log(Level.WARNING, t("createAccount.fail.log").replace("{0}", account.getUsername()), ex);

            event.executeFailureTasks();

            ReportedException.throwNew(ex);
        }

        return CancelledState.NOT_CANCELLED;
    }

    public void insertAccounts(Account... accounts) {
        if (accounts == null)
            throw new IllegalArgumentException();

        try {
            storage.setAutobatchEnabled(true);

            for (Account account : accounts) {
                insertAccount(account);
            }

            storage.executeBatch();
            storage.clearBatch();
        } catch (IOException ex) {
            log(Level.WARNING, ex);

            ReportedException.throwNew(ex);
        } finally {
            storage.setAutobatchEnabled(false);
        }
    }

    public void renameAccount(String username, String newUsername) {
        if (StringUtils.isBlank(username) || StringUtils.isBlank(newUsername)) {
            throw new IllegalArgumentException();
        }

        username = username.toLowerCase();
        newUsername = newUsername.toLowerCase();

        try {
            storage.updateEntries(unit, new Storage.Entry.Builder().put(keys().username(), newUsername).build(),
                    new SelectorCondition(keys.username(), Infix.EQUALS, username));

            if (buffer.get(username) != null) {
                buffer.put(newUsername, buffer.remove(username));
            }
        } catch (IOException ex) {
            log(Level.WARNING, ex);

            ReportedException.throwNew(ex);
        }
    }

    /**
     * Removes an account with the given username from the underlying storage unit.
     * 
     * <p> Removing an account does not entail logging out the corresponding player.
     * To log out a player, use {@link SessionManager#endSession(String)}
     * or {@link SessionManager#endSession(Player)}.
     * 
     * <p> This method emits the {@code AccountRemoveEvent} event.
     * 
     * @param username the username of an account to be removed.
     * 
     * @return a {@code CancellableState} indicating whether this operation
     *         has been cancelled by one of the {@code AccountRemoveEvent} handlers.
     * 
     * @throws IllegalArgumentException if {@code username} is {@code null} or blank.
     * 
     * @throws ReportedException        if an I/O error occurred,
     *                                  and it was reported to the logger.
     */
    public CancelledState removeAccount(String username) {
        if (StringUtils.isBlank(username))
            throw new IllegalArgumentException();

        username = username.toLowerCase();

        AccountEvent event = new AccountRemoveEvent(username);

        Bukkit.getPluginManager().callEvent(event);

        if (event.isCancelled())
            return CancelledState.CANCELLED;

        try {
            storage.removeEntries(unit, new SelectorCondition(keys.username(), Infix.EQUALS, username));

            buffer.put(username, null);

            log(Level.WARNING, t("removeAccount.success.log").replace("{0}", username));

            event.executeSuccessTasks();
        } catch (IOException ex) {
            log(Level.WARNING, t("removeAccount.fail.log").replace("{0}", username), ex);

            event.executeFailureTasks();

            ReportedException.throwNew(ex);
        }

        return CancelledState.NOT_CANCELLED;
    }

    public void removeAccounts(String... usernames) {
        if (usernames == null)
            throw new IllegalArgumentException();

        try {
            storage.setAutobatchEnabled(true);

            for (String username : usernames) {
                removeAccount(username);
            }

            storage.executeBatch();
            storage.clearBatch();
        } catch (IOException ex) {
            log(Level.WARNING, ex);

            ReportedException.throwNew(ex);
        } finally {
            storage.setAutobatchEnabled(false);
        }
    }

    private void flushBuffer() {
        if (buffer == null || buffer.isEmpty())
            return;

        if (storage == null)
            return;

        Map<String, Account> dirtyAccounts = new HashMap<>();
        QueuedMap<String, Storage.Entry> dirtyEntries = new QueuedMap<>();

        while (!buffer.isEmpty()) {
            Map.Entry<String, Account> e = buffer.remove();

            if (e.getValue() == null)
                continue;

            Storage.Entry dirtyEntry = e.getValue().getEntry().copyDirty();

            if (!dirtyEntry.getKeys().isEmpty()) {
                dirtyAccounts.put(e.getKey(), e.getValue());
                dirtyEntries.put(e.getKey(), dirtyEntry);
            }
        }

        if (dirtyEntries.isEmpty())
            return;

        log(CustomLevel.INTERNAL,
                "AccountManager#flushBuffer() {" + "dirtyEntries.size() = " + dirtyEntries.size() + "}");

        try {
            storage.setAutobatchEnabled(true);

            for (Map.Entry<String, Storage.Entry> e : dirtyEntries.entrySet()) {
                try {
                    storage.updateEntries(unit, e.getValue(),
                            new SelectorCondition(keys.username(), Infix.EQUALS, e.getKey().toLowerCase()));

                    dirtyAccounts.get(e.getKey()).runSaveCallbacks(true);
                } catch (IOException ex) {
                    log(Level.WARNING, ex);

                    dirtyAccounts.get(e.getKey()).runSaveCallbacks(false);
                }
            }

            storage.executeBatch();
            storage.clearBatch();
        } catch (IOException ex) {
            log(Level.WARNING, ex);
        } finally {
            storage.setAutobatchEnabled(false);
        }

        log(CustomLevel.INTERNAL, "end-of #flushBuffer()");
    }

    private void discardBuffer() {
        buffer.clear();
    }

    public Storage getStorage() {
        return storage;
    }

    public String getUnit() {
        return unit;
    }

    public AccountKeys getKeys() {
        return keys;
    }

    private Storage storage;
    private String unit;
    private AccountKeys keys;
    private BukkitTask pingerTask;
    private QueuedMap<String, Account> buffer = new QueuedMap<>();
}