From: David ‘Bombe’ Roden Date: Tue, 5 May 2015 20:04:44 +0000 (+0200) Subject: Parse options from environment X-Git-Tag: v2~190 X-Git-Url: https://git.pterodactylus.net/?p=rhynodge.git;a=commitdiff_plain;h=addfbc56099ebd7609b79a1f9de1a6659d5441e2 Parse options from environment --- diff --git a/pom.xml b/pom.xml index 287d593..9c22d87 100644 --- a/pom.xml +++ b/pom.xml @@ -169,10 +169,5 @@ jackson-databind 2.1.2 - - com.lexicalscope.jewelcli - jewelcli - 0.8.3 - diff --git a/src/main/java/net/pterodactylus/rhynodge/engine/Configuration.java b/src/main/java/net/pterodactylus/rhynodge/engine/Configuration.java deleted file mode 100644 index 9f1ca84..0000000 --- a/src/main/java/net/pterodactylus/rhynodge/engine/Configuration.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.pterodactylus.rhynodge.engine; - -import java.io.IOException; -import java.io.InputStream; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Stores general configuration of Rhynodge. - * - * @author David ‘Bombe’ Roden - */ -public class Configuration { - - @JsonProperty - private final String smtpHostname = null; - - @JsonProperty - private String errorEmailSender = null; - - @JsonProperty - private String errorEmailRecipient = null; - - public String getSmtpHostname() { - return smtpHostname; - } - - public String getErrorEmailSender() { - return errorEmailSender; - } - - public String getErrorEmailRecipient() { - return errorEmailRecipient; - } - - public static Configuration from(InputStream inputStream) throws IOException { - return new ObjectMapper().readValue(inputStream, Configuration.class); - } - -} diff --git a/src/main/java/net/pterodactylus/rhynodge/engine/Options.java b/src/main/java/net/pterodactylus/rhynodge/engine/Options.java new file mode 100644 index 0000000..a9c6e8f --- /dev/null +++ b/src/main/java/net/pterodactylus/rhynodge/engine/Options.java @@ -0,0 +1,27 @@ +package net.pterodactylus.rhynodge.engine; + +import net.pterodactylus.util.envopt.Option; + +/** + * Options for Rhynodge which must be set as environment variables. + * + * @author David ‘Bombe’ Roden + */ +public class Options { + + @Option(name = "SMTP_HOSTNAME") + public final String smtpHostname = "localhost"; + + @Option(name = "ERROR_EMAIL_SENDER", required = true) + public final String errorEmailSender = null; + + @Option(name = "ERROR_EMAIL_RECIPIENT", required = true) + public final String errorEmailRecipient = null; + + @Option(name = "STATE_DIRECTORY") + public final String stateDirectory = "states"; + + @Option(name = "CHAIN_DIRECTORY") + public final String chainDirectory = "chains"; + +} diff --git a/src/main/java/net/pterodactylus/rhynodge/engine/Starter.java b/src/main/java/net/pterodactylus/rhynodge/engine/Starter.java index 05aff64..7ca75ca 100644 --- a/src/main/java/net/pterodactylus/rhynodge/engine/Starter.java +++ b/src/main/java/net/pterodactylus/rhynodge/engine/Starter.java @@ -17,15 +17,12 @@ package net.pterodactylus.rhynodge.engine; -import java.io.FileInputStream; import java.io.IOException; import net.pterodactylus.rhynodge.actions.EmailAction; import net.pterodactylus.rhynodge.loader.ChainWatcher; import net.pterodactylus.rhynodge.states.StateManager; - -import com.lexicalscope.jewel.cli.CliFactory; -import com.lexicalscope.jewel.cli.Option; +import net.pterodactylus.util.envopt.Parser; /** * Rhynodge main starter class. @@ -42,57 +39,21 @@ public class Starter { */ public static void main(String... arguments) throws IOException { - /* parse command line. */ - Parameters parameters = CliFactory.parseArguments(Parameters.class, arguments); - Configuration configuration = loadConfiguration(parameters.getConfigurationFile()); + Options options = Parser.fromSystemEnvironment().parseEnvironment(Options::new); /* create the state manager. */ - StateManager stateManager = new StateManager(parameters.getStateDirectory()); + StateManager stateManager = new StateManager(options.stateDirectory); /* create the engine. */ - Engine engine = new Engine(stateManager, createErrorEmailAction(configuration)); + Engine engine = new Engine(stateManager, createErrorEmailAction(options.smtpHostname, options.errorEmailSender, options.errorEmailRecipient)); /* start a watcher. */ - ChainWatcher chainWatcher = new ChainWatcher(engine, parameters.getChainDirectory()); + ChainWatcher chainWatcher = new ChainWatcher(engine, options.chainDirectory); chainWatcher.start(); } - private static Configuration loadConfiguration(String configurationFile) throws IOException { - try (FileInputStream configInputStream = new FileInputStream(configurationFile)) { - return Configuration.from(configInputStream); - } - } - - private static EmailAction createErrorEmailAction(Configuration configuration) { - return new EmailAction(configuration.getSmtpHostname(), configuration.getErrorEmailSender(), configuration.getErrorEmailRecipient()); - } - - /** - * Definition of the command-line parameters. - * - * @author David ‘Bombe’ Roden - */ - private static interface Parameters { - - /** - * Returns the directory to watch for chains. - * - * @return The chain directory - */ - @Option(defaultValue = "chains", longName = "chains", shortName = "c", description = "The directory to watch for chains") - String getChainDirectory(); - - /** - * Returns the directory to store states in. - * - * @return The states directory - */ - @Option(defaultValue = "states", longName = "states", shortName = "s", description = "The directory to store states in") - String getStateDirectory(); - - @Option(defaultValue = "/etc/rhynodge/rhynodge.json", longName = "config", shortName = "C", description = "The name of the configuration file") - String getConfigurationFile(); - + private static EmailAction createErrorEmailAction(String smtpHostname, String errorEmailSender, String errorEmailRecipient) { + return new EmailAction(smtpHostname, errorEmailSender, errorEmailRecipient); } } diff --git a/src/main/java/net/pterodactylus/util/envopt/Environment.java b/src/main/java/net/pterodactylus/util/envopt/Environment.java new file mode 100644 index 0000000..a16edc8 --- /dev/null +++ b/src/main/java/net/pterodactylus/util/envopt/Environment.java @@ -0,0 +1,14 @@ +package net.pterodactylus.util.envopt; + +import java.util.Optional; + +/** + * An environment is a read-only key-value store, with keys and values both being {@link String}s. + * + * @author David ‘Bombe’ Roden + */ +public interface Environment { + + Optional getValue(String name); + +} diff --git a/src/main/java/net/pterodactylus/util/envopt/Option.java b/src/main/java/net/pterodactylus/util/envopt/Option.java new file mode 100644 index 0000000..640d4fa --- /dev/null +++ b/src/main/java/net/pterodactylus/util/envopt/Option.java @@ -0,0 +1,21 @@ +package net.pterodactylus.util.envopt; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for an option that is to be parsed from the environment by {@link + * Parser}. + * + * @author David ‘Bombe’ Roden + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Option { + + String name(); + boolean required() default false; + +} diff --git a/src/main/java/net/pterodactylus/util/envopt/Parser.java b/src/main/java/net/pterodactylus/util/envopt/Parser.java new file mode 100644 index 0000000..208d6f4 --- /dev/null +++ b/src/main/java/net/pterodactylus/util/envopt/Parser.java @@ -0,0 +1,51 @@ +package net.pterodactylus.util.envopt; + +import java.lang.reflect.Field; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Parses values from an {@link Environment} into {@link Option}-annotated fields of an object. + * + * @author David ‘Bombe’ Roden + */ +public class Parser { + + private final Environment environment; + + public Parser(Environment environment) { + this.environment = environment; + } + + public T parseEnvironment(Supplier optionsObjectSupplier) { + T optionsObject = optionsObjectSupplier.get(); + Class optionsClass = optionsObject.getClass(); + for (Field field : optionsClass.getDeclaredFields()) { + Option[] options = field.getAnnotationsByType(Option.class); + if (options.length == 0) { + continue; + } + for (Option option : options) { + String variableName = option.name(); + Optional value = environment.getValue(variableName); + if (option.required() && !value.isPresent()) { + throw new RequiredOptionIsMissing(); + } + field.setAccessible(true); + try { + field.set(optionsObject, value.orElse(null)); + } catch (IllegalAccessException iae1) { + /* swallow. */ + } + } + } + return optionsObject; + } + + public static Parser fromSystemEnvironment() { + return new Parser(new SystemEnvironment()); + } + + public static class RequiredOptionIsMissing extends RuntimeException { } + +} diff --git a/src/main/java/net/pterodactylus/util/envopt/SystemEnvironment.java b/src/main/java/net/pterodactylus/util/envopt/SystemEnvironment.java new file mode 100644 index 0000000..32398ba --- /dev/null +++ b/src/main/java/net/pterodactylus/util/envopt/SystemEnvironment.java @@ -0,0 +1,18 @@ +package net.pterodactylus.util.envopt; + +import java.util.Optional; + +/** + * {@link Environment} implementation that reads variables from the system environment. + * + * @author David ‘Bombe’ Roden + * @see System#getenv(String) + */ +public class SystemEnvironment implements Environment { + + @Override + public Optional getValue(String name) { + return Optional.ofNullable(System.getenv(name)); + } + +} diff --git a/src/test/java/net/pterodactylus/rhynodge/engine/ConfigurationTest.java b/src/test/java/net/pterodactylus/rhynodge/engine/ConfigurationTest.java deleted file mode 100644 index 6a497e0..0000000 --- a/src/test/java/net/pterodactylus/rhynodge/engine/ConfigurationTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.pterodactylus.rhynodge.engine; - -import java.io.IOException; -import java.io.InputStream; - -import org.hamcrest.MatcherAssert; -import org.hamcrest.Matchers; -import org.junit.Test; - -/** - * Unit test for {@link Configuration}. - * - * @author David ‘Bombe’ Roden - */ -public class ConfigurationTest { - - @Test - public void configurationCanBeReadFromJsonFile() throws IOException { - InputStream inputStream = getClass().getResourceAsStream("configuration.json"); - Configuration configuration = Configuration.from(inputStream); - MatcherAssert.assertThat(configuration.getSmtpHostname(), Matchers.is("localhost")); - MatcherAssert.assertThat(configuration.getErrorEmailSender(), Matchers.is("errors@rhynodge.net")); - MatcherAssert.assertThat(configuration.getErrorEmailRecipient(), Matchers.is("errors@user.net")); - } - -} diff --git a/src/test/java/net/pterodactylus/util/envopt/ParserTest.java b/src/test/java/net/pterodactylus/util/envopt/ParserTest.java new file mode 100644 index 0000000..5a729dc --- /dev/null +++ b/src/test/java/net/pterodactylus/util/envopt/ParserTest.java @@ -0,0 +1,112 @@ +package net.pterodactylus.util.envopt; + +import java.util.Optional; + +import net.pterodactylus.util.envopt.Parser.RequiredOptionIsMissing; + +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * Unit test for {@link Parser}. + * + * @author David ‘Bombe’ Roden + */ +public class ParserTest { + + private final Environment environment = Mockito.mock(Environment.class); + private final Parser parser = new Parser(environment); + + @Test + public void parserCanParseEnvironmentIntoOptions() { + Mockito.when(environment.getValue("foo")).thenReturn(Optional.of("test")); + TestOptions testOptions = parser.parseEnvironment(TestOptions::new); + MatcherAssert.assertThat(testOptions.getOptionOne(), Matchers.is("test")); + } + + @Test + public void parserIgnoresOptionsWithoutAnnotations() { + MoreTestOptions moreTestOptions = parser.parseEnvironment(MoreTestOptions::new); + MatcherAssert.assertThat(moreTestOptions.getOptionOne(), Matchers.nullValue()); + } + + @Test + public void parserCanAssignToFinalValues() { + Mockito.when(environment.getValue("foo")).thenReturn(Optional.of("test")); + FinalTestOptions finalTestOptions = parser.parseEnvironment(FinalTestOptions::new); + MatcherAssert.assertThat(finalTestOptions.getOptionOne(), Matchers.is("test")); + } + + @Test(expected = RequiredOptionIsMissing.class) + public void parserThrowsIfRequiredOptionIsMissing() { + Mockito.when(environment.getValue("foo")).thenReturn(Optional.empty()); + RequiredTestOptions requiredTestOptions = parser.parseEnvironment(RequiredTestOptions::new); + requiredTestOptions.getOptionOne(); + } + + /** + * Test class with options used by {@link Parser}. + * + * @author David ‘Bombe’ Roden + */ + private static class TestOptions { + + @Option(name = "foo") + private String optionOne; + + public String getOptionOne() { + return optionOne; + } + + } + + /** + * Test class with options used by {@link Parser}. + * + * @author David ‘Bombe’ Roden + */ + private static class MoreTestOptions { + + private String optionOne; + + public String getOptionOne() { + return optionOne; + } + + } + + /** + * Test class with options used by {@link Parser}. + * + * @author David ‘Bombe’ Roden + */ + private static class FinalTestOptions { + + @Option(name = "foo") + private final String optionOne = null; + + public String getOptionOne() { + return optionOne; + } + + } + + /** + * Test class with options used by {@link Parser}. + * + * @author David ‘Bombe’ Roden + */ + private static class RequiredTestOptions { + + @Option(name = "foo", required = true) + private final String optionOne = null; + + public String getOptionOne() { + return optionOne; + } + + } + +} diff --git a/src/test/java/net/pterodactylus/util/envopt/SystemEnvironmentTest.java b/src/test/java/net/pterodactylus/util/envopt/SystemEnvironmentTest.java new file mode 100644 index 0000000..ac9fe52 --- /dev/null +++ b/src/test/java/net/pterodactylus/util/envopt/SystemEnvironmentTest.java @@ -0,0 +1,47 @@ +package net.pterodactylus.util.envopt; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; + +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Test; + +/** + * Unit test for {@link SystemEnvironment}. + * + * @author David ‘Bombe’ Roden + */ +public class SystemEnvironmentTest { + + private final SystemEnvironment environment = new SystemEnvironment(); + + @Test + public void accessorCanAccessTheSystemEnvironment() { + Map systemEnvironment = System.getenv(); + MatcherAssert.assertThat(systemEnvironment.entrySet(), Matchers.not(Matchers.empty())); + for (Entry environmentEntry : systemEnvironment.entrySet()) { + MatcherAssert.assertThat(environment.getValue(environmentEntry.getKey()), Matchers.is( + Optional.of(environmentEntry.getValue()))); + } + } + + @Test + public void accessorRecognizesNonExistingVariables() { + String randomName = generateRandomName(); + MatcherAssert.assertThat(environment.getValue(randomName), Matchers.is(Optional.empty())); + } + + private String generateRandomName() { + StringBuilder stringBuilder = new StringBuilder(); + do { + stringBuilder.setLength(0); + for (int i = 0; i < 10; i++) { + stringBuilder.append((char) ('A' + (Math.random() * 26))); + } + } while (System.getenv(stringBuilder.toString()) != null); + return stringBuilder.toString(); + } + +} diff --git a/src/test/resources/net/pterodactylus/rhynodge/engine/configuration.json b/src/test/resources/net/pterodactylus/rhynodge/engine/configuration.json deleted file mode 100644 index 799a773..0000000 --- a/src/test/resources/net/pterodactylus/rhynodge/engine/configuration.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "smtpHostname": "localhost", - "errorEmailSender": "errors@rhynodge.net", - "errorEmailRecipient": "errors@user.net" -}