Skip to content

Commit

Permalink
more flexible command line value description (#50)
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Kraschewski <daniel.kraschewski@tngtech.com>
  • Loading branch information
krasched committed Dec 27, 2020
1 parent c5d1220 commit 972ae9d
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 36 deletions.
8 changes: 3 additions & 5 deletions src/main/java/com/tngtech/configbuilder/ConfigBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import com.tngtech.propertyloader.impl.DefaultPropertySuffixContainer;
import com.tngtech.propertyloader.impl.interfaces.PropertyLoaderFilter;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -61,7 +60,6 @@ public class ConfigBuilder<T> {
private final ConstructionHelper<T> constructionHelper;

private Class<T> configClass;
private Options commandLineOptions;
private PropertyLoader propertyLoader;
private Properties additionalProperties;
private String[] commandLineArgs = {};
Expand All @@ -77,8 +75,7 @@ protected ConfigBuilder(Class<T> configClass, ConfigBuilderFactory configBuilder
this.constructionHelper = configBuilderFactory.getInstance(ConstructionHelper.class);
this.additionalProperties = configBuilderFactory.createInstance(Properties.class);

propertyLoader = configBuilderFactory.getInstance(PropertyLoaderConfigurator.class).configurePropertyLoader(configClass);
commandLineOptions = commandLineHelper.getOptions(configClass);
this.propertyLoader = configBuilderFactory.getInstance(PropertyLoaderConfigurator.class).configurePropertyLoader(configClass);
}

/**
Expand Down Expand Up @@ -241,9 +238,10 @@ public final ConfigBuilder<T> withPropertyFilters(Class<? extends PropertyLoader
* Prints a help message for all command line options that are configured in the config class.
*/
public void printCommandLineHelp() {
initializeErrorMessageSetup(propertyLoader);
HelpFormatter formatter = new HelpFormatter();
formatter.setSyntaxPrefix("Command Line Options for class " + configClass.getSimpleName() + ":");
formatter.printHelp(" ", commandLineOptions);
formatter.printHelp(" ", commandLineHelper.getOptions(configClass));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.tngtech.configbuilder.annotation.valueextractor;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* This annotation is used to mark a method as description text supplier for command line options.
* The annotated method must be static and accept a single String parameter, which is the longOpt name of a command line option.
* There may be at most one such method per class.<br>
* If a field is annotated with {@link com.tngtech.configbuilder.annotation.valueextractor.CommandLineValue} but has no description,
* then this method is called to generate the description text.<br>
* <b>Usage:</b> <code>@CommandLineValueDescriptor</code>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CommandLineValueDescriptor {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.tngtech.configbuilder.exception;

public class InvalidDescriptionMethodException extends RuntimeException {}
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.tngtech.configbuilder.util;

import com.tngtech.configbuilder.annotation.valueextractor.CommandLineValue;
import com.tngtech.configbuilder.annotation.valueextractor.CommandLineValueDescriptor;
import com.tngtech.configbuilder.configuration.ErrorMessageSetup;
import com.tngtech.configbuilder.exception.ConfigBuilderException;
import com.tngtech.configbuilder.exception.InvalidDescriptionMethodException;
import org.apache.commons.cli.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Set;

import static com.google.common.collect.Iterables.getOnlyElement;

public class CommandLineHelper {

Expand All @@ -32,27 +38,44 @@ public CommandLine getCommandLine(Class configClass, String[] args) {
public Options getOptions(Class configClass) {
Options options = configBuilderFactory.createInstance(Options.class);
for (Field field : annotationHelper.getFieldsAnnotatedWith(configClass, CommandLineValue.class)) {
if (field.isSynthetic()) {
continue;
if (!field.isSynthetic()) {
options.addOption(getOption(field, configClass));
}
options.addOption(getOption(field));
}
return options;
}

@SuppressWarnings("AccessStaticViaInstance")
private Option getOption(Field field) {
private Option getOption(Field field, Class configClass) {
CommandLineValue commandLineValue = field.getAnnotation(CommandLineValue.class);
log.debug("adding command line option {} for field {}", commandLineValue.shortOpt(), field.getName());
return Option.builder(commandLineValue.shortOpt())
.longOpt(commandLineValue.longOpt())
.hasArg()
.required(commandLineValue.required())
.desc(commandLineValue.description())
.desc(extractDescriptionString(commandLineValue, configClass))
.hasArg(commandLineValue.hasArg())
.build();
}

private String extractDescriptionString(CommandLineValue commandLineValue, Class configClass) {
if (!commandLineValue.description().isEmpty()) {
return commandLineValue.description();
}

Set<Method> descriptorMethods = annotationHelper.getMethodsAnnotatedWith(configClass, CommandLineValueDescriptor.class);
if (descriptorMethods.isEmpty()) {
return "";
}

try {
Method descriptorMethod = getOnlyElement(descriptorMethods);
descriptorMethod.setAccessible(true);
return descriptorMethod.invoke(null, commandLineValue.longOpt()).toString();
} catch (Exception e) {
throw new ConfigBuilderException(errorMessageSetup.getErrorMessage(InvalidDescriptionMethodException.class), e);
}
}

private CommandLine parseCommandLine(String[] args, Options options) {
CommandLine commandLine;
try {
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/errors.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ com.tngtech.configbuilder.exception.TypeTransformerException = couldn't find a t
com.tngtech.configbuilder.exception.PrimitiveParsingException = unable to parse "%s" to %s
com.tngtech.configbuilder.exception.ImportedConfigurationException = couldn't find a field with the name %s
com.tngtech.configbuilder.exception.FactoryInstantiationException = could not create an instance of %s
com.tngtech.configbuilder.exception.InvalidDescriptionMethodException = invalid or multiple use of the @CommandLineValueDescriptor annotation (the annotated method must be static and accept a single String parameter)
standardMessage = %s was thrown
1 change: 1 addition & 0 deletions src/main/resources/errors_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ com.tngtech.configbuilder.exception.TypeTransformerException = Konnte keinen Tra
com.tngtech.configbuilder.exception.PrimitiveParsingException = Kann "%s" nicht zu %s verarbeiten!
com.tngtech.configbuilder.exception.ImportedConfigurationException = Konnte kein Feld mit dem Namen %s finden.
com.tngtech.configbuilder.exception.FactoryInstantiationException = Konnte keine Instanz von %s erzeugen.
com.tngtech.configbuilder.exception.InvalidDescriptionMethodException = ungültige oder mehrfache Verwendung der @CommandLineValueDescriptor-Annotation (die annotierte Methode muss statisch sein und einen einzelnen String-Parameter haben)
standardMessage = Es gab eine Exception vom Typ %s
1 change: 1 addition & 0 deletions src/main/resources/errors_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ com.tngtech.configbuilder.exception.TypeTransformerException = couldn't find a t
com.tngtech.configbuilder.exception.PrimitiveParsingException = unable to parse "%s" to %s
com.tngtech.configbuilder.exception.ImportedConfigurationException = couldn't find a field with the name %s
com.tngtech.configbuilder.exception.FactoryInstantiationException = could not create an instance of %s
com.tngtech.configbuilder.exception.InvalidDescriptionMethodException = invalid or multiple use of the @CommandLineValueDescriptor annotation (the annotated method must be static and accept a single String parameter)
standardMessage = %s was thrown
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.tngtech.configbuilder.util;

import com.tngtech.configbuilder.ConfigBuilder;
import com.tngtech.configbuilder.annotation.valueextractor.CommandLineValue;
import com.tngtech.configbuilder.annotation.valueextractor.CommandLineValueDescriptor;
import com.tngtech.configbuilder.exception.ConfigBuilderException;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class CommandLineHelperExceptionHandlingTest {

private static class TestConfig {

@CommandLineValue(shortOpt = "u", longOpt = "user", required = true)
public String user;
}

private static class TestConfigWithInvalidCommandLineValueDescriptor {

@CommandLineValue(shortOpt = "u", longOpt = "user")
public String user;

@CommandLineValueDescriptor
private static void description() { }
}

private static class TestConfigWithMultipleCommandLineValueDescriptors {

@CommandLineValue(shortOpt = "u", longOpt = "user")
public String user;

@CommandLineValueDescriptor
private static String description1() {
return "";
}

@CommandLineValueDescriptor
private static String description2() {
return "";
}
}

@Test
public void testUndefinedCommandLineOption() {
String[] args = new String[]{"nd", "notDefined"};
assertThatThrownBy(() -> ConfigBuilder.on(TestConfig.class).withCommandLineArgs(args).build())
.isInstanceOf(ConfigBuilderException.class)
.hasMessageContaining("unable to parse command line arguments");
}

@Test
public void testInvalidCommandLineValueDescriptor() {
assertThatThrownBy(() -> ConfigBuilder.on(TestConfigWithInvalidCommandLineValueDescriptor.class).printCommandLineHelp())
.isInstanceOf(ConfigBuilderException.class)
.hasMessageContaining("invalid or multiple use of the @CommandLineValueDescriptor annotation");

assertThatThrownBy(() -> ConfigBuilder.on(TestConfigWithInvalidCommandLineValueDescriptor.class).build())
.isInstanceOf(ConfigBuilderException.class)
.hasMessageContaining("invalid or multiple use of the @CommandLineValueDescriptor annotation");
}

@Test
public void testMultipleCommandLineValueDescriptors() {
assertThatThrownBy(() -> ConfigBuilder.on(TestConfigWithMultipleCommandLineValueDescriptors.class).printCommandLineHelp())
.isInstanceOf(ConfigBuilderException.class)
.hasMessageContaining("invalid or multiple use of the @CommandLineValueDescriptor annotation");

assertThatThrownBy(() -> ConfigBuilder.on(TestConfigWithMultipleCommandLineValueDescriptors.class).build())
.isInstanceOf(ConfigBuilderException.class)
.hasMessageContaining("invalid or multiple use of the @CommandLineValueDescriptor annotation");
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
package com.tngtech.configbuilder.util;

import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.tngtech.configbuilder.annotation.valueextractor.CommandLineValue;
import com.tngtech.configbuilder.annotation.valueextractor.CommandLineValueDescriptor;
import com.tngtech.configbuilder.configuration.ErrorMessageSetup;
import com.tngtech.configbuilder.exception.ConfigBuilderException;
import org.apache.commons.cli.*;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import static com.google.common.collect.Sets.newHashSet;
import static java.util.Comparator.comparing;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class CommandLineHelperTest {

private static class TestConfig {
@CommandLineValue(shortOpt = "u", longOpt = "user", required = true)

@CommandLineValue(shortOpt = "u", longOpt = "user", required = true, description = "some static description string")
public String aString;
@CommandLineValue(shortOpt = "v", longOpt = "vir", required = false)
public String anotherString;

@CommandLineValueDescriptor
private static String description(String longOpt) {
switch (longOpt) {
case "vir":
return "some dynamically generated description";
default:
return "";
}
}
}

private CommandLineHelper commandLineHelper;
Expand All @@ -44,19 +54,15 @@ private static class TestConfig {
@Mock
private ConfigBuilderFactory configBuilderFactory;
@Mock
private AnnotationHelper annotationHelper;
@Mock
private ErrorMessageSetup errorMessageSetup;

@Before
public void setUp() throws Exception {
when(configBuilderFactory.getInstance(AnnotationHelper.class)).thenReturn(annotationHelper);
when(configBuilderFactory.getInstance(AnnotationHelper.class)).thenReturn(new AnnotationHelper());
when(configBuilderFactory.getInstance(ErrorMessageSetup.class)).thenReturn(errorMessageSetup);

commandLineHelper = new CommandLineHelper(configBuilderFactory);

Set<Field> fields = newHashSet(TestConfig.class.getDeclaredFields());
when(annotationHelper.getFieldsAnnotatedWith(TestConfig.class, CommandLineValue.class)).thenReturn(fields);
when(parser.parse(options, args)).thenReturn(commandLine);
}

Expand All @@ -68,11 +74,11 @@ public void testGetCommandLine() throws Exception {
assertThat(commandLineHelper.getCommandLine(TestConfig.class, args)).isSameAs(commandLine);
verify(options, times(2)).addOption(captor.capture());
verify(parser).parse(options, args);
List<Option> options = captor.getAllValues();

assertThat(options).hasSize(2);
List<Option> sortedOptions = new ArrayList<>(captor.getAllValues());
sortedOptions.sort(comparing(Option::getLongOpt));

final ImmutableList<Option> sortedOptions = FluentIterable.from(options).toSortedList((o1, o2) -> o1.getLongOpt().compareTo(o2.getLongOpt()));
assertThat(sortedOptions).hasSize(2);

assertThat(sortedOptions.get(0).getLongOpt()).isEqualTo("user");
assertThat(sortedOptions.get(0).getOpt()).isEqualTo("u");
Expand All @@ -83,20 +89,14 @@ public void testGetCommandLine() throws Exception {
assertThat(sortedOptions.get(1).isRequired()).isEqualTo(false);
}

@Test(expected = ConfigBuilderException.class)
public void testGetCommandLineThrowsException() {
when(configBuilderFactory.createInstance(DefaultParser.class)).thenReturn(new DefaultParser());
when(configBuilderFactory.createInstance(Options.class)).thenReturn(new Options());
args = new String[]{"nd", "notDefined"};
commandLineHelper.getCommandLine(TestConfig.class, args);
}

@Test
public void testGetOptions() {
Options options1 = new Options();
when(configBuilderFactory.createInstance(Options.class)).thenReturn(options1);
assertThat(commandLineHelper.getOptions(TestConfig.class)).isEqualTo(options1);
assertThat(options1.getOption("user").getLongOpt()).isEqualTo("user");
assertThat(options1.getOption("user").getDescription()).isEqualTo("some static description string");
assertThat(options1.getOption("vir").getOpt()).isEqualTo("v");
assertThat(options1.getOption("vir").getDescription()).isEqualTo("some dynamically generated description");
}
}

0 comments on commit 972ae9d

Please sign in to comment.