Skip to content

Commit a1a31ae

Browse files
committed
Added ValueConverter SPI
1 parent 281ee60 commit a1a31ae

5 files changed

Lines changed: 160 additions & 3 deletions

File tree

jooby/src/main/java/io/jooby/Value.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.jooby.internal.MissingValue;
1111
import io.jooby.internal.SingleValue;
1212
import io.jooby.internal.ValueInjector;
13+
import io.jooby.spi.ValueContainer;
1314

1415
import javax.annotation.Nonnull;
1516
import javax.annotation.Nullable;
@@ -52,7 +53,7 @@
5253
* @since 2.0.0
5354
* @author edgar
5455
*/
55-
public interface Value extends Iterable<Value> {
56+
public interface Value extends Iterable<Value>, ValueContainer {
5657

5758
/**
5859
* Convert this value to long (if possible).

jooby/src/main/java/io/jooby/internal/ValueInjector.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.jooby.TypeMismatchException;
1515
import io.jooby.Value;
1616
import io.jooby.internal.reflect.$Types;
17+
import io.jooby.spi.ValueConverters;
1718

1819
import javax.inject.Inject;
1920
import javax.inject.Named;
@@ -147,6 +148,10 @@ private static Object resolve(Value scope, Class type)
147148
throws IllegalAccessException, InvocationTargetException, InstantiationException,
148149
NoSuchMethodException {
149150
if (scope.isObject() || scope.isSingle()) {
151+
Object o = ValueConverters.getInstance().convert(scope, type);
152+
if (o != null) {
153+
return o;
154+
}
150155
return newInstance(type, scope);
151156
} else if (scope.isMissing()) {
152157
if (type.isPrimitive()) {
Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
package io.jooby.spi;
22

3+
import javax.annotation.Nonnull;
4+
import javax.annotation.Nullable;
35

4-
public interface ValueConverter {
6+
import io.jooby.TypeMismatchException;
7+
import io.jooby.Value;
58

9+
public interface ValueConverter {
10+
/**
11+
* This defaults to true to allow a functional interface.
12+
* @param type
13+
* @return
14+
*/
15+
default boolean supportsType(@Nonnull Class<?> type) {
16+
return true;
17+
}
18+
/**
19+
* Converts values for {@link Value#to} and friends. Returning null indicates the type is not supported
20+
* or the converter chose not to do the conversion.
21+
* @param value
22+
* @param type
23+
* @return <code>null</code> indicates that the converter chose to delegate to other converters down the chain.
24+
* @throws TypeMismatchException if the converter cannot convert the type and does not want to delegate.
25+
*/
26+
@Nullable Object convert(@Nonnull ValueContainer value, @Nonnull Class<?> type) throws TypeMismatchException;
627
}
Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,70 @@
11
package io.jooby.spi;
22

3+
import java.util.ServiceLoader;
4+
import java.util.concurrent.CopyOnWriteArrayList;
35

4-
public class ValueConverters {
6+
import javax.annotation.Nullable;
7+
8+
import io.jooby.TypeMismatchException;
9+
10+
public final class ValueConverters {
11+
// Allow thread safe adding of ValueConverters.
12+
private final CopyOnWriteArrayList<ValueConverter> valueConverters;
13+
14+
// Initialization on demand
15+
private static final class Hidden {
16+
private static final ValueConverters INSTANCE = ValueConverters.create().fromServiceLoader();
17+
}
18+
19+
private ValueConverters(CopyOnWriteArrayList<ValueConverter> valueConverters) {
20+
super();
21+
this.valueConverters = valueConverters;
22+
}
23+
24+
static ValueConverters create() {
25+
return new ValueConverters(new CopyOnWriteArrayList<>());
26+
}
27+
28+
public @Nullable Object convert(ValueContainer v, Class<?> c) throws TypeMismatchException {
29+
Object result = null;
30+
for (ValueConverter vc : valueConverters) {
31+
if (vc.supportsType(c)) {
32+
result = vc.convert(v, c);
33+
if (result != null) {
34+
return result;
35+
}
36+
}
37+
}
38+
return result;
39+
}
40+
41+
final ValueConverters fromServiceLoader() {
42+
ServiceLoader<ValueConverter> sl = ServiceLoader.load(ValueConverter.class);
43+
//If any failes to load we will fail entirely.
44+
//The value converters found earlier in the classpath take precedence.
45+
sl.forEach(this::add);
46+
return this;
47+
}
48+
/**
49+
* You can add value converters programmatic. For now its protected.
50+
* Its also to aid unit testing since serviceloader is inherently static singleton.
51+
* @param vc
52+
* @return
53+
*/
54+
/* private */ final ValueConverters add(ValueConverter vc) {
55+
valueConverters.add(vc);
56+
return this;
57+
}
58+
59+
final ValueConverters clear() {
60+
valueConverters.clear();
61+
return this;
62+
}
63+
64+
65+
66+
public static final ValueConverters getInstance() {
67+
return ValueConverters.Hidden.INSTANCE;
68+
}
569

670
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.jooby.spi;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.util.function.Consumer;
6+
7+
import org.junit.jupiter.api.AfterAll;
8+
import org.junit.jupiter.api.Test;
9+
10+
import io.jooby.MissingValueException;
11+
import io.jooby.QueryString;
12+
import io.jooby.internal.UrlParser;
13+
14+
15+
class ValueConvertersTest {
16+
17+
@AfterAll
18+
static void restoreFromServiceLoader() {
19+
//Restore ValueConvert list for other unit tests.
20+
ValueConverters.getInstance().clear().fromServiceLoader();
21+
}
22+
23+
@Test
24+
void testConvert() {
25+
ValueConverters.getInstance()
26+
.clear()
27+
.add((value, type) -> {
28+
if (type == MyValue.class) {
29+
MyValue mv = new MyValue();
30+
// we have chosen simple parameters names to make sure we don't get a false positve
31+
// from the reflection converter.
32+
mv.name = value.get("n").value();
33+
//TODO: ValueContainer probably should have primitive convert methods.
34+
mv.order = Integer.parseInt(value.get("o").value());
35+
return mv;
36+
}
37+
return null;
38+
});
39+
queryString("n=stuff&o=1", queryString -> {
40+
MyValue mv = queryString.to(MyValue.class);
41+
assertEquals("stuff", mv.name);
42+
assertEquals(1, mv.order);
43+
});
44+
queryString("n=stuff&missingOrder=1", queryString -> {
45+
try {
46+
queryString.to(MyValue.class);
47+
fail();
48+
}
49+
catch (MissingValueException mve) {
50+
assertEquals("Missing value: 'o'", mve.getMessage());
51+
}
52+
});
53+
}
54+
55+
static class MyValue {
56+
public String name;
57+
public int order;
58+
}
59+
60+
61+
62+
private void queryString(String queryString, Consumer<QueryString> consumer) {
63+
consumer.accept(UrlParser.queryString(queryString));
64+
}
65+
66+
}

0 commit comments

Comments
 (0)