/*
* ExperienceMod - Bukkit server plugin for modifying the experience system in Minecraft.
* Copyright (C) 2012 Kristian S. Stangeland
*
* 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.xp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import net.milkbowl.vault.chat.Chat;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import com.comphenix.xp.listeners.PlayerCleanupListener;
import com.comphenix.xp.lookup.PresetQuery;
import com.comphenix.xp.lookup.PresetTree;
import com.comphenix.xp.parser.ParsingException;
import com.comphenix.xp.parser.primitives.StringParser;
import com.comphenix.xp.parser.text.ParameterParser;
import com.comphenix.xp.parser.text.PresetParser;
import com.google.common.base.Optional;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.collect.Lists;
/**
* Contains every loaded configuration preset.
*/
public class Presets implements PlayerCleanupListener {
public static final String OPTION_PRESET_SETTING = "experiencePreset";
private static final String IMPORT_FILE_SETTING = "file";
private static final String LOCAL_SETTING = "local";
// MineCraft servers with more than 1000 players? Ridiculous.
private static final int MAXIMUM_CACHE_SIZE = 1000;
// We can reference players directly as the Guava Cache is using WeakReferences under the hood
private Cache<Player, Optional<Configuration>> configCache;
private Supplier<Configuration> defaultCached;
// Mapping of preset name and configuration
private PresetTree presets;
// Parser
private PresetParser presetParser = new PresetParser();
private ParameterParser<String> stringParser = new ParameterParser<String>(new StringParser());
private Debugger logger;
// Chat
private Chat chat;
public Presets(ConfigurationSection config, ConfigurationLoader loader,
int cacheTimeout, Debugger logger, Chat chat) {
this.presets = new PresetTree();
this.logger = logger;
this.chat = chat;
if (config != null) {
loadPresets(config, loader);
}
initializeCache(cacheTimeout);
}
private void initializeCache(int cacheTimeout) {
// Don't create a cache if we're just going to throw away the values all the time
if (cacheTimeout > 0) {
// Construct our cache
configCache = CacheBuilder.newBuilder().
weakKeys().
weakValues().
maximumSize(MAXIMUM_CACHE_SIZE).
expireAfterWrite(cacheTimeout, TimeUnit.SECONDS).
build(new CacheLoader<Player, Optional<Configuration>>() {
@Override
public Optional<Configuration> load(Player player) throws Exception {
return Optional.fromNullable(getPlayerConfiguration(player));
}
});
}
// Cache the default configuration too
defaultCached = Suppliers.memoizeWithExpiration(new Supplier<Configuration>() {
@Override
public Configuration get() {
try {
return getConfiguration(null, null);
} catch (ParsingException e) {
throw new RuntimeException("Parsing problem.", e);
}
}
}, cacheTimeout, TimeUnit.SECONDS);
}
/**
* Retrieves a stored configuration from a key value. Note that while NULL in rules will match
* any query, a query with null will NOT match any rule. In that case, it will only match rules with
* null in the corresponding parameters.
*
* @param presetNames - key value(s) of the configuration to retrieve.
* @param worldName - name of the world the preset is associated with.
* @return The stored configuration, or NULL if no configuration exists.
* @throws ParsingException If the given list of keys is malformed.
*/
public Configuration getConfiguration(String presetNames, String worldName) throws ParsingException {
List<String> names = stringParser.parseExact(presetNames);
PresetQuery query = PresetQuery.fromExact(names, worldName);
Configuration result = presets.get(query);
// Determine what to return
if (result != null) {
return result;
}
// Error
return null;
}
/**
* Retrieves the default configuration for the given player.
* @param sender - the sender, or NULL to retrieve the generic/default configuration.
* @return Configuration, or NULL if no configuration could be found.
* @throws ParsingException - If the sender has a malformed preset list.
*/
public Configuration getConfiguration(CommandSender sender) throws ParsingException {
// See if we in fact can use presets
if (chat != null && sender instanceof Player) {
// Use the cache if it's present
if (configCache != null) {
try {
return configCache.get((Player) sender).orNull();
} catch (Exception e) {
throw new ParsingException("Cannot load configuration.", e);
}
} else {
return getPlayerConfiguration((Player) sender);
}
// Default configuration
} else {
try {
return defaultCached.get();
// Catch our runtime error
} catch (RuntimeException e) {
if (e.getCause() instanceof ParsingException)
throw (ParsingException) e.getCause();
else
throw e;
}
}
}
private Configuration getPlayerConfiguration(Player sender) throws ParsingException {
Player player = (Player) sender;
String preset = null;
String world = null;
try {
preset = chat.getPlayerInfoString(player, OPTION_PRESET_SETTING, null);
} catch (RuntimeException e) {
// Must be a runtime exception, otherwise we'd have to handle it from the method above.
if (!ignorableException(e)) {
throw e;
} else {
logger.printDebug(this, "Ignored NPE from mChat.");
}
}
world = player.getWorld().getName();
return getConfiguration(preset, world);
}
public boolean usesPresetParameters() {
return presets.usesPresetNames();
}
private void loadPresets(ConfigurationSection section, ConfigurationLoader loader) {
for (String key : section.getKeys(false)) {
// Load query
try {
PresetQuery query = presetParser.parse(key);
// Load section
Configuration data = loadPreset(
section.getConfigurationSection(key), loader);
// Remember if we have presets or not
if (data != null) {
data.setPreset(query.hasPresetNames());
presets.put(query, data);
}
} catch (ParsingException ex) {
if (logger != null)
logger.printWarning(this, "Cannot parse preset - %s", ex.getMessage());
}
}
}
private Configuration loadPreset(ConfigurationSection data, ConfigurationLoader loader) {
List<Configuration> files = getConfigurations(data, loader);
Configuration local = getLocal(data, loader);
Configuration result = null;
// Local configuration has the highest priority
if (local != null) {
files.add(local);
}
// Make sure there is anything to return
if (files.isEmpty())
result = new Configuration(logger, loader.getActionTypes());
else
result = Configuration.fromMultiple(files, logger);
return result;
}
public boolean containsPreset(String preset, String world) throws ParsingException {
Configuration result = getConfiguration(preset, world);
// Whether or not the matching rule has specified presets
return result.hasPreset();
}
private Configuration getLocal(ConfigurationSection data, ConfigurationLoader loader) {
// Retrieve using the configuration section
if (data.isConfigurationSection(LOCAL_SETTING)) {
return loader.getFromSection(data.getConfigurationSection(LOCAL_SETTING));
} else {
return null;
}
}
private List<Configuration> getConfigurations(ConfigurationSection data, ConfigurationLoader loader) {
List<Configuration> result = new ArrayList<Configuration>();
for (String path : getFiles(data)) {
// Load from folder
Configuration config = loader.getFromPath(path);
if (config != null)
result.add(config);
else if (logger != null)
logger.printWarning(this, "Cannot find configuration file %s.", path);
}
return result;
}
private List<String> getFiles(ConfigurationSection data) {
if (data.isString(IMPORT_FILE_SETTING))
return Lists.newArrayList(data.getString(IMPORT_FILE_SETTING));
else if (data.isList(IMPORT_FILE_SETTING))
return data.getStringList(IMPORT_FILE_SETTING);
else
return Lists.newArrayList();
}
public Collection<Configuration> getConfigurations() {
return presets.getValues();
}
public void onTick() {
// Make sure messages are being sent
if (presets != null) {
for (Configuration config : presets.getValues()) {
config.onTick();
}
}
}
@Override
public void removePlayerCache(Player player) {
// Make sure messages are being sent
if (presets != null) {
for (Configuration config : presets.getValues()) {
config.removePlayerCache(player);
}
}
}
/**
* Determines whether or not the given exception can be ignored if thrown from getGroupInfoString.
* @param e - the exception to test.
* @see net.milkbowl.vault.chat.Chat#getGroupInfoString(String, String, String, String) Chat.getGroupInfoString
* @return TRUE if it can be ignored, FALSE otherwise.
*/
public boolean ignorableException(Exception e) {
// This plugin insists on using NullPointerExceptions as a good flag for "not found". Excellent.
if (e instanceof NullPointerException && chat.getName().equals("mChatSuite"))
return true;
if (e instanceof UnsupportedOperationException && chat.getName().equals("iChat"))
return true;
// Treat it normally
return false;
}
}