Merge branch '933-mysql-to-sqlite-converter' of https://github.com/AuthMe-Team/AuthMeReloaded

This commit is contained in:
ljacqu 2016-09-04 14:48:34 +02:00
commit 8dfd66d10b
10 changed files with 348 additions and 123 deletions

View File

@ -282,7 +282,7 @@ public class CommandInitializer {
.description("Converter command")
.detailedDescription("Converter command for AuthMeReloaded.")
.withArgument("job", "Conversion job: xauth / crazylogin / rakamak / " +
"royalauth / vauth / sqlitetosql", false)
"royalauth / vauth / sqliteToSql / mysqlToSqlite", false)
.permission(AdminPermission.CONVERTER)
.executableCommand(ConverterCommand.class)
.build();

View File

@ -2,11 +2,13 @@ package fr.xephi.authme.command.executable.authme;
import ch.jalu.injector.Injector;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.command.CommandService;
import fr.xephi.authme.command.ExecutableCommand;
import fr.xephi.authme.converter.Converter;
import fr.xephi.authme.converter.CrazyLoginConverter;
import fr.xephi.authme.converter.MySqlToSqlite;
import fr.xephi.authme.converter.RakamakConverter;
import fr.xephi.authme.converter.RoyalAuthConverter;
import fr.xephi.authme.converter.SqliteToSql;
@ -18,12 +20,16 @@ import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.List;
import java.util.Map;
/**
* Converter command: launches conversion based on its parameters.
*/
public class ConverterCommand implements ExecutableCommand {
@VisibleForTesting
static final Map<String, Class<? extends Converter>> CONVERTERS = getConverters();
@Inject
private CommandService commandService;
@ -39,14 +45,14 @@ public class ConverterCommand implements ExecutableCommand {
String job = arguments.get(0);
// Determine the job type
ConvertType jobType = ConvertType.fromName(job);
if (jobType == null) {
commandService.send(sender, MessageKey.ERROR);
Class<? extends Converter> converterClass = CONVERTERS.get(job.toLowerCase());
if (converterClass == null) {
sender.sendMessage("[AuthMe] Converter does not exist!");
return;
}
// Get the proper converter instance
final Converter converter = injector.newInstance(jobType.getConverterClass());
final Converter converter = injector.newInstance(converterClass);
// Run the convert job
bukkitService.runTaskAsynchronously(new Runnable() {
@ -55,47 +61,31 @@ public class ConverterCommand implements ExecutableCommand {
try {
converter.execute(sender);
} catch (Exception e) {
commandService.send(sender, MessageKey.ERROR);
ConsoleLogger.logException("Error during conversion:", e);
}
}
});
// Show a status message
sender.sendMessage("[AuthMe] Successfully converted from " + jobType.getName());
sender.sendMessage("[AuthMe] Successfully started " + job);
}
@VisibleForTesting
enum ConvertType {
XAUTH("xauth", xAuthConverter.class),
CRAZYLOGIN("crazylogin", CrazyLoginConverter.class),
RAKAMAK("rakamak", RakamakConverter.class),
ROYALAUTH("royalauth", RoyalAuthConverter.class),
VAUTH("vauth", vAuthConverter.class),
SQLITETOSQL("sqlitetosql", SqliteToSql.class);
private final String name;
private final Class<? extends Converter> converterClass;
ConvertType(String name, Class<? extends Converter> converterClass) {
this.name = name;
this.converterClass = converterClass;
}
public static ConvertType fromName(String name) {
for (ConvertType type : ConvertType.values()) {
if (type.getName().equalsIgnoreCase(name)) {
return type;
}
}
return null;
}
public String getName() {
return this.name;
}
public Class<? extends Converter> getConverterClass() {
return converterClass;
}
/**
* Initializes a map with all available converters.
*
* @return map with all available converters
*/
private static Map<String, Class<? extends Converter>> getConverters() {
return ImmutableMap.<String, Class<? extends Converter>>builder()
.put("xauth", xAuthConverter.class)
.put("crazylogin", CrazyLoginConverter.class)
.put("rakamak", RakamakConverter.class)
.put("royalauth", RoyalAuthConverter.class)
.put("vauth", vAuthConverter.class)
.put("sqlitetosql", SqliteToSql.class)
.put("mysqltosqlite", MySqlToSqlite.class)
.build();
}
}

View File

@ -0,0 +1,90 @@
package fr.xephi.authme.converter;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceType;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import java.util.ArrayList;
import java.util.List;
/**
* Converts from one AuthMe data source type to another.
*
* @param <S> the source type to convert from
*/
public abstract class AbstractDataSourceConverter<S extends DataSource> implements Converter {
private DataSource destination;
private DataSourceType destinationType;
/**
* Constructor.
*
* @param destination the data source to convert to
* @param destinationType the data source type of the destination. The given data source is checked that its
* type corresponds to this type before the conversion is started, enabling us to just pass
* the current data source and letting this class check that the types correspond.
*/
public AbstractDataSourceConverter(DataSource destination, DataSourceType destinationType) {
this.destination = destination;
this.destinationType = destinationType;
}
// Implementation note: Because of ForceFlatToSqlite it is possible that the CommandSender is null,
// which is never the case when a converter is launched from the /authme converter command.
@Override
public void execute(CommandSender sender) {
if (!destinationType.equals(destination.getType())) {
if (sender != null) {
sender.sendMessage("Please configure your connection to "
+ destinationType + " and re-run this command");
}
return;
}
S source;
try {
source = getSource();
} catch (Exception e) {
logAndSendMessage(sender, "The data source to convert from could not be initialized");
ConsoleLogger.logException("Could not initialize source:", e);
return;
}
List<String> skippedPlayers = new ArrayList<>();
for (PlayerAuth auth : source.getAllAuths()) {
if (destination.isAuthAvailable(auth.getNickname())) {
skippedPlayers.add(auth.getNickname());
} else {
destination.saveAuth(auth);
destination.updateQuitLoc(auth);
}
}
if (!skippedPlayers.isEmpty()) {
logAndSendMessage(sender, "Skipped conversion for players which were already in "
+ destinationType + ": " + StringUtils.join(", ", skippedPlayers));
}
logAndSendMessage(sender, "Database successfully converted from " + source.getType()
+ " to " + destinationType);
}
/**
* @return the data source to convert from
* @throws Exception during initialization of source
*/
protected abstract S getSource() throws Exception;
private static void logAndSendMessage(CommandSender sender, String message) {
ConsoleLogger.info(message);
// Make sure sender is not console user, which will see the message from ConsoleLogger already
if (sender != null && !(sender instanceof ConsoleCommandSender)) {
sender.sendMessage(message);
}
}
}

View File

@ -1,21 +1,14 @@
package fr.xephi.authme.converter;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.FlatFile;
import fr.xephi.authme.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Mandatory migration from the deprecated flat file datasource to SQLite.
*/
public class ForceFlatToSqlite {
public class ForceFlatToSqlite extends AbstractDataSourceConverter<FlatFile> {
private final DataSource source;
private final DataSource destination;
private final FlatFile source;
/**
* Constructor.
@ -24,29 +17,12 @@ public class ForceFlatToSqlite {
* @param destination The datasource to copy the data to (sqlite)
*/
public ForceFlatToSqlite(FlatFile source, DataSource destination) {
super(destination, destination.getType());
this.source = source;
this.destination = destination;
}
/**
* Perform the conversion.
*/
public void run() {
List<String> skippedPlayers = new ArrayList<>();
for (PlayerAuth auth : source.getAllAuths()) {
if (destination.isAuthAvailable(auth.getNickname())) {
skippedPlayers.add(auth.getNickname());
} else {
destination.saveAuth(auth);
destination.updateQuitLoc(auth);
}
}
if (!skippedPlayers.isEmpty()) {
ConsoleLogger.warning("Warning: skipped conversion for players which were already in SQLite: "
+ StringUtils.join(", ", skippedPlayers));
}
ConsoleLogger.info("Database successfully converted from " + source.getClass().getSimpleName()
+ " to " + destination.getClass().getSimpleName());
@Override
public FlatFile getSource() {
return source;
}
}

View File

@ -0,0 +1,28 @@
package fr.xephi.authme.converter;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceType;
import fr.xephi.authme.datasource.MySQL;
import fr.xephi.authme.settings.Settings;
import javax.inject.Inject;
import java.sql.SQLException;
/**
* Converts from MySQL to SQLite.
*/
public class MySqlToSqlite extends AbstractDataSourceConverter<MySQL> {
private final Settings settings;
@Inject
MySqlToSqlite(DataSource dataSource, Settings settings) {
super(dataSource, DataSourceType.SQLITE);
this.settings = settings;
}
@Override
protected MySQL getSource() throws SQLException, ClassNotFoundException {
return new MySQL(settings);
}
}

View File

@ -1,45 +1,28 @@
package fr.xephi.authme.converter;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceType;
import fr.xephi.authme.datasource.SQLite;
import fr.xephi.authme.output.MessageKey;
import fr.xephi.authme.output.Messages;
import fr.xephi.authme.settings.Settings;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.sql.SQLException;
public class SqliteToSql implements Converter {
/**
* Converts from SQLite to MySQL.
*/
public class SqliteToSql extends AbstractDataSourceConverter<SQLite> {
private final Settings settings;
private final DataSource dataSource;
private final Messages messages;
@Inject
SqliteToSql(Settings settings, DataSource dataSource, Messages messages) {
SqliteToSql(Settings settings, DataSource dataSource) {
super(dataSource, DataSourceType.MYSQL);
this.settings = settings;
this.dataSource = dataSource;
this.messages = messages;
}
@Override
public void execute(CommandSender sender) {
if (dataSource.getType() != DataSourceType.MYSQL) {
sender.sendMessage("Please configure your mySQL connection and re-run this command");
return;
}
try {
SQLite data = new SQLite(settings);
for (PlayerAuth auth : data.getAllAuths()) {
dataSource.saveAuth(auth);
}
} catch (Exception e) {
messages.send(sender, MessageKey.ERROR);
ConsoleLogger.logException("Problem during SQLite to SQL conversion:", e);
}
protected SQLite getSource() throws SQLException, ClassNotFoundException {
return new SQLite(settings);
}
}

View File

@ -69,7 +69,7 @@ public final class MigrationService {
try {
SQLite sqlite = new SQLite(settings);
ForceFlatToSqlite converter = new ForceFlatToSqlite(flatFile, sqlite);
converter.run();
converter.execute(null);
settings.setProperty(DatabaseSettings.BACKEND, DataSourceType.SQLITE);
settings.save();
return sqlite;

View File

@ -3,25 +3,32 @@ package fr.xephi.authme.command.executable.authme;
import ch.jalu.injector.Injector;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.command.CommandService;
import fr.xephi.authme.converter.Converter;
import fr.xephi.authme.converter.RakamakConverter;
import fr.xephi.authme.converter.vAuthConverter;
import fr.xephi.authme.output.MessageKey;
import fr.xephi.authme.util.BukkitService;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.command.CommandSender;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@ -45,6 +52,11 @@ public class ConverterCommandTest {
@Mock
private Injector injector;
@BeforeClass
public static void initLogger() {
TestHelper.setupLogger();
}
@Test
public void shouldHandleUnknownConversionType() {
// given
@ -54,51 +66,68 @@ public class ConverterCommandTest {
command.executeCommand(sender, Collections.singletonList("invalid"));
// then
verify(commandService).send(sender, MessageKey.ERROR);
verify(sender).sendMessage(argThat(containsString("Converter does not exist")));
verifyNoMoreInteractions(commandService);
verifyZeroInteractions(injector);
verifyZeroInteractions(bukkitService);
}
@Test
public void shouldHaveUniqueNameAndClassForEachType() {
public void shouldHaveUniqueClassForEachConverter() {
// given
ConverterCommand.ConvertType[] types = ConverterCommand.ConvertType.values();
List<String> names = new ArrayList<>(types.length);
List<Class<?>> classes = new ArrayList<>(types.length);
Set<Class<? extends Converter>> classes = new HashSet<>();
// when / then
for (ConverterCommand.ConvertType type : types) {
assertThat("Name for '" + type + "' is not null",
type.getName(), not(nullValue()));
assertThat("Class for '" + type + "' is not null",
type.getConverterClass(), not(nullValue()));
assertThat("Name '" + type.getName() + "' is unique",
names, not(hasItem(type.getName())));
assertThat("Class '" + type.getConverterClass() + "' is unique",
classes, not(hasItem(type.getConverterClass())));
names.add(type.getName());
classes.add(type.getConverterClass());
for (Map.Entry<String, Class<? extends Converter>> entry : ConverterCommand.CONVERTERS.entrySet()) {
assertThat("Name is not null or empty",
StringUtils.isEmpty(entry.getKey()), equalTo(false));
assertThat("Converter class is unique for each entry",
classes.add(entry.getValue()), equalTo(true));
}
}
@Test
public void shouldLaunchConverterForAllTypes() {
// given
ConverterCommand.ConvertType type = ConverterCommand.ConvertType.RAKAMAK;
RakamakConverter converter = mock(RakamakConverter.class);
given(injector.newInstance(RakamakConverter.class)).willReturn(converter);
String converterName = "rakamak";
Class<? extends Converter> converterClass = ConverterCommand.CONVERTERS.get(converterName);
// Keep concrete class reference in mock: if this class is ever removed, we need to use another converterName
Converter converter = mock(RakamakConverter.class);
given(injector.newInstance(converterClass)).willReturn(converter);
CommandSender sender = mock(CommandSender.class);
// when
command.executeCommand(sender, Collections.singletonList(type.getName()));
command.executeCommand(sender, Collections.singletonList(converterName));
TestHelper.runInnerRunnable(bukkitService);
// then
verify(converter).execute(sender);
verifyNoMoreInteractions(converter);
verify(injector).newInstance(type.getConverterClass());
verify(injector).newInstance(converterClass);
verifyNoMoreInteractions(injector);
}
@Test
public void shouldCatchExceptionInConverterAndInformSender() {
// given
String converterName = "vauth";
Class<? extends Converter> converterClass = ConverterCommand.CONVERTERS.get(converterName);
Converter converter = mock(vAuthConverter.class);
doThrow(IllegalStateException.class).when(converter).execute(any(CommandSender.class));
given(injector.newInstance(converterClass)).willReturn(converter);
CommandSender sender = mock(CommandSender.class);
// when
command.executeCommand(sender, Collections.singletonList(converterName.toUpperCase()));
TestHelper.runInnerRunnable(bukkitService);
// then
verify(converter).execute(sender);
verifyNoMoreInteractions(converter);
verify(injector).newInstance(converterClass);
verifyNoMoreInteractions(injector);
verify(commandService).send(sender, MessageKey.ERROR);
}
}

View File

@ -0,0 +1,126 @@
package fr.xephi.authme.converter;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceType;
import org.bukkit.command.CommandSender;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.Matchers.containsString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
/**
* Test for {@link AbstractDataSourceConverter}.
*/
public class AbstractDataSourceConverterTest {
@BeforeClass
public static void initLogger() {
TestHelper.setupLogger();
}
@Test
public void shouldThrowForDestinationTypeMismatch() {
// given
DataSource destination = mock(DataSource.class);
given(destination.getType()).willReturn(DataSourceType.MYSQL);
DataSourceType destinationType = DataSourceType.SQLITE;
DataSource source = mock(DataSource.class);
Converter converter = new DataSourceConverterTestImpl<>(source, destination, destinationType);
CommandSender sender = mock(CommandSender.class);
// when
converter.execute(sender);
// then
verify(sender).sendMessage(argThat(containsString("Please configure your connection to SQLITE")));
verify(destination, only()).getType();
verifyZeroInteractions(source);
}
@Test
public void shouldHandleSourceThrowingException() {
// given
DataSource source = mock(DataSource.class);
DataSource destination = mock(DataSource.class);
DataSourceType destinationType = DataSourceType.SQLITE;
given(destination.getType()).willReturn(destinationType);
DataSourceConverterTestImpl converter =
Mockito.spy(new DataSourceConverterTestImpl<>(source, destination, destinationType));
doThrow(IllegalStateException.class).when(converter).getSource();
CommandSender sender = mock(CommandSender.class);
// when
converter.execute(sender);
// then
verify(sender).sendMessage("The data source to convert from could not be initialized");
verify(destination, only()).getType();
verifyZeroInteractions(source);
}
@Test
public void shouldConvertAndSkipExistingPlayers() {
// given
DataSource source = mock(DataSource.class);
DataSource destination = mock(DataSource.class);
DataSourceType destinationType = DataSourceType.MYSQL;
given(destination.getType()).willReturn(destinationType);
List<PlayerAuth> auths =
Arrays.asList(mockAuthWithName("Steven"), mockAuthWithName("bobby"), mockAuthWithName("Jack"));
given(source.getAllAuths()).willReturn(auths);
given(destination.isAuthAvailable(auths.get(0).getNickname())).willReturn(true);
Converter converter = new DataSourceConverterTestImpl<>(source, destination, destinationType);
CommandSender sender = mock(CommandSender.class);
// when
converter.execute(sender);
// then
verify(destination).getType();
verify(destination, times(3)).isAuthAvailable(anyString());
verify(destination, times(2)).saveAuth(any(PlayerAuth.class));
verify(destination, times(2)).updateQuitLoc(any(PlayerAuth.class));
verifyNoMoreInteractions(destination);
verify(sender).sendMessage(argThat(containsString(auths.get(0).getNickname())));
verify(sender).sendMessage(argThat(containsString("successfully converted")));
}
private static PlayerAuth mockAuthWithName(String name) {
PlayerAuth auth = mock(PlayerAuth.class);
given(auth.getNickname()).willReturn(name);
return auth;
}
private static class DataSourceConverterTestImpl<S extends DataSource> extends AbstractDataSourceConverter<S> {
private final S source;
DataSourceConverterTestImpl(S source, DataSource destination, DataSourceType destinationType) {
super(destination, destinationType);
this.source = source;
}
@Override
protected S getSource() {
return source;
}
}
}

View File

@ -4,6 +4,7 @@ import com.google.common.io.Files;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.datasource.DataSourceType;
import fr.xephi.authme.datasource.FlatFile;
import org.junit.Before;
import org.junit.BeforeClass;
@ -20,6 +21,7 @@ import static fr.xephi.authme.AuthMeMatchers.hasAuthBasicData;
import static fr.xephi.authme.AuthMeMatchers.hasAuthLocation;
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ -51,10 +53,11 @@ public class ForceFlatToSqliteTest {
public void shouldConvertToSqlite() {
// given
DataSource dataSource = mock(DataSource.class);
given(dataSource.getType()).willReturn(DataSourceType.MYSQL);
ForceFlatToSqlite converter = new ForceFlatToSqlite(flatFile, dataSource);
// when
converter.run();
converter.execute(null);
// then
ArgumentCaptor<PlayerAuth> authCaptor = ArgumentCaptor.forClass(PlayerAuth.class);