Skip to content

Commit 0f4697b

Browse files
committed
bean parser: support null values fix #408
1 parent 86ab10a commit 0f4697b

8 files changed

Lines changed: 135 additions & 14 deletions

File tree

coverage-report/src/test/java/org/jooby/hbv/BeanValidationFeature.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import javax.validation.ConstraintViolationException;
1010
import javax.validation.Path;
1111

12+
import org.jooby.Parser;
1213
import org.jooby.hbm.data.Car;
1314
import org.jooby.json.Jackson;
1415
import org.jooby.test.ServerFeature;
@@ -17,6 +18,8 @@
1718
public class BeanValidationFeature extends ServerFeature {
1819

1920
{
21+
parser(Parser.bean(true));
22+
2023
use(new Jackson());
2124

2225
use(new Hbv(Car.class));
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.jooby.issues;
2+
3+
import java.util.Date;
4+
5+
import org.jooby.Parser;
6+
import org.jooby.test.ServerFeature;
7+
import org.junit.Test;
8+
9+
public class Issue408 extends ServerFeature {
10+
11+
public static class Bean408 {
12+
public Integer id;
13+
14+
public String title;
15+
16+
public Date releaseDate;
17+
18+
@Override
19+
public String toString() {
20+
return id + ":" + title + ":" + releaseDate;
21+
}
22+
}
23+
24+
{
25+
parser(Parser.bean(true));
26+
27+
get("/408", req -> req.params().to(Bean408.class).toString());
28+
}
29+
30+
@Test
31+
public void shouldIgnoreEmptyValues() throws Exception {
32+
request()
33+
.get("/408?id=1&title=Title&releaseDate=")
34+
.expect("1:Title:null");
35+
36+
request()
37+
.get("/408")
38+
.expect("null:null:null");
39+
}
40+
41+
}

jooby/src/main/java/org/jooby/Err.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@
4141
@SuppressWarnings("serial")
4242
public class Err extends RuntimeException {
4343

44+
public static class Missing extends Err {
45+
46+
public Missing(final String name) {
47+
super(Status.BAD_REQUEST, name);
48+
}
49+
50+
}
51+
4452
/**
4553
* Default err handler it does content negotation. On <code>text/html</code> requests the err
4654
* handler creates an <code>err</code> view and set as model the {@link Err#toMap()}.

jooby/src/main/java/org/jooby/Jooby.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,9 @@ public EnvDep(final Predicate<String> predicate, final Consumer<Config> callback
640640
/** Don't add same mapper twice . */
641641
private Set<String> mappers = new HashSet<>();
642642

643+
/** Bean parser . */
644+
private Optional<Parser> beanParser = Optional.empty();
645+
643646
public Jooby() {
644647
this(null);
645648
}
@@ -1120,7 +1123,11 @@ public Session.Definition session(final Session.Store store) {
11201123
* @return This jooby instance.
11211124
*/
11221125
public Jooby parser(final Parser parser) {
1123-
bag.add(requireNonNull(parser, "A parser is required."));
1126+
if (parser instanceof BeanParser) {
1127+
beanParser = Optional.of(parser);
1128+
} else {
1129+
bag.add(requireNonNull(parser, "A parser is required."));
1130+
}
11241131
return this;
11251132
}
11261133

@@ -3919,7 +3926,7 @@ private Injector bootstrap(final Config args,
39193926
parsers.addBinding().toInstance(new DateParser(dateFormat));
39203927
parsers.addBinding().toInstance(new LocalDateParser(dateTimeFormatter));
39213928
parsers.addBinding().toInstance(new LocaleParser());
3922-
parsers.addBinding().toInstance(new BeanParser());
3929+
parsers.addBinding().toInstance(beanParser.orElseGet(() -> new BeanParser(false)));
39233930
parsers.addBinding().toInstance(new StaticMethodParser("valueOf"));
39243931
parsers.addBinding().toInstance(new StaticMethodParser("fromString"));
39253932
parsers.addBinding().toInstance(new StaticMethodParser("forName"));

jooby/src/main/java/org/jooby/Parser.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.util.Map;
2525
import java.util.Optional;
2626

27+
import org.jooby.internal.parser.BeanParser;
28+
2729
import com.google.inject.Key;
2830
import com.google.inject.TypeLiteral;
2931

@@ -417,4 +419,44 @@ interface Context extends Builder {
417419
*/
418420
Object parse(TypeLiteral<?> type, Context ctx) throws Throwable;
419421

422+
/**
423+
* Overwrite the default bean parser with <code>null</code> supports. The default bean parser
424+
* doesn't allow <code>null</code>, so if a parameter is optional you must declare it as
425+
* {@link Optional} otherwise parsing fails with a <code>404</code> status code.
426+
*
427+
* For example:
428+
* <pre>{@code
429+
*
430+
* public class Book {
431+
*
432+
* public String title;
433+
*
434+
* public Date releaseDate;
435+
*
436+
* public String toString() {
437+
* return title + ":" + releaseDate;
438+
* }
439+
* }
440+
*
441+
* {
442+
* parser(Parser.bean(true));
443+
*
444+
* post("/", req -> {
445+
* return req.params(Book.class).toString();
446+
* });
447+
* }
448+
* }</pre>
449+
*
450+
* With <code>/?title=Title&releaseDate=</code> prints <code>Title:null</code>.
451+
*
452+
* Now, same call with <code>allowNulls=false</code> results in <code>Bad Request: 400</code>
453+
* because <code>releaseDate</code> if required and isn't present in the HTTP request.
454+
*
455+
* @param allowNulls Enabled null supports while parsing HTTP params as Java Beans.
456+
* @return A new parser.
457+
*/
458+
static Parser bean(final boolean allowNulls) {
459+
return new BeanParser(allowNulls);
460+
}
461+
420462
}

jooby/src/main/java/org/jooby/internal/MutantImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public <T> T to(final TypeLiteral<T> type, final MediaType mtype) {
8989
results.put(type, result);
9090
} catch (ParamNotFound ex) {
9191
Tuple3<String, String, Status> md = md();
92-
throw new Err(parser.statusCode(ex), String.format(REQUIRED, md._2));
92+
throw new Err.Missing(String.format(REQUIRED, md._2));
9393
} catch (Err ex) {
9494
throw ex;
9595
} catch (Throwable ex) {

jooby/src/main/java/org/jooby/internal/parser/BeanParser.java

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
import java.util.List;
2626
import java.util.Map;
2727
import java.util.Map.Entry;
28+
import java.util.function.Function;
2829
import java.util.stream.Collectors;
2930

3031
import javax.inject.Inject;
3132

33+
import org.jooby.Err;
3234
import org.jooby.Mutant;
3335
import org.jooby.Parser;
3436
import org.jooby.Request;
@@ -46,8 +48,22 @@
4648
import com.google.common.reflect.Reflection;
4749
import com.google.inject.TypeLiteral;
4850

51+
import javaslang.control.Try;
52+
4953
public class BeanParser implements Parser {
5054

55+
private Function<? super Throwable, Try<? extends Object>> MISSING = x -> {
56+
return x instanceof Err.Missing ? Try.success(null) : Try.failure(x);
57+
};
58+
59+
private Function<? super Throwable, Try<? extends Object>> RETHROW = Try::failure;
60+
61+
private Function<? super Throwable, Try<? extends Object>> recoverMissing;
62+
63+
public BeanParser(final boolean allowNulls) {
64+
this.recoverMissing = allowNulls ? MISSING : RETHROW;
65+
}
66+
5167
@Override
5268
public Object parse(final TypeLiteral<?> type, final Context ctx) throws Throwable {
5369
Class<?> beanType = type.getRawType();
@@ -58,7 +74,7 @@ public Object parse(final TypeLiteral<?> type, final Context ctx) throws Throwab
5874
return ctx.ifparams(map -> {
5975
final Object bean;
6076
if (beanType.isInterface()) {
61-
bean = newBeanInterface(ctx.require(Request.class), beanType);
77+
bean = newBeanInterface(ctx.require(Request.class), ctx.require(Response.class), beanType);
6278
} else {
6379
bean = newBean(ctx.require(Request.class), ctx.require(Response.class), map, beanType);
6480
}
@@ -85,17 +101,16 @@ private Object newBean(final Request req, final Response rsp,
85101
if (constructors.size() > 1) {
86102
return null;
87103
}
88-
final Object bean;
89104
Constructor<?> constructor = constructors.get(0);
90105
RequestParamProvider provider = new RequestParamProviderImpl(
91106
new RequestParamNameProviderImpl(classInfo));
92107
List<RequestParam> parameters = provider.parameters(constructor);
93108
Object[] args = new Object[parameters.size()];
94109
for (int i = 0; i < args.length; i++) {
95-
args[i] = parameters.get(i).value(req, rsp);
110+
args[i] = value(parameters.get(i), req, rsp);
96111
}
97112
// inject args
98-
bean = constructor.newInstance(args);
113+
final Object bean = constructor.newInstance(args);
99114

100115
// inject fields
101116
for (Entry<String, Mutant> param : params.entrySet()) {
@@ -109,9 +124,7 @@ private Object newBean(final Request req, final Response rsp,
109124
int mods = field.getModifiers();
110125
if (!Modifier.isFinal(mods) && !Modifier.isStatic(mods) && !Modifier.isTransient(mods)) {
111126
// get
112-
RequestParam fparam = new RequestParam(field, pname, field.getGenericType());
113-
Object value = fparam.value(req, rsp);
114-
127+
Object value = value(new RequestParam(field, pname, field.getGenericType()), req, rsp);
115128
// set
116129
field.setAccessible(true);
117130
field.set(root, value);
@@ -148,17 +161,24 @@ private Object seek(final Object bean, final List<String> path) throws Exception
148161
return it;
149162
}
150163

151-
private Object newBeanInterface(final Request req, final Class<?> beanType) {
152-
164+
private Object newBeanInterface(final Request req, final Response rsp, final Class<?> beanType) {
153165
return Reflection.newProxy(beanType, (proxy, method, args) -> {
154166
StringBuilder name = new StringBuilder(method.getName()
155167
.replace("get", "")
156168
.replace("is", ""));
157169
name.setCharAt(0, Character.toLowerCase(name.charAt(0)));
158-
return req.param(name.toString()).to(TypeLiteral.get(method.getGenericReturnType()));
170+
return value(new RequestParam(method, name.toString(), method.getGenericReturnType()), req,
171+
rsp);
159172
});
160173
}
161174

175+
private Object value(final RequestParam param, final Request req, final Response rsp)
176+
throws Throwable {
177+
return Try.of(() -> param.value(req, rsp))
178+
.recoverWith(recoverMissing)
179+
.getOrElseThrow(Function.identity());
180+
}
181+
162182
private static List<String> name(final String name) {
163183
return Splitter.on(new CharMatcher() {
164184
@Override

md/req.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ public class Contact {
235235
```java
236236
{
237237
post("/save", req -> {
238-
Contact contact = req.body().to(Contact.class);
238+
Contact contact = req.params().to(Contact.class);
239239
// save contact...
240240
});
241241
}

0 commit comments

Comments
 (0)