diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 15a37ab4..d547f3ae 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -26,11 +26,13 @@ import fr.xephi.authme.service.BackupService; import fr.xephi.authme.service.BukkitService; import fr.xephi.authme.service.MigrationService; import fr.xephi.authme.service.bungeecord.BungeeReceiver; +import fr.xephi.authme.service.yaml.YamlParseException; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.SettingsWarner; 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.ExceptionUtils; import org.apache.commons.lang.SystemUtils; import org.bukkit.Server; import org.bukkit.command.Command; @@ -133,7 +135,13 @@ public class AuthMe extends JavaPlugin { try { initialize(); } catch (Throwable th) { - ConsoleLogger.logException("Aborting initialization of AuthMe:", th); + YamlParseException yamlParseException = ExceptionUtils.findThrowableInCause(YamlParseException.class, th); + if (yamlParseException == null) { + ConsoleLogger.logException("Aborting initialization of AuthMe:", th); + } else { + ConsoleLogger.logException("File '" + yamlParseException.getFile() + "' contains invalid YAML. " + + "Please run its contents through http://yamllint.com", yamlParseException); + } stopOrUnload(); return; } diff --git a/src/main/java/fr/xephi/authme/initialization/SettingsProvider.java b/src/main/java/fr/xephi/authme/initialization/SettingsProvider.java index e5e69e66..77133715 100644 --- a/src/main/java/fr/xephi/authme/initialization/SettingsProvider.java +++ b/src/main/java/fr/xephi/authme/initialization/SettingsProvider.java @@ -2,7 +2,7 @@ package fr.xephi.authme.initialization; import ch.jalu.configme.configurationdata.ConfigurationData; import ch.jalu.configme.resource.PropertyResource; -import ch.jalu.configme.resource.YamlFileResource; +import fr.xephi.authme.service.yaml.YamlFileResourceProvider; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.SettingsMigrationService; import fr.xephi.authme.settings.properties.AuthMeSettingsRetriever; @@ -37,7 +37,7 @@ public class SettingsProvider implements Provider { if (!configFile.exists()) { FileUtils.create(configFile); } - PropertyResource resource = new YamlFileResource(configFile); + PropertyResource resource = YamlFileResourceProvider.loadFromFile(configFile); ConfigurationData configurationData = AuthMeSettingsRetriever.buildConfigurationData(); return new Settings(dataFolder, resource, migrationService, configurationData); } diff --git a/src/main/java/fr/xephi/authme/service/yaml/YamlFileResourceProvider.java b/src/main/java/fr/xephi/authme/service/yaml/YamlFileResourceProvider.java new file mode 100644 index 00000000..b1329437 --- /dev/null +++ b/src/main/java/fr/xephi/authme/service/yaml/YamlFileResourceProvider.java @@ -0,0 +1,30 @@ +package fr.xephi.authme.service.yaml; + +import ch.jalu.configme.resource.YamlFileResource; +import org.yaml.snakeyaml.parser.ParserException; + +import java.io.File; + +/** + * Creates {@link YamlFileResource} objects. + */ +public final class YamlFileResourceProvider { + + private YamlFileResourceProvider() { + } + + /** + * Creates a {@link YamlFileResource} instance for the given file. Wraps SnakeYAML's parse exception + * into an AuthMe exception. + * + * @param file the file to load + * @return the generated resource + */ + public static YamlFileResource loadFromFile(File file) { + try { + return new YamlFileResource(file); + } catch (ParserException e) { + throw new YamlParseException(file.getPath(), e); + } + } +} diff --git a/src/main/java/fr/xephi/authme/service/yaml/YamlParseException.java b/src/main/java/fr/xephi/authme/service/yaml/YamlParseException.java new file mode 100644 index 00000000..b070bcf3 --- /dev/null +++ b/src/main/java/fr/xephi/authme/service/yaml/YamlParseException.java @@ -0,0 +1,26 @@ +package fr.xephi.authme.service.yaml; + +import org.yaml.snakeyaml.parser.ParserException; + +/** + * Exception when a YAML file could not be parsed. + */ +public class YamlParseException extends RuntimeException { + + private final String file; + + /** + * Constructor. + * + * @param file the file a parsing exception occurred with + * @param snakeYamlException the caught exception from SnakeYAML + */ + public YamlParseException(String file, ParserException snakeYamlException) { + super(snakeYamlException); + this.file = file; + } + + public String getFile() { + return file; + } +} diff --git a/src/main/java/fr/xephi/authme/settings/commandconfig/CommandManager.java b/src/main/java/fr/xephi/authme/settings/commandconfig/CommandManager.java index 12579cba..50d73c9a 100644 --- a/src/main/java/fr/xephi/authme/settings/commandconfig/CommandManager.java +++ b/src/main/java/fr/xephi/authme/settings/commandconfig/CommandManager.java @@ -1,11 +1,11 @@ package fr.xephi.authme.settings.commandconfig; import ch.jalu.configme.SettingsManager; -import ch.jalu.configme.resource.YamlFileResource; 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.service.yaml.YamlFileResourceProvider; import fr.xephi.authme.util.FileUtils; import fr.xephi.authme.util.PlayerUtils; import fr.xephi.authme.util.lazytags.Tag; @@ -149,7 +149,7 @@ public class CommandManager implements Reloadable { FileUtils.copyFileFromResource(file, "commands.yml"); SettingsManager settingsManager = new SettingsManager( - new YamlFileResource(file), commandMigrationService, CommandSettingsHolder.class); + YamlFileResourceProvider.loadFromFile(file), commandMigrationService, CommandSettingsHolder.class); CommandConfig commandConfig = settingsManager.getProperty(CommandSettingsHolder.COMMANDS); onJoinCommands = newReplacer(commandConfig.getOnJoin()); onLoginCommands = newOnLoginCmdReplacer(commandConfig.getOnLogin()); diff --git a/src/main/java/fr/xephi/authme/util/ExceptionUtils.java b/src/main/java/fr/xephi/authme/util/ExceptionUtils.java new file mode 100644 index 00000000..6a5adde6 --- /dev/null +++ b/src/main/java/fr/xephi/authme/util/ExceptionUtils.java @@ -0,0 +1,36 @@ +package fr.xephi.authme.util; + +import com.google.common.collect.Sets; + +import java.util.Set; + +/** + * Utilities for exceptions. + */ +public final class ExceptionUtils { + + private ExceptionUtils() { + } + + /** + * Returns the first throwable of the given {@code wantedThrowableType} by visiting the provided + * throwable and its causes recursively. + * + * @param wantedThrowableType the throwable type to find + * @param throwable the throwable to start with + * @param the desired throwable subtype + * @return the first throwable found of the given type, or null if none found + */ + public static T findThrowableInCause(Class wantedThrowableType, Throwable throwable) { + Set visitedObjects = Sets.newIdentityHashSet(); + Throwable currentThrowable = throwable; + while (currentThrowable != null && !visitedObjects.contains(currentThrowable)) { + if (wantedThrowableType.isInstance(currentThrowable)) { + return wantedThrowableType.cast(currentThrowable); + } + visitedObjects.add(currentThrowable); + currentThrowable = currentThrowable.getCause(); + } + return null; + } +} diff --git a/src/test/java/fr/xephi/authme/service/yaml/YamlFileResourceProviderTest.java b/src/test/java/fr/xephi/authme/service/yaml/YamlFileResourceProviderTest.java new file mode 100644 index 00000000..f870a6b4 --- /dev/null +++ b/src/test/java/fr/xephi/authme/service/yaml/YamlFileResourceProviderTest.java @@ -0,0 +1,48 @@ +package fr.xephi.authme.service.yaml; + +import ch.jalu.configme.resource.YamlFileResource; +import fr.xephi.authme.TestHelper; +import org.junit.Test; +import org.yaml.snakeyaml.parser.ParserException; + +import java.io.File; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * Test for {@link YamlFileResourceProvider}. + */ +public class YamlFileResourceProviderTest { + + @Test + public void shouldLoadValidFile() { + // given + File yamlFile = TestHelper.getJarFile(TestHelper.PROJECT_ROOT + "service/yaml/validYaml.yml"); + + // when + YamlFileResource resource = YamlFileResourceProvider.loadFromFile(yamlFile); + + // then + assertThat(resource.getString("test.jkl"), equalTo("Test test")); + } + + @Test + public void shouldThrowForInvalidFile() { + // given + File yamlFile = TestHelper.getJarFile(TestHelper.PROJECT_ROOT + "service/yaml/invalidYaml.yml"); + + // when + try { + YamlFileResourceProvider.loadFromFile(yamlFile); + + // then + fail("Expected exception to be thrown"); + } catch (YamlParseException e) { + assertThat(e.getFile(), equalTo(yamlFile.getPath())); + assertThat(e.getCause(), instanceOf(ParserException.class)); + } + } +} diff --git a/src/test/java/fr/xephi/authme/util/ExceptionUtilsTest.java b/src/test/java/fr/xephi/authme/util/ExceptionUtilsTest.java new file mode 100644 index 00000000..af154537 --- /dev/null +++ b/src/test/java/fr/xephi/authme/util/ExceptionUtilsTest.java @@ -0,0 +1,54 @@ +package fr.xephi.authme.util; + +import fr.xephi.authme.ReflectionTestUtils; +import org.junit.Test; + +import java.util.ConcurrentModificationException; + +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link ExceptionUtils}. + */ +public class ExceptionUtilsTest { + + @Test + public void shouldFindWantedThrowable() { + // given + ConcurrentModificationException initialCme = new ConcurrentModificationException(); + Throwable th = new Throwable(initialCme); + ConcurrentModificationException cme = new ConcurrentModificationException(th); + IllegalStateException ise = new IllegalStateException(cme); + UnsupportedOperationException uoe = new UnsupportedOperationException(ise); + ReflectiveOperationException roe = new ReflectiveOperationException(uoe); + + // when + IllegalStateException resultIse = ExceptionUtils.findThrowableInCause(IllegalStateException.class, roe); + ConcurrentModificationException resultCme = ExceptionUtils.findThrowableInCause(ConcurrentModificationException.class, cme); + StackOverflowError resultSoe = ExceptionUtils.findThrowableInCause(StackOverflowError.class, cme); + + // then + assertThat(resultIse, sameInstance(ise)); + assertThat(resultCme, sameInstance(cme)); + assertThat(resultSoe, nullValue()); + } + + @Test + public void shouldHandleCircularCausesGracefully() { + // given + IllegalStateException ise = new IllegalStateException(); + UnsupportedOperationException uoe = new UnsupportedOperationException(ise); + ReflectiveOperationException roe = new ReflectiveOperationException(uoe); + ReflectionTestUtils.setField(Throwable.class, ise, "cause", roe); + + // when + NullPointerException resultNpe = ExceptionUtils.findThrowableInCause(NullPointerException.class, uoe); + UnsupportedOperationException resultUoe = ExceptionUtils.findThrowableInCause(UnsupportedOperationException.class, uoe); + + // then + assertThat(resultNpe, nullValue()); + assertThat(resultUoe, sameInstance(uoe)); + } +} diff --git a/src/test/resources/fr/xephi/authme/service/yaml/invalidYaml.yml b/src/test/resources/fr/xephi/authme/service/yaml/invalidYaml.yml new file mode 100644 index 00000000..02daab8c --- /dev/null +++ b/src/test/resources/fr/xephi/authme/service/yaml/invalidYaml.yml @@ -0,0 +1,5 @@ +# File with invalid YAML +test: + abc: 'test' + def: 'Going to forget a quote here + jkl: 'Test test' diff --git a/src/test/resources/fr/xephi/authme/service/yaml/validYaml.yml b/src/test/resources/fr/xephi/authme/service/yaml/validYaml.yml new file mode 100644 index 00000000..cebbd0b9 --- /dev/null +++ b/src/test/resources/fr/xephi/authme/service/yaml/validYaml.yml @@ -0,0 +1,4 @@ +test: + abc: 'test' + def: 'All quotes are good here' + jkl: 'Test test'