Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions owner-site/site/docs/type-conversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<? implements Collection>`](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<Server> servers();
}

public class CollectionServerConverter
implements Converter<List<Server>> {
public List<Server> convert(Method targetMethod, String text) {
String[] split = text.split(",", -1);
ServerConverter converter = new ServerConverter();
List<Server> list = new ArrayList<Server>(split.length);
for (String server : split) {
list.add(converter.convert(targetMethod, server.trim());
}
return Collection.unmodifiableList(list);
}
}

MyConfig cfg = ConfigFactory.create(MyConfig.class);
List<Server> 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
--------------------------------

Expand Down
12 changes: 12 additions & 0 deletions owner/src/main/java/org/aeonbits/owner/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -354,6 +355,17 @@ enum DisableableFeature {
Class<? extends Converter> value();
}

/**
* Specifies a <code>{@link Converter}</code> 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<? extends Converter<? extends Collection<?>>> value();
}

/**
* Specifies a <code>{@link Preprocessor}</code> class to allow the user to define a custom logic to pre-process
* the property value before being used by the library.
Expand Down
12 changes: 12 additions & 0 deletions owner/src/main/java/org/aeonbits/owner/Converters.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<? extends Converter> converterClass = annotation.value();
return convertWithConverterClass(targetMethod, text, converterClass);
}
},

COLLECTION {
@Override
Object tryConvert(Method targetMethod, Class<?> targetType, String text) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Server> serversWithoutSeparatorOrTokenizer();

@DefaultValue("google.com; yahoo.com:8080; owner.aeonbits.org:4000")
@CollectionConverterClass(UnmodifiableListConverter.class)
@ConverterClass(ServerConverter.class)
@Separator(";")
Collection<Server> serversWithSeparator();

@DefaultValue("google.com^yahoo.com:8080^owner.aeonbits.org:4000")
@CollectionConverterClass(UnmodifiableListConverter.class)
@ConverterClass(ServerConverter.class)
@TokenizerClass(SimpleTokenizer.class)
Collection<Server> serversWithTokenizer();
}

public static class ServerConverter implements Converter<Server> {
@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<List<?>> {

@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<Server> 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<Server> 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<Server> expected = Arrays.asList(
new Server("google.com", 80),
new Server("yahoo.com", 8080),
new Server("owner.aeonbits.org", 4000));
assertEquals(expected, cfg.serversWithTokenizer());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -61,14 +64,33 @@ public interface CollectionConfig extends Config {

@DefaultValue(INTEGERS)
CollectionWithoutDefaultConstructor<Integer> badCollection();

@CollectionConverterClass(CollectionWithoutDefaultConstructorConverter.class)
@DefaultValue(COLORS)
CollectionWithoutDefaultConstructor<String> collectionConverterClassCollection();
}

static class CollectionWithoutDefaultConstructor<E> extends ArrayList<E> {
static public class CollectionWithoutDefaultConstructor<E> extends ArrayList<E> {
public CollectionWithoutDefaultConstructor(int size) {
super(size);
}
}

static public class CollectionWithoutDefaultConstructorConverter implements Converter<CollectionWithoutDefaultConstructor<String>> {

@Override
public CollectionWithoutDefaultConstructor<String> convert(Method method, String input) {
final String[] inputs = input.split(",");
final CollectionWithoutDefaultConstructor<String> collection =
new CollectionWithoutDefaultConstructor<String>(inputs.length);
for (String value : inputs) {
collection.add(value);
}
return collection;
}

}

@Before
public void setUp() throws Exception {
cfg = ConfigFactory.create(CollectionConfig.class);
Expand Down Expand Up @@ -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());
}
}