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
88import static io .jooby .SneakyThrows .propagate ;
99
1818import io .jooby .Usage ;
1919import io .jooby .exception .BadRequestException ;
2020import io .jooby .exception .ProvisioningException ;
21+ import io .jooby .exception .TypeMismatchException ;
2122import io .jooby .internal .reflect .$Types ;
22- import io .jooby .value .ConversionHint ;
23- import io .jooby .value .Converter ;
24- import io .jooby .value .Value ;
2523import jakarta .inject .Inject ;
2624import 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
0 commit comments