Skip to content

Commit 9a75e7b

Browse files
committed
value api: move reflective and standard converter to public package
- javadoc for reflective converter
1 parent cf45b0f commit 9a75e7b

File tree

4 files changed

+100
-112
lines changed

4 files changed

+100
-112
lines changed

jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java renamed to jooby/src/main/java/io/jooby/value/ReflectiveBeanConverter.java

Lines changed: 79 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
44
* Copyright 2014 Edgar Espina
55
*/
6-
package io.jooby.internal.converter;
6+
package io.jooby.value;
77

88
import static io.jooby.SneakyThrows.propagate;
99

@@ -18,17 +18,17 @@
1818
import io.jooby.Usage;
1919
import io.jooby.exception.BadRequestException;
2020
import io.jooby.exception.ProvisioningException;
21+
import io.jooby.exception.TypeMismatchException;
2122
import io.jooby.internal.reflect.$Types;
22-
import io.jooby.value.ConversionHint;
23-
import io.jooby.value.Converter;
24-
import io.jooby.value.Value;
2523
import jakarta.inject.Inject;
2624
import jakarta.inject.Named;
2725

2826
/**
2927
* Creates an object from {@link Value}. Value might come from HTTP Context (Query, Path, Form,
3028
* etc.) or from configuration value.
3129
*
30+
* <p>This is the fallback/default converter for a JavaBeans object.
31+
*
3232
* @author edgar
3333
* @since 1.0.0
3434
*/
@@ -47,53 +47,89 @@ public void invoke(MethodHandles.Lookup lookup, Object instance) throws Throwabl
4747

4848
private final MethodHandles.Lookup lookup;
4949

50+
/**
51+
* Creates a new instance using a lookup.
52+
*
53+
* @param lookup Method handle lookup.
54+
*/
5055
public ReflectiveBeanConverter(MethodHandles.Lookup lookup) {
5156
this.lookup = lookup;
5257
}
5358

59+
/**
60+
* Convert a value into a JavaBean object.
61+
*
62+
* <p>Selected constructor follows one of these rules:
63+
*
64+
* <ul>
65+
* <li>It is the default (no args) constructor.
66+
* <li>There is only when constructor. If the constructor has non-null arguments a {@link
67+
* ProvisioningException} will be thrown when {@link Value} fails to resolve the non-null
68+
* argument
69+
* <li>There are multiple constructor but only one is annotated with {@link Inject}. If the
70+
* constructor has non-null arguments a {@link ProvisioningException} will be thrown when
71+
* {@link Value} fails to resolve the non-null argument
72+
* </ul>
73+
*
74+
* <p>Any other value is matched against a setter like method. Method might or might not be
75+
* prefixed with <code>set</code>.
76+
*
77+
* <p>Argument might be annotated with nullable like annotations. Optionally with {@link Named}
78+
* annotation for non-standard Java Names.
79+
*
80+
* @param type Requested type.
81+
* @param value Value value.
82+
* @param hint Requested hint.
83+
* @return Object instance.
84+
* @throws TypeMismatchException when convert returns <code>null</code> and hint is set to {@link
85+
* ConversionHint#Strict}.
86+
* @throws ProvisioningException when convert target type constructor requires a non-null value
87+
* and value is missing or null.
88+
*/
5489
@Override
55-
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
56-
return convert(value, $Types.parameterizedType0(type), hint == ConversionHint.Empty);
57-
}
58-
59-
private Object convert(@NonNull Value node, @NonNull Class type, boolean allowEmptyBean) {
90+
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint)
91+
throws TypeMismatchException, ProvisioningException {
92+
var rawType = $Types.parameterizedType0(type);
93+
var allowEmptyBean = hint == ConversionHint.Empty;
6094
try {
61-
return newInstance(type, node, allowEmptyBean);
95+
var constructors = rawType.getConstructors();
96+
Set<Value> state = new HashSet<>();
97+
Constructor<?> constructor;
98+
if (constructors.length == 0) {
99+
//noinspection unchecked
100+
constructor = rawType.getDeclaredConstructor();
101+
} else {
102+
constructor = selectConstructor(constructors);
103+
}
104+
var args = inject(value, constructor, state::add);
105+
var setters = setters(rawType, value, state);
106+
Object instance;
107+
if (!allowEmptyBean && state.stream().allMatch(Value::isMissing)) {
108+
instance = null;
109+
} else {
110+
var handle = lookup.unreflectConstructor(constructor);
111+
instance = handle.invokeWithArguments(args);
112+
for (var setter : setters) {
113+
setter.invoke(lookup, instance);
114+
}
115+
}
116+
if (instance == null && hint == ConversionHint.Strict) {
117+
throw new TypeMismatchException(value.name(), type);
118+
}
119+
return instance;
62120
} catch (InvocationTargetException x) {
63121
throw propagate(x.getCause());
64122
} catch (Throwable x) {
65123
throw propagate(x);
66124
}
67125
}
68126

69-
private Object newInstance(Class type, Value node, boolean allowEmptyBean) throws Throwable {
70-
var constructors = type.getConstructors();
71-
Set<Value> state = new HashSet<>();
72-
Constructor<?> constructor;
73-
if (constructors.length == 0) {
74-
constructor = type.getDeclaredConstructor();
75-
} else {
76-
constructor = selectConstructor(constructors);
77-
}
78-
var args = inject(node, constructor, state::add);
79-
var setters = setters(type, node, state);
80-
if (!allowEmptyBean && state.stream().allMatch(Value::isMissing)) {
81-
return null;
82-
}
83-
var handle = lookup.unreflectConstructor(constructor);
84-
var instance = handle.invokeWithArguments(args);
85-
for (var setter : setters) {
86-
setter.invoke(lookup, instance);
87-
}
88-
return instance;
89-
}
90-
91-
private static Constructor selectConstructor(Constructor[] constructors) {
127+
private static Constructor<?> selectConstructor(Constructor<?>[] constructors) {
92128
if (constructors.length == 1) {
93129
return constructors[0];
94130
} else {
95-
Constructor injectConstructor = null;
96-
Constructor defaultConstructor = null;
131+
Constructor<?> injectConstructor = null;
132+
Constructor<?> defaultConstructor = null;
97133
for (var constructor : constructors) {
98134
if (Modifier.isPublic(constructor.getModifiers())) {
99135
var inject = constructor.getAnnotation(Inject.class);
@@ -124,9 +160,8 @@ public static List<Object> inject(Value scope, Executable method, Consumer<Value
124160
return List.of();
125161
}
126162
var args = new ArrayList<>(parameters.length);
127-
for (int i = 0; i < parameters.length; i++) {
128-
var parameter = parameters[i];
129-
var name = paramName(parameter);
163+
for (var parameter : parameters) {
164+
var name = parameterName(parameter);
130165
var param = scope.get(name);
131166
var arg = value(parameter, scope, param);
132167
if (arg == null) {
@@ -139,9 +174,9 @@ public static List<Object> inject(Value scope, Executable method, Consumer<Value
139174
return args;
140175
}
141176

142-
private static String paramName(Parameter parameter) {
143-
Named named = parameter.getAnnotation(Named.class);
144-
if (named != null && named.value().length() > 0) {
177+
private static String parameterName(Parameter parameter) {
178+
var named = parameter.getAnnotation(Named.class);
179+
if (named != null && !named.value().isEmpty()) {
145180
return named.value();
146181
}
147182
if (parameter.isNamePresent()) {
@@ -169,7 +204,7 @@ private static Set<String> names(Value node) {
169204
return names;
170205
}
171206

172-
private static List<Setter> setters(Class type, Value node, Set<Value> nodes) {
207+
private static List<Setter> setters(Class<?> type, Value node, Set<Value> nodes) {
173208
var methods = type.getMethods();
174209
var result = new ArrayList<Setter>();
175210
for (String name : names(node)) {
@@ -194,6 +229,7 @@ private static List<Setter> setters(Class type, Value node, Set<Value> nodes) {
194229
return result;
195230
}
196231

232+
@SuppressWarnings("unchecked")
197233
private static Object value(Parameter parameter, Value node, Value value) {
198234
try {
199235
if (isFileUpload(node, parameter)) {
@@ -263,7 +299,7 @@ private static boolean isFileUpload(Value node, Parameter parameter) {
263299
|| isFileUpload($Types.parameterizedType0(parameter.getParameterizedType()));
264300
}
265301

266-
private static boolean isFileUpload(Class type) {
302+
private static boolean isFileUpload(Class<?> type) {
267303
return FileUpload.class == type;
268304
}
269305

jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java renamed to jooby/src/main/java/io/jooby/value/StandardConverter.java

Lines changed: 20 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
44
* Copyright 2014 Edgar Espina
55
*/
6-
package io.jooby.internal.converter;
6+
package io.jooby.value;
77

88
import static java.lang.Double.parseDouble;
99
import static java.lang.Long.parseLong;
@@ -30,10 +30,6 @@
3030
import edu.umd.cs.findbugs.annotations.NonNull;
3131
import io.jooby.SneakyThrows;
3232
import io.jooby.StatusCode;
33-
import io.jooby.value.ConversionHint;
34-
import io.jooby.value.Converter;
35-
import io.jooby.value.Value;
36-
import io.jooby.value.ValueFactory;
3733

3834
public enum StandardConverter implements Converter {
3935
String {
@@ -51,131 +47,89 @@ public Object convert(@NonNull Type type, @NonNull Value value, @NonNull Convers
5147
@Override
5248
protected void add(ValueFactory factory) {
5349
factory.put(int.class, this);
54-
}
55-
56-
@Override
57-
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
58-
return value.intValue();
59-
}
60-
},
61-
IntNullable {
62-
@Override
63-
protected void add(ValueFactory factory) {
6450
factory.put(Integer.class, this);
6551
}
6652

6753
@Override
6854
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
55+
if (type == int.class) {
56+
return value.intValue();
57+
}
6958
return value.isMissing() ? null : value.intValue();
7059
}
7160
},
7261
Long {
7362
@Override
7463
protected void add(ValueFactory factory) {
7564
factory.put(long.class, this);
76-
}
77-
78-
@Override
79-
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
80-
return value.longValue();
81-
}
82-
},
83-
LongNullable {
84-
@Override
85-
protected void add(ValueFactory factory) {
8665
factory.put(Long.class, this);
8766
}
8867

8968
@Override
9069
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
70+
if (type == long.class) {
71+
return value.longValue();
72+
}
9173
return value.isMissing() ? null : value.longValue();
9274
}
9375
},
9476
Float {
9577
@Override
9678
protected void add(ValueFactory factory) {
9779
factory.put(float.class, this);
98-
}
99-
100-
@Override
101-
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
102-
return value.floatValue();
103-
}
104-
},
105-
FloatNullable {
106-
@Override
107-
protected void add(ValueFactory factory) {
10880
factory.put(Float.class, this);
10981
}
11082

11183
@Override
11284
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
85+
if (type == float.class) {
86+
return value.floatValue();
87+
}
11388
return value.isMissing() ? null : value.floatValue();
11489
}
11590
},
11691
Double {
11792
@Override
11893
protected void add(ValueFactory factory) {
11994
factory.put(double.class, this);
120-
}
121-
122-
@Override
123-
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
124-
return value.doubleValue();
125-
}
126-
},
127-
DoubleNullable {
128-
@Override
129-
protected void add(ValueFactory factory) {
13095
factory.put(Double.class, this);
13196
}
13297

13398
@Override
13499
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
100+
if (type == double.class) {
101+
return value.doubleValue();
102+
}
135103
return value.isMissing() ? null : value.doubleValue();
136104
}
137105
},
138106
Boolean {
139107
@Override
140108
protected void add(ValueFactory factory) {
141109
factory.put(boolean.class, this);
142-
}
143-
144-
@Override
145-
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
146-
return value.booleanValue();
147-
}
148-
},
149-
BooleanNullable {
150-
@Override
151-
protected void add(ValueFactory factory) {
152110
factory.put(Boolean.class, this);
153111
}
154112

155113
@Override
156114
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
115+
if (type == boolean.class) {
116+
return value.booleanValue();
117+
}
157118
return value.isMissing() ? null : value.booleanValue();
158119
}
159120
},
160121
Byte {
161122
@Override
162123
protected void add(ValueFactory factory) {
163124
factory.put(byte.class, this);
164-
}
165-
166-
@Override
167-
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
168-
return value.byteValue();
169-
}
170-
},
171-
ByteNullable {
172-
@Override
173-
protected void add(ValueFactory factory) {
174125
factory.put(Byte.class, this);
175126
}
176127

177128
@Override
178129
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
130+
if (type == byte.class) {
131+
return value.byteValue();
132+
}
179133
return value.isMissing() ? null : value.byteValue();
180134
}
181135
},
@@ -209,7 +163,7 @@ protected void add(ValueFactory factory) {
209163

210164
@Override
211165
public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) {
212-
String charset = value.value();
166+
var charset = value.value();
213167
return switch (charset.toLowerCase()) {
214168
case "utf-8" -> StandardCharsets.UTF_8;
215169
case "us-ascii" -> StandardCharsets.US_ASCII;

jooby/src/main/java/io/jooby/value/ValueFactory.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
import io.jooby.SneakyThrows;
1717
import io.jooby.exception.ProvisioningException;
1818
import io.jooby.exception.TypeMismatchException;
19-
import io.jooby.internal.converter.ReflectiveBeanConverter;
20-
import io.jooby.internal.converter.StandardConverter;
2119
import io.jooby.internal.reflect.$Types;
2220

2321
/**

jooby/src/test/java/io/jooby/internal/DurationConverterTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
import org.junit.jupiter.api.Test;
1515
import org.mockito.Mockito;
1616

17-
import io.jooby.internal.converter.StandardConverter;
1817
import io.jooby.value.ConversionHint;
18+
import io.jooby.value.StandardConverter;
1919
import io.jooby.value.Value;
2020

2121
public class DurationConverterTest {

0 commit comments

Comments
 (0)