<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>
+++ /dev/null
-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);
- }
-
-}
--- /dev/null
+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";
+
+}
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.
*/
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);
}
}
--- /dev/null
+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);
+
+}
--- /dev/null
+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;
+
+}
--- /dev/null
+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 { }
+
+}
--- /dev/null
+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));
+ }
+
+}
+++ /dev/null
-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"));
- }
-
-}
--- /dev/null
+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;
+ }
+
+ }
+
+}
--- /dev/null
+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();
+ }
+
+}
+++ /dev/null
-{
- "smtpHostname": "localhost",
- "errorEmailSender": "errors@rhynodge.net",
- "errorEmailRecipient": "errors@user.net"
-}