/* * CommandBook * Copyright (C) 2011 sk89q <http://www.sk89q.com> * * 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 com.sk89q.commandbook.session; import com.sk89q.commandbook.CommandBook; import com.sk89q.commandbook.util.entity.player.UUIDUtil; import com.sk89q.minecraft.util.commands.Command; import com.sk89q.minecraft.util.commands.CommandContext; import com.sk89q.minecraft.util.commands.CommandException; import com.sk89q.util.yaml.YAMLFormat; import com.sk89q.util.yaml.YAMLNode; import com.sk89q.util.yaml.YAMLProcessor; import com.zachsthings.libcomponents.ComponentInformation; import com.zachsthings.libcomponents.bukkit.BukkitComponent; import com.zachsthings.libcomponents.bukkit.YAMLNodeConfigurationNode; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; import java.io.File; import java.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; @ComponentInformation(friendlyName = "Sessions", desc = "Handles player sessions") public class SessionComponent extends BukkitComponent implements Runnable, Listener { public static final long CHECK_FREQUENCY = 60 * 20; private final Map<Class<? extends CommandSender>, Map<String, Map<Class<? extends PersistentSession>, PersistentSession>>> sessions = new ConcurrentHashMap<Class<? extends CommandSender>, Map<String, Map<Class<? extends PersistentSession>, PersistentSession>>>(); private final Map<Class<? extends PersistentSession>, SessionFactory<?>> sessionFactories = new ConcurrentHashMap<Class<? extends PersistentSession>, SessionFactory<?>>(); private File sessionsDir; private final Map<String, Map<String, YAMLProcessor>> sessionDataStores = new HashMap<String, Map<String, YAMLProcessor>>(); @Override public void enable() { CommandBook.server().getScheduler().scheduleSyncRepeatingTask(CommandBook.inst(), this, CHECK_FREQUENCY, CHECK_FREQUENCY); CommandBook.registerEvents(this); registerCommands(Commands.class); sessionsDir = new File(CommandBook.inst().getDataFolder(), "sessions"); if (!sessionsDir.exists()) { sessionsDir.mkdirs(); } } @Override public void disable() { for (Player player : CommandBook.server().getOnlinePlayers()) { String type = getType(player.getClass()); for (PersistentSession session : getSessions(player)) { session.handleDisconnect(); session.save(new YAMLNodeConfigurationNode(getSessionConfiguration(type, UUIDUtil.toUniqueString(player), session.getClass()))); } YAMLProcessor proc = getUserConfiguration(type, UUIDUtil.toUniqueString(player), false); if (proc != null) { proc.save(); } } } // -- Getting sessions /** * Get a session. * * @param user The user to get a session for * @return The user's session * @deprecated see {@link #getSession(Class, org.bukkit.command.CommandSender)} with args (UserSession.class, user) */ @Deprecated public UserSession getSession(CommandSender user) { return getSession(UserSession.class, user); } /** * Get sessions. * * @return UserSessions * @deprecated use {@link #getSessions(Class)} with UserSession.class */ @Deprecated public Map<String, UserSession> getSessions() { return getSessions(UserSession.class); } /** * Get a session. * * @param user The player to get this session for * @return The user's session * @deprecated see {@link #getSession(Class, org.bukkit.command.CommandSender)} with args (AdministrativeSession.class, user) */ @Deprecated public AdministrativeSession getAdminSession(Player user) { return getSession(AdministrativeSession.class, user); } /** * Get sessions. * * @return Administrative sessions which currently exist * @deprecated use {@link #getSessions(Class)} with UserSession.class */ @Deprecated public Map<String, AdministrativeSession> getAdminSessions() { return getSessions(AdministrativeSession.class); } /** * Return all the currently registered sessions of the given type * @param type The type of session to get * @param <T> The type parameter for the session * @return The currently registered session types */ public <T extends PersistentSession> Map<String, T> getSessions(Class<T> type) { Map<String, T> ret = new HashMap<String, T>(); synchronized (sessions) { for (Map<String, Map<Class<? extends PersistentSession>, PersistentSession>> parent : sessions.values()) { for (Map.Entry<String, Map<Class<? extends PersistentSession>, PersistentSession>> entry : parent.entrySet()) { PersistentSession session = entry.getValue().get(type); if (session != null) { ret.put(entry.getKey(), type.cast(session)); } } } } return ret; } /** * Return the sessions which currently exist for the specified user * @param user The user to get a session for * @return The sessions which currently exist for this user */ public Collection<PersistentSession> getSessions(CommandSender user) { Map<Class<? extends PersistentSession>, PersistentSession> ret = getSessionM(user.getClass()).get(UUIDUtil.toUniqueString(user)); if (ret == null) { ret = Collections.emptyMap(); } return Collections.unmodifiableCollection(ret.values()); } /** * Gets the session of type for user, creating a new instance if none currently exists * * @see #getSessionFactory(Class) * @param type The type of session to get * @param user The user to get the session for * @param <T> The type of session * @return The player's session, or null if the session could not be correctly created */ public <T extends PersistentSession> T getSession(Class<T> type, CommandSender user) { synchronized (sessions) { Map<String, Map<Class<? extends PersistentSession>, PersistentSession>> typeMap = getSessionM(user.getClass()); Map<Class<? extends PersistentSession>, PersistentSession> userSessions = typeMap.get(UUIDUtil.toUniqueString(user)); if (userSessions == null) { userSessions = new HashMap<Class<? extends PersistentSession>, PersistentSession>(); typeMap.put(UUIDUtil.toUniqueString(user), userSessions); } // Do we have an existing session? T session = type.cast(userSessions.get(type)); if (session == null) { session = getSessionFactory(type).createSession(user); if (session != null) { YAMLNode node = getSessionConfiguration(getType(user.getClass()), UUIDUtil.toUniqueString(user), type, false); if (node != null) { session.load(new YAMLNodeConfigurationNode(node)); } session.handleReconnect(user); userSessions.put(type, session); } } return session; } } /** * Return a SessionFactory used to create new instances of a certain type of session. * If no SessionFactory has been previously registered with {@link #registerSessionFactory(Class, SessionFactory)}, * a new {@link ReflectiveSessionFactory} will be instantiated. This will only create * sessions for PersistentSession subclasses with an empty constructor. * @param type The subclass of PersistentSession to get a SessionFactory for * @param <T> The type of PersistentSession * @return The required SessionFactory */ @SuppressWarnings("unchecked") public <T extends PersistentSession> SessionFactory<T> getSessionFactory(Class<T> type) { synchronized (sessionFactories) { SessionFactory<?> factory = sessionFactories.get(type); if (factory == null) { factory = new ReflectiveSessionFactory(type); sessionFactories.put(type, factory); } return (SessionFactory<T>) factory; } } public <T extends PersistentSession> void registerSessionFactory(Class<T> type, SessionFactory<T> factory) { sessionFactories.put(type, factory); } /** * Add {@code session} to the sessions list for {@code user}, overwriting any sessions with the same class. * @param session The session to add * @param user The user to add the session to */ public void addSession(PersistentSession session, CommandSender user) { Map<String, Map<Class<? extends PersistentSession>, PersistentSession>> typeMap = getSessionM(user.getClass()); Map<Class<? extends PersistentSession>, PersistentSession> userSessions = typeMap.get(UUIDUtil.toUniqueString(user)); if (userSessions == null) { userSessions = new HashMap<Class<? extends PersistentSession>, PersistentSession>(); typeMap.put(UUIDUtil.toUniqueString(user), userSessions); } userSessions.put(session.getClass(), session); } // Persistence-related methods private YAMLProcessor getUserConfiguration(String type, String commander, boolean create) { Map<String, YAMLProcessor> typeMap = getDataStore(type); YAMLProcessor processor = typeMap.get(commander); if (processor == null) { File userFile = new File(sessionsDir.getPath() + File.separator + type + File.separator + commander + ".yml"); if (!userFile.exists()) { File dir = userFile.getParentFile(); if (!dir.exists()) { dir.mkdirs(); } if (!migrate(commander, userFile)) { if (!create) { return null; } try { userFile.createNewFile(); } catch (IOException e) { CommandBook.logger().log(Level.WARNING, "Could not create sessions persistence file for user " + commander, e); } } } processor = new YAMLProcessor(userFile, false, YAMLFormat.COMPACT); try { processor.load(); } catch (IOException e) { CommandBook.logger().log(Level.WARNING, "Error loading sessions persistence file for user " + commander, e); } typeMap.put(commander, processor); } return processor; } private YAMLNode getSessionConfiguration(String type, String commander, Class<? extends PersistentSession> sessType) { return getSessionConfiguration(type, commander, sessType, true); } private YAMLNode getSessionConfiguration(String type, String commander, Class<? extends PersistentSession> sessType, boolean create) { YAMLProcessor proc = getUserConfiguration(type, commander, create); if (proc == null) { return null; } String className = sessType.getCanonicalName().replaceAll("\\.", "/"); YAMLNode sessionNode = proc.getNode(className); if (sessionNode == null && create) { sessionNode = proc.addNode(className); } return sessionNode; } // - Migration Functions private boolean migrate(String commander, File dest) { boolean result = false; try { // Try to parse the commander as a player's UUID OfflinePlayer player = Bukkit.getOfflinePlayer(UUID.fromString(commander)); if (player != null) { // A player was found, see if they have an old file based on their name to migrate File oldUserFile = new File(sessionsDir.getPath() + File.separator + player.getName() + ".yml"); if (oldUserFile.exists()) { // Move the file, and print an error if the move operation failed result = oldUserFile.renameTo(dest); if (!result) { CommandBook.logger().warning("Could not update a player's session file to use UUID: " + commander); } } } } catch (IllegalArgumentException ignored) { } // Wasn't a player return result; } // - Utility Functions private String getType(Class<? extends CommandSender> clazz) { String[] split = clazz.getName().split("\\."); return split[split.length - 1]; } private Map<String, Map<Class<? extends PersistentSession>, PersistentSession>> getSessionM(Class<? extends CommandSender> clazz) { Map<String, Map<Class<? extends PersistentSession>, PersistentSession>> typeMapping = sessions.get(clazz); if (typeMapping == null) { typeMapping = new HashMap<String, Map<Class<? extends PersistentSession>, PersistentSession>>(); sessions.put(clazz, typeMapping); } return typeMapping; } private Map<String, YAMLProcessor> getDataStore(String type) { Map<String, YAMLProcessor> typeMap = sessionDataStores.get(type); if (typeMap == null) { typeMap = new HashMap<String, YAMLProcessor>(); sessionDataStores.put(type, typeMap); } return typeMap; } // -- Garbage collection public void run() { synchronized (sessions) { for (Map.Entry<Class<? extends CommandSender>, Map<String, Map<Class<? extends PersistentSession>, PersistentSession>>> parent : sessions.entrySet()) { outer: for (Iterator<Map.Entry<String, Map<Class<? extends PersistentSession>, PersistentSession>>> i = parent.getValue().entrySet().iterator(); i.hasNext(); ) { Map.Entry<String, Map<Class<? extends PersistentSession>, PersistentSession>> entry = i.next(); for (Iterator<PersistentSession> i2 = entry.getValue().values().iterator(); i2.hasNext(); ) { PersistentSession sess = i2.next(); if (sess.getOwner() != null) { continue outer; } if (!sess.isRecent()) { i2.remove(); String sender = sess.getUniqueName(); if (sender != null) { YAMLProcessor processor = getUserConfiguration(getType(parent.getKey()), sender, false); if (processor != null) { processor.removeProperty(sess.getClass().getCanonicalName().replaceAll("\\.", "/")); } } } } if (entry.getValue().size() == 0) { i.remove(); } } } } } // -- Events @EventHandler(priority = EventPriority.LOWEST) public void onLogin(PlayerLoginEvent event) { Player player = event.getPlayer(); String type = getType(player.getClass()); // Trigger the session for (PersistentSession session : getSessions(player)) { session.load(new YAMLNodeConfigurationNode(getSessionConfiguration(type, UUIDUtil.toUniqueString(player), session.getClass()))); session.handleReconnect(event.getPlayer()); } } /** * Called on player disconnect. * * @param event Relevant event details */ @EventHandler(priority = EventPriority.MONITOR) public void onPlayerQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); String type = getType(player.getClass()); for (PersistentSession session : getSessions(event.getPlayer())) { session.handleDisconnect(); session.save(new YAMLNodeConfigurationNode(getSessionConfiguration(type, UUIDUtil.toUniqueString(player), session.getClass()))); } YAMLProcessor proc = getUserConfiguration(type, UUIDUtil.toUniqueString(player), false); if (proc != null) { proc.save(); } } public class Commands { @Command(aliases = {"confirm", "conf"}, desc = "Confirm an action", max = 0, flags = "vc") public void confirm(CommandContext args, CommandSender sender) throws CommandException { UserSession session = getSession(UserSession.class, sender); final String cmd = session.getCommandToConfirm(false); if (cmd == null) throw new CommandException("No command to confirm!"); if (args.hasFlag('v')) { sender.sendMessage(ChatColor.YELLOW + "Current command to confirm: " + cmd); } else if (args.hasFlag('c')) { session.getCommandToConfirm(true); sender.sendMessage(ChatColor.YELLOW + "Cleared command to confirm"); } else { sender.sendMessage(ChatColor.YELLOW + "Command confirmed: " + cmd); CommandBook.server().dispatchCommand(sender, cmd); session.getCommandToConfirm(true); } } } }