diff --git a/docs/asciidoc/static-files.adoc b/docs/asciidoc/static-files.adoc
index 35ee43d374..4d4c60a469 100644
--- a/docs/asciidoc/static-files.adoc
+++ b/docs/asciidoc/static-files.adoc
@@ -134,11 +134,9 @@ control these headers programmatically:
[source, java, role="primary"]
----
{
- AssetSource www = AssetSource.create(Paths.get("www"));
- assets("/static/*", new AssetHandler(www)
+ assets("/static/*", Paths.get("www"))
.setLastModified(false)
- .setEtag(false)
- );
+ .setEtag(false);
}
----
@@ -146,11 +144,9 @@ control these headers programmatically:
[source, kotlin, role="secondary"]
----
{
- val www = AssetSource.create(Paths.get("www"))
- assets("/static/*", AssetHandler(www)
+ assets("/static/*", Paths.get("www"))
.setLastModified(false)
.setEtag(false)
- );
}
----
@@ -160,10 +156,8 @@ The `maxAge` option set a `Cache-Control` header:
[source, java, role="primary"]
----
{
- AssetSource www = AssetSource.create(Paths.get("www"));
- assets("/static/*", new AssetHandler(www)
+ assets("/static/*", Paths.get("www"))
.setMaxAge(Duration.ofDays(365))
- );
}
----
@@ -171,10 +165,8 @@ The `maxAge` option set a `Cache-Control` header:
[source, kotlin, role="secondary"]
----
{
- val www = AssetSource.create(Paths.get("www"))
- assets("/static/*", AssetHandler(www)
+ assets("/static/*", Paths.get("www"))
.setMaxAge(Duration.ofDays(365))
- );
}
----
@@ -188,8 +180,7 @@ specify a function via javadoc:AssetHandler[cacheControl, java.util.Function]:
[source, java, role="primary"]
----
{
- AssetSource www = AssetSource.create(Paths.get("www"));
- assets("/static/*", new AssetHandler(www)
+ assets("/static/*", Paths.get("www"))
.cacheControl(path -> {
if (path.endsWith("dont-cache-me.html")) {
return CacheControl.noCache(); // disable caching
@@ -200,7 +191,7 @@ specify a function via javadoc:AssetHandler[cacheControl, java.util.Function]:
} else {
return CacheControl.defaults(); // AssetHandler defaults
}
- }));
+ });
}
----
@@ -208,8 +199,7 @@ specify a function via javadoc:AssetHandler[cacheControl, java.util.Function]:
[source, kotlin, role="secondary"]
----
{
- val www = AssetSource.create(Paths.get("www"))
- assets("/static/*", AssetHandler(www)
+ assets("/static/*", Paths.get("www"))
.cacheControl {
when {
it.endsWith("dont-cache-me.html") -> CacheControl.noCache() // disable caching
@@ -218,6 +208,39 @@ specify a function via javadoc:AssetHandler[cacheControl, java.util.Function]:
.setMaxAge(Duration.ofDays(365))
else -> CacheControl.defaults() // AssetHandler defaults
}
- })
+ }
+}
+----
+
+The asset handler generates a `404` response code when requested path is not found. You can change this by throwing
+an exception or generating any other content you want:
+
+
+.Custom not found:
+[source, java, role="primary"]
+----
+{
+ assets("/static/*", Paths.get("www"))
+ .notFound(ctx -> {
+ throw new MyAssetException();
+ });
+
+ error(MyAssetException.class, (ctx, cause, code) -> {
+ // render MyAssetException as you want
+ });
}
----
+
+.Kotlin
+[source, kotlin, role="secondary"]
+----
+{
+ assets("/static/*", Paths.get("www"))
+ .notFound { _ ->
+ throw MyAssetException()
+ }
+ error(MyAssetException::class) {
+ // render MyAssetException as you want
+ }
+}
+----
\ No newline at end of file
diff --git a/jooby/pom.xml b/jooby/pom.xml
index c8c464b4d7..682fe2816a 100644
--- a/jooby/pom.xml
+++ b/jooby/pom.xml
@@ -6,7 +6,7 @@
io.jooby
jooby-project
- 3.3.0-SNAPSHOT
+ 3.2.10-SNAPSHOT
4.0.0
@@ -49,6 +49,12 @@
jakarta.inject-api
+
+
+ jakarta.validation
+ jakarta.validation-api
+
+
com.typesafe
diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java
index 21302366ff..20c35289ad 100644
--- a/jooby/src/main/java/io/jooby/DefaultContext.java
+++ b/jooby/src/main/java/io/jooby/DefaultContext.java
@@ -20,13 +20,12 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Instant;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
+import java.util.*;
import java.util.stream.Collectors;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Validator;
import org.slf4j.Logger;
import edu.umd.cs.findbugs.annotations.NonNull;
@@ -417,7 +416,20 @@ default boolean isSecure() {
T result = ValueConverters.convert(body(), type, getRouter());
return result;
}
- return (T) decoder(contentType).decode(this, type);
+ T object = (T) decoder(contentType).decode(this, type);
+
+ MessageValidator messageValidator = getRouter().getMessageValidator();
+ if (messageValidator != null) {
+ if (messageValidator.predicate().test(type)) {
+ Validator validator = messageValidator.validator();
+ Set> violations = validator.validate(object);
+ if (!violations.isEmpty()) {
+ throw new ConstraintViolationException(violations);
+ }
+ }
+ }
+ return object;
+
} catch (Exception x) {
throw SneakyThrows.propagate(x);
}
diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java
index 12d3d0d985..43aa2f0285 100644
--- a/jooby/src/main/java/io/jooby/Jooby.java
+++ b/jooby/src/main/java/io/jooby/Jooby.java
@@ -12,6 +12,7 @@
import java.io.IOException;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -41,6 +42,7 @@
import java.util.function.Supplier;
import java.util.stream.Collectors;
+import jakarta.validation.Validator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -627,6 +629,19 @@ public Jooby decoder(@NonNull MediaType contentType, @NonNull MessageDecoder dec
return this;
}
+ @NonNull
+ @Override
+ public Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate) {
+ router.messageValidator(validator, predicate);
+ return this;
+ }
+
+ @Nullable
+ @Override
+ public MessageValidator getMessageValidator() {
+ return router.getMessageValidator();
+ }
+
@NonNull @Override
public Jooby encoder(@NonNull MediaType contentType, @NonNull MessageEncoder encoder) {
router.encoder(contentType, encoder);
diff --git a/jooby/src/main/java/io/jooby/MessageValidator.java b/jooby/src/main/java/io/jooby/MessageValidator.java
new file mode 100644
index 0000000000..f4dcb0f420
--- /dev/null
+++ b/jooby/src/main/java/io/jooby/MessageValidator.java
@@ -0,0 +1,9 @@
+package io.jooby;
+
+import jakarta.validation.Validator;
+
+import java.lang.reflect.Type;
+import java.util.function.Predicate;
+
+public record MessageValidator(Validator validator, Predicate predicate) {
+}
diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java
index 1ed9754edf..7073d82de8 100644
--- a/jooby/src/main/java/io/jooby/Router.java
+++ b/jooby/src/main/java/io/jooby/Router.java
@@ -8,6 +8,7 @@
import static java.util.Collections.unmodifiableList;
import static java.util.Objects.requireNonNull;
+import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -28,6 +29,7 @@
import java.util.stream.IntStream;
import java.util.stream.Stream;
+import jakarta.validation.Validator;
import org.slf4j.Logger;
import com.typesafe.config.Config;
@@ -508,6 +510,8 @@ default Object execute(@NonNull Context context) {
*/
@NonNull Router decoder(@NonNull MediaType contentType, @NonNull MessageDecoder decoder);
+ @NonNull Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate);
+ @Nullable MessageValidator getMessageValidator();
/**
* Returns the worker thread pool. This thread pool is used to run application blocking code.
*
@@ -708,7 +712,7 @@ default Object execute(@NonNull Context context) {
* @param source File system directory.
* @return A route.
*/
- default @NonNull Route assets(@NonNull String pattern, @NonNull Path source) {
+ default @NonNull AssetHandler assets(@NonNull String pattern, @NonNull Path source) {
return assets(pattern, AssetSource.create(source));
}
@@ -722,9 +726,9 @@ default Object execute(@NonNull Context context) {
*
* @param pattern Path pattern.
* @param source File-System folder when exists, or fallback to a classpath folder.
- * @return A route.
+ * @return AssetHandler.
*/
- default @NonNull Route assets(@NonNull String pattern, @NonNull String source) {
+ default @NonNull AssetHandler assets(@NonNull String pattern, @NonNull String source) {
Path path =
Stream.of(source.split("/"))
.reduce(Paths.get(System.getProperty("user.dir")), Path::resolve, Path::resolve);
@@ -742,7 +746,7 @@ default Object execute(@NonNull Context context) {
* @param sources additional Asset sources.
* @return A route.
*/
- default @NonNull Route assets(
+ default @NonNull AssetHandler assets(
@NonNull String pattern, @NonNull AssetSource source, @NonNull AssetSource... sources) {
AssetSource[] allSources;
if (sources.length == 0) {
@@ -762,8 +766,9 @@ default Object execute(@NonNull Context context) {
* @param handler Asset handler.
* @return A route.
*/
- default @NonNull Route assets(@NonNull String pattern, @NonNull AssetHandler handler) {
- return route(GET, pattern, handler);
+ default @NonNull AssetHandler assets(@NonNull String pattern, @NonNull AssetHandler handler) {
+ route(GET, pattern, handler);
+ return handler;
}
/**
diff --git a/jooby/src/main/java/io/jooby/handler/AssetHandler.java b/jooby/src/main/java/io/jooby/handler/AssetHandler.java
index 704cad16df..f7f13a960f 100644
--- a/jooby/src/main/java/io/jooby/handler/AssetHandler.java
+++ b/jooby/src/main/java/io/jooby/handler/AssetHandler.java
@@ -14,10 +14,8 @@
import java.util.function.Function;
import edu.umd.cs.findbugs.annotations.NonNull;
-import io.jooby.Context;
-import io.jooby.MediaType;
-import io.jooby.Route;
-import io.jooby.StatusCode;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import io.jooby.*;
/**
* Handler for static resources represented by the {@link Asset} contract.
@@ -28,6 +26,8 @@
* @since 2.0.0
*/
public class AssetHandler implements Route.Handler {
+ private static final SneakyThrows.Consumer NOT_FOUND =
+ ctx -> ctx.send(StatusCode.NOT_FOUND);
private static final int ONE_SEC = 1000;
private final AssetSource[] sources;
@@ -41,6 +41,7 @@ public class AssetHandler implements Route.Handler {
private Function cacheControl = path -> defaults;
private Function mediaTypeResolver = Asset::getContentType;
+ private SneakyThrows.Consumer notFound = NOT_FOUND;
/**
* Creates a new asset handler that fallback to the given fallback asset when the asset is not
@@ -82,7 +83,7 @@ public Object apply(@NonNull Context ctx) throws Exception {
}
// Still null?
if (asset == null) {
- ctx.send(StatusCode.NOT_FOUND);
+ notFound.accept(ctx);
return ctx;
} else {
resolvedPath = fallback;
@@ -230,7 +231,19 @@ public AssetHandler cacheControl(@NonNull Function cacheCo
return this;
}
- private Asset resolve(String filepath) {
+ /**
+ * Sets a custom handler for 404
asset/resource. By default, generates a 404
+ *
status code response.
+ *
+ * @param handler Handler.
+ * @return This handler.
+ */
+ public AssetHandler notFound(@NonNull SneakyThrows.Consumer handler) {
+ this.notFound = handler;
+ return this;
+ }
+
+ private @Nullable Asset resolve(String filepath) {
for (AssetSource source : sources) {
Asset asset = source.resolve(filepath);
if (asset != null) {
@@ -243,7 +256,7 @@ private Asset resolve(String filepath) {
@Override
public void setRoute(Route route) {
List keys = route.getPathKeys();
- this.filekey = keys.size() == 0 ? route.getPattern().substring(1) : keys.get(0);
+ this.filekey = keys.isEmpty() ? route.getPattern().substring(1) : keys.get(0);
// NOTE: It send an inputstream we don't need a renderer
route.setReturnType(Context.class);
}
diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java
index 4c7819e191..33f7f6689b 100644
--- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java
+++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java
@@ -8,6 +8,7 @@
import static java.util.Objects.requireNonNull;
import java.io.FileNotFoundException;
+import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
@@ -35,6 +36,7 @@
import java.util.stream.IntStream;
import java.util.stream.Stream;
+import jakarta.validation.Validator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -147,6 +149,8 @@ public Stack executor(Executor executor) {
private Map decoders = new HashMap<>();
+ private MessageValidator messageValidator = null;
+
private Map attributes = new ConcurrentHashMap<>();
private ServiceRegistry services = new ServiceRegistryImpl();
@@ -368,6 +372,18 @@ public Router decoder(@NonNull MediaType contentType, @NonNull MessageDecoder de
return this;
}
+ @NonNull
+ @Override
+ public Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate) {
+ this.messageValidator = new MessageValidator(validator, predicate);
+ return this;
+ }
+
+ @Override
+ public MessageValidator getMessageValidator() {
+ return messageValidator;
+ }
+
@NonNull @Override
public Executor getWorker() {
return worker;
@@ -911,7 +927,8 @@ private void copy(Route src, Route it) {
it.setExecutorKey(src.getExecutorKey());
it.setTags(src.getTags());
it.setDescription(src.getDescription());
- it.setDecoders(src.getDecoders());
+ // DO NOT COPY: See https://github.com/jooby-project/jooby/issues/3500
+ // it.setDecoders(src.getDecoders());
it.setMvcMethod(src.getMvcMethod());
it.setNonBlocking(src.isNonBlocking());
it.setSummary(src.getSummary());
@@ -983,7 +1000,7 @@ private static void override(
Jooby app = (Jooby) router;
override(src, app.getRouter(), consumer);
} else if (router instanceof RouterImpl that) {
- consumer.accept((RouterImpl) src, that);
+ consumer.accept(src, that);
}
}
diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java
index cedb71a4ac..3d3521cb4f 100644
--- a/jooby/src/main/java/module-info.java
+++ b/jooby/src/main/java/module-info.java
@@ -22,6 +22,7 @@
* True core deps
*/
requires jakarta.inject;
+ requires jakarta.validation;
requires org.slf4j;
requires static com.github.spotbugs.annotations;
requires typesafe.config;
diff --git a/modules/jooby-apt/pom.xml b/modules/jooby-apt/pom.xml
index aedb169917..20f4d4c121 100644
--- a/modules/jooby-apt/pom.xml
+++ b/modules/jooby-apt/pom.xml
@@ -6,7 +6,7 @@
io.jooby
modules
- 3.3.0-SNAPSHOT
+ 3.2.10-SNAPSHOT
4.0.0
@@ -19,6 +19,12 @@
+
+ io.jooby
+ jooby-validation
+ ${jooby.version}
+
+
io.jooby
jooby
diff --git a/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java b/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java
index d37fe69c6d..b3396b3ca6 100644
--- a/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java
+++ b/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java
@@ -15,7 +15,7 @@
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
-import java.util.function.Consumer;
+import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -71,10 +71,10 @@ static String string(ProcessingEnvironment environment, String option, String de
}
protected MvcContext context;
- private Consumer output;
+ private BiConsumer output;
private final Set
+
+
+ jakarta.validation
+ jakarta.validation-api
+ ${jakarta.validation.version}
+
+
org.flywaydb
diff --git a/tests/pom.xml b/tests/pom.xml
index 29cd9bd048..088a9ed487 100644
--- a/tests/pom.xml
+++ b/tests/pom.xml
@@ -6,7 +6,7 @@
io.jooby
jooby-project
- 3.3.0-SNAPSHOT
+ 3.2.10-SNAPSHOT
4.0.0
@@ -33,6 +33,11 @@
jooby-jackson
${jooby.version}
+
+ io.jooby
+ jooby-gson
+ ${jooby.version}
+
io.jooby
jooby-avaje-jsonb
@@ -250,7 +255,6 @@
1.37
test
-
diff --git a/tests/src/test/java/io/jooby/i3400/Issue3400.java b/tests/src/test/java/io/jooby/i3400/Issue3400.java
new file mode 100644
index 0000000000..23f367de6c
--- /dev/null
+++ b/tests/src/test/java/io/jooby/i3400/Issue3400.java
@@ -0,0 +1,66 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.i3400;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.jooby.Jooby;
+import io.jooby.jackson.JacksonModule;
+import io.jooby.junit.ServerTest;
+import io.jooby.junit.ServerTestRunner;
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+
+public class Issue3400 {
+
+ static class AppA extends Jooby {
+ {
+ post("/pets", ctx -> ctx.body(Pet3400.class));
+ }
+ }
+
+ @ServerTest
+ public void shouldShareDecodersOnMountedResources(ServerTestRunner runner) {
+ runner
+ .define(
+ app -> {
+ app.install(new JacksonModule());
+ app.mount(new AppA());
+ })
+ .ready(
+ http -> {
+ http.post(
+ "/pets",
+ RequestBody.create(
+ "{\"id\": 1, \"name\": \"Cheddar\"}", MediaType.parse("application/json")),
+ rsp -> {
+ assertEquals("{\"id\":1,\"name\":\"Cheddar\"}", rsp.body().string());
+ assertEquals("application/json;charset=UTF-8", rsp.header("Content-Type"));
+ });
+ });
+ }
+
+ @ServerTest
+ public void shouldShareDecodersOnInstalledResources(ServerTestRunner runner) {
+ runner
+ .define(
+ app -> {
+ app.install(new JacksonModule());
+ app.install(AppA::new);
+ })
+ .ready(
+ http -> {
+ http.post(
+ "/pets",
+ RequestBody.create(
+ "{\"id\": 1, \"name\": \"Cheddar\"}", MediaType.parse("application/json")),
+ rsp -> {
+ assertEquals("{\"id\":1,\"name\":\"Cheddar\"}", rsp.body().string());
+ assertEquals("application/json;charset=UTF-8", rsp.header("Content-Type"));
+ });
+ });
+ }
+}
diff --git a/tests/src/test/java/io/jooby/i3400/Pet3400.java b/tests/src/test/java/io/jooby/i3400/Pet3400.java
new file mode 100644
index 0000000000..a7fbb20a4f
--- /dev/null
+++ b/tests/src/test/java/io/jooby/i3400/Pet3400.java
@@ -0,0 +1,32 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.i3400;
+
+public class Pet3400 {
+ private int id;
+ private String name;
+
+ public Pet3400(int id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/tests/src/test/java/io/jooby/i3500/Issue3500.java b/tests/src/test/java/io/jooby/i3500/Issue3500.java
new file mode 100644
index 0000000000..85fecbc6b6
--- /dev/null
+++ b/tests/src/test/java/io/jooby/i3500/Issue3500.java
@@ -0,0 +1,37 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.i3500;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.jooby.junit.ServerTest;
+import io.jooby.junit.ServerTestRunner;
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+
+public class Issue3500 {
+
+ @ServerTest
+ public void shouldShareDecodersOnMountedResources(ServerTestRunner runner) {
+ runner
+ .use(WidgetService::new)
+ .ready(
+ http -> {
+ http.post(
+ "/api/widgets1",
+ RequestBody.create("{\"id\": 1}", MediaType.get("application/json")),
+ rsp -> {
+ assertEquals(201, rsp.code());
+ });
+ http.post(
+ "/api/widgets2",
+ RequestBody.create("{\"id\": 1}", MediaType.get("application/json")),
+ rsp -> {
+ assertEquals(201, rsp.code());
+ });
+ });
+ }
+}
diff --git a/tests/src/test/java/io/jooby/i3500/Widget.java b/tests/src/test/java/io/jooby/i3500/Widget.java
new file mode 100644
index 0000000000..e2ad4a7a5b
--- /dev/null
+++ b/tests/src/test/java/io/jooby/i3500/Widget.java
@@ -0,0 +1,18 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.i3500;
+
+public class Widget {
+ private int id;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+}
diff --git a/tests/src/test/java/io/jooby/i3500/WidgetService.java b/tests/src/test/java/io/jooby/i3500/WidgetService.java
new file mode 100644
index 0000000000..59330c3ce9
--- /dev/null
+++ b/tests/src/test/java/io/jooby/i3500/WidgetService.java
@@ -0,0 +1,44 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.i3500;
+
+import io.jooby.Jooby;
+import io.jooby.StatusCode;
+import io.jooby.gson.GsonModule;
+
+public class WidgetService extends Jooby {
+
+ public WidgetService() {
+ install(new GsonModule());
+
+ post(
+ "/api/widgets1",
+ ctx -> {
+ Widget widget = ctx.body().to(Widget.class);
+ System.out.println("Created " + widget);
+ return ctx.send(StatusCode.CREATED);
+ });
+
+ mount(new WidgetRouter());
+ }
+
+ public static void main(String[] args) {
+ new WidgetService().start();
+ }
+}
+
+class WidgetRouter extends Jooby {
+
+ public WidgetRouter() {
+
+ post(
+ "/api/widgets2",
+ ctx -> {
+ Widget widget = ctx.body().to(Widget.class);
+ return ctx.send(StatusCode.CREATED);
+ });
+ }
+}
diff --git a/tests/src/test/java/io/jooby/i3501/Issue3501.java b/tests/src/test/java/io/jooby/i3501/Issue3501.java
new file mode 100644
index 0000000000..cb9bc430d5
--- /dev/null
+++ b/tests/src/test/java/io/jooby/i3501/Issue3501.java
@@ -0,0 +1,43 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.i3501;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.jooby.junit.ServerTest;
+import io.jooby.junit.ServerTestRunner;
+
+public class Issue3501 {
+
+ @ServerTest
+ public void assetHandlerShouldGenerateCustom404Response(ServerTestRunner runner) {
+ runner
+ .define(
+ app -> {
+ app.assets("/issue3501/*", "/static")
+ .notFound(
+ ctx -> {
+ throw new UnsupportedOperationException();
+ });
+
+ app.error(
+ UnsupportedOperationException.class,
+ ((ctx, cause, code) -> {
+ ctx.send(cause.getClass().getName());
+ }));
+ })
+ .ready(
+ http -> {
+ http.get(
+ "/issue3501/index.js",
+ rsp -> {
+ assertEquals(
+ UnsupportedOperationException.class.getName(), rsp.body().string());
+ assertEquals(500, rsp.code());
+ });
+ });
+ }
+}
diff --git a/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java
index 332c7ea4fc..ae0ff6701b 100644
--- a/tests/src/test/java/io/jooby/test/FeaturedTest.java
+++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java
@@ -2625,7 +2625,7 @@ public void staticAssetsCaching(ServerTestRunner runner) {
runner
.define(
app -> {
- app.assets("/www/?*", new AssetHandler(source).setNoCache());
+ app.assets("/www/?*", source).setNoCache();
})
.ready(
client -> {
@@ -2642,19 +2642,17 @@ public void staticAssetsCaching(ServerTestRunner runner) {
runner
.define(
app -> {
- app.assets(
- "/www/?*",
- new AssetHandler(source)
- .cacheControl(
- path -> {
- if (path.endsWith("about.html")) {
- return CacheControl.noCache();
- } else if (path.equals("foo.js")) {
- return CacheControl.defaults().setETag(false);
- } else {
- return CacheControl.defaults();
- }
- }));
+ app.assets("/www/?*", source)
+ .cacheControl(
+ path -> {
+ if (path.endsWith("about.html")) {
+ return CacheControl.noCache();
+ } else if (path.equals("foo.js")) {
+ return CacheControl.defaults().setETag(false);
+ } else {
+ return CacheControl.defaults();
+ }
+ });
})
.ready(
client -> {
diff --git a/tests/src/test/kotlin/i3490/C3490.kt b/tests/src/test/kotlin/i3490/C3490.kt
new file mode 100644
index 0000000000..b9f2eb5be3
--- /dev/null
+++ b/tests/src/test/kotlin/i3490/C3490.kt
@@ -0,0 +1,67 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package i3490
+
+import io.jooby.Context
+import io.jooby.annotation.*
+import java.io.IOException
+import java.util.concurrent.CompletableFuture
+import kotlinx.coroutines.delay
+
+@Path("/")
+class C3490 {
+ @GET
+ suspend fun sayHi(@QueryParam n: String?, @QueryParam bit: String?): String {
+ return Thread.currentThread().name + ": " + n + ": " + bit
+ }
+
+ @GET("/bye")
+ suspend fun sayGoodBye(@QueryParam n: String): String {
+ val from = Thread.currentThread().name
+ delay(100)
+ return from + ":" + Thread.currentThread().name + ": " + n
+ }
+
+ @GET("/completable")
+ fun completable(): CompletableFuture {
+ val from = Thread.currentThread().name
+ return CompletableFuture.supplyAsync { from + ": " + Thread.currentThread().name + ": Async" }
+ }
+
+ @Throws(IOException::class)
+ @GET("/fo\"o")
+ fun foo(ctx: Context) {
+ ctx.send("fff")
+ }
+
+ @GET("/bean") fun bean(@BindParam bean: Bean3490) = bean
+
+ @GET("/gen") fun gen(@BindParam bean: Bean3490): List = listOf()
+
+ @GET("/gene") fun genE(@BindParam bean: Bean3490): List = listOf()
+
+ @GET("/genlist") fun genE(@QueryParam bean: List): List = listOf()
+
+ @GET("/context") fun contextAttr(@ContextParam attributes: Map) = attributes
+
+ @GET("/box") fun box(@QueryParam box: Box3490) = box
+
+ @GET("/list")
+ fun box(@QueryParam id: Int?): Box3490> =
+ Box3490(listOf(Bean3490(id?.toString() ?: "none")))
+}
+
+data class Bean3490(val value: String) {
+ override fun toString(): String {
+ return value
+ }
+
+ companion object {
+ @JvmStatic fun of(ctx: Context) = Bean3490(ctx.query("value").toString())
+ }
+}
+
+data class Box3490(val value: T)
diff --git a/tests/src/test/kotlin/i3490/Issue3490.kt b/tests/src/test/kotlin/i3490/Issue3490.kt
new file mode 100644
index 0000000000..b5752191f5
--- /dev/null
+++ b/tests/src/test/kotlin/i3490/Issue3490.kt
@@ -0,0 +1,20 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package i3490
+
+import io.jooby.junit.ServerTest
+import io.jooby.junit.ServerTestRunner
+import io.jooby.kt.Kooby
+
+class Issue3490 {
+ @ServerTest
+ fun shouldBootComplexGenericTypes(runner: ServerTestRunner) =
+ runner
+ .use { Kooby { mvc(C3490_()) } }
+ .ready { _ ->
+ // NOOP
+ }
+}