Skip to content

Commit f236119

Browse files
authored
Merge pull request #1386 from agentgt/feature/more-value-types-1377
Feature/more value types 1377
2 parents be05f06 + bbf7e51 commit f236119

6 files changed

Lines changed: 301 additions & 1 deletion

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: 8 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()) {
@@ -250,6 +255,9 @@ public static boolean isSimple(Class rawType, Type type) {
250255
if (FileUpload.class == rawType) {
251256
return true;
252257
}
258+
if (rawType.isEnum()) {
259+
return true;
260+
}
253261
/**********************************************************************************************
254262
* Static method: valueOf
255263
* ********************************************************************************************
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.jooby.spi;
2+
3+
import javax.annotation.Nonnull;
4+
import javax.annotation.Nullable;
5+
6+
import io.jooby.Value;
7+
8+
/**
9+
* A restricted base type of {@link Value} for the {@link ValueConverter} SPI.
10+
* @author agentgt
11+
*
12+
*/
13+
public interface ValueContainer {
14+
15+
/**
16+
* Get a value at the given position.
17+
*
18+
* @param index Position.
19+
* @return A value at the given position.
20+
*/
21+
@Nonnull ValueContainer get(@Nonnull int index);
22+
23+
/**
24+
* Get a value that matches the given name.
25+
*
26+
* @param name Field name.
27+
* @return Field value.
28+
*/
29+
@Nonnull ValueContainer get(@Nonnull String name);
30+
31+
/**
32+
* Get string value.
33+
*
34+
* @return String value.
35+
*/
36+
@Nonnull String value();
37+
38+
/**
39+
* Convert this value to String (if possible) or <code>null</code> when missing.
40+
*
41+
* @return Convert this value to String (if possible) or <code>null</code> when missing.
42+
*/
43+
@Nullable String valueOrNull();
44+
45+
/**
46+
* True for missing values.
47+
*
48+
* @return True for missing values.
49+
*/
50+
boolean isMissing();
51+
52+
/**
53+
* The number of values this one has. For single values size is <code>0</code>.
54+
*
55+
* @return Number of values. Mainly for array and hash values.
56+
*/
57+
int size();
58+
59+
/**
60+
* True if this value is an array/sequence (not single or hash).
61+
*
62+
* @return True if this value is an array/sequence.
63+
*/
64+
boolean isArray();
65+
66+
/**
67+
* True if this is a single value (not a hash or array).
68+
*
69+
* @return True if this is a single value (not a hash or array).
70+
*/
71+
boolean isSingle();
72+
73+
/**
74+
* True if this is a hash/object value (not single or array).
75+
*
76+
* @return True if this is a hash/object value (not single or array).
77+
*/
78+
boolean isObject();
79+
80+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.jooby.spi;
2+
3+
import javax.annotation.Nonnull;
4+
import javax.annotation.Nullable;
5+
6+
import io.jooby.TypeMismatchException;
7+
import io.jooby.Value;
8+
9+
/**
10+
* An SPI for value conversion.
11+
* @author agentgt
12+
*/
13+
public interface ValueConverter {
14+
/**
15+
* A short circuit to see if the converter supports the given type.
16+
*
17+
* This defaults to true to allow a functional interface since convert can return null
18+
* to indicate it does not support the type.
19+
*
20+
* @param type class or interface
21+
* @return true if the converter can convert for the type
22+
*/
23+
default boolean supportsType(@Nonnull Class<?> type) {
24+
return true;
25+
}
26+
/**
27+
* Converts values for {@link Value#to} and friends. Returning null indicates the type is not supported
28+
* or the converter chose not to do the conversion.
29+
* @param value the value to be converted
30+
* @param type the desired type. The type should be the equal or a super of the resulting instance returned.
31+
* @return <code>null</code> indicates that the converter chose to delegate to other converters down the chain.
32+
* @throws TypeMismatchException if the converter cannot convert the type and does not want to delegate.
33+
*/
34+
@Nullable Object convert(@Nonnull ValueContainer value, @Nonnull Class<?> type) throws TypeMismatchException;
35+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package io.jooby.spi;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.ServiceLoader;
6+
7+
import javax.annotation.Nullable;
8+
9+
import io.jooby.TypeMismatchException;
10+
11+
/**
12+
* Contains the {@link ValueConverter}s loaded via the ServiceLoader. It is is a
13+
* singleton and an instance can be retrieved with {@link #getInstance()}. The
14+
* ValueConverters are stored in an ordered collection and thus resolution of
15+
* type to be converted is based on the order of the converters.
16+
*
17+
* @author agentgt
18+
*
19+
*/
20+
public final class ValueConverters {
21+
22+
// Allow thread safe adding of ValueConverters.
23+
private final Iterable<ValueConverter> valueConverters;
24+
25+
// Initialization on demand
26+
private static final class Hidden {
27+
28+
private static volatile ValueConverters instance = ValueConverters.builder().fromServiceLoader().build();
29+
}
30+
31+
private ValueConverters(Iterable<ValueConverter> valueConverters) {
32+
super();
33+
this.valueConverters = valueConverters;
34+
}
35+
36+
static Builder builder() {
37+
return new Builder();
38+
}
39+
40+
static final class Builder {
41+
42+
private final List<ValueConverter> valueConverters = new ArrayList<>();
43+
44+
Builder fromServiceLoader() {
45+
ServiceLoader<ValueConverter> sl = ServiceLoader.load(ValueConverter.class);
46+
// If any failes to load we will fail entirely.
47+
// The value converters found earlier in the classpath take precedence.
48+
sl.forEach(this::add);
49+
return this;
50+
}
51+
52+
/**
53+
* You can add value converters programmatic. For now its protected. Its
54+
* also to aid unit testing since serviceloader is inherently static
55+
* singleton.
56+
*
57+
* @param vc
58+
* @return
59+
*/
60+
Builder add(ValueConverter vc) {
61+
valueConverters.add(vc);
62+
return this;
63+
}
64+
65+
Builder clear() {
66+
valueConverters.clear();
67+
return this;
68+
}
69+
70+
ValueConverters build() {
71+
return new ValueConverters(valueConverters);
72+
}
73+
74+
ValueConverters set() {
75+
ValueConverters vc = build();
76+
Hidden.instance = vc;
77+
return vc;
78+
}
79+
}
80+
81+
/**
82+
* Attempts to convert values to an object based on the provided type.
83+
*
84+
* @param v
85+
* value
86+
* @param c
87+
* desired type
88+
* @return the type if converted or null if conversion was not possible.
89+
* @throws TypeMismatchException
90+
* failure in a converter
91+
*/
92+
public @Nullable Object convert(ValueContainer v, Class<?> c) throws TypeMismatchException {
93+
Object result = null;
94+
for (ValueConverter vc : valueConverters) {
95+
if (vc.supportsType(c)) {
96+
result = vc.convert(v, c);
97+
if (result != null) {
98+
return result;
99+
}
100+
}
101+
}
102+
return result;
103+
}
104+
105+
/**
106+
* The ValueConverters singleton usually preloaded by the ServiceLoader.
107+
*
108+
* @return the shared singleton used by Jooby
109+
*/
110+
public static ValueConverters getInstance() {
111+
return ValueConverters.Hidden.instance;
112+
}
113+
114+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
class ValueConvertersTest {
15+
16+
@AfterAll
17+
static void restoreFromServiceLoader() {
18+
// Restore ValueConvert list for other unit tests.
19+
ValueConverters.builder().fromServiceLoader().set();
20+
}
21+
22+
@Test
23+
void testConvert() {
24+
ValueConverters.builder().add((value, type) -> {
25+
if (type == MyValue.class) {
26+
MyValue mv = new MyValue();
27+
// we have chosen simple parameters names to make sure we don't get a
28+
// false positve
29+
// from the reflection converter.
30+
mv.name = value.get("n").value();
31+
// TODO: ValueContainer probably should have primitive convert methods.
32+
mv.order = Integer.parseInt(value.get("o").value());
33+
return mv;
34+
}
35+
return null;
36+
}).set();
37+
queryString("n=stuff&o=1", queryString -> {
38+
MyValue mv = queryString.to(MyValue.class);
39+
assertEquals("stuff", mv.name);
40+
assertEquals(1, mv.order);
41+
});
42+
queryString("n=stuff&missingOrder=1", queryString -> {
43+
try {
44+
queryString.to(MyValue.class);
45+
fail();
46+
} catch (MissingValueException mve) {
47+
assertEquals("Missing value: 'o'", mve.getMessage());
48+
}
49+
});
50+
}
51+
52+
static class MyValue {
53+
54+
public String name;
55+
public int order;
56+
}
57+
58+
private void queryString(String queryString, Consumer<QueryString> consumer) {
59+
consumer.accept(UrlParser.queryString(queryString));
60+
}
61+
62+
}

0 commit comments

Comments
 (0)