diff --git a/owner-site/site/docs/type-conversion.md b/owner-site/site/docs/type-conversion.md index 76734a70..d48daf2d 100644 --- a/owner-site/site/docs/type-conversion.md +++ b/owner-site/site/docs/type-conversion.md @@ -297,6 +297,48 @@ To see the complete test cases supported by owner see [ConverterClassTest] on Gi [ConverterClassTest]: https://github.com/lviggiano/owner/blob/master/owner/src/test/java/org/aeonbits/owner/typeconversion/ConverterClassTest.java +The @CollectionConverterClass annotation +------------------------------ + +For cases when user wants to override the default collection creation (for example to provide custom implementation of Collection +without exposing its type in the interface), +OWNER provides the +[`@CollectionConverterClass`](http://owner.aeonbits.org/apidocs/latest/org/aeonbits/owner/Config.CollectionConverterClass.html) +annotation that allows user to specify a fully customized conversion logic implementing the +[`Converter`](http://owner.aeonbits.org/apidocs/latest/org/aeonbits/owner/Converter.html) interface. + +```java +interface MyConfig extends Config { + @DefaultValue( + "google.com, yahoo.com:8080, owner.aeonbits.org:4000") + @CollectionConverterClass(CollectionServerConverter.class) + List servers(); +} + +public class CollectionServerConverter + implements Converter> { + public List convert(Method targetMethod, String text) { + String[] split = text.split(",", -1); + ServerConverter converter = new ServerConverter(); + List list = new ArrayList(split.length); + for (String server : split) { + list.add(converter.convert(targetMethod, server.trim()); + } + return Collection.unmodifiableList(list); + } +} + +MyConfig cfg = ConfigFactory.create(MyConfig.class); +List ss = cfg.servers(); //immutable list +``` + +In the above example, the converter is fully responsible for the whole process, including proper +handling of possible @ClassConverter, @Separator and @TokenizerClass. + +To see the example of simple handling of these in test cases see [CollectionConverterClassTest] on GitHub. + + [CollectionConverterClassTest]: https://github.com/lviggiano/owner/blob/master/owner/src/test/java/org/aeonbits/owner/typeconversion/CollectionConverterClassTest.java + All the types supported by OWNER -------------------------------- diff --git a/owner/src/main/java/org/aeonbits/owner/Config.java b/owner/src/main/java/org/aeonbits/owner/Config.java index 930cf515..dbbd6346 100644 --- a/owner/src/main/java/org/aeonbits/owner/Config.java +++ b/owner/src/main/java/org/aeonbits/owner/Config.java @@ -18,6 +18,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.net.URI; +import java.util.Collection; import java.util.List; import java.util.Properties; import java.util.concurrent.TimeUnit; @@ -354,6 +355,17 @@ enum DisableableFeature { Class value(); } + /** + * Specifies a {@link Converter} class to allow the user to define a custom conversion logic for the + * collection type returned by the method. The converter is used once for the whole collection. + */ + @Retention(RUNTIME) + @Target(METHOD) + @Documented + public @interface CollectionConverterClass { + Class>> value(); + } + /** * Specifies a {@link Preprocessor} class to allow the user to define a custom logic to pre-process * the property value before being used by the library. diff --git a/owner/src/main/java/org/aeonbits/owner/Converters.java b/owner/src/main/java/org/aeonbits/owner/Converters.java index ab422884..0019a8c8 100644 --- a/owner/src/main/java/org/aeonbits/owner/Converters.java +++ b/owner/src/main/java/org/aeonbits/owner/Converters.java @@ -9,6 +9,7 @@ package org.aeonbits.owner; import org.aeonbits.owner.Config.ConverterClass; +import org.aeonbits.owner.Config.CollectionConverterClass; import java.beans.PropertyEditor; import java.beans.PropertyEditorManager; @@ -61,6 +62,17 @@ Object tryConvert(Method targetMethod, Class targetType, String text) { } }, + METHOD_WITH_COLLECTION_CONVERTER_CLASS_ANNOTATION { + @Override + Object tryConvert(Method targetMethod, Class targetType, String text) { + CollectionConverterClass annotation = targetMethod.getAnnotation(CollectionConverterClass.class); + if (annotation == null) return SKIP; + + Class converterClass = annotation.value(); + return convertWithConverterClass(targetMethod, text, converterClass); + } + }, + COLLECTION { @Override Object tryConvert(Method targetMethod, Class targetType, String text) { diff --git a/owner/src/test/java/org/aeonbits/owner/typeconversion/collections/CollectionConverterClassTest.java b/owner/src/test/java/org/aeonbits/owner/typeconversion/collections/CollectionConverterClassTest.java new file mode 100644 index 00000000..79e71091 --- /dev/null +++ b/owner/src/test/java/org/aeonbits/owner/typeconversion/collections/CollectionConverterClassTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2012-2015, Luigi R. Viggiano + * All rights reserved. + * + * This software is distributable under the BSD license. + * See the terms of the BSD license in the documentation provided with this software. + */ +package org.aeonbits.owner.typeconversion.collections; + +import java.lang.reflect.Method; +import org.aeonbits.owner.Config.CollectionConverterClass; +import org.aeonbits.owner.Config; +import org.aeonbits.owner.ConfigFactory; +import org.aeonbits.owner.Converter; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.aeonbits.owner.Config.ConverterClass; +import org.aeonbits.owner.Config.Separator; +import org.aeonbits.owner.Config.TokenizerClass; +import org.aeonbits.owner.Tokenizer; + +import static org.junit.Assert.assertEquals; + +/** + * + * @author Adam Huječek + */ +public class CollectionConverterClassTest { + + private MyConfig cfg; + + static public class Server { + private final String name; + private final Integer port; + + public Server(String name, Integer port) { + this.name = name; + this.port = port; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null || !(obj instanceof Server)) { + return false; + } + final Server other = (Server) obj; + if (this.name == null) { + if (other.name != null) { + return false; + } + } else if(!this.name.equals(other.name)) { + return false; + } + if (this.port == null) { + if (other.port != null) { + return false; + } + } else if(!this.port.equals(other.port)) { + return false; + } + return true; + } + } + + public interface MyConfig extends Config { + @DefaultValue("google.com, yahoo.com:8080, owner.aeonbits.org:4000") + @CollectionConverterClass(UnmodifiableListConverter.class) + @ConverterClass(ServerConverter.class) + Collection serversWithoutSeparatorOrTokenizer(); + + @DefaultValue("google.com; yahoo.com:8080; owner.aeonbits.org:4000") + @CollectionConverterClass(UnmodifiableListConverter.class) + @ConverterClass(ServerConverter.class) + @Separator(";") + Collection serversWithSeparator(); + + @DefaultValue("google.com^yahoo.com:8080^owner.aeonbits.org:4000") + @CollectionConverterClass(UnmodifiableListConverter.class) + @ConverterClass(ServerConverter.class) + @TokenizerClass(SimpleTokenizer.class) + Collection serversWithTokenizer(); + } + + public static class ServerConverter implements Converter { + @Override + public Server convert(Method targetMethod, String text) { + String[] split = text.split(":", -1); + String name = split[0]; + Integer port = 80; + if (split.length >= 2) + port = Integer.valueOf(split[1]); + return new Server(name, port); + } + } + + public static class UnmodifiableListConverter implements Converter> { + + @SuppressWarnings("unchecked") + @Override + public List convert(Method targetMethod, String text) { + String[] tokens; + TokenizerClass tokenizer = + targetMethod.getAnnotation(TokenizerClass.class); + if (tokenizer != null) { + try { + Tokenizer t = tokenizer.value().getDeclaredConstructor().newInstance(); + tokens = t.tokens(text); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + Separator sep = targetMethod.getAnnotation(Separator.class); + String delimiter = sep != null ? sep.value() : ","; + tokens = text.split(delimiter, -1); + } + ConverterClass converter = + targetMethod.getAnnotation(ConverterClass.class); + try { + Converter c = converter.value().getDeclaredConstructor().newInstance(); + List list = new ArrayList(tokens.length); + for (String token : tokens) { + list.add(c.convert(targetMethod, token.trim())); + } + return Collections.unmodifiableList(list); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public static class SimpleTokenizer implements Tokenizer { + + @Override + public String[] tokens(String values) { + return values.split("\\^", -1); + } + + } + + @Before + public void setUp() throws Exception { + cfg = ConfigFactory.create(MyConfig.class); + } + + @Test + public void itShouldWorkWithoutSeparatorOrTokenizer() throws Exception { + List expected = Arrays.asList( + new Server("google.com", 80), + new Server("yahoo.com", 8080), + new Server("owner.aeonbits.org", 4000)); + assertEquals(expected, cfg.serversWithoutSeparatorOrTokenizer()); + } + + @Test + public void shouldWorkWithSeparator() throws Exception { + List expected = Arrays.asList( + new Server("google.com", 80), + new Server("yahoo.com", 8080), + new Server("owner.aeonbits.org", 4000)); + assertEquals(expected, cfg.serversWithSeparator()); + } + + @Test + public void shouldWorkWithTokenizer() throws Exception { + List expected = Arrays.asList( + new Server("google.com", 80), + new Server("yahoo.com", 8080), + new Server("owner.aeonbits.org", 4000)); + assertEquals(expected, cfg.serversWithTokenizer()); + } +} diff --git a/owner/src/test/java/org/aeonbits/owner/typeconversion/collections/CollectionSupportTest.java b/owner/src/test/java/org/aeonbits/owner/typeconversion/collections/CollectionSupportTest.java index 03a5ffa0..602d0dce 100644 --- a/owner/src/test/java/org/aeonbits/owner/typeconversion/collections/CollectionSupportTest.java +++ b/owner/src/test/java/org/aeonbits/owner/typeconversion/collections/CollectionSupportTest.java @@ -8,8 +8,11 @@ package org.aeonbits.owner.typeconversion.collections; +import java.lang.reflect.Method; +import org.aeonbits.owner.Config.CollectionConverterClass; import org.aeonbits.owner.Config; import org.aeonbits.owner.ConfigFactory; +import org.aeonbits.owner.Converter; import org.junit.Before; import org.junit.Test; @@ -61,14 +64,33 @@ public interface CollectionConfig extends Config { @DefaultValue(INTEGERS) CollectionWithoutDefaultConstructor badCollection(); + + @CollectionConverterClass(CollectionWithoutDefaultConstructorConverter.class) + @DefaultValue(COLORS) + CollectionWithoutDefaultConstructor collectionConverterClassCollection(); } - static class CollectionWithoutDefaultConstructor extends ArrayList { + static public class CollectionWithoutDefaultConstructor extends ArrayList { public CollectionWithoutDefaultConstructor(int size) { super(size); } } + static public class CollectionWithoutDefaultConstructorConverter implements Converter> { + + @Override + public CollectionWithoutDefaultConstructor convert(Method method, String input) { + final String[] inputs = input.split(","); + final CollectionWithoutDefaultConstructor collection = + new CollectionWithoutDefaultConstructor(inputs.length); + for (String value : inputs) { + collection.add(value); + } + return collection; + } + + } + @Before public void setUp() throws Exception { cfg = ConfigFactory.create(CollectionConfig.class); @@ -116,5 +138,9 @@ public void itShouldWorkWithRawCollectionAsWithCollectionOfStrings() throws Exce assertEquals(Arrays.asList("1", "2", "3"), cfg.rawCollection()); } + @Test + public void itShouldWorkWithCollectionConverterClass() throws Exception { + assertEquals(Arrays.asList("pink", "black"), cfg.collectionConverterClassCollection()); + } }