#1497 Show specific message for invalid YAML files (#1506)

* #1497 Throw dedicated exception for invalid YAML files and handle it on startup
- Wrap SnakeYAML exceptions when loading config.yml and commands.yml on startup into own exception type
- Handle exception type on startup with specific error message

* #1497 Fix inaccurate JavaDoc comment
This commit is contained in:
ljacqu 2018-02-23 23:31:22 +01:00 committed by GitHub
parent 7864bb06ac
commit 329657bd5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 216 additions and 5 deletions

View File

@ -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) {
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;
}

View File

@ -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<Settings> {
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);
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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());

View File

@ -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 <T> the desired throwable subtype
* @return the first throwable found of the given type, or null if none found
*/
public static <T extends Throwable> T findThrowableInCause(Class<T> wantedThrowableType, Throwable throwable) {
Set<Throwable> 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;
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}

View File

@ -0,0 +1,5 @@
# File with invalid YAML
test:
abc: 'test'
def: 'Going to forget a quote here
jkl: 'Test test'

View File

@ -0,0 +1,4 @@
test:
abc: 'test'
def: 'All quotes are good here'
jkl: 'Test test'