#1015 Implement lazy replace of tags for welcome message

- Check which tags are used when loading the welcome message and only apply their replacements afterwards
- Moves AuthMe#replaceAllInfo to a more appropriate place
- Avoid fetching data again for each line
This commit is contained in:
ljacqu 2017-01-21 13:53:11 +01:00
parent 666e242212
commit 811bdee128
7 changed files with 311 additions and 67 deletions

View File

@ -27,7 +27,6 @@ import fr.xephi.authme.permission.PermissionsSystemType;
import fr.xephi.authme.security.crypts.SHA256;
import fr.xephi.authme.service.BackupService;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.GeoIpService;
import fr.xephi.authme.service.MigrationService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.PluginSettings;
@ -35,11 +34,9 @@ import fr.xephi.authme.settings.properties.RestrictionSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import fr.xephi.authme.task.CleanupTask;
import fr.xephi.authme.task.purge.PurgeService;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
@ -72,8 +69,6 @@ public class AuthMe extends JavaPlugin {
private DataSource database;
private BukkitService bukkitService;
private Injector injector;
private GeoIpService geoIpService;
private PlayerCache playerCache;
/**
* Constructor.
@ -242,14 +237,13 @@ public class AuthMe extends JavaPlugin {
*/
protected void instantiateServices(Injector injector) {
// PlayerCache is still injected statically sometimes
playerCache = PlayerCache.getInstance();
PlayerCache playerCache = PlayerCache.getInstance();
injector.register(PlayerCache.class, playerCache);
database = injector.getSingleton(DataSource.class);
permsMan = injector.getSingleton(PermissionsManager.class);
bukkitService = injector.getSingleton(BukkitService.class);
commandHandler = injector.getSingleton(CommandHandler.class);
geoIpService = injector.getSingleton(GeoIpService.class);
// Trigger construction of API classes; they will keep track of the singleton
injector.getSingleton(NewAPI.class);
@ -344,24 +338,6 @@ public class AuthMe extends JavaPlugin {
ConsoleLogger.close();
}
public String replaceAllInfo(String message, Player player) {
String playersOnline = Integer.toString(bukkitService.getOnlinePlayers().size());
String ipAddress = PlayerUtils.getPlayerIp(player);
Server server = getServer();
return message
.replace("&", "\u00a7")
.replace("{PLAYER}", player.getName())
.replace("{ONLINE}", playersOnline)
.replace("{MAXPLAYERS}", Integer.toString(server.getMaxPlayers()))
.replace("{IP}", ipAddress)
.replace("{LOGINS}", Integer.toString(playerCache.getLogged()))
.replace("{WORLD}", player.getWorld().getName())
.replace("{SERVER}", server.getServerName())
.replace("{VERSION}", server.getBukkitVersion())
// TODO: We should cache info like this, maybe with a class that extends Player?
.replace("{COUNTRY}", geoIpService.getCountryName(ipAddress));
}
/**
* Handle Bukkit commands.
*

View File

@ -13,15 +13,16 @@ import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.BungeeService;
import fr.xephi.authme.service.TeleportationService;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.WelcomeMessageConfiguration;
import fr.xephi.authme.settings.commandconfig.CommandManager;
import fr.xephi.authme.settings.properties.RegistrationSettings;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.bukkit.potion.PotionEffectType;
import javax.inject.Inject;
import java.util.List;
import static fr.xephi.authme.settings.properties.RestrictionSettings.PROTECT_INVENTORY_BEFORE_LOGIN;
@ -54,6 +55,9 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess {
@Inject
private Settings settings;
@Inject
private WelcomeMessageConfiguration welcomeMessageConfiguration;
ProcessSyncPlayerLogin() {
}
@ -103,15 +107,12 @@ public class ProcessSyncPlayerLogin implements SynchronousProcess {
player.saveData();
// Login is done, display welcome message
List<String> welcomeMessage = welcomeMessageConfiguration.getWelcomeMessage(player);
if (settings.getProperty(RegistrationSettings.USE_WELCOME_MESSAGE)) {
if (settings.getProperty(RegistrationSettings.BROADCAST_WELCOME_MESSAGE)) {
for (String s : settings.getWelcomeMessage()) {
Bukkit.getServer().broadcastMessage(plugin.replaceAllInfo(s, player));
}
welcomeMessage.forEach(bukkitService::broadcastMessage);
} else {
for (String s : settings.getWelcomeMessage()) {
player.sendMessage(plugin.replaceAllInfo(s, player));
}
welcomeMessage.forEach(player::sendMessage);
}
}

View File

@ -19,7 +19,6 @@ import static fr.xephi.authme.util.FileUtils.copyFileFromResource;
public class Settings extends SettingsManager {
private final File pluginFolder;
private String[] welcomeMessage;
private String passwordEmailMessage;
private String recoveryCodeEmailMessage;
@ -56,19 +55,9 @@ public class Settings extends SettingsManager {
return recoveryCodeEmailMessage;
}
/**
* Return the lines to output after an in-game registration.
*
* @return The welcome message
*/
public String[] getWelcomeMessage() {
return welcomeMessage;
}
private void loadSettingsFromFiles() {
passwordEmailMessage = readFile("email.html");
recoveryCodeEmailMessage = readFile("recovery_code_email.html");
welcomeMessage = readFile("welcome.txt").split("\\n");
}
@Override

View File

@ -0,0 +1,143 @@
package fr.xephi.authme.settings;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.initialization.Reloadable;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.GeoIpService;
import fr.xephi.authme.util.PlayerUtils;
import fr.xephi.authme.util.lazytags.Tag;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import static fr.xephi.authme.util.FileUtils.copyFileFromResource;
/**
* Configuration for the welcome message (welcome.txt).
*/
public class WelcomeMessageConfiguration implements Reloadable {
@DataFolder
@Inject
private File pluginFolder;
@Inject
private Server server;
@Inject
private GeoIpService geoIpService;
@Inject
private BukkitService bukkitService;
@Inject
private PlayerCache playerCache;
/** List of all supported tags for the welcome message. */
private final List<Tag> availableTags = Arrays.asList(
new Tag("&", () -> "\u00a7"),
new Tag("{PLAYER}", pl -> pl.getName()),
new Tag("{ONLINE}", () -> Integer.toString(bukkitService.getOnlinePlayers().size())),
new Tag("{MAXPLAYERS}", () -> Integer.toString(server.getMaxPlayers())),
new Tag("{IP}", pl -> PlayerUtils.getPlayerIp(pl)),
new Tag("{LOGINS}", () -> Integer.toString(playerCache.getLogged())),
new Tag("{WORLD}", pl -> pl.getWorld().getName()),
new Tag("{SERVER}", () -> server.getServerName()),
new Tag("{VERSION}", () -> server.getBukkitVersion()),
new Tag("{COUNTRY}", pl -> geoIpService.getCountryName(PlayerUtils.getPlayerIp(pl))));
/** Welcome message, by lines. */
private List<String> welcomeMessage;
/** Tags used in the welcome message. */
private List<Tag> usedTags;
@PostConstruct
@Override
public void reload() {
welcomeMessage = readWelcomeFile();
usedTags = determineUsedTags(welcomeMessage);
}
/**
* Returns the welcome message for the given player.
*
* @param player the player for whom the welcome message should be prepared
* @return the welcome message
*/
public List<String> getWelcomeMessage(Player player) {
// Note ljacqu 20170121: Using a Map might seem more natural here but we avoid doing so for performance
// Although the performance gain here is probably minimal...
List<TagValue> tagValues = new LinkedList<>();
for (Tag tag : usedTags) {
tagValues.add(new TagValue(tag.getName(), tag.getValue(player)));
}
List<String> adaptedMessages = new LinkedList<>();
for (String line : welcomeMessage) {
String adaptedLine = line;
for (TagValue tagValue : tagValues) {
adaptedLine = adaptedLine.replace(tagValue.tag, tagValue.value);
}
adaptedMessages.add(adaptedLine);
}
return adaptedMessages;
}
/**
* @return the lines of the welcome message file
*/
private List<String> readWelcomeFile() {
File welcomeFile = new File(pluginFolder, "welcome.txt");
if (copyFileFromResource(welcomeFile, "welcome.txt")) {
try {
return Files.readAllLines(welcomeFile.toPath(), StandardCharsets.UTF_8);
} catch (IOException e) {
ConsoleLogger.logException("Failed to read welcome.txt file:", e);
}
} else {
ConsoleLogger.warning("Failed to copy welcome.txt from JAR");
}
return Collections.emptyList();
}
/**
* Determines which tags are used in the message.
*
* @param welcomeMessage the lines of the welcome message
* @return the tags
*/
private List<Tag> determineUsedTags(List<String> welcomeMessage) {
return availableTags.stream()
.filter(tag -> welcomeMessage.stream().anyMatch(msg -> msg.contains(tag.getName())))
.collect(Collectors.toList());
}
private static final class TagValue {
private final String tag;
private final String value;
TagValue(String tag, String value) {
this.tag = tag;
this.value = value;
}
@Override
public String toString() {
return "TagValue[tag='" + tag + "', value='" + value + "']";
}
}
}

View File

@ -0,0 +1,53 @@
package fr.xephi.authme.util.lazytags;
import org.bukkit.entity.Player;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Represents a tag in a text that can be replaced with data (which may depend on the Player).
*/
public class Tag {
private final String name;
private final Function<Player, String> replacementFunction;
/**
* Constructor.
*
* @param name the tag (placeholder) that will be replaced
* @param replacementFunction the function producing the replacement
*/
public Tag(String name, Function<Player, String> replacementFunction) {
this.name = name;
this.replacementFunction = replacementFunction;
}
/**
* Constructor.
*
* @param name the tag (placeholder) that will be replaced
* @param replacementFunction supplier providing the text to replace the tag with
*/
public Tag(String name, Supplier<String> replacementFunction) {
this(name, p -> replacementFunction.get());
}
/**
* @return the tag
*/
public String getName() {
return name;
}
/**
* Returns the value to replace the tag with for the given player.
*
* @param player the player to evaluate the replacement for
* @return the replacement
*/
public String getValue(Player player) {
return replacementFunction.apply(player);
}
}

View File

@ -4,7 +4,6 @@ import ch.jalu.configme.configurationdata.ConfigurationData;
import ch.jalu.configme.configurationdata.ConfigurationDataBuilder;
import ch.jalu.configme.resource.PropertyResource;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.settings.properties.RegistrationSettings;
import fr.xephi.authme.settings.properties.TestConfiguration;
import org.junit.Before;
import org.junit.BeforeClass;
@ -16,11 +15,8 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
@ -45,26 +41,6 @@ public class SettingsTest {
testPluginFolder = temporaryFolder.newFolder();
}
@Test
public void shouldLoadWelcomeMessage() throws IOException {
// given
String welcomeMessage = "This is my welcome message for testing\nBye!";
File welcomeFile = new File(testPluginFolder, "welcome.txt");
createFile(welcomeFile);
Files.write(welcomeFile.toPath(), welcomeMessage.getBytes());
PropertyResource resource = mock(PropertyResource.class);
given(resource.getBoolean(RegistrationSettings.USE_WELCOME_MESSAGE.getPath())).willReturn(true);
Settings settings = new Settings(testPluginFolder, resource, null, CONFIG_DATA);
// when
String[] result = settings.getWelcomeMessage();
// then
assertThat(result, arrayWithSize(2));
assertThat(result, arrayContaining(welcomeMessage.split("\\n")));
}
@Test
public void shouldLoadEmailMessage() throws IOException {
// given

View File

@ -0,0 +1,106 @@
package fr.xephi.authme.settings;
import ch.jalu.injector.testing.BeforeInjecting;
import ch.jalu.injector.testing.DelayedInjectionRunner;
import ch.jalu.injector.testing.InjectDelayed;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.data.auth.PlayerCache;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.service.GeoIpService;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Test for {@link WelcomeMessageConfiguration}.
*/
@RunWith(DelayedInjectionRunner.class)
public class WelcomeMessageConfigurationTest {
@InjectDelayed
private WelcomeMessageConfiguration welcomeMessageConfiguration;
@Mock
private Server server;
@Mock
private BukkitService bukkitService;
@Mock
private GeoIpService geoIpService;
@Mock
private PlayerCache playerCache;
@DataFolder
private File testPluginFolder;
private File welcomeFile;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@BeforeInjecting
public void createPluginFolder() throws IOException {
testPluginFolder = temporaryFolder.newFolder();
welcomeFile = new File(testPluginFolder, "welcome.txt");
welcomeFile.createNewFile();
}
@Test
public void shouldLoadWelcomeMessage() throws IOException {
// given
String welcomeMessage = "This is my welcome message for testing\nBye!";
Files.write(welcomeFile.toPath(), welcomeMessage.getBytes());
Player player = mock(Player.class);
welcomeMessageConfiguration.reload();
// when
List<String> result = welcomeMessageConfiguration.getWelcomeMessage(player);
// then
assertThat(result, hasSize(2));
assertThat(result, contains(welcomeMessage.split("\\n")));
verifyZeroInteractions(player, playerCache, geoIpService, bukkitService, server);
}
@Test
public void shouldReplaceNameAndIpAndCountry() throws IOException {
// given
String welcomeMessage = "Hello {PLAYER}, your IP is {IP}\nYour country is {COUNTRY}.\nWelcome to {SERVER}!";
Files.write(welcomeFile.toPath(), welcomeMessage.getBytes());
welcomeMessageConfiguration.reload();
Player player = mock(Player.class);
given(player.getName()).willReturn("Bobby");
TestHelper.mockPlayerIp(player, "123.45.66.77");
given(geoIpService.getCountryName("123.45.66.77")).willReturn("Syldavia");
given(server.getServerName()).willReturn("CrazyServer");
// when
List<String> result = welcomeMessageConfiguration.getWelcomeMessage(player);
// then
assertThat(result, hasSize(3));
assertThat(result.get(0), equalTo("Hello Bobby, your IP is 123.45.66.77"));
assertThat(result.get(1), equalTo("Your country is Syldavia."));
assertThat(result.get(2), equalTo("Welcome to CrazyServer!"));
verify(server, only()).getServerName();
verifyZeroInteractions(playerCache);
}
}