Parse options from environment
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 5 May 2015 20:04:44 +0000 (22:04 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 5 May 2015 20:04:44 +0000 (22:04 +0200)
12 files changed:
pom.xml
src/main/java/net/pterodactylus/rhynodge/engine/Configuration.java [deleted file]
src/main/java/net/pterodactylus/rhynodge/engine/Options.java [new file with mode: 0644]
src/main/java/net/pterodactylus/rhynodge/engine/Starter.java
src/main/java/net/pterodactylus/util/envopt/Environment.java [new file with mode: 0644]
src/main/java/net/pterodactylus/util/envopt/Option.java [new file with mode: 0644]
src/main/java/net/pterodactylus/util/envopt/Parser.java [new file with mode: 0644]
src/main/java/net/pterodactylus/util/envopt/SystemEnvironment.java [new file with mode: 0644]
src/test/java/net/pterodactylus/rhynodge/engine/ConfigurationTest.java [deleted file]
src/test/java/net/pterodactylus/util/envopt/ParserTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/util/envopt/SystemEnvironmentTest.java [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/engine/configuration.json [deleted file]

diff --git a/pom.xml b/pom.xml
index 287d593..9c22d87 100644 (file)
--- a/pom.xml
+++ b/pom.xml
                        <artifactId>jackson-databind</artifactId>
                        <version>2.1.2</version>
                </dependency>
-               <dependency>
-                       <groupId>com.lexicalscope.jewelcli</groupId>
-                       <artifactId>jewelcli</artifactId>
-                       <version>0.8.3</version>
-               </dependency>
        </dependencies>
 </project>
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 (file)
index 9f1ca84..0000000
+++ /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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-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 (file)
index 0000000..a9c6e8f
--- /dev/null
@@ -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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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";
+
+}
index 05aff64..7ca75ca 100644 (file)
 
 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
-        */
-       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 (file)
index 0000000..a16edc8
--- /dev/null
@@ -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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Environment {
+
+       Optional<String> 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 (file)
index 0000000..640d4fa
--- /dev/null
@@ -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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+@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 (file)
index 0000000..208d6f4
--- /dev/null
@@ -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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Parser {
+
+       private final Environment environment;
+
+       public Parser(Environment environment) {
+               this.environment = environment;
+       }
+
+       public <T> T parseEnvironment(Supplier<T> 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<String> 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 (file)
index 0000000..32398ba
--- /dev/null
@@ -0,0 +1,18 @@
+package net.pterodactylus.util.envopt;
+
+import java.util.Optional;
+
+/**
+ * {@link Environment} implementation that reads variables from the system environment.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ * @see System#getenv(String)
+ */
+public class SystemEnvironment implements Environment {
+
+       @Override
+       public Optional<String> 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 (file)
index 6a497e0..0000000
+++ /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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-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 (file)
index 0000000..5a729dc
--- /dev/null
@@ -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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private static class TestOptions {
+
+               @Option(name = "foo")
+               private String optionOne;
+
+               public String getOptionOne() {
+                       return optionOne;
+               }
+
+       }
+
+       /**
+        * Test class with options used by {@link Parser}.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private static class MoreTestOptions {
+
+               private String optionOne;
+
+               public String getOptionOne() {
+                       return optionOne;
+               }
+
+       }
+
+       /**
+        * Test class with options used by {@link Parser}.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       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 (file)
index 0000000..ac9fe52
--- /dev/null
@@ -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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SystemEnvironmentTest {
+
+       private final SystemEnvironment environment = new SystemEnvironment();
+
+       @Test
+       public void accessorCanAccessTheSystemEnvironment() {
+               Map<String, String> systemEnvironment = System.getenv();
+               MatcherAssert.assertThat(systemEnvironment.entrySet(), Matchers.not(Matchers.empty()));
+               for (Entry<String, String> 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 (file)
index 799a773..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "smtpHostname": "localhost",
-  "errorEmailSender": "errors@rhynodge.net",
-  "errorEmailRecipient": "errors@user.net"
-}