diff --git a/docs/asciidoc/body.adoc b/docs/asciidoc/body.adoc index e1b3bd4d59..7175788ac3 100644 --- a/docs/asciidoc/body.adoc +++ b/docs/asciidoc/body.adoc @@ -48,7 +48,7 @@ Raw `request body` is available via javadoc:Context[body] method: <2> `HTTP Body` as `byte array` <3> `HTTP Body` as `InputStream` -This give us the `raw body`. +This gives us the `raw body`. ==== Message Decoder diff --git a/docs/asciidoc/index.adoc b/docs/asciidoc/index.adoc index 2e3c706b3f..700c5a4c27 100644 --- a/docs/asciidoc/index.adoc +++ b/docs/asciidoc/index.adoc @@ -62,6 +62,7 @@ Latest Release: https://github.com/jooby-project/jooby/releases/tag/v{joobyVersi Looking for a previous version? +* Access to link:v3[3.x] documentation. See link:/migration/4.x[migrating from 3.x to 4.x] * Access to link:v2[2.x] documentation. See link:/migration/3.x[migrating from 2.x to 3.x] * Access to link:v1[1.x] documentation. ==== diff --git a/docs/asciidoc/migration.adoc b/docs/asciidoc/migration.adoc index eb825bc663..7ca8109281 100644 --- a/docs/asciidoc/migration.adoc +++ b/docs/asciidoc/migration.adoc @@ -1,3 +1,4 @@ +include::migration/4.x.adoc[] include::migration/3.x.adoc[] === Upgrading from 1.x to 2.x diff --git a/docs/asciidoc/migration/4.x.adoc b/docs/asciidoc/migration/4.x.adoc new file mode 100644 index 0000000000..6d9e34ea8b --- /dev/null +++ b/docs/asciidoc/migration/4.x.adoc @@ -0,0 +1,80 @@ +=== Upgrading from 3.x to 4.x +You will find here notes/tips about how to migrate from 3.x to 4.x. + +[NOTE] +===== +This is a **work in progress** document, if something is wrong or missing please https://github.com/jooby-project/jooby/issues/new[report to Github] or better https://github.com/jooby-project/jooby/edit/3.x/docs/asciidoc/migration/4.x.adoc[edit this file and fix it] +===== + +==== Requirements + +- Java 21 as minimum + +==== module-info.java + +Jooby is now compatible with Java Module system. + +Almost all Jooby components are now Java Modules, but not all them. For those where wasn't +possible the Jooby module contains the `Automatic-Module-Name` manifest entry. + +==== Buffer API + +The package `io.jooby.buffer` is gone. It was replaced by `io.jooby.output` these classes +are used mainly by the javadoc:MessageEncoder[] API, the new API is easier to use and has better +performance. + +==== Value API + +The new package is now `io.jooby.value`. The API has now decoupled from javadoc:Context[] +in future release will be the basis of a new configuration system. + +Also, the `io.jooby.ValueNode` and `io.jooby.ValueNodeConverter` are gone. + +==== Session API + +For security reasons, the default HTTP session was removed. You need to configure the session +explicitly and provide a cookie session name. The `jooby.sid` cookie name was removed from project. + +==== Server configuration + +The `install(Server)`, `setServerOptions`, `start()` method are gone. With the new support for +multiple applications in a single server, these methods are useless. + +The new way: + +.New way to boot +---- +runApp(args, new NettyServer(new ServerOptions()), App::new); +---- + + +==== Packages +|=== +|3.x|4.x|Module +|io.jooby.buffer| io.jooby.output | replacement jooby (core) +|=== + +==== Classes +|=== +|3.x|4.x|Description|Module +|io.jooby.buffer.*|-| removed | jooby (core) +||io.jooby.output.*| new output API | jooby (core) +|io.jooby.MvcFactory|-| was deprecated and now removed | jooby (core) +|io.jooby.annotation.ResultType|-| removed | jooby (core) +|io.jooby.ValueNode|io.jooby.value.Value| replaced/merged | jooby (core) +|io.jooby.ValueNodeConverter|io.jooby.value.ValueConverter| replaced/merged | jooby (core) +|io.jooby.RouteSet|io.jooby.Route.Set| moved into Route and renamed to Set | jooby (core) +|=== + +==== Method +|=== +|2.x|3.x|Description +|io.jooby.Jooby.setServerOptions()|Server.setOptions()| removed in favor of `Server.setOptions()` +|io.jooby.Router.mvc|-| it was deprecated and now removed +|io.jooby.Router.decorator|-| it was deprecated and now removed +|io.jooby.Router.getConverters|io.jooby.Router.getValueFactory| replaced +|io.jooby.Router.getBeanConverters|io.jooby.Router.getValueFactory| replaced +|io.jooby.Router.attribute(String)|Router.getAttribute(String)| Renamed +|io.jooby.Router.RouteOption|io.jooby.RouterOptions| Moved to `RouterOptions` +|io.jooby.Router.setTrustProxy|RouterOptions.setTrustProxy| Moved to `RouterOptions` +|=== diff --git a/docs/asciidoc/modules/redis.adoc b/docs/asciidoc/modules/redis.adoc index 86d4f516e8..ca8b3d237d 100644 --- a/docs/asciidoc/modules/redis.adoc +++ b/docs/asciidoc/modules/redis.adoc @@ -140,12 +140,12 @@ import io.jooby.redis.RedisSessionStore; import io.lettuce.core.RedisClient; { - install(new RedisModule()); <1> + install(new RedisModule()); <1> - setSessionStore(new RedisSessionStore(require(RedisClient.class))); <2> + setSessionStore(new RedisSessionStore(Cookie.session("myappid"), require(RedisClient.class))); <2> get("/", ctx -> { - Session httpSession = ctx.session(); <3> + Session httpSession = ctx.session(); <3> // HTTP session is backed by Redis }); } @@ -160,12 +160,12 @@ import io.jooby.redis.RedisSessionStore import io.lettuce.core.RedisClient { - install(RedisModule()) <1> + install(RedisModule()) <1> - sessionStore = RedisSessionStore(require(RedisClient::class)) <2> + sessionStore = RedisSessionStore(Cookie.session("myappid"), require(RedisClient::class)) <2> get("/") { - val httpSession = ctx.session() <3> + val httpSession = ctx.session() <3> // HTTP session is backed by Redis } } @@ -179,4 +179,3 @@ More Options: - javadoc:redis.RedisSessionStore[setTimeout, java.time.Duration, artifact="jooby-redis"]: Set session timeout. Default is: `30 minutes` - javadoc:redis.RedisSessionStore[setNamespace, java.lang.String, artifact="jooby-redis"]: Set key prefix. Default is: `sessions` -- javadoc:redis.RedisSessionStore[setToken, io.jooby.SessionToken, artifact="jooby-redis"]: Set session token. Default is a cookie token: `jooby.sid` diff --git a/docs/asciidoc/session.adoc b/docs/asciidoc/session.adoc index 4f4cfa3226..eedda65527 100644 --- a/docs/asciidoc/session.adoc +++ b/docs/asciidoc/session.adoc @@ -13,10 +13,13 @@ objects. It's intended as a simple mechanism to store basic data (not an object Jooby provides the following javadoc:SessionStore[]: -- In-Memory sessions - which you should combine with an a sticky sessions proxy if you plan to run multiple instances. +- In-Memory sessions—which you should combine with a sticky sessions proxy if you plan to run multiple instances. - Cookie sessions signed with a secret key - JSON Web Token sessions +Since 4.0.0 no session is configured by default. Attempt to access to a session at runtime results +in exception. + === In-Memory Session Default session store uses memory to save session data. This store: @@ -28,6 +31,8 @@ Default session store uses memory to save session data. This store: [source,java,role="primary"] ---- { + setSessionStore(SessionStore.memory(Cookie.session("myappid"))); + get("/", ctx -> { Session session = ctx.session(); // <1> @@ -42,6 +47,8 @@ Default session store uses memory to save session data. This store: [source,kotlin,role="secondary"] ---- { + setSessionStore(SessionStore.memory(Cookie.session("myappid"))) + get("/") { val session = ctx.session() // <1> @@ -56,7 +63,7 @@ Default session store uses memory to save session data. This store: <2> Set a session attribute <3> Get a session attribute -Session token/ID is retrieved it from request cookie. Default session cookie is javadoc:SessionToken[SID, text=jooby.sid]. To customize cookie details: +Session token/ID is retrieved it from request cookie. Default session cookie never expires, it is http only under the `/` path. To customize cookie details: .In-Memory Session with Custom Cookie [source,java,role="primary"] @@ -92,7 +99,7 @@ Session token/ID is retrieved it from request cookie. Default session cookie is <1> Set an `in-memory` session store with a custom cookie named: `SESSION` -Alternative you can use a request header to retrieve a session token/ID: +Alternatively, you can use a request header to retrieve a session token/ID: .In-Memory Session with HTTP Header [source,java,role="primary"] @@ -177,9 +184,9 @@ Data sign/unsign is done using javadoc:Cookie[sign, java.lang.String, java.lang. [source,java,role="primary"] ---- { - String secret = "super secret key"; // <1> + String secret = "super secret key"; // <1> - setSessionStore(SessionStore.signed(secret)); // <2> + setSessionStore(SessionStore.signed(Cookie.session("myappid"), secret)); // <2> get("/", ctx -> { Session session = ctx.session(); @@ -195,9 +202,9 @@ Data sign/unsign is done using javadoc:Cookie[sign, java.lang.String, java.lang. [source,kotlin,role="secondary"] ---- { - val secret = "super secret key" // <1> + val secret = "super secret key" // <1> - sessionStore = SessionStore.signed(secret) // <2> + sessionStore = SessionStore.signed(Cookie.session("myappid"),secret) // <2> get("/") { val session = ctx.session() @@ -220,7 +227,7 @@ Like with `memory` session store you can use HTTP headers: { String secret = "super secret key"; // <1> - setSessionStore(SessionStore.signed(secret, SessionToken.header("TOKEN"))); // <2> + setSessionStore(SessionStore.signed(SessionToken.header("TOKEN"), secret)); // <2> get("/", ctx -> { Session session = ctx.session(); @@ -238,7 +245,7 @@ Like with `memory` session store you can use HTTP headers: { val secret = "super secret key" // <1> - sessionStore = SessionStore.signed(secret, SessionToken.header("TOKEN")) // <2> + sessionStore = SessionStore.signed(SessionToken.header("TOKEN"), secret) // <2> get("/") { val session = ctx.session() diff --git a/jooby/pom.xml b/jooby/pom.xml index 370edeeedf..a30ea5b2aa 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -95,7 +95,6 @@ 3.4.15 test - diff --git a/jooby/src/main/java/io/jooby/BeanConverter.java b/jooby/src/main/java/io/jooby/BeanConverter.java deleted file mode 100644 index e1e9865f61..0000000000 --- a/jooby/src/main/java/io/jooby/BeanConverter.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby; - -import edu.umd.cs.findbugs.annotations.NonNull; - -/** - * Value converter for complex values that come from query, path, form, etc... parameters into more - * specific type. - * - *

It is an extension point for {@link ValueNode#to(Class)} calls. - */ -public interface BeanConverter extends ValueConverter { - - /** - * True if the converter applies for the given type. - * - * @param type Conversion type. - * @return True if the converter applies for the given type. - */ - boolean supports(@NonNull Class type); - - /** - * Convert a node value into more specific type. - * - * @param node Value value. - * @param type Requested type. - * @return Converted value. - */ - Object convert(@NonNull ValueNode node, @NonNull Class type); -} diff --git a/jooby/src/main/java/io/jooby/Body.java b/jooby/src/main/java/io/jooby/Body.java index ffc0fb0f53..48128b4415 100644 --- a/jooby/src/main/java/io/jooby/Body.java +++ b/jooby/src/main/java/io/jooby/Body.java @@ -10,7 +10,7 @@ import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.file.Path; -import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Set; @@ -20,6 +20,8 @@ import io.jooby.internal.ByteArrayBody; import io.jooby.internal.FileBody; import io.jooby.internal.InputStreamBody; +import io.jooby.internal.MissingValue; +import io.jooby.value.Value; /** * HTTP body value. Allows to access HTTP body as string, byte[], stream, etc.. @@ -30,7 +32,7 @@ * @author edgar * @since 2.0.0 */ -public interface Body extends ValueNode { +public interface Body extends Value { /** * HTTP body as string. @@ -38,7 +40,7 @@ public interface Body extends ValueNode { * @param charset Charset. * @return Body as string. */ - default @NonNull String value(@NonNull Charset charset) { + default String value(@NonNull Charset charset) { byte[] bytes = bytes(); if (bytes.length == 0) { throw new MissingValueException("body"); @@ -46,6 +48,26 @@ public interface Body extends ValueNode { return new String(bytes, charset); } + @Override + default int size() { + return 1; + } + + @Override + default Value get(int index) { + return get(Integer.toString(index)); + } + + @Override + default Value get(@NonNull String name) { + return new MissingValue(name); + } + + @Override + default Iterator iterator() { + return List.of((Value) this).iterator(); + } + /** * HTTP body as byte array. * @@ -69,34 +91,34 @@ public interface Body extends ValueNode { long getSize(); /** - * Body as readable channel. + * Body as a readable channel. * - * @return Body as readable channel. + * @return Body as a readable channel. */ - @NonNull ReadableByteChannel channel(); + ReadableByteChannel channel(); /** * Body as input stream. * * @return Body as input stream. */ - @NonNull InputStream stream(); + InputStream stream(); - @NonNull @Override + @Override default List toList(@NonNull Class type) { return to(Reified.list(type).getType()); } - default @NonNull @Override List toList() { - return Collections.singletonList(value()); + default @Override List toList() { + return List.of(value()); } - default @NonNull @Override Set toSet() { - return Collections.singleton(value()); + default @Override Set toSet() { + return Set.of(value()); } @Override - default @NonNull T to(@NonNull Class type) { + default T to(@NonNull Class type) { return to((Type) type); } @@ -111,7 +133,7 @@ default List toList(@NonNull Class type) { * @param Generic type. * @return Converted value. */ - @NonNull T to(@NonNull Type type); + T to(@NonNull Type type); /** * Convert this body into the given type. @@ -133,7 +155,7 @@ default List toList(@NonNull Class type) { * @param ctx Current context. * @return Empty body. */ - static @NonNull Body empty(@NonNull Context ctx) { + static Body empty(@NonNull Context ctx) { return ByteArrayBody.empty(ctx); } @@ -145,7 +167,7 @@ default List toList(@NonNull Class type) { * @param size Size in bytes or -1. * @return A new body. */ - static @NonNull Body of(@NonNull Context ctx, @NonNull InputStream stream, long size) { + static Body of(@NonNull Context ctx, @NonNull InputStream stream, long size) { return new InputStreamBody(ctx, stream, size); } @@ -156,7 +178,7 @@ default List toList(@NonNull Class type) { * @param bytes byte array. * @return A new body. */ - static @NonNull Body of(@NonNull Context ctx, @NonNull byte[] bytes) { + static Body of(@NonNull Context ctx, @NonNull byte[] bytes) { return new ByteArrayBody(ctx, bytes); } @@ -167,7 +189,7 @@ default List toList(@NonNull Class type) { * @param file File. * @return A new body. */ - static @NonNull Body of(@NonNull Context ctx, @NonNull Path file) { + static Body of(@NonNull Context ctx, @NonNull Path file) { return new FileBody(ctx, file); } } diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java index 690e2a58ab..10d4e79d56 100644 --- a/jooby/src/main/java/io/jooby/Context.java +++ b/jooby/src/main/java/io/jooby/Context.java @@ -18,7 +18,6 @@ import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; @@ -30,13 +29,13 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.DataBuffer; -import io.jooby.buffer.DataBufferFactory; -import io.jooby.exception.TypeMismatchException; import io.jooby.internal.LocaleUtils; -import io.jooby.internal.ParamLookupImpl; import io.jooby.internal.ReadOnlyContext; import io.jooby.internal.WebSocketSender; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; /** * HTTP context allows you to interact with the HTTP Request and manipulate the HTTP Response. @@ -116,7 +115,7 @@ private static Selector single() { * * @return Mutable Context attributes. */ - @NonNull Map getAttributes(); + Map getAttributes(); /** * Get an attribute by his key. This is just an utility method around {@link #getAttributes()}. @@ -135,16 +134,16 @@ private static Selector single() { * @param value Attribute value. * @return This router. */ - @NonNull Context setAttribute(@NonNull String key, Object value); + Context setAttribute(@NonNull String key, Object value); /** * Get the HTTP router (usually this represents an instance of {@link Jooby}. * * @return HTTP router (usually this represents an instance of {@link Jooby}. */ - @NonNull Router getRouter(); + Router getRouter(); - @NonNull DataBufferFactory getBufferFactory(); + OutputFactory getOutputFactory(); /** * Forward executing to another route. We use the given path to find a matching route. @@ -154,33 +153,7 @@ private static Selector single() { * @param path Path to forward the request. * @return Forward result. */ - @NonNull Object forward(@NonNull String path); - - /** - * Converts a value (single or hash) into the given type. - * - * @param value Value to convert. - * @param type Expected type. - * @param Generic type. - * @return Converted value. - */ - default @NonNull T convert(@NonNull ValueNode value, @NonNull Class type) { - T result = convertOrNull(value, type); - if (result == null) { - throw new TypeMismatchException(value.name(), type); - } - return result; - } - - /** - * Converts a value (single or hash) into the given type. - * - * @param value Value to convert. - * @param type Expected type. - * @param Generic type. - * @return Converted value or null. - */ - @Nullable T convertOrNull(@NonNull ValueNode value, @NonNull Class type); + Object forward(@NonNull String path); /* * ********************************************************************************************** @@ -193,7 +166,7 @@ private static Selector single() { * * @return Flash map. */ - @NonNull FlashMap flash(); + FlashMap flash(); /** * Flash map or null when no flash cookie exists. @@ -202,20 +175,22 @@ private static Selector single() { */ @Nullable FlashMap flashOrNull(); + ValueFactory getValueFactory(); + /** * Get a flash attribute. * * @param name Attribute's name. * @return Flash attribute. */ - @NonNull Value flash(@NonNull String name); + Value flash(@NonNull String name); /** * Find a session or creates a new session. * * @return Session. */ - @NonNull Session session(); + Session session(); /** * Find a session attribute using the given name. If there is no session or attribute under that @@ -224,7 +199,7 @@ private static Selector single() { * @param name Attribute's name. * @return Session's attribute or missing. */ - @NonNull Value session(@NonNull String name); + Value session(@NonNull String name); /** * Find an existing session. @@ -239,21 +214,21 @@ private static Selector single() { * @param name Cookie's name. * @return Cookie value. */ - @NonNull Value cookie(@NonNull String name); + Value cookie(@NonNull String name); /** * Request cookies. * * @return Request cookies. */ - @NonNull Map cookieMap(); + Map cookieMap(); /** * HTTP method in upper-case form. * * @return HTTP method in upper-case form. */ - @NonNull String getMethod(); + String getMethod(); /** * Set HTTP method in upper-case form. @@ -261,14 +236,14 @@ private static Selector single() { * @param method HTTP method in upper-case form. * @return This context. */ - @NonNull Context setMethod(@NonNull String method); + Context setMethod(@NonNull String method); /** * Matching route. * * @return Matching route. */ - @NonNull Route getRoute(); + Route getRoute(); /** * Check if the request path matches the given pattern. @@ -284,14 +259,14 @@ private static Selector single() { * @param route Matching route. * @return This context. */ - @NonNull Context setRoute(@NonNull Route route); + Context setRoute(@NonNull Route route); /** * Get application context path (a.k.a as base path). * * @return Application context path (a.k.a as base path). */ - default @NonNull String getContextPath() { + default String getContextPath() { return getRouter().getContextPath(); } @@ -300,7 +275,7 @@ private static Selector single() { * * @return Request path without decoding (a.k.a raw Path) without query string. */ - @NonNull String getRequestPath(); + String getRequestPath(); /** * Set request path. This is usually done by Web Server or framework, but by user. @@ -308,7 +283,7 @@ private static Selector single() { * @param path Request path. * @return This context. */ - @NonNull Context setRequestPath(@NonNull String path); + Context setRequestPath(@NonNull String path); /** * Path variable. Value is decoded. @@ -316,7 +291,7 @@ private static Selector single() { * @param name Path key. * @return Associated value or a missing value, but never a null reference. */ - @NonNull Value path(@NonNull String name); + Value path(@NonNull String name); /** * Convert the {@link #pathMap()} to the given type. @@ -325,14 +300,14 @@ private static Selector single() { * @param Target type. * @return Instance of target type. */ - @NonNull T path(@NonNull Class type); + T path(@NonNull Class type); /** - * Convert {@link #pathMap()} to a {@link ValueNode} object. + * Convert {@link #pathMap()} to a {@link Value} object. * * @return A value object. */ - @NonNull ValueNode path(); + Value path(); /** * Path map represent all the path keys with their values. @@ -351,7 +326,7 @@ private static Selector single() { * * @return Path map from path pattern. */ - @NonNull Map pathMap(); + Map pathMap(); /** * Set path map. This method is part of public API but shouldn't be use it by application code. @@ -359,7 +334,7 @@ private static Selector single() { * @param pathMap Path map. * @return This context. */ - @NonNull Context setPathMap(@NonNull Map pathMap); + Context setPathMap(@NonNull Map pathMap); /* ********************************************************************************************** * Query String API @@ -367,11 +342,11 @@ private static Selector single() { */ /** - * Query string as {@link ValueNode} object. + * Query string as {@link Value} object. * - * @return Query string as {@link ValueNode} object. + * @return Query string as {@link Value} object. */ - @NonNull QueryString query(); + QueryString query(); /** * Get a query parameter that matches the given name. @@ -389,7 +364,7 @@ private static Selector single() { * @param name Parameter name. * @return A query value. */ - @NonNull ValueNode query(@NonNull String name); + Value query(@NonNull String name); /** * Query string with the leading ? or empty string. This is the raw query string, @@ -398,7 +373,7 @@ private static Selector single() { * @return Query string with the leading ? or empty string. This is the raw query * string, without decoding it. */ - @NonNull String queryString(); + String queryString(); /** * Convert the queryString to the given type. @@ -407,12 +382,12 @@ private static Selector single() { * @param Target type. * @return Query string converted to target type. */ - @NonNull T query(@NonNull Class type); + T query(@NonNull Class type); /** * Query string as simple map. * - *

{@code/search?q=jooby&sort=name}
+ *
{@code /search?q=jooby&sort=name}
* * Produces * @@ -420,7 +395,7 @@ private static Selector single() { * * @return Query string as map. */ - @NonNull Map queryMap(); + Map queryMap(); /* ********************************************************************************************** * Header API @@ -428,11 +403,11 @@ private static Selector single() { */ /** - * Request headers as {@link ValueNode}. + * Request headers as {@link Value}. * - * @return Request headers as {@link ValueNode}. + * @return Request headers as {@link Value}. */ - @NonNull ValueNode header(); + Value header(); /** * Get a header that matches the given name. @@ -440,14 +415,14 @@ private static Selector single() { * @param name Header name. Case insensitive. * @return A header value or missing value, never a null reference. */ - @NonNull Value header(@NonNull String name); + Value header(@NonNull String name); /** * Header as single-value map. * * @return Header as single-value map, with case insensitive keys. */ - @NonNull Map headerMap(); + Map headerMap(); /** * True if the given type matches the `Accept` header. This method returns true if @@ -490,7 +465,7 @@ default boolean isPreflight() { * @param defaults Default content type to use when the header is missing. * @return Request Content-Type header or null when missing. */ - @NonNull MediaType getRequestType(MediaType defaults); + MediaType getRequestType(MediaType defaults); /** * Request Content-Length header or -1 when missing. @@ -517,7 +492,7 @@ default boolean isPreflight() { * @param filter A locale filter. * @return A list of matching locales. */ - @NonNull default List locales( + default List locales( BiFunction, List, List> filter) { return filter.apply( header("Accept-Language") @@ -533,7 +508,7 @@ default boolean isPreflight() { * @return A list of matching locales or empty list. * @see #locales(BiFunction) */ - @NonNull default List locales() { + default List locales() { return locales(Locale::filter); } @@ -555,7 +530,7 @@ default boolean isPreflight() { * @param filter A locale filter. * @return A matching locale. */ - @NonNull default Locale locale(BiFunction, List, Locale> filter) { + default Locale locale(BiFunction, List, Locale> filter) { return filter.apply( header("Accept-Language") .toOptional() @@ -570,7 +545,7 @@ default boolean isPreflight() { * * @return A matching locale. */ - @NonNull default Locale locale() { + default Locale locale() { return locale( (priorityList, locales) -> Optional.ofNullable(Locale.lookup(priorityList, locales)).orElse(locales.get(0))); @@ -590,7 +565,7 @@ default boolean isPreflight() { * @param user Current user. * @return This context. */ - @NonNull Context setUser(@Nullable Object user); + Context setUser(@Nullable Object user); /** * Recreates full/entire url of the current request using the Host header. @@ -600,7 +575,7 @@ default boolean isPreflight() { * * @return Full/entire request url using the Host header. */ - @NonNull String getRequestURL(); + String getRequestURL(); /** * Recreates full/entire request url using the Host header with a custom path/suffix. @@ -611,7 +586,7 @@ default boolean isPreflight() { * @param path Path or suffix to use, can also include query string parameters. * @return Full/entire request url using the Host header. */ - @NonNull String getRequestURL(@NonNull String path); + String getRequestURL(@NonNull String path); /** * The IP address of the client or last proxy that sent the request. @@ -622,7 +597,7 @@ default boolean isPreflight() { * @return The IP address of the client or last proxy that sent the request or empty string * for interrupted requests. */ - @NonNull String getRemoteAddress(); + String getRemoteAddress(); /** * Set IP address of client or last proxy that sent the request. @@ -630,7 +605,7 @@ default boolean isPreflight() { * @param remoteAddress Remote Address. * @return This context. */ - @NonNull Context setRemoteAddress(@NonNull String remoteAddress); + Context setRemoteAddress(@NonNull String remoteAddress); /** * Return the host that this request was sent to, in general this will be the value of the Host @@ -643,7 +618,7 @@ default boolean isPreflight() { * @return Return the host that this request was sent to, in general this will be the value of the * Host header, minus the port specifier. */ - @NonNull String getHost(); + String getHost(); /** * Set the host (without the port value). @@ -653,7 +628,7 @@ default boolean isPreflight() { * @param host Host value. * @return This context. */ - @NonNull Context setHost(@NonNull String host); + Context setHost(@NonNull String host); /** * Return the host and port that this request was sent to, in general this will be the value of @@ -665,7 +640,7 @@ default boolean isPreflight() { * @return Return the host that this request was sent to, in general this will be the value of the * Host header. */ - @NonNull String getHostAndPort(); + String getHostAndPort(); /** * Return the port that this request was sent to. In general this will be the value of the Host @@ -684,14 +659,14 @@ default boolean isPreflight() { * @param port Port. * @return This context. */ - @NonNull Context setPort(int port); + Context setPort(int port); /** * The name of the protocol the request. Always in lower-case. * * @return The name of the protocol the request. Always in lower-case. */ - @NonNull String getProtocol(); + String getProtocol(); /** * The certificates presented by the client for mutual TLS. Empty if ssl is not enabled, or client @@ -700,7 +675,7 @@ default boolean isPreflight() { * @return The certificates presented by the client for mutual TLS. Empty if ssl is not enabled, * or client authentication is not required. */ - @NonNull List getClientCertificates(); + List getClientCertificates(); /** * Server port for current request. @@ -714,7 +689,7 @@ default boolean isPreflight() { * * @return Server host. */ - @NonNull String getServerHost(); + String getServerHost(); /** * Returns a boolean indicating whether this request was made using a secure channel, such as @@ -729,7 +704,7 @@ default boolean isPreflight() { * * @return HTTP scheme in lower case. */ - @NonNull String getScheme(); + String getScheme(); /** * Set HTTP scheme in lower case. @@ -737,7 +712,7 @@ default boolean isPreflight() { * @param scheme HTTP scheme in lower case. * @return This context. */ - @NonNull Context setScheme(@NonNull String scheme); + Context setScheme(@NonNull String scheme); /* ********************************************************************************************** * Form/Multipart API @@ -752,7 +727,7 @@ default boolean isPreflight() { * * @return Multipart value. */ - @NonNull Formdata form(); + Formdata form(); /** * Get a form field that matches the given name. @@ -764,7 +739,7 @@ default boolean isPreflight() { * @param name Field name. * @return Multipart value. */ - @NonNull ValueNode form(@NonNull String name); + Value form(@NonNull String name); /** * Convert form data to the given type. @@ -776,7 +751,7 @@ default boolean isPreflight() { * @param Target type. * @return Target value. */ - @NonNull T form(@NonNull Class type); + T form(@NonNull Class type); /** * Form data as single-value map. @@ -786,14 +761,14 @@ default boolean isPreflight() { * * @return Single-value map. */ - @NonNull Map formMap(); + Map formMap(); /** * All file uploads. Only for multipart/form-data request. * * @return All file uploads. */ - @NonNull List files(); + List files(); /** * All file uploads that matches the given field name. @@ -803,7 +778,7 @@ default boolean isPreflight() { * @param name Field name. Please note this is the form field name, not the actual file name. * @return All file uploads. */ - @NonNull List files(@NonNull String name); + List files(@NonNull String name); /** * A file upload that matches the given field name. @@ -813,7 +788,7 @@ default boolean isPreflight() { * @param name Field name. Please note this is the form field name, not the actual file name. * @return A file upload. */ - @NonNull FileUpload file(@NonNull String name); + FileUpload file(@NonNull String name); /* ********************************************************************************************** * Parameter Lookup @@ -845,17 +820,7 @@ default Value lookup(String name) { * none found. * @throws IllegalArgumentException If no {@link ParamSource}s are specified. */ - default Value lookup(@NonNull String name, ParamSource... sources) { - if (sources.length == 0) { - throw new IllegalArgumentException("No parameter sources were specified."); - } - - return Arrays.stream(sources) - .map(source -> source.provider.apply(this, name)) - .filter(value -> !value.isMissing()) - .findFirst() - .orElseGet(() -> Value.missing(name)); - } + Value lookup(@NonNull String name, ParamSource... sources); /** * Returns a {@link ParamLookup} instance which is a fluent interface covering the functionality @@ -872,9 +837,7 @@ default Value lookup(@NonNull String name, ParamSource... sources) { * @see ParamLookup * @see #lookup(String, ParamSource...) */ - default ParamLookup lookup() { - return new ParamLookupImpl(this); - } + ParamLookup lookup(); /* ********************************************************************************************** * Request Body @@ -886,7 +849,7 @@ default ParamLookup lookup() { * * @return HTTP body which provides access to body content. */ - @NonNull Body body(); + Body body(); /** * Convert the HTTP body to the given type. @@ -895,7 +858,7 @@ default ParamLookup lookup() { * @param Conversion type. * @return Instance of conversion type. */ - @NonNull T body(@NonNull Class type); + T body(@NonNull Class type); /** * Convert the HTTP body to the given type. @@ -904,7 +867,7 @@ default ParamLookup lookup() { * @param Conversion type. * @return Instance of conversion type. */ - @NonNull T body(@NonNull Type type); + T body(@NonNull Type type); /** * Convert the HTTP body to the given type. @@ -914,7 +877,7 @@ default ParamLookup lookup() { * @param Conversion type. * @return Instance of conversion type. */ - @NonNull T decode(@NonNull Type type, @NonNull MediaType contentType); + T decode(@NonNull Type type, @NonNull MediaType contentType); /* ********************************************************************************************** * Body MessageDecoder @@ -927,7 +890,7 @@ default ParamLookup lookup() { * @param contentType Content type. * @return MessageDecoder. */ - @NonNull MessageDecoder decoder(@NonNull MediaType contentType); + MessageDecoder decoder(@NonNull MediaType contentType); /* ********************************************************************************************** * Dispatch methods @@ -962,7 +925,7 @@ default ParamLookup lookup() { * @param action Application code. * @return This context. */ - @NonNull Context dispatch(@NonNull Runnable action); + Context dispatch(@NonNull Runnable action); /** * Dispatch context to the given executor. @@ -985,7 +948,7 @@ default ParamLookup lookup() { * @param action Application code. * @return This context. */ - @NonNull Context dispatch(@NonNull Executor executor, @NonNull Runnable action); + Context dispatch(@NonNull Executor executor, @NonNull Runnable action); /** * Tells context that response will be generated form a different thread. This operation is @@ -997,7 +960,7 @@ default ParamLookup lookup() { * @return This context. * @throws Exception When detach operation fails. */ - @NonNull Context detach(@NonNull Route.Handler next) throws Exception; + Context detach(@NonNull Route.Handler next) throws Exception; /** * Perform a websocket handsahke and upgrade a HTTP GET into a websocket protocol. @@ -1007,7 +970,7 @@ default ParamLookup lookup() { * @param handler Web socket initializer. * @return This context. */ - @NonNull Context upgrade(@NonNull WebSocket.Initializer handler); + Context upgrade(@NonNull WebSocket.Initializer handler); /** * Perform a server-sent event handshake and upgrade HTTP GET into a Server-Sent protocol. @@ -1017,7 +980,7 @@ default ParamLookup lookup() { * @param handler Server-Sent event handler. * @return This context. */ - @NonNull Context upgrade(@NonNull ServerSentEmitter.Handler handler); + Context upgrade(@NonNull ServerSentEmitter.Handler handler); /* * ********************************************************************************************** @@ -1032,7 +995,7 @@ default ParamLookup lookup() { * @param value Header value. * @return This context. */ - @NonNull Context setResponseHeader(@NonNull String name, @NonNull Date value); + Context setResponseHeader(@NonNull String name, @NonNull Date value); /** * Set response header. @@ -1041,7 +1004,7 @@ default ParamLookup lookup() { * @param value Header value. * @return This context. */ - @NonNull Context setResponseHeader(@NonNull String name, @NonNull Instant value); + Context setResponseHeader(@NonNull String name, @NonNull Instant value); /** * Set response header. @@ -1050,7 +1013,7 @@ default ParamLookup lookup() { * @param value Header value. * @return This context. */ - @NonNull Context setResponseHeader(@NonNull String name, @NonNull Object value); + Context setResponseHeader(@NonNull String name, @NonNull Object value); /** * Set response header. @@ -1059,7 +1022,7 @@ default ParamLookup lookup() { * @param value Header value. * @return This context. */ - @NonNull Context setResponseHeader(@NonNull String name, @NonNull String value); + Context setResponseHeader(@NonNull String name, @NonNull String value); /** * Remove a response header. @@ -1067,14 +1030,14 @@ default ParamLookup lookup() { * @param name Header's name. * @return This context. */ - @NonNull Context removeResponseHeader(@NonNull String name); + Context removeResponseHeader(@NonNull String name); /** * Clear/reset all the headers, including cookies. * * @return This context. */ - @NonNull Context removeResponseHeaders(); + Context removeResponseHeaders(); /** * Set response content length header. @@ -1082,7 +1045,7 @@ default ParamLookup lookup() { * @param length Response length. * @return This context. */ - @NonNull Context setResponseLength(long length); + Context setResponseLength(long length); /** * Get response header. @@ -1105,7 +1068,7 @@ default ParamLookup lookup() { * @param cookie Cookie to add. * @return This context. */ - @NonNull Context setResponseCookie(@NonNull Cookie cookie); + Context setResponseCookie(@NonNull Cookie cookie); /** * Set response content type header. @@ -1113,7 +1076,7 @@ default ParamLookup lookup() { * @param contentType Content type. * @return This context. */ - @NonNull Context setResponseType(@NonNull String contentType); + Context setResponseType(@NonNull String contentType); /** * Set response content type header. @@ -1121,7 +1084,7 @@ default ParamLookup lookup() { * @param contentType Content type. * @return This context. */ - @NonNull Context setResponseType(@NonNull MediaType contentType); + Context setResponseType(@NonNull MediaType contentType); /** * Set response content type header. @@ -1130,7 +1093,7 @@ default ParamLookup lookup() { * @param charset Charset. * @return This context. */ - @NonNull Context setResponseType(@NonNull MediaType contentType, @Nullable Charset charset); + Context setResponseType(@NonNull MediaType contentType, @Nullable Charset charset); /** * Set the default response content type header. It is used if the response content type header @@ -1139,14 +1102,14 @@ default ParamLookup lookup() { * @param contentType Content type. * @return This context. */ - @NonNull Context setDefaultResponseType(@NonNull MediaType contentType); + Context setDefaultResponseType(@NonNull MediaType contentType); /** * Get response content type. * * @return Response content type. */ - @NonNull MediaType getResponseType(); + MediaType getResponseType(); /** * Set response status code. @@ -1154,7 +1117,7 @@ default ParamLookup lookup() { * @param statusCode Status code. * @return This context. */ - @NonNull Context setResponseCode(@NonNull StatusCode statusCode); + Context setResponseCode(@NonNull StatusCode statusCode); /** * Set response status code. @@ -1162,14 +1125,14 @@ default ParamLookup lookup() { * @param statusCode Status code. * @return This context. */ - @NonNull Context setResponseCode(int statusCode); + Context setResponseCode(int statusCode); /** * Get response status code. * * @return Response status code. */ - @NonNull StatusCode getResponseCode(); + StatusCode getResponseCode(); /** * Render a value and send the response to client. @@ -1177,14 +1140,14 @@ default ParamLookup lookup() { * @param value Object value. * @return This context. */ - @NonNull Context render(@NonNull Object value); + Context render(@NonNull Object value); /** * HTTP response channel as output stream. Usually for chunked responses. * * @return HTTP channel as output stream. Usually for chunked responses. */ - @NonNull OutputStream responseStream(); + OutputStream responseStream(); /** * HTTP response channel as output stream. Usually for chunked responses. @@ -1192,7 +1155,7 @@ default ParamLookup lookup() { * @param contentType Media type. * @return HTTP channel as output stream. Usually for chunked responses. */ - @NonNull OutputStream responseStream(@NonNull MediaType contentType); + OutputStream responseStream(@NonNull MediaType contentType); /** * HTTP response channel as output stream. Usually for chunked responses. @@ -1202,8 +1165,8 @@ default ParamLookup lookup() { * @return HTTP channel as output stream. Usually for chunked responses. * @throws Exception Is something goes wrong. */ - @NonNull Context responseStream( - @NonNull MediaType contentType, @NonNull SneakyThrows.Consumer consumer) + Context responseStream( + MediaType contentType, @NonNull SneakyThrows.Consumer consumer) throws Exception; /** @@ -1213,21 +1176,21 @@ default ParamLookup lookup() { * @return HTTP channel as output stream. Usually for chunked responses. * @throws Exception Is something goes wrong. */ - @NonNull Context responseStream(@NonNull SneakyThrows.Consumer consumer) throws Exception; + Context responseStream(@NonNull SneakyThrows.Consumer consumer) throws Exception; /** * HTTP response channel as chunker. * * @return HTTP channel as chunker. Usually for chunked response. */ - @NonNull Sender responseSender(); + Sender responseSender(); /** * HTTP response channel as response writer. * * @return HTTP channel as response writer. Usually for chunked response. */ - @NonNull PrintWriter responseWriter(); + PrintWriter responseWriter(); /** * HTTP response channel as response writer. @@ -1235,7 +1198,7 @@ default ParamLookup lookup() { * @param contentType Content type. * @return HTTP channel as response writer. Usually for chunked response. */ - @NonNull PrintWriter responseWriter(@NonNull MediaType contentType); + PrintWriter responseWriter(@NonNull MediaType contentType); /** * HTTP response channel as response writer. @@ -1244,7 +1207,7 @@ default ParamLookup lookup() { * @param charset Charset. * @return HTTP channel as response writer. Usually for chunked response. */ - @NonNull PrintWriter responseWriter(@NonNull MediaType contentType, @Nullable Charset charset); + PrintWriter responseWriter(@NonNull MediaType contentType, @Nullable Charset charset); /** * HTTP response channel as response writer. @@ -1253,7 +1216,7 @@ default ParamLookup lookup() { * @return This context. * @throws Exception Is something goes wrong. */ - @NonNull Context responseWriter(@NonNull SneakyThrows.Consumer consumer) throws Exception; + Context responseWriter(@NonNull SneakyThrows.Consumer consumer) throws Exception; /** * HTTP response channel as response writer. @@ -1263,9 +1226,8 @@ default ParamLookup lookup() { * @return This context. * @throws Exception Is something goes wrong. */ - @NonNull Context responseWriter( - @NonNull MediaType contentType, @NonNull SneakyThrows.Consumer consumer) - throws Exception; + Context responseWriter( + MediaType contentType, @NonNull SneakyThrows.Consumer consumer) throws Exception; /** * HTTP response channel as response writer. @@ -1276,10 +1238,8 @@ default ParamLookup lookup() { * @return This context. * @throws Exception Is something goes wrong. */ - @NonNull Context responseWriter( - @NonNull MediaType contentType, - @Nullable Charset charset, - @NonNull SneakyThrows.Consumer consumer) + Context responseWriter( + MediaType contentType, @Nullable Charset charset, SneakyThrows.Consumer consumer) throws Exception; /** @@ -1288,7 +1248,7 @@ default ParamLookup lookup() { * @param location Location. * @return This context. */ - @NonNull Context sendRedirect(@NonNull String location); + Context sendRedirect(@NonNull String location); /** * Send a redirect response. @@ -1297,7 +1257,7 @@ default ParamLookup lookup() { * @param location Location. * @return This context. */ - @NonNull Context sendRedirect(@NonNull StatusCode redirect, @NonNull String location); + Context sendRedirect(@NonNull StatusCode redirect, @NonNull String location); /** * Send response data. @@ -1305,7 +1265,7 @@ default ParamLookup lookup() { * @param data Response. Use UTF-8 charset. * @return This context. */ - @NonNull Context send(@NonNull String data); + Context send(@NonNull String data); /** * Send response data. @@ -1314,7 +1274,7 @@ default ParamLookup lookup() { * @param charset Charset. * @return This context. */ - @NonNull Context send(@NonNull String data, @NonNull Charset charset); + Context send(@NonNull String data, @NonNull Charset charset); /** * Send response data. @@ -1322,7 +1282,7 @@ default ParamLookup lookup() { * @param data Response. * @return This context. */ - @NonNull Context send(@NonNull byte[] data); + Context send(@NonNull byte[] data); /** * Send response data. @@ -1330,15 +1290,15 @@ default ParamLookup lookup() { * @param data Response. * @return This context. */ - @NonNull Context send(@NonNull ByteBuffer data); + Context send(@NonNull ByteBuffer data); /** * Send response data. * - * @param data Response. + * @param output Output. * @return This context. */ - @NonNull Context send(@NonNull DataBuffer data); + Context send(@NonNull Output output); /** * Send response data. @@ -1346,7 +1306,7 @@ default ParamLookup lookup() { * @param data Response. * @return This context. */ - @NonNull Context send(@NonNull byte[]... data); + Context send(@NonNull byte[]... data); /** * Send response data. @@ -1354,7 +1314,7 @@ default ParamLookup lookup() { * @param data Response. * @return This context. */ - @NonNull Context send(@NonNull ByteBuffer[] data); + Context send(@NonNull ByteBuffer[] data); /** * Send response data. @@ -1362,7 +1322,7 @@ default ParamLookup lookup() { * @param channel Response input. * @return This context. */ - @NonNull Context send(@NonNull ReadableByteChannel channel); + Context send(@NonNull ReadableByteChannel channel); /** * Send response data. @@ -1370,7 +1330,7 @@ default ParamLookup lookup() { * @param input Response. * @return This context. */ - @NonNull Context send(@NonNull InputStream input); + Context send(@NonNull InputStream input); /** * Send a file download response. @@ -1378,7 +1338,7 @@ default ParamLookup lookup() { * @param file File download. * @return This context. */ - @NonNull Context send(@NonNull FileDownload file); + Context send(@NonNull FileDownload file); /** * Send a file response. @@ -1386,7 +1346,7 @@ default ParamLookup lookup() { * @param file File response. * @return This context. */ - @NonNull Context send(@NonNull Path file); + Context send(@NonNull Path file); /** * Send a file response. @@ -1394,7 +1354,7 @@ default ParamLookup lookup() { * @param file File response. * @return This context. */ - @NonNull Context send(@NonNull FileChannel file); + Context send(@NonNull FileChannel file); /** * Send an empty response with the given status code. @@ -1402,7 +1362,7 @@ default ParamLookup lookup() { * @param statusCode Status code. * @return This context. */ - @NonNull Context send(@NonNull StatusCode statusCode); + Context send(@NonNull StatusCode statusCode); /** * Send an error response. Status code is computed via {@link Router#errorCode(Throwable)}. @@ -1410,7 +1370,7 @@ default ParamLookup lookup() { * @param cause Error. If this is a fatal error it is going to be rethrow it. * @return This context. */ - @NonNull Context sendError(@NonNull Throwable cause); + Context sendError(@NonNull Throwable cause); /** * Send an error response. @@ -1419,7 +1379,7 @@ default ParamLookup lookup() { * @param statusCode Status code. * @return This context. */ - @NonNull Context sendError(@NonNull Throwable cause, @NonNull StatusCode statusCode); + Context sendError(@NonNull Throwable cause, @NonNull StatusCode statusCode); /** * True if response already started. @@ -1430,10 +1390,10 @@ default ParamLookup lookup() { /** * True if response headers are cleared on application error. If none set it uses the - * default/global value specified by {@link RouterOption#RESET_HEADERS_ON_ERROR}. + * default/global value specified by {@link RouterOptions#RESET_HEADERS_ON_ERROR}. * * @return True if response headers are cleared on application error. If none set it uses the - * default/global value specified by {@link RouterOption#RESET_HEADERS_ON_ERROR}. + * default/global value specified by {@link RouterOptions#RESET_HEADERS_ON_ERROR}. */ boolean getResetHeadersOnError(); @@ -1443,7 +1403,7 @@ default ParamLookup lookup() { * @param value True for reset/clear headers. * @return This context. */ - @NonNull Context setResetHeadersOnError(boolean value); + Context setResetHeadersOnError(boolean value); /** * Add a complete listener. @@ -1451,7 +1411,7 @@ default ParamLookup lookup() { * @param task Task to execute. * @return This context. */ - @NonNull Context onComplete(@NonNull Route.Complete task); + Context onComplete(@NonNull Route.Complete task); /* ********************************************************************************************** * Factory methods @@ -1465,7 +1425,7 @@ default ParamLookup lookup() { * @param ctx Originating context. * @return Read only context. */ - static @NonNull Context readOnly(@NonNull Context ctx) { + static Context readOnly(@NonNull Context ctx) { return new ReadOnlyContext(ctx); } @@ -1484,11 +1444,8 @@ default ParamLookup lookup() { * @param binary True for sending binary message. * @return Read only context. */ - static @NonNull Context websocket( - @NonNull Context ctx, - @NonNull WebSocket ws, - boolean binary, - WebSocket.WriteCallback callback) { + static Context websocket( + Context ctx, WebSocket ws, boolean binary, WebSocket.WriteCallback callback) { return new WebSocketSender(ctx, ws, binary, callback); } } diff --git a/jooby/src/main/java/io/jooby/Cookie.java b/jooby/src/main/java/io/jooby/Cookie.java index 1d9909dd16..8d8f1aa558 100644 --- a/jooby/src/main/java/io/jooby/Cookie.java +++ b/jooby/src/main/java/io/jooby/Cookie.java @@ -588,6 +588,17 @@ public String toString() { return Optional.empty(); } + /** + * Creates a session cookie which never expires (maxAge: -1), is http only and path is / + * . + * + * @param sid Session ID. + * @return Session's cookie. + */ + public static Cookie session(@NonNull String sid) { + return new Cookie(sid).setMaxAge(-1).setHttpOnly(true).setPath("/"); + } + private static void value( Config conf, String name, BiFunction mapper, Consumer consumer) { if (conf.hasPath(name)) { diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 7cea85c99a..34ac7811fd 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -20,24 +20,17 @@ 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.stream.Collectors; +import java.util.*; import org.slf4j.Logger; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.DataBufferFactory; import io.jooby.exception.RegistryException; -import io.jooby.internal.HashValue; -import io.jooby.internal.MissingValue; -import io.jooby.internal.SingleValue; -import io.jooby.internal.UrlParser; -import io.jooby.internal.ValueConverters; +import io.jooby.internal.*; +import io.jooby.output.OutputFactory; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; /*** * Like {@link Context} but with couple of default methods. @@ -47,44 +40,45 @@ */ public interface DefaultContext extends Context { - @NonNull @Override + @Override default T require(@NonNull Class type, @NonNull String name) throws RegistryException { return getRouter().require(type, name); } - @NonNull @Override + @Override default T require(@NonNull Class type) throws RegistryException { return getRouter().require(type); } - @NonNull @Override + @Override default T require(@NonNull Reified type) throws RegistryException { return getRouter().require(type); } - @NonNull @Override + @Override default T require(@NonNull Reified type, @NonNull String name) throws RegistryException { return getRouter().require(type, name); } - @NonNull @Override + @Override default T require(@NonNull ServiceKey key) throws RegistryException { return getRouter().require(key); } + @SuppressWarnings("unchecked") @Nullable @Override default T getUser() { return (T) getAttributes().get("user"); } - @NonNull @Override + @Override default Context setUser(@Nullable Object user) { getAttributes().put("user", user); return this; } @Override - default boolean matches(String pattern) { + default boolean matches(@NonNull String pattern) { return getRouter().match(pattern, getRequestPath()); } @@ -107,13 +101,13 @@ default boolean matches(String pattern) { } @Override - @NonNull default Context setAttribute(@NonNull String key, Object value) { + default Context setAttribute(@NonNull String key, Object value) { getAttributes().put(key, value); return this; } @Override - default @NonNull FlashMap flash() { + default FlashMap flash() { return (FlashMap) getAttributes() .computeIfAbsent( @@ -133,12 +127,12 @@ default FlashMap flashOrNull() { * @return Flash attribute. */ @Override - default @NonNull Value flash(@NonNull String name) { - return Value.create(this, name, flash().get(name)); + default Value flash(@NonNull String name) { + return Value.create(getValueFactory(), name, flash().get(name)); } @Override - default @NonNull Value session(@NonNull String name) { + default Value session(@NonNull String name) { Session session = sessionOrNull(); if (session != null) { return session.get(name); @@ -147,7 +141,7 @@ default FlashMap flashOrNull() { } @Override - default @NonNull Session session() { + default Session session() { Session session = sessionOrNull(); if (session == null) { SessionStore store = getRouter().getSessionStore(); @@ -172,7 +166,7 @@ default FlashMap flashOrNull() { } @Override - default @NonNull Object forward(@NonNull String path) { + default Object forward(@NonNull String path) { try { setRequestPath(path); Router.Match match = getRouter().match(this); @@ -183,27 +177,70 @@ default FlashMap flashOrNull() { } @Override - default @NonNull Value cookie(@NonNull String name) { + default Value cookie(@NonNull String name) { String value = cookieMap().get(name); - return value == null ? Value.missing(name) : Value.value(this, name, value); + return value == null ? Value.missing(name) : Value.value(getValueFactory(), name, value); + } + + /** + * Returns a {@link ParamLookup} instance which is a fluent interface covering the functionality + * of the {@link #lookup(String, ParamSource...)} method. + * + *
{@code
+   * Value foo = ctx.lookup()
+   *   .inQuery()
+   *   .inPath()
+   *   .get("foo");
+   * }
+ * + * @return A {@link ParamLookup} instance. + * @see ParamLookup + * @see #lookup(String, ParamSource...) + */ + default ParamLookup lookup() { + return new ParamLookupImpl(this); + } + + /** + * Searches for a parameter in the specified sources, in the specified order, returning the first + * non-missing {@link Value}, or a 'missing' {@link Value} if none found. + * + *

At least one {@link ParamSource} must be specified. + * + * @param name The name of the parameter. + * @param sources Sources to search in. + * @return The first non-missing {@link Value} or a {@link Value} representing a missing value if + * none found. + * @throws IllegalArgumentException If no {@link ParamSource}s are specified. + */ + default Value lookup(@NonNull String name, ParamSource... sources) { + if (sources.length == 0) { + throw new IllegalArgumentException("No parameter sources were specified."); + } + + return Arrays.stream(sources) + .map(source -> source.provider.apply(this, name)) + .filter(value -> !value.isMissing()) + .findFirst() + .orElseGet(() -> Value.missing(name)); } @Override - @NonNull default Value path(@NonNull String name) { + default Value path(@NonNull String name) { String value = pathMap().get(name); return value == null ? new MissingValue(name) - : new SingleValue(this, name, UrlParser.decodePathSegment(value)); + : new SingleValue(getValueFactory(), name, UrlParser.decodePathSegment(value)); } @Override - @NonNull default T path(@NonNull Class type) { + default T path(@NonNull Class type) { return path().to(type); } @Override - @NonNull default ValueNode path() { - HashValue path = new HashValue(this, null); + default Value path() { + var path = new HashValue(getValueFactory(), null); for (Map.Entry entry : pathMap().entrySet()) { path.put(entry.getKey(), entry.getValue()); } @@ -211,32 +248,32 @@ default FlashMap flashOrNull() { } @Override - @NonNull default ValueNode query(@NonNull String name) { + default Value query(@NonNull String name) { return query().get(name); } @Override - @NonNull default String queryString() { + default String queryString() { return query().queryString(); } @Override - @NonNull default T query(@NonNull Class type) { - return query().to(type); + default T query(@NonNull Class type) { + return query().toEmpty(type); } @Override - @NonNull default Map queryMap() { + default Map queryMap() { return query().toMap(); } @Override - @NonNull default Value header(@NonNull String name) { + default Value header(@NonNull String name) { return header().get(name); } @Override - @NonNull default Map headerMap() { + default Map headerMap() { return header().toMap(); } @@ -246,25 +283,25 @@ default boolean accept(@NonNull MediaType contentType) { } @Override - default MediaType accept(@NonNull List produceTypes) { - Value accept = header(ACCEPT); + default @Nullable MediaType accept(@NonNull List produceTypes) { + var accept = header(ACCEPT); if (accept.isMissing()) { // NO header? Pick first, which is the default. return produceTypes.isEmpty() ? null : produceTypes.get(0); } // Sort accept by most relevant/specific first: - List acceptTypes = + var acceptTypes = accept.toList().stream() .flatMap(value -> MediaType.parse(value).stream()) .distinct() .sorted() - .collect(Collectors.toList()); + .toList(); // Find most appropriated type: - int idx = Integer.MAX_VALUE; + var idx = Integer.MAX_VALUE; MediaType result = null; - for (MediaType produceType : produceTypes) { + for (var produceType : produceTypes) { for (int i = 0; i < acceptTypes.size(); i++) { MediaType acceptType = acceptTypes.get(i); if (produceType.matches(acceptType)) { @@ -280,21 +317,21 @@ default MediaType accept(@NonNull List produceTypes) { } @Override - default @NonNull String getRequestURL() { + default String getRequestURL() { return getRequestURL(getRequestPath() + queryString()); } @Override - default @NonNull String getRequestURL(@NonNull String path) { - String scheme = getScheme(); - String host = getHost(); + default String getRequestURL(@NonNull String path) { + var scheme = getScheme(); + var host = getHost(); int port = getPort(); - StringBuilder url = new StringBuilder(); + var url = new StringBuilder(); url.append(scheme).append("://").append(host); if (port > 0 && port != PORT && port != SECURE_PORT) { url.append(":").append(port); } - String contextPath = getContextPath(); + var contextPath = getContextPath(); if (!contextPath.equals("/") && !path.startsWith(contextPath)) { url.append(contextPath); } @@ -310,7 +347,7 @@ default MediaType accept(@NonNull List produceTypes) { } @Override - @NonNull default MediaType getRequestType(MediaType defaults) { + default MediaType getRequestType(MediaType defaults) { Value contentType = header("Content-Type"); return contentType.isMissing() ? defaults : MediaType.valueOf(contentType.value()); } @@ -322,10 +359,12 @@ default long getRequestLength() { } @Override - default @Nullable String getHostAndPort() { + default String getHostAndPort() { Optional header = - getRouter().isTrustProxy() ? header("X-Forwarded-Host").toOptional() : Optional.empty(); - String value = + getRouter().getRouterOptions().isTrustProxy() + ? header("X-Forwarded-Host").toOptional() + : Optional.empty(); + var value = header.orElseGet( () -> ofNullable(header("Host").valueOrNull()) @@ -339,14 +378,14 @@ default long getRequestLength() { } @Override - default @NonNull String getServerHost() { - String host = getRouter().getServerOptions().getHost(); + default String getServerHost() { + var host = require(ServerOptions.class).getHost(); return host.equals("0.0.0.0") ? "localhost" : host; } @Override default int getServerPort() { - ServerOptions options = getRouter().getServerOptions(); + var options = require(ServerOptions.class); return isSecure() // Buggy proxy where it report a https scheme but there is no HTTPS configured option ? ofNullable(options.getSecurePort()).orElse(options.getPort()) @@ -355,7 +394,7 @@ default int getServerPort() { @Override default int getPort() { - String hostAndPort = getHostAndPort(); + var hostAndPort = getHostAndPort(); if (hostAndPort != null) { int index = hostAndPort.indexOf(':'); if (index > 0) { @@ -367,7 +406,7 @@ default int getPort() { } @Override - default @NonNull String getHost() { + default String getHost() { String hostAndPort = getHostAndPort(); if (hostAndPort != null) { int index = hostAndPort.indexOf(':'); @@ -382,57 +421,56 @@ default boolean isSecure() { } @Override - @NonNull default ValueNode form(@NonNull String name) { + default Value form(@NonNull String name) { return form().get(name); } @Override - @NonNull default T form(@NonNull Class type) { + default T form(@NonNull Class type) { return form().to(type); } @Override - @NonNull default Map formMap() { + default Map formMap() { return form().toMap(); } @Override - @NonNull default List files() { + default List files() { return form().files(); } @Override - @NonNull default List files(@NonNull String name) { + default List files(@NonNull String name) { return form().files(name); } @Override - @NonNull default FileUpload file(@NonNull String name) { + default FileUpload file(@NonNull String name) { return form().file(name); } @Override - default @NonNull T body(@NonNull Class type) { + default T body(@NonNull Class type) { return body().to(type); } @Override - default @NonNull T body(@NonNull Type type) { + default T body(@NonNull Type type) { return body().to(type); } - @Override - default @NonNull T convertOrNull(@NonNull ValueNode value, @NonNull Class type) { - return ValueConverters.convert(value, type, getRouter()); + default ValueFactory getValueFactory() { + return getRouter().getValueFactory(); } @Override - default @NonNull T decode(@NonNull Type type, @NonNull MediaType contentType) { + default T decode(@NonNull Type type, @NonNull MediaType contentType) { try { if (MediaType.text.equals(contentType)) { - T result = ValueConverters.convert(body(), type, getRouter()); - return result; + return getValueFactory().convert(type, body()); } + //noinspection unchecked return (T) decoder(contentType).decode(this, type); } catch (Exception x) { throw SneakyThrows.propagate(x); @@ -440,22 +478,22 @@ default boolean isSecure() { } @Override - default @NonNull MessageDecoder decoder(@NonNull MediaType contentType) { + default MessageDecoder decoder(@NonNull MediaType contentType) { return getRoute().decoder(contentType); } @Override - @NonNull default Context setResponseHeader(@NonNull String name, @NonNull Date value) { + default Context setResponseHeader(@NonNull String name, @NonNull Date value) { return setResponseHeader(name, RFC1123.format(Instant.ofEpochMilli(value.getTime()))); } @Override - @NonNull default Context setResponseHeader(@NonNull String name, @NonNull Instant value) { + default Context setResponseHeader(@NonNull String name, @NonNull Instant value) { return setResponseHeader(name, RFC1123.format(value)); } @Override - @NonNull default Context setResponseHeader(@NonNull String name, @NonNull Object value) { + default Context setResponseHeader(@NonNull String name, @NonNull Object value) { if (value instanceof Date) { return setResponseHeader(name, (Date) value); } @@ -466,20 +504,20 @@ default boolean isSecure() { } @Override - @NonNull default Context setResponseType(@NonNull MediaType contentType) { + default Context setResponseType(@NonNull MediaType contentType) { return setResponseType(contentType, contentType.getCharset()); } @Override - @NonNull default Context setResponseCode(@NonNull StatusCode statusCode) { + default Context setResponseCode(@NonNull StatusCode statusCode) { return setResponseCode(statusCode.value()); } @Override - default @NonNull Context render(@NonNull Object value) { + default Context render(@NonNull Object value) { try { - Route route = getRoute(); - MessageEncoder encoder = route.getEncoder(); + var route = getRoute(); + var encoder = route.getEncoder(); var bytes = encoder.encode(this, value); if (bytes == null) { if (!isResponseStarted()) { @@ -495,13 +533,13 @@ default boolean isSecure() { } @Override - default @NonNull OutputStream responseStream(@NonNull MediaType contentType) { + default OutputStream responseStream(@NonNull MediaType contentType) { setResponseType(contentType); return responseStream(); } @Override - default @NonNull Context responseStream( + default Context responseStream( @NonNull MediaType contentType, @NonNull SneakyThrows.Consumer consumer) throws Exception { setResponseType(contentType); @@ -509,7 +547,7 @@ default boolean isSecure() { } @Override - default @NonNull Context responseStream(@NonNull SneakyThrows.Consumer consumer) + default Context responseStream(@NonNull SneakyThrows.Consumer consumer) throws Exception { try (OutputStream out = responseStream()) { consumer.accept(out); @@ -518,30 +556,30 @@ default boolean isSecure() { } @Override - default @NonNull PrintWriter responseWriter() { + default PrintWriter responseWriter() { return responseWriter(MediaType.text); } @Override - default @NonNull PrintWriter responseWriter(@NonNull MediaType contentType) { + default PrintWriter responseWriter(@NonNull MediaType contentType) { return responseWriter(contentType, contentType.getCharset()); } @Override - default @NonNull Context responseWriter(@NonNull SneakyThrows.Consumer consumer) + default Context responseWriter(@NonNull SneakyThrows.Consumer consumer) throws Exception { return responseWriter(MediaType.text, consumer); } @Override - default @NonNull Context responseWriter( + default Context responseWriter( @NonNull MediaType contentType, @NonNull SneakyThrows.Consumer consumer) throws Exception { return responseWriter(contentType, contentType.getCharset(), consumer); } @Override - default @NonNull Context responseWriter( + default Context responseWriter( @NonNull MediaType contentType, @Nullable Charset charset, @NonNull SneakyThrows.Consumer consumer) @@ -553,18 +591,18 @@ default boolean isSecure() { } @Override - default @NonNull Context sendRedirect(@NonNull String location) { + default Context sendRedirect(@NonNull String location) { return sendRedirect(StatusCode.FOUND, location); } @Override - default @NonNull Context sendRedirect(@NonNull StatusCode redirect, @NonNull String location) { + default Context sendRedirect(@NonNull StatusCode redirect, @NonNull String location) { setResponseHeader("location", location); return send(redirect); } @Override - default @NonNull Context send(@NonNull byte[]... data) { + default Context send(@NonNull byte[]... data) { ByteBuffer[] buffer = new ByteBuffer[data.length]; for (int i = 0; i < data.length; i++) { buffer[i] = ByteBuffer.wrap(data[i]); @@ -573,12 +611,12 @@ default boolean isSecure() { } @Override - default @NonNull Context send(@NonNull String data) { + default Context send(@NonNull String data) { return send(data, StandardCharsets.UTF_8); } @Override - default @NonNull Context send(@NonNull FileDownload file) { + default Context send(@NonNull FileDownload file) { setResponseHeader("Content-Disposition", file.getContentDisposition()); InputStream content = file.stream(); long length = file.getFileSize(); @@ -595,7 +633,7 @@ default boolean isSecure() { } @Override - default @NonNull Context send(@NonNull Path file) { + default Context send(@NonNull Path file) { try { setDefaultResponseType(MediaType.byFile(file)); return send(FileChannel.open(file)); @@ -605,7 +643,7 @@ default boolean isSecure() { } @Override - @NonNull default Context sendError(@NonNull Throwable cause) { + default Context sendError(@NonNull Throwable cause) { sendError(cause, getRouter().errorCode(cause)); return this; } @@ -618,7 +656,7 @@ default boolean isSecure() { * @return This context. */ @Override - @NonNull default Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { + default Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { Router router = getRouter(); Logger log = router.getLog(); if (isResponseStarted()) { @@ -646,14 +684,15 @@ default boolean isSecure() { } } } - /** rethrow fatal exceptions: */ + /* rethrow fatal exceptions: */ if (SneakyThrows.isFatal(cause)) { throw SneakyThrows.propagate(cause); } return this; } - @NonNull default DataBufferFactory getBufferFactory() { - return getRouter().getBufferFactory(); + @Override + default OutputFactory getOutputFactory() { + return getRouter().getOutputFactory().getContextFactory(); } } diff --git a/jooby/src/main/java/io/jooby/Formdata.java b/jooby/src/main/java/io/jooby/Formdata.java index 8c8dfdb9eb..c2bed23c49 100644 --- a/jooby/src/main/java/io/jooby/Formdata.java +++ b/jooby/src/main/java/io/jooby/Formdata.java @@ -10,6 +10,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.internal.MultipartNode; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; /** * Form class for direct MVC parameter provisioning. @@ -20,7 +22,7 @@ * @author edgar * @since 2.0.0 */ -public interface Formdata extends ValueNode { +public interface Formdata extends Value { /** * Add a form field. @@ -29,7 +31,7 @@ public interface Formdata extends ValueNode { * @param value Form value. */ @NonNull - void put(@NonNull String path, @NonNull ValueNode value); + void put(@NonNull String path, @NonNull Value value); /** * Add a form field. @@ -87,10 +89,10 @@ public interface Formdata extends ValueNode { /** * Creates a new multipart object. * - * @param ctx Current context. + * @param valueFactory Current context. * @return Multipart instance. */ - static @NonNull Formdata create(@NonNull Context ctx) { - return new MultipartNode(ctx); + static @NonNull Formdata create(@NonNull ValueFactory valueFactory) { + return new MultipartNode(valueFactory); } } diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index f8c68b7189..98f4cb92a9 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -23,9 +23,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.DataBuffer; -import io.jooby.buffer.DataBufferFactory; import io.jooby.exception.RegistryException; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; /** * Utility class that helps to wrap and delegate to another context. @@ -43,12 +45,12 @@ public ForwardingBody(Body body) { } @Override - @NonNull public String value(@NonNull Charset charset) { + public String value(@NonNull Charset charset) { return delegate.value(charset); } @Override - @NonNull public byte[] bytes() { + public byte[] bytes() { return delegate.bytes(); } @@ -63,32 +65,32 @@ public long getSize() { } @Override - @NonNull public ReadableByteChannel channel() { + public ReadableByteChannel channel() { return delegate.channel(); } @Override - @NonNull public InputStream stream() { + public InputStream stream() { return delegate.stream(); } @Override - @NonNull public List toList(@NonNull Class type) { + public List toList(@NonNull Class type) { return delegate.toList(type); } @Override - @NonNull public List toList() { + public List toList() { return delegate.toList(); } @Override - @NonNull public Set toSet() { + public Set toSet() { return delegate.toSet(); } @Override - @NonNull public T to(@NonNull Class type) { + public T to(@NonNull Class type) { return delegate.to(type); } @@ -98,7 +100,7 @@ public long getSize() { } @Override - @NonNull public T to(@NonNull Type type) { + public T to(@NonNull Type type) { return delegate.to(type); } @@ -108,12 +110,12 @@ public long getSize() { } @Override - @NonNull public ValueNode get(int index) { + public Value get(int index) { return delegate.get(index); } @Override - @NonNull public ValueNode get(@NonNull String name) { + public Value get(@NonNull String name) { return delegate.get(name); } @@ -123,28 +125,28 @@ public int size() { } @Override - @NonNull public Iterator iterator() { + public Iterator iterator() { return delegate.iterator(); } @Override - @NonNull public String resolve(@NonNull String expression) { + public String resolve(@NonNull String expression) { return delegate.resolve(expression); } @Override - @NonNull public String resolve(@NonNull String expression, boolean ignoreMissing) { + public String resolve(@NonNull String expression, boolean ignoreMissing) { return delegate.resolve(expression, ignoreMissing); } @Override - @NonNull public String resolve( + public String resolve( @NonNull String expression, @NonNull String startDelim, @NonNull String endDelim) { return delegate.resolve(expression, startDelim, endDelim); } @Override - @NonNull public String resolve( + public String resolve( @NonNull String expression, boolean ignoreMissing, @NonNull String startDelim, @@ -153,12 +155,12 @@ public int size() { } @Override - public void forEach(Consumer action) { + public void forEach(Consumer action) { delegate.forEach(action); } @Override - public Spliterator spliterator() { + public Spliterator spliterator() { return delegate.spliterator(); } @@ -223,7 +225,7 @@ public boolean booleanValue(boolean defaultValue) { } @Override - @NonNull public String value(@NonNull String defaultValue) { + public String value(@NonNull String defaultValue) { return delegate.value(defaultValue); } @@ -233,29 +235,29 @@ public boolean booleanValue(boolean defaultValue) { } @Override - @NonNull public T value(@NonNull SneakyThrows.Function fn) { + public T value(@NonNull SneakyThrows.Function fn) { return delegate.value(fn); } @Override - @NonNull public String value() { + public String value() { return delegate.value(); } @Override - @NonNull public > T toEnum(@NonNull SneakyThrows.Function fn) { + public > T toEnum(@NonNull SneakyThrows.Function fn) { return delegate.toEnum(fn); } @Override - @NonNull public > T toEnum( + public > T toEnum( @NonNull SneakyThrows.Function fn, @NonNull Function nameProvider) { return delegate.toEnum(fn, nameProvider); } @Override - @NonNull public Optional toOptional() { + public Optional toOptional() { return delegate.toOptional(); } @@ -290,40 +292,40 @@ public boolean isObject() { } @Override - @NonNull public Optional toOptional(@NonNull Class type) { + public Optional toOptional(@NonNull Class type) { return delegate.toOptional(type); } @Override - @NonNull public Set toSet(@NonNull Class type) { + public Set toSet(@NonNull Class type) { return delegate.toSet(type); } @Override - @NonNull public Map> toMultimap() { + public Map> toMultimap() { return delegate.toMultimap(); } @Override - @NonNull public Map toMap() { + public Map toMap() { return delegate.toMap(); } } - public static class ForwardingValueNode implements ValueNode { - protected final ValueNode delegate; + public static class ForwardingValue implements Value { + protected final Value delegate; - public ForwardingValueNode(ValueNode delegate) { + public ForwardingValue(Value delegate) { this.delegate = delegate; } @Override - @NonNull public ValueNode get(@NonNull int index) { + public Value get(@NonNull int index) { return delegate.get(index); } @Override - @NonNull public ValueNode get(@NonNull String name) { + public Value get(@NonNull String name) { return delegate.get(name); } @@ -333,28 +335,28 @@ public int size() { } @Override - @NonNull public Iterator iterator() { + public Iterator iterator() { return delegate.iterator(); } @Override - @NonNull public String resolve(@NonNull String expression) { + public String resolve(@NonNull String expression) { return delegate.resolve(expression); } @Override - @NonNull public String resolve(@NonNull String expression, boolean ignoreMissing) { + public String resolve(@NonNull String expression, boolean ignoreMissing) { return delegate.resolve(expression, ignoreMissing); } @Override - @NonNull public String resolve( + public String resolve( @NonNull String expression, @NonNull String startDelim, @NonNull String endDelim) { return delegate.resolve(expression, startDelim, endDelim); } @Override - @NonNull public String resolve( + public String resolve( @NonNull String expression, boolean ignoreMissing, @NonNull String startDelim, @@ -363,12 +365,12 @@ public int size() { } @Override - public void forEach(Consumer action) { + public void forEach(Consumer action) { delegate.forEach(action); } @Override - public Spliterator spliterator() { + public Spliterator spliterator() { return delegate.spliterator(); } @@ -433,7 +435,7 @@ public boolean booleanValue(boolean defaultValue) { } @Override - @NonNull public String value(@NonNull String defaultValue) { + public String value(@NonNull String defaultValue) { return delegate.value(defaultValue); } @@ -443,39 +445,39 @@ public boolean booleanValue(boolean defaultValue) { } @Override - @NonNull public T value(@NonNull SneakyThrows.Function fn) { + public T value(@NonNull SneakyThrows.Function fn) { return delegate.value(fn); } @Override - @NonNull public String value() { + public String value() { return delegate.value(); } @Override - @NonNull public List toList() { + public List toList() { return delegate.toList(); } @Override - @NonNull public Set toSet() { + public Set toSet() { return delegate.toSet(); } @Override - @NonNull public > T toEnum(@NonNull SneakyThrows.Function fn) { + public > T toEnum(@NonNull SneakyThrows.Function fn) { return delegate.toEnum(fn); } @Override - @NonNull public > T toEnum( + public > T toEnum( @NonNull SneakyThrows.Function fn, @NonNull Function nameProvider) { return delegate.toEnum(fn, nameProvider); } @Override - @NonNull public Optional toOptional() { + public Optional toOptional() { return delegate.toOptional(); } @@ -510,22 +512,22 @@ public boolean isObject() { } @Override - @NonNull public Optional toOptional(@NonNull Class type) { + public Optional toOptional(@NonNull Class type) { return delegate.toOptional(type); } @Override - @NonNull public List toList(@NonNull Class type) { + public List toList(@NonNull Class type) { return delegate.toList(type); } @Override - @NonNull public Set toSet(@NonNull Class type) { + public Set toSet(@NonNull Class type) { return delegate.toSet(type); } @Override - @NonNull public T to(@NonNull Class type) { + public T to(@NonNull Class type) { return delegate.to(type); } @@ -535,34 +537,39 @@ public boolean isObject() { } @Override - @NonNull public Map> toMultimap() { + public Map> toMultimap() { return delegate.toMultimap(); } @Override - @NonNull public Map toMap() { + public Map toMap() { return delegate.toMap(); } } - public static class ForwardingQueryString extends ForwardingValueNode implements QueryString { + public static class ForwardingQueryString extends ForwardingValue implements QueryString { public ForwardingQueryString(QueryString queryString) { super(queryString); } - @NonNull @Override + @Override + public T toEmpty(@NonNull Class type) { + return ((QueryString) delegate).toEmpty(type); + } + + @Override public String queryString() { return ((QueryString) delegate).queryString(); } } - public static class ForwardingFormdata extends ForwardingValueNode implements Formdata { + public static class ForwardingFormdata extends ForwardingValue implements Formdata { public ForwardingFormdata(Formdata delegate) { super(delegate); } @Override - public void put(@NonNull String path, @NonNull ValueNode value) { + public void put(@NonNull String path, @NonNull Value value) { ((Formdata) delegate).put(path, value); } @@ -581,17 +588,17 @@ public void put(@NonNull String name, @NonNull FileUpload file) { ((Formdata) delegate).put(name, file); } - @NonNull @Override + @Override public List files() { return ((Formdata) delegate).files(); } - @NonNull @Override + @Override public List files(@NonNull String name) { return ((Formdata) delegate).files(name); } - @NonNull @Override + @Override public FileUpload file(@NonNull String name) { return ((Formdata) delegate).file(name); } @@ -613,7 +620,7 @@ public T getUser() { return ctx.getUser(); } - @NonNull @Override + @Override public Context setUser(@Nullable Object user) { ctx.setUser(user); return this; @@ -623,7 +630,7 @@ public Context getDelegate() { return ctx; } - @NonNull @Override + @Override public Object forward(@NonNull String path) { Object result = ctx.forward(path); if (result instanceof Context) { @@ -643,7 +650,7 @@ public boolean isSecure() { } @Override - @NonNull public Map getAttributes() { + public Map getAttributes() { return ctx.getAttributes(); } @@ -652,23 +659,23 @@ public T getAttribute(@NonNull String key) { return ctx.getAttribute(key); } - @NonNull @Override + @Override public Context setAttribute(@NonNull String key, Object value) { ctx.setAttribute(key, value); return this; } @Override - @NonNull public Router getRouter() { + public Router getRouter() { return ctx.getRouter(); } - @NonNull @Override - public DataBufferFactory getBufferFactory() { - return ctx.getBufferFactory(); + @Override + public OutputFactory getOutputFactory() { + return ctx.getOutputFactory().getContextFactory(); } - @NonNull @Override + @Override public FlashMap flash() { return ctx.flash(); } @@ -678,17 +685,17 @@ public FlashMap flashOrNull() { return ctx.flashOrNull(); } - @NonNull @Override + @Override public Value flash(@NonNull String name) { return ctx.flash(name); } - @NonNull @Override + @Override public Value session(@NonNull String name) { return ctx.session(name); } - @NonNull @Override + @Override public Session session() { return ctx.session(); } @@ -698,110 +705,120 @@ public Session sessionOrNull() { return ctx.sessionOrNull(); } - @NonNull @Override + @Override public Value cookie(@NonNull String name) { return ctx.cookie(name); } @Override - @NonNull public Map cookieMap() { + public Map cookieMap() { return ctx.cookieMap(); } @Override - @NonNull public String getMethod() { + public String getMethod() { return ctx.getMethod(); } - @NonNull @Override + @Override public Context setMethod(@NonNull String method) { ctx.setMethod(method); return this; } @Override - @NonNull public Route getRoute() { + public Route getRoute() { return ctx.getRoute(); } @Override - @NonNull public Context setRoute(@NonNull Route route) { + public Context setRoute(@NonNull Route route) { return ctx.setRoute(route); } - @NonNull @Override + @Override public String getRequestPath() { return ctx.getRequestPath(); } - @NonNull @Override + @Override public Context setRequestPath(@NonNull String path) { ctx.setRequestPath(path); return this; } - @NonNull @Override + @Override + public ParamLookup lookup() { + return ctx.lookup(); + } + + @Override + public Value lookup(@NonNull String name, ParamSource... sources) { + return ctx.lookup(name, sources); + } + + @Override public Value path(@NonNull String name) { return ctx.path(name); } - @NonNull @Override + @Override public T path(@NonNull Class type) { return ctx.path(type); } - @NonNull @Override - public ValueNode path() { + @Override + public Value path() { return ctx.path(); } @Override - @NonNull public Map pathMap() { + public Map pathMap() { return ctx.pathMap(); } @Override - @NonNull public Context setPathMap(@NonNull Map pathMap) { + public Context setPathMap(@NonNull Map pathMap) { ctx.setPathMap(pathMap); return this; } @Override - @NonNull public QueryString query() { + public QueryString query() { return ctx.query(); } - @NonNull @Override - public ValueNode query(@NonNull String name) { + @Override + public Value query(@NonNull String name) { return ctx.query(name); } - @NonNull @Override + @Override public String queryString() { return ctx.queryString(); } - @NonNull @Override + @Override public T query(@NonNull Class type) { return ctx.query(type); } - @NonNull @Override + @Override public Map queryMap() { return ctx.queryMap(); } @Override - @NonNull public ValueNode header() { + public Value header() { return ctx.header(); } - @NonNull @Override + @Override public Value header(@NonNull String name) { return ctx.header(name); } - @NonNull @Override + @Override public Map headerMap() { return ctx.headerMap(); } @@ -821,7 +838,7 @@ public MediaType getRequestType() { return ctx.getRequestType(); } - @NonNull @Override + @Override public MediaType getRequestType(MediaType defaults) { return ctx.getRequestType(defaults); } @@ -832,22 +849,22 @@ public long getRequestLength() { } @Override - @NonNull public String getRemoteAddress() { + public String getRemoteAddress() { return ctx.getRemoteAddress(); } - @NonNull @Override + @Override public Context setRemoteAddress(@NonNull String remoteAddress) { ctx.setRemoteAddress(remoteAddress); return this; } - @NonNull @Override + @Override public String getHost() { return ctx.getHost(); } - @NonNull @Override + @Override public Context setHost(@NonNull String host) { ctx.setHost(host); return this; @@ -858,7 +875,7 @@ public int getServerPort() { return ctx.getServerPort(); } - @NonNull @Override + @Override public String getServerHost() { return ctx.getServerHost(); } @@ -868,114 +885,109 @@ public int getPort() { return ctx.getPort(); } - @NonNull @Override + @Override public Context setPort(int port) { this.ctx.setPort(port); return this; } - @NonNull @Override + @Override public String getHostAndPort() { return ctx.getHostAndPort(); } - @NonNull @Override + @Override public String getRequestURL() { return ctx.getRequestURL(); } - @NonNull @Override + @Override public String getRequestURL(@NonNull String path) { return ctx.getRequestURL(path); } @Override - @NonNull public String getProtocol() { + public String getProtocol() { return ctx.getProtocol(); } @Override - @NonNull public List getClientCertificates() { + public List getClientCertificates() { return ctx.getClientCertificates(); } @Override - @NonNull public String getScheme() { + public String getScheme() { return ctx.getScheme(); } - @NonNull @Override + @Override public Context setScheme(@NonNull String scheme) { this.ctx.setScheme(scheme); return this; } @Override - @NonNull public Formdata form() { + public Formdata form() { return ctx.form(); } - @NonNull @Override - public ValueNode form(@NonNull String name) { + @Override + public Value form(@NonNull String name) { return ctx.form(name); } - @NonNull @Override + @Override public T form(@NonNull Class type) { return ctx.form(type); } - @NonNull @Override + @Override public Map formMap() { return ctx.formMap(); } - @NonNull @Override + @Override public List files() { return ctx.files(); } - @NonNull @Override + @Override public List files(@NonNull String name) { return ctx.files(name); } - @NonNull @Override + @Override public FileUpload file(@NonNull String name) { return ctx.file(name); } @Override - @NonNull public Body body() { + public Body body() { return ctx.body(); } - @NonNull @Override + @Override public T body(@NonNull Class type) { return ctx.body(type); } - @NonNull @Override + @Override public T body(@NonNull Type type) { return ctx.body(type); } - @NonNull @Override - public T convert(@NonNull ValueNode value, @NonNull Class type) { - return ctx.convert(value, type); - } - - @Nullable @Override - public T convertOrNull(@NonNull ValueNode value, @NonNull Class type) { - return ctx.convertOrNull(value, type); + @Override + public ValueFactory getValueFactory() { + return ctx.getValueFactory(); } - @NonNull @Override + @Override public T decode(@NonNull Type type, @NonNull MediaType contentType) { return ctx.decode(type, contentType); } - @NonNull @Override + @Override public MessageDecoder decoder(@NonNull MediaType contentType) { return ctx.decoder(contentType); } @@ -986,66 +998,66 @@ public boolean isInIoThread() { } @Override - @NonNull public Context dispatch(@NonNull Runnable action) { + public Context dispatch(@NonNull Runnable action) { ctx.dispatch(action); return this; } @Override - @NonNull public Context dispatch(@NonNull Executor executor, @NonNull Runnable action) { + public Context dispatch(@NonNull Executor executor, @NonNull Runnable action) { ctx.dispatch(executor, action); return this; } @Override - @NonNull public Context detach(@NonNull Route.Handler next) throws Exception { + public Context detach(@NonNull Route.Handler next) throws Exception { ctx.detach(next); return this; } - @NonNull @Override + @Override public Context upgrade(@NonNull WebSocket.Initializer handler) { ctx.upgrade(handler); return this; } - @NonNull @Override + @Override public Context upgrade(@NonNull ServerSentEmitter.Handler handler) { ctx.upgrade(handler); return this; } - @NonNull @Override + @Override public Context setResponseHeader(@NonNull String name, @NonNull Date value) { ctx.setResponseHeader(name, value); return this; } - @NonNull @Override + @Override public Context setResponseHeader(@NonNull String name, @NonNull Instant value) { ctx.setResponseHeader(name, value); return this; } - @NonNull @Override + @Override public Context setResponseHeader(@NonNull String name, @NonNull Object value) { ctx.setResponseHeader(name, value); return this; } @Override - @NonNull public Context setResponseHeader(@NonNull String name, @NonNull String value) { + public Context setResponseHeader(@NonNull String name, @NonNull String value) { ctx.setResponseHeader(name, value); return this; } @Override - @NonNull public Context removeResponseHeader(@NonNull String name) { + public Context removeResponseHeader(@NonNull String name) { ctx.removeResponseHeader(name); return this; } - @NonNull @Override + @Override public Context removeResponseHeaders() { ctx.removeResponseHeaders(); return this; @@ -1062,126 +1074,126 @@ public long getResponseLength() { } @Override - @NonNull public Context setResponseLength(long length) { + public Context setResponseLength(long length) { ctx.setResponseLength(length); return this; } @Override - @NonNull public Context setResponseCookie(@NonNull Cookie cookie) { + public Context setResponseCookie(@NonNull Cookie cookie) { ctx.setResponseCookie(cookie); return this; } @Override - @NonNull public Context setResponseType(@NonNull String contentType) { + public Context setResponseType(@NonNull String contentType) { ctx.setResponseType(contentType); return this; } - @NonNull @Override + @Override public Context setResponseType(@NonNull MediaType contentType) { ctx.setResponseType(contentType); return this; } @Override - @NonNull public Context setResponseType(@NonNull MediaType contentType, @Nullable Charset charset) { + public Context setResponseType(@NonNull MediaType contentType, @Nullable Charset charset) { ctx.setResponseType(contentType, charset); return this; } @Override - @NonNull public Context setDefaultResponseType(@NonNull MediaType contentType) { + public Context setDefaultResponseType(@NonNull MediaType contentType) { ctx.setResponseType(contentType); return this; } @Override - @NonNull public MediaType getResponseType() { + public MediaType getResponseType() { return ctx.getResponseType(); } - @NonNull @Override + @Override public Context setResponseCode(@NonNull StatusCode statusCode) { ctx.setResponseCode(statusCode); return this; } @Override - @NonNull public Context setResponseCode(int statusCode) { + public Context setResponseCode(int statusCode) { ctx.setResponseCode(statusCode); return this; } @Override - @NonNull public StatusCode getResponseCode() { + public StatusCode getResponseCode() { return ctx.getResponseCode(); } - @NonNull @Override + @Override public Context render(@NonNull Object value) { ctx.render(value); return this; } @Override - @NonNull public OutputStream responseStream() { + public OutputStream responseStream() { return ctx.responseStream(); } - @NonNull @Override + @Override public OutputStream responseStream(@NonNull MediaType contentType) { return ctx.responseStream(contentType); } - @NonNull @Override + @Override public Context responseStream( @NonNull MediaType contentType, @NonNull SneakyThrows.Consumer consumer) throws Exception { return ctx.responseStream(contentType, consumer); } - @NonNull @Override + @Override public Context responseStream(@NonNull SneakyThrows.Consumer consumer) throws Exception { return ctx.responseStream(consumer); } @Override - @NonNull public Sender responseSender() { + public Sender responseSender() { return ctx.responseSender(); } - @NonNull @Override + @Override public PrintWriter responseWriter() { return ctx.responseWriter(); } - @NonNull @Override + @Override public PrintWriter responseWriter(@NonNull MediaType contentType) { return ctx.responseWriter(contentType); } @Override - @NonNull public PrintWriter responseWriter(@NonNull MediaType contentType, @Nullable Charset charset) { + public PrintWriter responseWriter(@NonNull MediaType contentType, @Nullable Charset charset) { return ctx.responseWriter(contentType, charset); } - @NonNull @Override + @Override public Context responseWriter(@NonNull SneakyThrows.Consumer consumer) throws Exception { return ctx.responseWriter(consumer); } - @NonNull @Override + @Override public Context responseWriter( @NonNull MediaType contentType, @NonNull SneakyThrows.Consumer consumer) throws Exception { return ctx.responseWriter(contentType, consumer); } - @NonNull @Override + @Override public Context responseWriter( @NonNull MediaType contentType, @Nullable Charset charset, @@ -1190,103 +1202,103 @@ public Context responseWriter( return ctx.responseWriter(contentType, charset, consumer); } - @NonNull @Override + @Override public Context sendRedirect(@NonNull String location) { ctx.sendRedirect(location); return this; } - @NonNull @Override + @Override public Context sendRedirect(@NonNull StatusCode redirect, @NonNull String location) { ctx.sendRedirect(redirect, location); return this; } - @NonNull @Override + @Override public Context send(@NonNull String data) { ctx.send(data); return this; } @Override - @NonNull public Context send(@NonNull String data, @NonNull Charset charset) { + public Context send(@NonNull String data, @NonNull Charset charset) { ctx.send(data, charset); return this; } @Override - @NonNull public Context send(@NonNull byte[] data) { + public Context send(@NonNull byte[] data) { ctx.send(data); return this; } @Override - @NonNull public Context send(@NonNull ByteBuffer data) { + public Context send(@NonNull ByteBuffer data) { ctx.send(data); return this; } - @NonNull @Override - public Context send(@NonNull DataBuffer data) { - ctx.send(data); + @Override + public Context send(@NonNull Output output) { + ctx.send(output); return this; } - @NonNull @Override + @Override public Context send(@NonNull byte[]... data) { ctx.send(data); return this; } - @NonNull @Override + @Override public Context send(@NonNull ByteBuffer[] data) { ctx.send(data); return this; } @Override - @NonNull public Context send(@NonNull ReadableByteChannel channel) { + public Context send(@NonNull ReadableByteChannel channel) { ctx.send(channel); return this; } @Override - @NonNull public Context send(@NonNull InputStream input) { + public Context send(@NonNull InputStream input) { ctx.send(input); return this; } - @NonNull @Override + @Override public Context send(@NonNull FileDownload file) { ctx.send(file); return this; } - @NonNull @Override + @Override public Context send(@NonNull Path file) { ctx.send(file); return this; } @Override - @NonNull public Context send(@NonNull FileChannel file) { + public Context send(@NonNull FileChannel file) { ctx.send(file); return this; } @Override - @NonNull public Context send(@NonNull StatusCode statusCode) { + public Context send(@NonNull StatusCode statusCode) { ctx.send(statusCode); return this; } - @NonNull @Override + @Override public Context sendError(@NonNull Throwable cause) { ctx.sendError(cause); return this; } - @NonNull @Override + @Override public Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { ctx.sendError(cause, code); return this; @@ -1308,33 +1320,33 @@ public Context setResetHeadersOnError(boolean value) { return this; } - @NonNull @Override + @Override public Context onComplete(@NonNull Route.Complete task) { ctx.onComplete(task); return this; } - @NonNull @Override + @Override public T require(@NonNull Class type) throws RegistryException { return ctx.require(type); } - @NonNull @Override + @Override public T require(@NonNull Class type, @NonNull String name) throws RegistryException { return ctx.require(type, name); } - @NonNull @Override + @Override public T require(@NonNull Reified type) throws RegistryException { return ctx.require(type); } - @NonNull @Override + @Override public T require(@NonNull Reified type, @NonNull String name) throws RegistryException { return ctx.require(type, name); } - @NonNull @Override + @Override public T require(@NonNull ServiceKey key) throws RegistryException { return ctx.require(key); } diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index cf17659fc5..c1eaeabe3f 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -7,7 +7,6 @@ import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; -import static java.util.stream.StreamSupport.stream; import java.io.IOException; import java.lang.reflect.Constructor; @@ -26,9 +25,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Properties; -import java.util.ServiceConfigurationError; -import java.util.ServiceLoader; -import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; @@ -37,7 +33,6 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,15 +40,15 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.DataBufferFactory; import io.jooby.exception.RegistryException; import io.jooby.exception.StartupException; import io.jooby.internal.LocaleUtils; import io.jooby.internal.MutedServer; import io.jooby.internal.RegistryRef; import io.jooby.internal.RouterImpl; +import io.jooby.output.OutputFactory; import io.jooby.problem.ProblemDetailsHandler; -import jakarta.inject.Provider; +import io.jooby.value.ValueFactory; /** * Welcome to Jooby! @@ -84,6 +79,7 @@ public class Jooby implements Router, Registry { static final String APP_NAME = "___app_name__"; private static final String JOOBY_RUN_HOOK = "___jooby_run_hook__"; + private static final Logger log = LoggerFactory.getLogger(Jooby.class); private final transient AtomicBoolean started = new AtomicBoolean(true); @@ -91,10 +87,12 @@ public class Jooby implements Router, Registry { private static Jooby owner; private static ExecutionMode BOOT_EXECUTION_MODE = ExecutionMode.DEFAULT; + private static OutputFactory OUTPUT_FACTORY; + private static ServerOptions SERVER_OPTIONS; private RouterImpl router; - private ExecutionMode mode = BOOT_EXECUTION_MODE; + private ExecutionMode mode; private Path tmpdir; @@ -110,8 +108,6 @@ public class Jooby implements Router, Registry { private RegistryRef registry = new RegistryRef(); - private ServerOptions serverOptions; - private List startupSummary; private EnvironmentOptions environmentOptions; @@ -126,54 +122,32 @@ public class Jooby implements Router, Registry { private String version; - private Server server; - /** Creates a new Jooby instance. */ public Jooby() { if (owner == null) { ClassLoader classLoader = getClass().getClassLoader(); + mode = BOOT_EXECUTION_MODE; environmentOptions = new EnvironmentOptions().setClassLoader(classLoader); router = new RouterImpl(); stopCallbacks = new LinkedList<>(); startingCallbacks = new ArrayList<>(); readyCallbacks = new ArrayList<>(); lateExtensions = new ArrayList<>(); + // NOTE: fallback to default, this is required for direct instance creation of class + // app bootstrap always ensures server instance. + router.setOutputFactory(Optional.ofNullable(OUTPUT_FACTORY).orElseGet(OutputFactory::create)); + router.setServerOptions(Optional.ofNullable(SERVER_OPTIONS).orElseGet(ServerOptions::new)); } else { copyState(owner, this); } } - /** - * Server options or null. - * - * @return Server options or null. - * @deprecated Use {@link Server#getOptions()} - */ - @Deprecated(since = "3.8.0", forRemoval = true) - public @Nullable ServerOptions getServerOptions() { - return serverOptions; - } - - /** - * Set server options. - * - * @param serverOptions Server options. - * @return This application. - * @deprecated Use {@link Server#setOptions(ServerOptions)} - */ - @Deprecated(since = "3.8.0", forRemoval = true) - public @NonNull Jooby setServerOptions(@NonNull ServerOptions serverOptions) { - this.serverOptions = serverOptions; - return this; - } - @NonNull @Override - public Set getRouterOptions() { + public RouterOptions getRouterOptions() { return router.getRouterOptions(); } - @NonNull @Override - public Jooby setRouterOptions(@NonNull RouterOption... options) { + @NonNull public Jooby setRouterOptions(@NonNull RouterOptions options) { router.setRouterOptions(options); return this; } @@ -338,9 +312,9 @@ public String getContextPath() { * you don't configure the same service twice or more in the main and imported applications too. * * @param factory Application factory. - * @return This application. + * @return Created routes. */ - @NonNull public Jooby install(@NonNull SneakyThrows.Supplier factory) { + @NonNull public Route.Set install(@NonNull SneakyThrows.Supplier factory) { return install("/", factory); } @@ -372,13 +346,12 @@ public String getContextPath() { * * @param path Path prefix. * @param factory Application factory. - * @return This application. + * @return Created routes. */ - @NonNull public Jooby install(@NonNull String path, @NonNull SneakyThrows.Supplier factory) { + @NonNull public Route.Set install(@NonNull String path, @NonNull SneakyThrows.Supplier factory) { try { owner = this; - path(path, factory::get); - return this; + return path(path, factory::get); } finally { owner = null; } @@ -472,9 +445,9 @@ public String getContextPath() { return router; } - @Override - public boolean isTrustProxy() { - return router.isTrustProxy(); + @NonNull @Override + public ServerOptions getServerOptions() { + return router.getServerOptions(); } @Override @@ -488,101 +461,56 @@ public boolean isStopped() { } @NonNull @Override - public Jooby setTrustProxy(boolean trustProxy) { - this.router.setTrustProxy(trustProxy); - return this; + public Route.Set domain(@NonNull String domain, @NonNull Router subrouter) { + return this.router.domain(domain, subrouter); } @NonNull @Override - public Router domain(@NonNull String domain, @NonNull Router subrouter) { - this.router.domain(domain, subrouter); - return this; - } - - @NonNull @Override - public RouteSet domain(@NonNull String domain, @NonNull Runnable body) { + public Route.Set domain(@NonNull String domain, @NonNull Runnable body) { return router.domain(domain, body); } @NonNull @Override - public RouteSet mount(@NonNull Predicate predicate, @NonNull Runnable body) { + public Route.Set mount(@NonNull Predicate predicate, @NonNull Runnable body) { return router.mount(predicate, body); } @NonNull @Override - public Jooby mount(@NonNull Predicate predicate, @NonNull Router subrouter) { - this.router.mount(predicate, subrouter); - return this; + public Route.Set mount(@NonNull Predicate predicate, @NonNull Router subrouter) { + return this.router.mount(predicate, subrouter); } @NonNull @Override - public Jooby mount(@NonNull String path, @NonNull Router router) { - this.router.mount(path, router); + public Route.Set mount(@NonNull String path, @NonNull Router router) { + var rs = this.router.mount(path, router); if (router instanceof Jooby) { Jooby child = (Jooby) router; child.registry = this.registry; } - return this; + return rs; } @NonNull @Override - public Jooby mount(@NonNull Router router) { + public Route.Set mount(@NonNull Router router) { return mount("/", router); } - @NonNull @Override - public Jooby mvc(@NonNull MvcExtension router) { + /** + * Add controller routes. + * + * @param router Mvc extension. + * @return Route set. + */ + @NonNull public Route.Set mvc(@NonNull Extension router) { try { + int start = this.router.getRoutes().size(); router.install(this); - return this; + return new Route.Set(this.router.getRoutes().subList(start, this.router.getRoutes().size())); } catch (Exception cause) { throw SneakyThrows.propagate(cause); } } - @NonNull @Override - @Deprecated(since = "3.8.0", forRemoval = true) - public Jooby mvc(@NonNull Object router) { - Provider provider = () -> router; - return mvc(router.getClass(), provider); - } - - @NonNull @Override - @Deprecated(since = "3.8.0", forRemoval = true) - public Jooby mvc(@NonNull Class router) { - return mvc(router, () -> require(router)); - } - - @NonNull @Override - @Deprecated(since = "3.8.0", forRemoval = true) - public Jooby mvc(@NonNull Class router, @NonNull Provider provider) { - try { - MvcFactory module = loadModule(router); - Extension extension = module.create(provider::get); - extension.install(this); - return this; - } catch (Exception x) { - throw SneakyThrows.propagate(x); - } - } - - @Deprecated(since = "3.8.0", forRemoval = true) - private MvcFactory loadModule(Class router) { - try { - ServiceLoader modules = ServiceLoader.load(MvcFactory.class); - return stream(modules.spliterator(), false) - .filter(it -> it.supports(router)) - .findFirst() - .orElseGet( - () -> - /* Make happy IDE incremental build: */ - mvcReflectionFallback(router, getClassLoader())); - } catch (ServiceConfigurationError notfound) { - /* Make happy IDE incremental build: */ - return mvcReflectionFallback(router, getClassLoader()); - } - } - @NonNull @Override public Route ws(@NonNull String pattern, @NonNull WebSocket.Initializer handler) { return router.ws(pattern, handler); @@ -672,12 +600,12 @@ public Jooby dispatch(@NonNull Executor executor, @NonNull Runnable action) { } @NonNull @Override - public RouteSet path(@NonNull String pattern, @NonNull Runnable action) { + public Route.Set path(@NonNull String pattern, @NonNull Runnable action) { return router.path(pattern, action); } @NonNull @Override - public RouteSet routes(@NonNull Runnable action) { + public Route.Set routes(@NonNull Runnable action) { return router.routes(action); } @@ -728,17 +656,6 @@ public Jooby setDefaultWorker(@NonNull Executor worker) { return this; } - @NonNull @Override - public DataBufferFactory getBufferFactory() { - return router.getBufferFactory(); - } - - @NonNull @Override - public Jooby setBufferFactory(@NonNull DataBufferFactory bufferFactory) { - router.setBufferFactory(bufferFactory); - return this; - } - @NonNull @Override public Logger getLog() { return LoggerFactory.getLogger(getClass()); @@ -796,14 +713,14 @@ public Map getAttributes() { } @NonNull @Override - public Jooby attribute(@NonNull String key, @NonNull Object value) { - router.attribute(key, value); + public Jooby setAttribute(@NonNull String key, @NonNull Object value) { + router.setAttribute(key, value); return this; } @NonNull @Override - public T attribute(@NonNull String key) { - return router.attribute(key); + public T getAttribute(@NonNull String key) { + return router.getAttribute(key); } @NonNull @Override @@ -908,19 +825,19 @@ public Jooby setFlashCookie(@NonNull Cookie flashCookie) { } @NonNull @Override - public Jooby converter(@NonNull ValueConverter converter) { - router.converter(converter); - return this; + public ValueFactory getValueFactory() { + return router.getValueFactory(); } @NonNull @Override - public List getConverters() { - return router.getConverters(); + public Jooby setValueFactory(@NonNull ValueFactory valueFactory) { + router.setValueFactory(valueFactory); + return this; } @NonNull @Override - public List getBeanConverters() { - return router.getBeanConverters(); + public OutputFactory getOutputFactory() { + return router.getOutputFactory(); } @NonNull @Override @@ -935,12 +852,6 @@ public Jooby setCurrentUser(@NonNull Function provider) { return this; } - @NonNull @Override - public Jooby setContextAsService(boolean contextAsService) { - router.setContextAsService(contextAsService); - return this; - } - @NonNull @Override public Jooby setHiddenMethod(@NonNull String parameterName) { router.setHiddenMethod(parameterName); @@ -968,48 +879,7 @@ public Jooby setStartupSummary(List startupSummary) { } /** - * Start application, find a web server, deploy application, start router, extension modules, - * etc.. - * - * @return Server. - * @deprecated Use {@link Server#start(Jooby[])} - */ - @Deprecated(since = "3.8.0", forRemoval = true) - public @NonNull Server start() { - if (server == null) { - this.server = Server.loadServer(); - } - if (!server.getLoggerOff().isEmpty()) { - this.server = MutedServer.mute(this.server); - } - try { - if (serverOptions == null) { - serverOptions = ServerOptions.from(getEnvironment().getConfig()).orElse(null); - } - if (serverOptions != null) { - serverOptions.setServer(server.getName()); - server.setOptions(serverOptions); - } - - return server.start(this); - } catch (Throwable startupError) { - stopped.set(true); - Logger log = getLog(); - log.error("Application startup resulted in exception", startupError); - try { - server.stop(); - } catch (Throwable stopError) { - log.debug("Server stop resulted in exception", stopError); - } - // rethrow - throw startupError instanceof StartupException - ? (StartupException) startupError - : new StartupException("Application startup resulted in exception", startupError); - } - } - - /** - * Call back method that indicates application was deploy it in the given server. + * Call back method that indicates application was deployed. * * @param server Server. * @return This application. @@ -1018,9 +888,7 @@ public Jooby setStartupSummary(List startupSummary) { Path tmpdir = getTmpdir(); ensureTmpdir(tmpdir); - if (mode == null) { - mode = ExecutionMode.DEFAULT; - } + log.trace("initialization context static variables {} {}", Context.RFC1123, Context.GMT); if (locales == null) { String path = AvailableSettings.LANG; @@ -1041,7 +909,7 @@ public Jooby setStartupSummary(List startupSummary) { .orElseGet(() -> singletonList(Locale.getDefault())); } - ServiceRegistry services = getServices(); + var services = getServices(); services.put(Environment.class, getEnvironment()); services.put(Config.class, getConfig()); @@ -1072,15 +940,12 @@ public Jooby setStartupSummary(List startupSummary) { * @return This application. */ public @NonNull Jooby ready(@NonNull Server server) { - this.serverOptions = server.getOptions(); - if (startupSummary == null) { Config config = env.getConfig(); if (config.hasPath(AvailableSettings.STARTUP_SUMMARY)) { Object value = config.getAnyRef(AvailableSettings.STARTUP_SUMMARY); List values = value instanceof List ? (List) value : List.of(value.toString()); - startupSummary = - values.stream().map(StartupSummary::create).collect(Collectors.toUnmodifiableList()); + startupSummary = values.stream().map(StartupSummary::create).toList(); } else { startupSummary = List.of(StartupSummary.DEFAULT, StartupSummary.ROUTES); } @@ -1370,37 +1235,28 @@ public static void runApp( var apps = new ArrayList(); var targetServer = server.getLoggerOff().isEmpty() ? server : MutedServer.mute(server); try { + // Init context static var for (var factory : provider) { - var app = createApp(executionMode, factory); - app.server = targetServer; + var app = createApp(server, executionMode, factory); /* When running a single app instance, there is no issue with server options, when multiple apps set options a warning will be printed */ - var options = app.serverOptions; - if (options == null) { - options = ServerOptions.from(app.getConfig()).orElse(null); - } - if (options != null) { - options.setServer(server.getName()); - server.setOptions(options); - } + ServerOptions.from(app.getConfig()).ifPresent(server::setOptions); apps.add(app); } + targetServer.start(apps.toArray(new Jooby[0])); - } catch (Throwable t) { - if (targetServer != null) { - try { - targetServer.stop(); - } catch (Exception ignore) { - } + } catch (Throwable startupError) { + try { + targetServer.stop(); + } catch (Throwable ignored) { + // no need to log here } - LoggerFactory.getLogger(Jooby.class) - .error("Application initialization resulted in exception", t); - - throw t instanceof StartupException - ? (StartupException) t - : new StartupException("Application initialization resulted in exception", t); + // rethrow + throw startupError instanceof StartupException + ? (StartupException) startupError + : new StartupException("Application initialization resulted in exception", startupError); } } @@ -1412,7 +1268,9 @@ public static void runApp( * @return Application. */ public static Jooby createApp( - @NonNull ExecutionMode executionMode, @NonNull Supplier provider) { + @NonNull Server server, + @NonNull ExecutionMode executionMode, + @NonNull Supplier provider) { configurePackage(provider.getClass().getPackage()); /* Find application.env: */ String logfile = @@ -1425,10 +1283,14 @@ public static Jooby createApp( Jooby app; try { - BOOT_EXECUTION_MODE = executionMode; + Jooby.OUTPUT_FACTORY = server.getOutputFactory(); + Jooby.SERVER_OPTIONS = server.getOptions(); + Jooby.BOOT_EXECUTION_MODE = executionMode; app = provider.get(); } finally { - BOOT_EXECUTION_MODE = ExecutionMode.DEFAULT; + Jooby.BOOT_EXECUTION_MODE = executionMode; + Jooby.OUTPUT_FACTORY = null; + Jooby.SERVER_OPTIONS = null; } return app; @@ -1458,7 +1320,7 @@ private static void configurePackage(Class owner) { } private static void configurePackage(String packageName) { - var defaultPackages = Set.of("io.jooby", "io.jooby.kt"); + var defaultPackages = java.util.Set.of("io.jooby", "io.jooby.kt"); if (!defaultPackages.contains(packageName)) { ifSystemProp( AvailableSettings.PACKAGE, @@ -1564,36 +1426,12 @@ private void joobyRunHook(ClassLoader loader, Server server) { } /** - * This method exists to integrate IDE incremental build with MVC annotation processor. It - * fallback to reflection to lookup for a generated mvc factory. - * - * @param source Controller class. - * @param classLoader Class loader. - * @return Mvc factory. - */ - private MvcFactory mvcReflectionFallback(Class source, ClassLoader classLoader) { - try { - var moduleName = - System.getProperty("jooby.routerPrefix", "") - + source.getName() - + System.getProperty("jooby.routerSuffix", "_"); - Class moduleType = classLoader.loadClass(moduleName); - Constructor constructor = moduleType.getDeclaredConstructor(); - getLog().debug("Loading mvc using reflection: " + source); - return (MvcFactory) constructor.newInstance(); - } catch (Exception x) { - throw Usage.mvcRouterNotFound(source); - } - } - - /** - * Copy internal state from one application into other. + * Copy the internal state from one application into others. * * @param source Source application. * @param dest Destination application. */ private static void copyState(Jooby source, Jooby dest) { - dest.serverOptions = source.serverOptions; dest.registry = source.registry; dest.mode = source.mode; dest.environmentOptions = source.environmentOptions; diff --git a/jooby/src/main/java/io/jooby/MessageEncoder.java b/jooby/src/main/java/io/jooby/MessageEncoder.java index 19a5e9f09b..eef31ecef7 100644 --- a/jooby/src/main/java/io/jooby/MessageEncoder.java +++ b/jooby/src/main/java/io/jooby/MessageEncoder.java @@ -5,15 +5,13 @@ */ package io.jooby; -import java.nio.charset.StandardCharsets; - import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.DataBuffer; import io.jooby.exception.NotAcceptableException; +import io.jooby.output.Output; /** - * Render a route output as byte array. + * Render a route output as a byte array. * * @author edgar * @since 2.0.0 @@ -24,7 +22,7 @@ public interface MessageEncoder { MessageEncoder TO_STRING = (ctx, value) -> { if (ctx.accept(ctx.getResponseType())) { - return ctx.getBufferFactory().wrap(value.toString().getBytes(StandardCharsets.UTF_8)); + return ctx.getOutputFactory().wrap(value.toString()); } throw new NotAcceptableException(ctx.header("Accept").valueOrNull()); }; @@ -37,5 +35,5 @@ public interface MessageEncoder { * @return Encoded value or null if given object isn't supported it. * @throws Exception If something goes wrong. */ - @Nullable DataBuffer encode(@NonNull Context ctx, @NonNull Object value) throws Exception; + @Nullable Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception; } diff --git a/jooby/src/main/java/io/jooby/MvcExtension.java b/jooby/src/main/java/io/jooby/MvcExtension.java deleted file mode 100644 index 039e459278..0000000000 --- a/jooby/src/main/java/io/jooby/MvcExtension.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby; - -/** - * Marker interface for generated MVC router. - * - * @author edgar - * @since 3.2.0 - */ -public interface MvcExtension extends Extension {} diff --git a/jooby/src/main/java/io/jooby/MvcFactory.java b/jooby/src/main/java/io/jooby/MvcFactory.java deleted file mode 100644 index faa4ccc126..0000000000 --- a/jooby/src/main/java/io/jooby/MvcFactory.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby; - -import edu.umd.cs.findbugs.annotations.NonNull; - -/** - * Created by a Jooby annotation processor tool using the {@link java.util.ServiceLoader} API. - * - * @since 2.1.0 - */ -@Deprecated(since = "3.8.0", forRemoval = true) -public interface MvcFactory { - /** - * Check if the factory applies for the given MVC route. - * - * @param type MVC route. - * @return True for matching factory. - */ - boolean supports(@NonNull Class type); - - /** - * Creates an extension module. The extension module are created at compilation time by Jooby APT. - * - * @param provider MVC route instance provider. - * @return All mvc route as extension module. - */ - @NonNull Extension create(@NonNull java.util.function.Supplier provider); -} diff --git a/jooby/src/main/java/io/jooby/ParamLookup.java b/jooby/src/main/java/io/jooby/ParamLookup.java index 81e09ade5f..b57fc32ec3 100644 --- a/jooby/src/main/java/io/jooby/ParamLookup.java +++ b/jooby/src/main/java/io/jooby/ParamLookup.java @@ -5,7 +5,7 @@ */ package io.jooby; -import java.util.Optional; +import io.jooby.value.Value; /** * Fluent interface allowing to conveniently search context parameters in multiple sources. @@ -117,16 +117,5 @@ interface Stage extends ParamLookup { * if none found. */ Value get(String name); - - /** - * Wraps the result of {@link #get(String)} in an {@link Optional} if the value is a {@link - * ValueNode} or returns an empty {@link Optional} otherwise. - * - * @param name The name of the parameter. - * @return An {@link Optional} wrapping the result of {@link #get(String)} - */ - default Optional getNode(String name) { - return Optional.of(get(name)).map(v -> v instanceof ValueNode ? (ValueNode) v : null); - } } } diff --git a/jooby/src/main/java/io/jooby/ParamSource.java b/jooby/src/main/java/io/jooby/ParamSource.java index 63ea0d1c69..0b1fd564d9 100644 --- a/jooby/src/main/java/io/jooby/ParamSource.java +++ b/jooby/src/main/java/io/jooby/ParamSource.java @@ -7,6 +7,8 @@ import java.util.function.BiFunction; +import io.jooby.value.Value; + /** * List of possible parameter sources supported by {@link Context#lookup(String, ParamSource...)}. * diff --git a/jooby/src/main/java/io/jooby/QueryString.java b/jooby/src/main/java/io/jooby/QueryString.java index 874bd9ef1d..02a91bbe73 100644 --- a/jooby/src/main/java/io/jooby/QueryString.java +++ b/jooby/src/main/java/io/jooby/QueryString.java @@ -8,6 +8,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.internal.UrlParser; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; /** * Query string class for direct MVC parameter provisioning. @@ -15,14 +17,25 @@ * @author edgar * @since 2.0.0 */ -public interface QueryString extends ValueNode { +public interface QueryString extends Value { /** * Query string with the leading ? or empty string. * * @return Query string with the leading ? or empty string. */ - @NonNull String queryString(); + String queryString(); + + /** + * Query string produces always a non-null result even if no property matches. This is bc the + * query string run with {@link io.jooby.value.ConversionHint#Empty}. It fits better for empty + * query string when target object has some default properties. + * + * @param type Type to convert. + * @return Non null result. + * @param Type result. + */ + T toEmpty(@NonNull Class type); /** * Query string hash value. @@ -33,11 +46,11 @@ public interface QueryString extends ValueNode { * *

{@code {q: foo, sort: name}}
* - * @param ctx Current context. + * @param valueFactory Current context. * @param queryString Query string. * @return A query string. */ - static @NonNull QueryString create(@NonNull Context ctx, @Nullable String queryString) { - return UrlParser.queryString(ctx, queryString); + static QueryString create(@NonNull ValueFactory valueFactory, @Nullable String queryString) { + return UrlParser.queryString(valueFactory, queryString); } } diff --git a/jooby/src/main/java/io/jooby/ReactiveSupport.java b/jooby/src/main/java/io/jooby/ReactiveSupport.java index 999f23010e..2b2e1127e5 100644 --- a/jooby/src/main/java/io/jooby/ReactiveSupport.java +++ b/jooby/src/main/java/io/jooby/ReactiveSupport.java @@ -8,7 +8,6 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.Flow; -import io.jooby.annotation.ResultType; import io.jooby.internal.handler.ChunkedSubscriber; import io.jooby.internal.handler.ConcurrentHandler; @@ -18,10 +17,6 @@ * @author edgar * @since 3.0.0 */ -@ResultType( - types = {Flow.Publisher.class, CompletionStage.class}, - handler = "concurrent", - nonBlocking = true) public class ReactiveSupport { private static final Route.Filter CONCURRENT = new ConcurrentHandler(); diff --git a/jooby/src/main/java/io/jooby/Route.java b/jooby/src/main/java/io/jooby/Route.java index 68d89988bc..c6ccda68d9 100644 --- a/jooby/src/main/java/io/jooby/Route.java +++ b/jooby/src/main/java/io/jooby/Route.java @@ -5,23 +5,16 @@ */ package io.jooby; +import static java.util.Optional.ofNullable; + import java.io.Serializable; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.TreeMap; +import java.util.*; import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.function.Predicate; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -106,12 +99,6 @@ public void setRoute(Route route) { } } - /** - * @deprecated use {@link Route.Filter}. - */ - @Deprecated - public interface Decorator extends Filter {} - /** * Decorates a handler and run logic before handler is executed. * @@ -411,17 +398,25 @@ public Method toMethod() { /** * Convert to {@link MethodHandle}. * + * @param lookup Lookup to use. * @return A {@link MethodHandle}. */ - public MethodHandle toMethodHandle() { - var lookup = MethodHandles.publicLookup(); - var methodType = MethodType.methodType(returnType, parameterTypes); + public MethodHandle toMethodHandle(MethodHandles.Lookup lookup) { try { - return lookup.findVirtual(declaringClass, name, methodType); - } catch (NoSuchMethodException | IllegalAccessException e) { + return lookup.unreflect(toMethod()); + } catch (IllegalAccessException e) { throw SneakyThrows.propagate(e); } } + + /** + * Convert to {@link MethodHandle} using a public lookup. + * + * @return A {@link MethodHandle}. + */ + public MethodHandle toMethodHandle() { + return toMethodHandle(MethodHandles.publicLookup()); + } } /** Favicon handler as a silent 404 error. */ @@ -449,17 +444,13 @@ public MethodHandle toMethodHandle() { private MessageEncoder encoder; - private Type returnType; - - private Object handle; - private List produces = EMPTY_LIST; private List consumes = EMPTY_LIST; private Map attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - private Set supportedMethod; + private java.util.Set supportedMethod; private String executorKey; @@ -486,7 +477,6 @@ public Route(@NonNull String method, @NonNull String pattern, @NonNull Handler h this.method = method.toUpperCase(); this.pattern = pattern; this.handler = handler; - this.handle = handler; } /** @@ -570,17 +560,6 @@ public Route(@NonNull String method, @NonNull String pattern, @NonNull Handler h return Router.reverse(getPattern(), values); } - /** - * Handler instance which might or might not be the same as {@link #getHandler()}. - * - *

The handle is required to extract correct metadata. - * - * @return Handle. - */ - public @NonNull Object getHandle() { - return handle; - } - /** * After filter or null. * @@ -621,17 +600,6 @@ public Route(@NonNull String method, @NonNull String pattern, @NonNull Handler h return this; } - /** - * Set route handle instance, required when handle is different from {@link #getHandler()}. - * - * @param handle Handle instance. - * @return This route. - */ - public @NonNull Route setHandle(@NonNull Object handle) { - this.handle = handle; - return this; - } - /** * Set route pipeline. This method is part of public API but isn't intended to be used by public. * @@ -699,30 +667,6 @@ public boolean isNonBlockingSet() { return this; } - /** - * Route return type. - * - * @return Return type. - * @deprecated Marked for removal on 4.0 - */ - @Deprecated - public @Nullable Type getReturnType() { - return returnType; - } - - /** - * Set route return type. - * - * @param returnType Return type. - * @return This route. - * @deprecated Marked for removal on 4.0 - */ - @Deprecated - public @NonNull Route setReturnType(@Nullable Type returnType) { - this.returnType = returnType; - return this; - } - /** * Response types (format) produces by this route. If set, we expect to find a match in the * Accept header. If none matches, we send a {@link StatusCode#NOT_ACCEPTABLE} response. @@ -812,7 +756,8 @@ public boolean isNonBlockingSet() { * @param Generic type. * @return value of the specific attribute. */ - public @Nullable T attribute(@NonNull String name) { + public @Nullable T getAttribute(@NonNull String name) { + //noinspection unchecked return (T) attributes.get(name); } @@ -834,7 +779,7 @@ public boolean isNonBlockingSet() { * @param value attribute value * @return This route. */ - public @NonNull Route attribute(@NonNull String name, @NonNull Object value) { + public @NonNull Route setAttribute(@NonNull String name, @NonNull Object value) { if (this.attributes == EMPTY_MAP) { this.attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } @@ -1078,7 +1023,7 @@ public boolean isHttpHead() { * @return whether this route should be considered as transactional */ public boolean isTransactional(boolean defaultValue) { - Object attribute = attribute(Transactional.ATTRIBUTE); + Object attribute = getAttribute(Transactional.ATTRIBUTE); if (attribute == null) { return defaultValue; @@ -1144,7 +1089,7 @@ private void addHttpMethod(boolean enabled, String httpMethod) { } private Route.Handler computePipeline() { - Route.Handler pipeline = computeHeadPipeline(); + Route.Handler pipeline = filter == null ? handler : filter.then(handler); if (after != null) { pipeline = pipeline.then(after); @@ -1152,9 +1097,238 @@ private Route.Handler computePipeline() { return pipeline; } - private Route.Handler computeHeadPipeline() { - Route.Handler pipeline = filter == null ? handler : filter.then(handler); + /** + * Give you access to all routes created inside a {@link Router#path(String, Runnable)}. Allow + * globally applying attributes or metadata. + * + * @author edgar + * @since 2.7.3 + */ + public static class Set implements Iterable { - return pipeline; + private List routes; + + private List tags; + + private String summary; + + private String description; + + public Set(List routes) { + this.routes = routes; + } + + /** + * Sub-routes. Always empty except when used it from {@link Router#path(String, Runnable)} or + * {@link Router#routes(Runnable)}. + * + * @return Sub-routes. + */ + public @NonNull List getRoutes() { + return routes; + } + + /** + * Set sub-routes. + * + * @param routes Sub-routes. + * @return This route. + */ + public @NonNull Set setRoutes(@NonNull List routes) { + this.routes = routes; + return this; + } + + /** + * Add one or more response types (format) produces by this route. + * + * @param produces Produce types. + * @return This route. + */ + public @NonNull Set produces(@NonNull MediaType... produces) { + return setProduces(Arrays.asList(produces)); + } + + /** + * Add one or more response types (format) produces by this route. + * + * @param produces Produce types. + * @return This route. + */ + public @NonNull Set setProduces(@NonNull Collection produces) { + routes.forEach( + it -> { + if (it.getProduces().isEmpty()) { + it.setProduces(produces); + } + }); + return this; + } + + /** + * Add one or more request types (format) consumed by this route. + * + * @param consumes Consume types. + * @return This route. + */ + public @NonNull Set consumes(@NonNull MediaType... consumes) { + return setConsumes(Arrays.asList(consumes)); + } + + /** + * Add one or more request types (format) consumed by this route. + * + * @param consumes Consume types. + * @return This route. + */ + public @NonNull Set setConsumes(@NonNull Collection consumes) { + routes.forEach( + it -> { + if (it.getConsumes().isEmpty()) { + it.setConsumes(consumes); + } + }); + return this; + } + + /** + * Add one or more attributes applied to this route. + * + * @param attributes . + * @return This route. + */ + public @NonNull Set setAttributes(@NonNull Map attributes) { + routes.forEach(it -> attributes.forEach((k, v) -> it.getAttributes().putIfAbsent(k, v))); + return this; + } + + /** + * Add one or more attributes applied to this route. + * + * @param name attribute name + * @param value attribute value + * @return This route. + */ + public @NonNull Set setAttribute(@NonNull String name, @NonNull Object value) { + routes.forEach(it -> it.getAttributes().putIfAbsent(name, value)); + return this; + } + + /** + * Set executor key. The route is going to use the given key to fetch an executor. Possible + * values are: + * + *

- null: no specific executor, uses the default Jooby logic to choose one, + * based on the value of {@link ExecutionMode}; - worker: use the executor provided + * by the server. - arbitrary name: use an named executor which as registered using + * {@link Router#executor(String, Executor)}. + * + * @param executorKey Executor key. + * @return This route. + */ + public @NonNull Set setExecutorKey(@Nullable String executorKey) { + routes.forEach(it -> it.setExecutorKey(ofNullable(it.getExecutorKey()).orElse(executorKey))); + return this; + } + + /** + * Route tags. + * + * @return Route tags. + */ + public @NonNull List getTags() { + return tags == null ? List.of() : tags; + } + + /** + * Tag this route. Tags are used for documentation purpose from openAPI generator. + * + * @param tags Tags. + * @return This route. + */ + public @NonNull Set setTags(@NonNull List tags) { + this.tags = tags; + routes.forEach(it -> tags.forEach(it::addTag)); + return this; + } + + /** + * Tag this route. Tags are used for documentation purpose from openAPI generator. + * + * @param tags Tags. + * @return This route. + */ + public @NonNull Set tags(@NonNull String... tags) { + return setTags(Arrays.asList(tags)); + } + + /** + * Route summary useful for documentation purpose from openAPI generator. + * + * @return Summary. + */ + public @Nullable String getSummary() { + return summary; + } + + /** + * Route summary useful for documentation purpose from openAPI generator. + * + * @param summary Summary. + * @return This route. + */ + public @NonNull Set summary(@Nullable String summary) { + return setSummary(summary); + } + + /** + * Route summary useful for documentation purpose from openAPI generator. + * + * @param summary Summary. + * @return This route. + */ + public @NonNull Set setSummary(@Nullable String summary) { + this.summary = summary; + return this; + } + + /** + * Route description useful for documentation purpose from openAPI generator. + * + * @return Route description. + */ + public @Nullable String getDescription() { + return description; + } + + /** + * Route description useful for documentation purpose from openAPI generator. + * + * @param description Description. + * @return This route. + */ + public @NonNull Set setDescription(@Nullable String description) { + this.description = description; + return this; + } + + /** + * Route description useful for documentation purpose from openAPI generator. + * + * @param description Description. + * @return This route. + */ + public @NonNull Set description(@Nullable String description) { + return setDescription(description); + } + + public void forEach(Predicate predicate, Consumer action) { + routes.stream().filter(predicate).forEach(action); + } + + @Override + public Iterator iterator() { + return routes.iterator(); + } } } diff --git a/jooby/src/main/java/io/jooby/RouteSet.java b/jooby/src/main/java/io/jooby/RouteSet.java deleted file mode 100644 index 2197c04636..0000000000 --- a/jooby/src/main/java/io/jooby/RouteSet.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby; - -import static java.util.Collections.EMPTY_LIST; -import static java.util.Optional.ofNullable; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executor; - -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; - -/** - * Give you access to all routes created inside a {@link Router#path(String, Runnable)}. Allow to - * globally apply attributes or metadata. - * - * @author edgar - * @since 2.7.3 - */ -public class RouteSet { - - private List routes; - - private List tags = EMPTY_LIST; - - private String summary; - - private String description; - - /** - * Sub-routes. Always empty except when used it from {@link Router#path(String, Runnable)} or - * {@link Router#routes(Runnable)}. - * - * @return Sub-routes. - */ - public @NonNull List getRoutes() { - return routes; - } - - /** - * Set sub-routes. - * - * @param routes Sub-routes. - * @return This route. - */ - public @NonNull RouteSet setRoutes(@NonNull List routes) { - this.routes = routes; - return this; - } - - /** - * Add one or more response types (format) produces by this route. - * - * @param produces Produce types. - * @return This route. - */ - public @NonNull RouteSet produces(@NonNull MediaType... produces) { - return setProduces(Arrays.asList(produces)); - } - - /** - * Add one or more response types (format) produces by this route. - * - * @param produces Produce types. - * @return This route. - */ - public @NonNull RouteSet setProduces(@NonNull Collection produces) { - routes.forEach( - it -> { - if (it.getProduces().isEmpty()) { - it.setProduces(produces); - } - }); - return this; - } - - /** - * Add one or more request types (format) consumed by this route. - * - * @param consumes Consume types. - * @return This route. - */ - public @NonNull RouteSet consumes(@NonNull MediaType... consumes) { - return setConsumes(Arrays.asList(consumes)); - } - - /** - * Add one or more request types (format) consumed by this route. - * - * @param consumes Consume types. - * @return This route. - */ - public @NonNull RouteSet setConsumes(@NonNull Collection consumes) { - routes.forEach( - it -> { - if (it.getConsumes().isEmpty()) { - it.setConsumes(consumes); - } - }); - return this; - } - - /** - * Add one or more attributes applied to this route. - * - * @param attributes . - * @return This route. - */ - public @NonNull RouteSet setAttributes(@NonNull Map attributes) { - routes.forEach(it -> attributes.forEach((k, v) -> it.getAttributes().putIfAbsent(k, v))); - return this; - } - - /** - * Add one or more attributes applied to this route. - * - * @param name attribute name - * @param value attribute value - * @return This route. - */ - public @NonNull RouteSet attribute(@NonNull String name, @NonNull Object value) { - routes.forEach(it -> it.getAttributes().putIfAbsent(name, value)); - return this; - } - - /** - * Set executor key. The route is going to use the given key to fetch an executor. Possible values - * are: - * - *

- null: no specific executor, uses the default Jooby logic to choose one, based - * on the value of {@link ExecutionMode}; - worker: use the executor provided by the - * server. - arbitrary name: use an named executor which as registered using {@link - * Router#executor(String, Executor)}. - * - * @param executorKey Executor key. - * @return This route. - */ - public @NonNull RouteSet setExecutorKey(@Nullable String executorKey) { - routes.forEach(it -> it.setExecutorKey(ofNullable(it.getExecutorKey()).orElse(executorKey))); - return this; - } - - /** - * Route tags. - * - * @return Route tags. - */ - public @NonNull List getTags() { - return tags; - } - - /** - * Tag this route. Tags are used for documentation purpose from openAPI generator. - * - * @param tags Tags. - * @return This route. - */ - public @NonNull RouteSet setTags(@NonNull List tags) { - if (this.tags == EMPTY_LIST) { - this.tags = new ArrayList<>(); - } - routes.forEach(it -> tags.forEach(it::addTag)); - return this; - } - - /** - * Tag this route. Tags are used for documentation purpose from openAPI generator. - * - * @param tags Tags. - * @return This route. - */ - public @NonNull RouteSet tags(@NonNull String... tags) { - return setTags(Arrays.asList(tags)); - } - - /** - * Route summary useful for documentation purpose from openAPI generator. - * - * @return Summary. - */ - public @Nullable String getSummary() { - return summary; - } - - /** - * Route summary useful for documentation purpose from openAPI generator. - * - * @param summary Summary. - * @return This route. - */ - public @NonNull RouteSet summary(@Nullable String summary) { - return setSummary(summary); - } - - /** - * Route summary useful for documentation purpose from openAPI generator. - * - * @param summary Summary. - * @return This route. - */ - public @NonNull RouteSet setSummary(@Nullable String summary) { - this.summary = summary; - return this; - } - - /** - * Route description useful for documentation purpose from openAPI generator. - * - * @return Route description. - */ - public @Nullable String getDescription() { - return description; - } - - /** - * Route description useful for documentation purpose from openAPI generator. - * - * @param description Description. - * @return This route. - */ - public @NonNull RouteSet setDescription(@Nullable String description) { - this.description = description; - return this; - } - - /** - * Route description useful for documentation purpose from openAPI generator. - * - * @param description Description. - * @return This route. - */ - public @NonNull RouteSet description(@Nullable String description) { - return setDescription(description); - } -} diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 544ab357a0..fc2f749733 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -18,7 +18,6 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; @@ -33,11 +32,11 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.DataBufferFactory; import io.jooby.exception.MissingValueException; import io.jooby.handler.AssetHandler; import io.jooby.handler.AssetSource; -import jakarta.inject.Provider; +import io.jooby.output.OutputFactory; +import io.jooby.value.ValueFactory; /** * Routing DSL functions. @@ -151,7 +150,8 @@ default Object execute(@NonNull Context context) { * @param Attribute type. * @return Attribute value. */ - @NonNull default T attribute(@NonNull String key) { + @NonNull default T getAttribute(@NonNull String key) { + @SuppressWarnings("unchecked") T attribute = (T) getAttributes().get(key); if (attribute == null) { throw new MissingValueException(key); @@ -166,7 +166,7 @@ default Object execute(@NonNull Context context) { * @param value Attribute value. * @return This router. */ - @NonNull default Router attribute(@NonNull String key, Object value) { + @NonNull default Router setAttribute(@NonNull String key, Object value) { getAttributes().put(key, value); return this; } @@ -181,6 +181,13 @@ default Object execute(@NonNull Context context) { */ @NonNull ServiceRegistry getServices(); + /** + * Server options. + * + * @return Server options. + */ + @NonNull ServerOptions getServerOptions(); + /** * Set application context path. Context path is the base path for all routes. Default is: / * . @@ -197,53 +204,10 @@ default Object execute(@NonNull Context context) { */ @NonNull String getContextPath(); - /** - * When true handles X-Forwarded-* headers by updating the values on the current context to match - * what was sent in the header(s). - * - *

This should only be installed behind a reverse proxy that has been configured to send the - * X-Forwarded-* header, otherwise a remote user can spoof their address by sending a - * header with bogus values. - * - *

The headers that are read/set are: - * - *

    - *
  • X-Forwarded-For: Set/update the remote address {@link Context#setRemoteAddress(String)}. - *
  • X-Forwarded-Proto: Set/update request scheme {@link Context#setScheme(String)}. - *
  • X-Forwarded-Host: Set/update the request host {@link Context#setHost(String)}. - *
  • X-Forwarded-Port: Set/update the request port {@link Context#setPort(int)}. - *
- * - * @return True when enabled. Default is false. - */ - boolean isTrustProxy(); - boolean isStarted(); boolean isStopped(); - /** - * When true handles X-Forwarded-* headers by updating the values on the current context to match - * what was sent in the header(s). - * - *

This should only be installed behind a reverse proxy that has been configured to send the - * X-Forwarded-* header, otherwise a remote user can spoof their address by sending a - * header with bogus values. - * - *

The headers that are read/set are: - * - *

    - *
  • X-Forwarded-For: Set/update the remote address {@link Context#setRemoteAddress(String)}. - *
  • X-Forwarded-Proto: Set/update request scheme {@link Context#setScheme(String)}. - *
  • X-Forwarded-Host: Set/update the request host {@link Context#setHost(String)}. - *
  • X-Forwarded-Port: Set/update the request port {@link Context#setPort(int)}. - *
- * - * @param trustProxy True to enable. - * @return This router. - */ - @NonNull Router setTrustProxy(boolean trustProxy); - /** * Provides a way to override the current HTTP method. Request must be: * @@ -273,15 +237,6 @@ default Object execute(@NonNull Context context) { */ @NonNull Router setCurrentUser(@NonNull Function provider); - /** - * If enabled, allows to retrieve the {@link Context} object associated with the current request - * via the service registry while the request is being processed. - * - * @param contextAsService whether to enable or disable this feature - * @return This router. - */ - @NonNull Router setContextAsService(boolean contextAsService); - /* *********************************************************************************************** * use(Router) * *********************************************************************************************** @@ -297,15 +252,16 @@ default Object execute(@NonNull Context context) { * } * } * - * NOTE: if you run behind a reverse proxy you might to enabled {@link #setTrustProxy(boolean)}. + * NOTE: if you run behind a reverse proxy you might to enabled {@link + * RouterOptions#setTrustProxy(boolean)}. * *

NOTE: ONLY routes are imported. Services, callback, etc.. are ignored. * * @param domain Predicate * @param subrouter Subrouter. - * @return This router. + * @return Created routes. */ - @NonNull Router domain(@NonNull String domain, @NonNull Router subrouter); + @NonNull Route.Set domain(@NonNull String domain, @NonNull Router subrouter); /** * Enabled routes for specific domain. Domain matching is done using the host header. @@ -321,13 +277,14 @@ default Object execute(@NonNull Context context) { * } * } * - * NOTE: if you run behind a reverse proxy you might to enabled {@link #setTrustProxy(boolean)}. + * NOTE: if you run behind a reverse proxy you might to enabled {@link + * RouterOptions#setTrustProxy(boolean)}. * * @param domain Predicate * @param body Route action. - * @return This router. + * @return Created routes. */ - @NonNull RouteSet domain(@NonNull String domain, @NonNull Runnable body); + @NonNull Route.Set domain(@NonNull String domain, @NonNull Runnable body); /** * Import routes from given router. Predicate works like a filter and only when predicate pass the @@ -345,15 +302,15 @@ default Object execute(@NonNull Context context) { * Imported routes are matched only when predicate pass. * *

NOTE: if you run behind a reverse proxy you might to enabled {@link - * #setTrustProxy(boolean)}. + * RouterOptions#setTrustProxy(boolean)}. * *

NOTE: ONLY routes are imported. Services, callback, etc.. are ignored. * * @param predicate Context predicate. * @param router Router to import. - * @return This router. + * @return Created routes. */ - @NonNull Router mount(@NonNull Predicate predicate, @NonNull Router router); + @NonNull Route.Set mount(@NonNull Predicate predicate, @NonNull Router router); /** * Import routes from given action. Predicate works like a filter and only when predicate pass the @@ -372,15 +329,16 @@ default Object execute(@NonNull Context context) { * } * } * - * NOTE: if you run behind a reverse proxy you might to enabled {@link #setTrustProxy(boolean)}. + * NOTE: if you run behind a reverse proxy you might to enabled {@link + * RouterOptions#setTrustProxy(boolean)}. * *

NOTE: ONLY routes are imported. Services, callback, etc.. are ignored. * * @param predicate Context predicate. * @param body Route action. - * @return This router. + * @return Created routes. */ - @NonNull RouteSet mount(@NonNull Predicate predicate, @NonNull Runnable body); + @NonNull Route.Set mount(@NonNull Predicate predicate, @NonNull Runnable body); /** * Import all routes from the given router and prefix them with the given path. @@ -389,9 +347,9 @@ default Object execute(@NonNull Context context) { * * @param path Prefix path. * @param router Router to import. - * @return This router. + * @return Created routes. */ - @NonNull Router mount(@NonNull String path, @NonNull Router router); + @NonNull Route.Set mount(@NonNull String path, @NonNull Router router); /** * Import all routes from the given router. @@ -399,56 +357,15 @@ default Object execute(@NonNull Context context) { *

NOTE: ONLY routes are imported. Services, callback, etc.. are ignored. * * @param router Router to import. - * @return This router. + * @return Created routes. */ - @NonNull Router mount(@NonNull Router router); + @NonNull Route.Set mount(@NonNull Router router); /* *********************************************************************************************** * Mvc * *********************************************************************************************** */ - /** - * Import all route method from the given controller class. - * - * @param router Router extension. - * @return This router. - */ - @NonNull Router mvc(@NonNull MvcExtension router); - - /** - * Import all route method from the given controller class. At runtime the controller instance is - * resolved by calling {@link Jooby#require(Class)}. - * - * @param router Controller class. - * @return This router. - * @deprecated See {{@link #mvc(MvcExtension)}} - */ - @Deprecated - @NonNull Router mvc(@NonNull Class router); - - /** - * Import all route method from the given controller class. - * - * @param router Controller class. - * @param provider Controller provider. - * @param Controller type. - * @return This router. - * @deprecated See {{@link #mvc(MvcExtension)}} - */ - @Deprecated - @NonNull Router mvc(@NonNull Class router, @NonNull Provider provider); - - /** - * Import all route methods from given controller instance. - * - * @param router Controller instance. - * @return This routes. - * @deprecated See {{@link #mvc(MvcExtension)}} - */ - @Deprecated - @NonNull Router mvc(@NonNull Object router); - /** * Add a websocket handler. * @@ -535,9 +452,7 @@ default Object execute(@NonNull Context context) { */ @NonNull Router setDefaultWorker(@NonNull Executor worker); - @NonNull DataBufferFactory getBufferFactory(); - - @NonNull Router setBufferFactory(@NonNull DataBufferFactory bufferFactory); + @NonNull OutputFactory getOutputFactory(); /** * Attach a filter to the route pipeline. @@ -547,18 +462,6 @@ default Object execute(@NonNull Context context) { */ @NonNull Router use(@NonNull Route.Filter filter); - /** - * Attach a filter to the route pipeline. - * - * @param filter Filter. - * @return This router. - * @deprecated Use {@link #use(Route.Filter)}. - */ - @Deprecated - default @NonNull Router decorator(@NonNull Route.Decorator filter) { - return use(filter); - } - /** * Add a before route decorator to the route pipeline. * @@ -601,7 +504,7 @@ default Object execute(@NonNull Context context) { * @param body Route body. * @return All routes created. */ - @NonNull RouteSet routes(@NonNull Runnable body); + @NonNull Route.Set routes(@NonNull Runnable body); /** * Group one or more routes under a common path prefix. Useful for applying cross-cutting concerns @@ -611,7 +514,7 @@ default Object execute(@NonNull Context context) { * @param body Route body. * @return All routes created. */ - @NonNull RouteSet path(@NonNull String pattern, @NonNull Runnable body); + @NonNull Route.Set path(@NonNull String pattern, @NonNull Runnable body); /** * Add a HTTP GET handler. @@ -889,7 +792,7 @@ default Object execute(@NonNull Context context) { * * @return Router options. */ - @NonNull Set getRouterOptions(); + @NonNull RouterOptions getRouterOptions(); /** * Set router options. @@ -897,12 +800,10 @@ default Object execute(@NonNull Context context) { * @param options router options. * @return This router. */ - @NonNull Router setRouterOptions(@NonNull RouterOption... options); + @NonNull Router setRouterOptions(@NonNull RouterOptions options); /** - * Session store. Default use a cookie ID with a memory storage. - * - *

See {@link SessionStore#memory()}. + * Session store. Default is {@link SessionStore#UNSUPPORTED}. * * @return Session store. */ @@ -951,34 +852,9 @@ default Object execute(@NonNull Context context) { */ @NonNull Router setFlashCookie(@NonNull Cookie flashCookie); - /** - * Add a custom string value converter. - * - * @param converter Custom value converter. - * @return This router. - */ - @NonNull Router converter(@NonNull ValueConverter converter); - - /** - * Get all simple/string value converters. - * - * @return All simple/string value converters. - */ - @NonNull List getConverters(); + @NonNull ValueFactory getValueFactory(); - /** - * Get all complex/bean value converters. - * - * @return All complex/bean value converters. - */ - @NonNull List getBeanConverters(); - - /** - * Available server options. - * - * @return Server options. - */ - @NonNull ServerOptions getServerOptions(); + @NonNull Router setValueFactory(@NonNull ValueFactory valueFactory); /** * Ensure path start with a /(leading slash). diff --git a/jooby/src/main/java/io/jooby/RouterOption.java b/jooby/src/main/java/io/jooby/RouterOption.java deleted file mode 100644 index 155a45c70b..0000000000 --- a/jooby/src/main/java/io/jooby/RouterOption.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby; - -/** - * Router matching options. Specify whenever ignore case and trailing slash. Options: - * - *

    - *
  • IGNORE_CASE: Indicates whenever routing algorithm does case-sensitive matching on incoming - * request path. Default is case sensitive. - *
  • IGNORE_TRAILING_SLASH: Indicates whenever a trailing slash is ignored on incoming request - * path. - *
  • NORMALIZE_SLASH: Normalize incoming request path by removing consecutive / - * (slashes). - *
  • RESET_HEADERS_ON_ERROR: Indicates whenever response headers are clear/reset in case of - * exception. - *
- * - * @author edgar - * @since 2.4.0 - */ -public enum RouterOption { - /** - * Indicates whenever routing algorithm does case-sensitive matching on incoming request path. - * Default is case sensitive. - */ - IGNORE_CASE, - - /** Indicates whenever a trailing slash is ignored on incoming request path. */ - IGNORE_TRAILING_SLASH, - - /** Normalize incoming request path by removing multiple slash sequences. */ - NORMALIZE_SLASH, - - /** Indicates whenever response headers are clear/reset in case of exception. */ - RESET_HEADERS_ON_ERROR -} diff --git a/jooby/src/main/java/io/jooby/RouterOptions.java b/jooby/src/main/java/io/jooby/RouterOptions.java new file mode 100644 index 0000000000..41d5a2eca3 --- /dev/null +++ b/jooby/src/main/java/io/jooby/RouterOptions.java @@ -0,0 +1,250 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Router options: + * + *
    + *
  • ignoreCase: Indicates whenever routing algorithm does case-sensitive matching on an + * incoming request path. Default is false (case sensitive). + *
  • ignoreTrailingSlash: Indicates whenever a trailing slash is ignored on an incoming request + * path. + *
  • normalizeSlash: Normalize an incoming request path by removing consecutive / + * (slashes). + *
  • resetHeadersOnError: Indicates whenever response headers are clear/reset in case of + * exception. + *
+ * + * @author edgar + * @since 2.4.0 + */ +public class RouterOptions { + /** + * Indicates whenever routing algorithm does case-sensitive matching on an incoming request path. + * Default is case sensitive. + */ + private boolean ignoreCase; + + /** Indicates whenever a trailing slash is ignored on an incoming request path. */ + private boolean ignoreTrailingSlash; + + /** Normalize an incoming request path by removing multiple slash sequences. */ + private boolean normalizeSlash; + + /** Indicates whenever response headers are clear/reset in case of exception. */ + private boolean resetHeadersOnError; + + /** + * When true handles X-Forwarded-* headers by updating the values on the current context to match + * what was sent in the header(s). + * + *

This should only be installed behind a reverse proxy that has been configured to send the + * X-Forwarded-* header, otherwise a remote user can spoof their address by sending a + * header with bogus values. + * + *

The headers that are read/set are: + * + *

    + *
  • X-Forwarded-For: Set/update the remote address {@link Context#setRemoteAddress(String)}. + *
  • X-Forwarded-Proto: Set/update request scheme {@link Context#setScheme(String)}. + *
  • X-Forwarded-Host: Set/update the request host {@link Context#setHost(String)}. + *
  • X-Forwarded-Port: Set/update the request port {@link Context#setPort(int)}. + *
+ */ + private boolean trustProxy; + + /** + * Indicates whenever routing algorithm does case-sensitive matching on an incoming request path. + * Default is false (case sensitive). + * + * @return True when case is ignored. + */ + public boolean isIgnoreCase() { + return ignoreCase; + } + + /** + * Indicates whenever routing algorithm does case-sensitive matching on an incoming request path. + * Default is false (case sensitive). + * + * @param ignoreCase True for case-insensitive. + * @return This options. + */ + public RouterOptions setIgnoreCase(boolean ignoreCase) { + this.ignoreCase = ignoreCase; + return this; + } + + /** + * Indicates whenever routing algorithm does case-sensitive matching on an incoming request path. + * Default is false (case sensitive). + * + * @param ignoreCase True for case-insensitive. + * @return This options. + */ + public RouterOptions ignoreCase(boolean ignoreCase) { + return setIgnoreCase(ignoreCase); + } + + /** + * Indicates whenever a trailing slash is ignored on an incoming request path. + * + * @return Indicates whenever a trailing slash is ignored on an incoming request path. + */ + public boolean isIgnoreTrailingSlash() { + return ignoreTrailingSlash; + } + + /** + * Set whenever a trailing slash is ignored on an incoming request path. + * + * @param ignoreTrailingSlash whenever a trailing slash is ignored on an incoming request path. + * @return This options. + */ + public RouterOptions setIgnoreTrailingSlash(boolean ignoreTrailingSlash) { + this.ignoreTrailingSlash = ignoreTrailingSlash; + return this; + } + + /** + * Set whenever a trailing slash is ignored on an incoming request path. + * + * @param ignoreTrailingSlash whenever a trailing slash is ignored on an incoming request path. + * @return This options. + */ + public RouterOptions ignoreTrailingSlash(boolean ignoreTrailingSlash) { + return setIgnoreTrailingSlash(ignoreTrailingSlash); + } + + /** + * Normalize an incoming request path by removing multiple slash sequences. + * + * @return Normalize an incoming request path by removing multiple slash sequences. + */ + public boolean isNormalizeSlash() { + return normalizeSlash; + } + + /** + * Set whenever normalize an incoming request path by removing multiple slash sequences. + * + * @param normalizeSlash True for normalize a path. + * @return This options. + */ + public RouterOptions setNormalizeSlash(boolean normalizeSlash) { + this.normalizeSlash = normalizeSlash; + return this; + } + + /** + * Set whenever normalize an incoming request path by removing multiple slash sequences. + * + * @param normalizeSlash True for normalize a path. + * @return This options. + */ + public RouterOptions normalizeSlash(boolean normalizeSlash) { + return setNormalizeSlash(normalizeSlash); + } + + /** + * Indicates whenever response headers are clear/reset in case of exception. + * + * @return Indicates whenever response headers are clear/reset in case of exception. + */ + public boolean isResetHeadersOnError() { + return resetHeadersOnError; + } + + /** + * Set whenever response headers are clear/reset in case of exception. + * + * @param resetHeadersOnError whenever response headers are clear/reset in case of exception. + * @return This options. + */ + public RouterOptions setResetHeadersOnError(boolean resetHeadersOnError) { + this.resetHeadersOnError = resetHeadersOnError; + return this; + } + + /** + * Set whenever response headers are clear/reset in case of exception. + * + * @param resetHeadersOnError whenever response headers are clear/reset in case of exception. + * @return This options. + */ + public RouterOptions resetHeaderOnError(boolean resetHeadersOnError) { + return setResetHeadersOnError(resetHeadersOnError); + } + + /** + * Case-sensitive with reset headers on error enabled. + * + * @return Default options. + */ + public static RouterOptions defaults() { + return new RouterOptions().resetHeaderOnError(true); + } + + /** + * Case-inSensitive with reset headers on error enabled. + * + * @return Default options. + */ + public static RouterOptions caseInsensitive() { + return new RouterOptions().ignoreCase(true).resetHeaderOnError(true); + } + + /** + * When true handles X-Forwarded-* headers by updating the values on the current context to match + * what was sent in the header(s). + * + *

This should only be installed behind a reverse proxy that has been configured to send the + * X-Forwarded-* header, otherwise a remote user can spoof their address by sending a + * header with bogus values. + * + *

The headers that are read/set are: + * + *

    + *
  • X-Forwarded-For: Set/update the remote address {@link Context#setRemoteAddress(String)}. + *
  • X-Forwarded-Proto: Set/update request scheme {@link Context#setScheme(String)}. + *
  • X-Forwarded-Host: Set/update the request host {@link Context#setHost(String)}. + *
  • X-Forwarded-Port: Set/update the request port {@link Context#setPort(int)}. + *
+ * + * @return True when enabled. Default is false. + */ + public boolean isTrustProxy() { + return trustProxy; + } + + /** + * When true handles X-Forwarded-* headers by updating the values on the current context to match + * what was sent in the header(s). + * + *

This should only be installed behind a reverse proxy that has been configured to send the + * X-Forwarded-* header, otherwise a remote user can spoof their address by sending a + * header with bogus values. + * + *

The headers that are read/set are: + * + *

    + *
  • X-Forwarded-For: Set/update the remote address {@link Context#setRemoteAddress(String)}. + *
  • X-Forwarded-Proto: Set/update request scheme {@link Context#setScheme(String)}. + *
  • X-Forwarded-Host: Set/update the request host {@link Context#setHost(String)}. + *
  • X-Forwarded-Port: Set/update the request port {@link Context#setPort(int)}. + *
+ * + * @param trustProxy True to enable. + * @return This options. + */ + @NonNull public RouterOptions setTrustProxy(boolean trustProxy) { + this.trustProxy = trustProxy; + return this; + } +} diff --git a/jooby/src/main/java/io/jooby/Sender.java b/jooby/src/main/java/io/jooby/Sender.java index 2461268bf6..a5dae5fb97 100644 --- a/jooby/src/main/java/io/jooby/Sender.java +++ b/jooby/src/main/java/io/jooby/Sender.java @@ -10,10 +10,10 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; /** - * Non-blocking sender. Reactive responses uses this class to send partial data in non-blocking + * Non-blocking sender. Reactive responses use this class to send partial data in a non-blocking * manner. * *

RxJava example: @@ -86,15 +86,15 @@ interface Callback { } /** - * Write a bytes chunk. Chunk is flushed immediately. + * Write a byte chunk. Chunk is flushed immediately. * * @param data Bytes chunk. * @param callback Callback. * @return This sender. */ - @NonNull Sender write(@NonNull byte[] data, @NonNull Callback callback); + Sender write(@NonNull byte[] data, @NonNull Callback callback); - @NonNull Sender write(@NonNull DataBuffer data, @NonNull Callback callback); + Sender write(@NonNull Output output, @NonNull Callback callback); /** Close the sender. */ void close(); diff --git a/jooby/src/main/java/io/jooby/Server.java b/jooby/src/main/java/io/jooby/Server.java index d233ceda6f..0a8222b3a1 100644 --- a/jooby/src/main/java/io/jooby/Server.java +++ b/jooby/src/main/java/io/jooby/Server.java @@ -30,6 +30,7 @@ import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.exception.StartupException; import io.jooby.internal.MutedServer; +import io.jooby.output.OutputFactory; /** * Web server contract. Defines operations to start, join and stop a web server. Jooby comes with @@ -110,10 +111,12 @@ protected void fireReady(@NonNull List applications) { } } - protected void fireStop(@NonNull List applications) { - if (stopping.compareAndSet(false, true)) { - for (Jooby app : applications) { - app.stop(); + protected void fireStop(@Nullable List applications) { + if (applications != null) { + if (stopping.compareAndSet(false, true)) { + for (Jooby app : applications) { + app.stop(); + } } } } @@ -153,9 +156,13 @@ public Server setOptions(@NonNull ServerOptions options) { return this; } - protected abstract ServerOptions defaultOptions(); + protected ServerOptions defaultOptions() { + return new ServerOptions(); + } } + @NonNull OutputFactory getOutputFactory(); + /** * Set server options. This method should be called once, calling this method multiple times will * print a warning. @@ -293,11 +300,6 @@ static Server loadServer(@Nullable ServerOptions options) { var log = LoggerFactory.getLogger(servers.get(0).getClass()); log.warn("Multiple servers found {}. Using: {}", names, names.get(0)); } - var server = servers.get(0); - if (options != null) { - options.setServer(server.getName()); - server.setOptions(options); - } - return server; + return servers.get(0); } } diff --git a/jooby/src/main/java/io/jooby/ServerOptions.java b/jooby/src/main/java/io/jooby/ServerOptions.java index a565a742ad..6de52e5fd1 100644 --- a/jooby/src/main/java/io/jooby/ServerOptions.java +++ b/jooby/src/main/java/io/jooby/ServerOptions.java @@ -26,6 +26,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.internal.SslContextProvider; +import io.jooby.output.OutputOptions; /** * Available server options. To load server options from configuration files, just do: @@ -47,7 +48,7 @@ public class ServerOptions { public static final int SERVER_PORT = Integer.parseInt(System.getProperty("server.port", "8080")); /** - * Default application secure port 8443 or the value of system property + * Default application secures port 8443 or the value of system property * server.securePort. */ public static final int SEVER_SECURE_PORT = @@ -70,26 +71,32 @@ public class ServerOptions { private static final String LOCAL_HOST = "0.0.0.0"; - /** Buffer size used by server. Usually for reading/writing data. */ - private int bufferSize = _16KB; - /** Number of available threads, but never smaller than 2. */ - public static final int IO_THREADS = Runtime.getRuntime().availableProcessors() * 2; + public static final int IO_THREADS = + Integer.parseInt( + System.getProperty( + "server.ioThreads", Integer.toString(Runtime.getRuntime().availableProcessors()))); + + private static final String SERVER_NAME = System.getProperty("server.name"); /** * Number of worker (a.k.a application) threads. It is the number of processors multiply by * 8. */ - public static final int WORKER_THREADS = Runtime.getRuntime().availableProcessors() * 8; + public static final int WORKER_THREADS = + Integer.parseInt( + System.getProperty( + "server.workerThreads", + Integer.toString(Runtime.getRuntime().availableProcessors() * 8))); /** HTTP port. Default is 8080 or 0 for random port. */ private int port = SERVER_PORT; /** Number of IO threads used by the server. Used by Netty and Undertow. */ - private Integer ioThreads; + private int ioThreads = IO_THREADS; /** Number of worker threads (a.k.a application) to use. */ - private Integer workerThreads; + private int workerThreads = WORKER_THREADS; /** * Configure server to default headers: Date, Content-Type and @@ -98,7 +105,9 @@ public class ServerOptions { private boolean defaultHeaders = true; /** Name of server: Jetty, Netty or Undertow. */ - private String server; + private String server = SERVER_NAME; + + private OutputOptions output = OutputOptions.defaults(); /** * Maximum request size in bytes. Request exceeding this value results in {@link @@ -146,8 +155,11 @@ public class ServerOptions { if (conf.hasPath("server.name")) { options.setServer(conf.getString("server.name")); } - if (conf.hasPath("server.bufferSize")) { - options.setBufferSize(conf.getInt("server.bufferSize")); + if (conf.hasPath("server.output.size")) { + options.output.setSize(conf.getInt("server.output.size")); + } + if (conf.hasPath("server.output.useDirectBuffers")) { + options.output.setDirectBuffers(conf.getBoolean("server.output.useDirectBuffers")); } if (conf.hasPath("server.defaultHeaders")) { options.setDefaultHeaders(conf.getBoolean("server.defaultHeaders")); @@ -190,11 +202,9 @@ public String toString() { StringBuilder buff = new StringBuilder(); buff.append(Optional.ofNullable(server).orElse("server")).append(" {"); buff.append("port: ").append(port); - if (!"jetty".equals(server)) { - buff.append(", ioThreads: ").append(Optional.ofNullable(ioThreads).orElse(IO_THREADS)); - } + buff.append(", ioThreads: ").append(getIoThreads()); buff.append(", workerThreads: ").append(getWorkerThreads()); - buff.append(", bufferSize: ").append(bufferSize); + buff.append(", output: ").append(getOutput()); buff.append(", maxRequestSize: ").append(maxRequestSize); buff.append(", httpsOnly: ").append(httpsOnly); if (compressionLevel != null) { @@ -304,17 +314,7 @@ public boolean isHttpsOnly() { * @return Number of IO threads used by the server. Required by Netty and Undertow. */ public int getIoThreads() { - return getIoThreads(IO_THREADS); - } - - /** - * Number of IO threads used by the server. Required by Netty and Undertow. - * - * @param defaultIoThreads Default number of threads if none was set. - * @return Number of IO threads used by the server. Required by Netty and Undertow. - */ - public int getIoThreads(int defaultIoThreads) { - return ioThreads == null ? defaultIoThreads : ioThreads; + return ioThreads; } /** @@ -336,19 +336,7 @@ public int getIoThreads(int defaultIoThreads) { * allowed to block. */ public int getWorkerThreads() { - return getWorkerThreads(WORKER_THREADS); - } - - /** - * Number of worker threads (a.k.a application) to use. These are the threads which are allowed to - * block. - * - * @param defaultWorkerThreads Default worker threads is none was set. - * @return Number of worker threads (a.k.a application) to use. These are the threads which are - * allowed to block. - */ - public int getWorkerThreads(int defaultWorkerThreads) { - return workerThreads == null ? defaultWorkerThreads : workerThreads; + return workerThreads; } /** @@ -408,24 +396,12 @@ public boolean getDefaultHeaders() { return this; } - /** - * Server buffer size in bytes. Default is: 16kb. Used for reading/writing data. - * - * @return Server buffer size in bytes. Default is: 16kb. Used for reading/writing - * data. - */ - public int getBufferSize() { - return bufferSize; + public OutputOptions getOutput() { + return output; } - /** - * Set buffer size. - * - * @param bufferSize Buffer size. - * @return This options. - */ - public @NonNull ServerOptions setBufferSize(int bufferSize) { - this.bufferSize = bufferSize; + public ServerOptions setOutput(@NonNull OutputOptions output) { + this.output = output; return this; } diff --git a/jooby/src/main/java/io/jooby/ServerSentMessage.java b/jooby/src/main/java/io/jooby/ServerSentMessage.java index 041a45e9b5..540f8c34f6 100644 --- a/jooby/src/main/java/io/jooby/ServerSentMessage.java +++ b/jooby/src/main/java/io/jooby/ServerSentMessage.java @@ -7,11 +7,14 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; import java.util.function.IntPredicate; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; /** * Server-Sent message. @@ -129,42 +132,40 @@ public ServerSentMessage(@NonNull Object data) { * @param ctx Web context. To encode complex objects. * @return Encoded data. */ - public @NonNull DataBuffer encode(@NonNull Context ctx) { + public @NonNull Output encode(@NonNull Context ctx) { try { - Route route = ctx.getRoute(); - MessageEncoder encoder = route.getEncoder(); - var bufferFactory = ctx.getBufferFactory(); - var buffer = bufferFactory.allocateBuffer(); - var message = encoder.encode(ctx, data); + var route = ctx.getRoute(); + var encoder = route.getEncoder(); + var bufferFactory = ctx.getOutputFactory(); + var buffer = bufferFactory.allocate(); if (id != null) { buffer.write(ID); - buffer.write(id.toString().getBytes(UTF_8)); + buffer.write(id.toString()); buffer.write(SEPARATOR); } if (event != null) { buffer.write(EVENT); - buffer.write(event.getBytes(UTF_8)); + buffer.write(event); buffer.write(SEPARATOR); } if (retry != null) { buffer.write(RETRY); - buffer.write(retry.toString().getBytes(UTF_8)); + buffer.write(retry.toString()); buffer.write(SEPARATOR); } /* do multi-line processing: */ buffer.write(DATA); + IntPredicate nl = ch -> ch == '\n'; - var i = message.indexOf(nl, 0); - while (i > 0) { - buffer.write(message.split(i + 1)); - if (message.readableByteCount() > 0) { + var message = encoder.encode(ctx, data); + var lines = split(message.iterator(), nl); + while (lines.hasNext()) { + buffer.write(lines.next()); + if (lines.hasNext()) { buffer.write(DATA); } - i = message.indexOf(nl, 1); } - // write any pending bytes - buffer.write(message); buffer.write(SEPARATOR); buffer.write(SEPARATOR); return buffer; @@ -172,4 +173,46 @@ public ServerSentMessage(@NonNull Object data) { throw SneakyThrows.propagate(x); } } + + private Iterator split(Iterator buffers, IntPredicate predicate) { + var chunks = new ArrayList(); + ByteBuffer left = null; + while (buffers.hasNext()) { + var buffer = buffers.next(); + var offset = 0; + for (int i = 0; i < buffer.remaining(); i++) { + var b = buffer.get(i); + if (predicate.test(b)) { + if (left == null) { + chunks.add(buffer.duplicate().position(offset).limit(i + 1)); + } else { + chunks.add(merge(left, buffer, offset, i + 1)); + left = null; + } + offset = i + 1; + } + } + if (offset < buffer.remaining()) { + if (left == null) { + left = buffer.duplicate().position(offset); + } else { + left = merge(left, buffer, offset, buffer.remaining()); + } + } else { + left = null; + } + } + if (left != null) { + chunks.add(left); + } + return chunks.iterator(); + } + + private ByteBuffer merge(ByteBuffer tail, ByteBuffer buffer, int offset, int index) { + ByteBuffer chunk = ByteBuffer.allocate(tail.remaining() + index - offset); + chunk.put(tail); + chunk.put(buffer.duplicate().position(offset).limit(index)); + chunk.flip(); + return chunk; + } } diff --git a/jooby/src/main/java/io/jooby/Session.java b/jooby/src/main/java/io/jooby/Session.java index 61df340652..08c099d854 100644 --- a/jooby/src/main/java/io/jooby/Session.java +++ b/jooby/src/main/java/io/jooby/Session.java @@ -11,6 +11,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.internal.SessionImpl; +import io.jooby.value.Value; /** * HTTP session. Only basic data types can be saved into session. diff --git a/jooby/src/main/java/io/jooby/SessionStore.java b/jooby/src/main/java/io/jooby/SessionStore.java index c363b088ec..8c19350f46 100644 --- a/jooby/src/main/java/io/jooby/SessionStore.java +++ b/jooby/src/main/java/io/jooby/SessionStore.java @@ -27,6 +27,40 @@ public interface SessionStore { /** Default session timeout in minutes. */ int DEFAULT_TIMEOUT = 30; + SessionStore UNSUPPORTED = + new SessionStore() { + + @NonNull @Override + public Session newSession(@NonNull Context ctx) { + throw Usage.noSession(); + } + + @Nullable @Override + public Session findSession(@NonNull Context ctx) { + throw Usage.noSession(); + } + + @Override + public void deleteSession(@NonNull Context ctx, @NonNull Session session) { + throw Usage.noSession(); + } + + @Override + public void touchSession(@NonNull Context ctx, @NonNull Session session) { + throw Usage.noSession(); + } + + @Override + public void saveSession(@NonNull Context ctx, @NonNull Session session) { + throw Usage.noSession(); + } + + @Override + public void renewSessionId(@NonNull Context ctx, @NonNull Session session) { + throw Usage.noSession(); + } + }; + /** * Base class for in-memory session store. * @@ -35,9 +69,9 @@ public interface SessionStore { */ abstract class InMemory implements SessionStore { protected static class Data { - private Instant lastAccessedTime; - private Instant creationTime; - private Map hash; + private final Instant lastAccessedTime; + private final Instant creationTime; + private final Map hash; public Data(Instant creationTime, Instant lastAccessedTime, Map hash) { this.creationTime = creationTime; @@ -64,12 +98,12 @@ protected InMemory(@NonNull SessionToken token) { @Override public @NonNull Session newSession(@NonNull Context ctx) { - String sessionId = token.newToken(); - Data data = + var sessionId = token.newToken(); + var data = getOrCreate( sessionId, sid -> new Data(Instant.now(), Instant.now(), new ConcurrentHashMap())); - Session session = restore(ctx, sessionId, data); + var session = restore(ctx, sessionId, data); token.saveToken(ctx, sessionId); return session; @@ -78,7 +112,7 @@ protected InMemory(@NonNull SessionToken token) { /** * Session token. * - * @return Session token. Uses a cookie by default: {@link SessionToken#SID}. + * @return Session token. */ public @NonNull SessionToken getToken() { return token; @@ -95,7 +129,7 @@ protected InMemory(@NonNull SessionToken token) { return this; } - protected abstract @NonNull Data getOrCreate( + protected abstract Data getOrCreate( @NonNull String sessionId, @NonNull Function factory); protected abstract @Nullable Data getOrNull(@NonNull String sessionId); @@ -105,7 +139,7 @@ protected InMemory(@NonNull SessionToken token) { protected abstract void put(@NonNull String sessionId, @NonNull Data data); @Override - public Session findSession(Context ctx) { + public @Nullable Session findSession(@NonNull Context ctx) { String sessionId = token.findToken(ctx); if (sessionId == null) { return null; @@ -133,7 +167,7 @@ public void touchSession(@NonNull Context ctx, @NonNull Session session) { } @Override - public void saveSession(Context ctx, @NonNull Session session) { + public void saveSession(@NonNull Context ctx, @NonNull Session session) { String sessionId = session.getId(); put(sessionId, new Data(session.getCreationTime(), Instant.now(), session.toMap())); } @@ -219,48 +253,6 @@ private Session restore(Context ctx, String sessionId, Data data) { */ void renewSessionId(@NonNull Context ctx, @NonNull Session session); - /** - * Creates a cookie based session and store data in memory. Session data is not keep after - * restart. - * - *

It uses the default session cookie: {@link SessionToken#SID}. - * - *

- Session data is not keep after restart. - * - * @param timeout Timeout in seconds. Use -1 for no timeout. - * @return Session store. - */ - static @NonNull SessionStore memory(int timeout) { - return memory(SessionToken.SID, Duration.ofSeconds(timeout)); - } - - /** - * Creates a cookie based session and store data in memory. Session data is not keep after - * restart. - * - *

It uses the default session cookie: {@link SessionToken#SID}. - * - *

- Session expires after 30 minutes of inactivity. - Session data is not keep after restart. - * - * @return Session store. - */ - static @NonNull SessionStore memory() { - return memory(SessionToken.SID); - } - - /** - * Creates a cookie based session and store data in memory. Session data is not keep after - * restart. - * - *

It uses the default session cookie: {@link SessionToken#SID}. - * - * @param timeout Expires session after amount of inactivity time. - * @return Session store. - */ - static @NonNull SessionStore memory(@NonNull Duration timeout) { - return memory(SessionToken.SID, timeout); - } - /** * Creates a cookie based session and store data in memory. * @@ -313,25 +305,12 @@ private Session restore(Context ctx, String sessionId, Data data) { * *

See {@link Cookie#sign(String, String)} and {@link Cookie#unsign(String, String)}. * - * @param secret Secret token to signed data. - * @return A browser session store. - */ - static @NonNull SessionStore signed(@NonNull String secret) { - return signed(secret, SessionToken.SID); - } - - /** - * Creates a session store that uses (un)signed data. Session data is signed it using - * HMAC_SHA256. - * - *

See {@link Cookie#sign(String, String)} and {@link Cookie#unsign(String, String)}. - * - * @param secret Secret token to signed data. * @param cookie Cookie to use. + * @param secret Secret token to signed data. * @return A browser session store. */ - static @NonNull SessionStore signed(@NonNull String secret, @NonNull Cookie cookie) { - return signed(secret, SessionToken.signedCookie(cookie)); + static @NonNull SessionStore signed(@NonNull Cookie cookie, @NonNull String secret) { + return signed(SessionToken.signedCookie(cookie), secret); } /** @@ -340,11 +319,11 @@ private Session restore(Context ctx, String sessionId, Data data) { * *

See {@link Cookie#sign(String, String)} and {@link Cookie#unsign(String, String)}. * - * @param secret Secret token to signed data. * @param token Session token to use. + * @param secret Secret token to signed data. * @return A browser session store. */ - static @NonNull SessionStore signed(@NonNull String secret, @NonNull SessionToken token) { + static @NonNull SessionStore signed(@NonNull SessionToken token, @NonNull String secret) { SneakyThrows.Function> decoder = value -> { String unsign = Cookie.unsign(value, secret); diff --git a/jooby/src/main/java/io/jooby/SessionToken.java b/jooby/src/main/java/io/jooby/SessionToken.java index 724a40cd53..0f6925f7be 100644 --- a/jooby/src/main/java/io/jooby/SessionToken.java +++ b/jooby/src/main/java/io/jooby/SessionToken.java @@ -46,12 +46,7 @@ public String findToken(@NonNull Context ctx) { @Override public void saveToken(@NonNull Context ctx, @NonNull String token) { - // FIXME: Review, bc we don;t need this - // String existingId = findToken(ctx); - // write cookie for new or expiring session - // if (existingId == null || cookie.getMaxAge() > 0) { ctx.setResponseCookie(cookie.clone().setValue(token)); - // } } @Override @@ -64,7 +59,7 @@ public void deleteToken(@NonNull Context ctx, @NonNull String token) { * Looks for a session ID from request headers. This strategy: * *

- find a token from a request header. - on save, send the header back as response header. - - * on session destroy. don't send response header back. + * on session destruction. don't send response header back. */ class HeaderID implements SessionToken { @@ -130,12 +125,6 @@ public void deleteToken(@NonNull Context ctx, @NonNull String token) { } } - /** - * Default cookie for cookie based session stores. Uses jooby.sid as name. It never - * expires, use the root, only for HTTP. - */ - Cookie SID = new Cookie("jooby.sid").setMaxAge(-1).setHttpOnly(true).setPath("/"); - /** Secure random for default session token generator. */ SecureRandom RND = new SecureRandom(); diff --git a/jooby/src/main/java/io/jooby/TemplateEngine.java b/jooby/src/main/java/io/jooby/TemplateEngine.java index 5c1d41b5f6..ab4dd426c8 100644 --- a/jooby/src/main/java/io/jooby/TemplateEngine.java +++ b/jooby/src/main/java/io/jooby/TemplateEngine.java @@ -9,7 +9,7 @@ import java.util.List; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; /** * Template engine renderer. This class renderer instances of {@link ModelAndView} objects. Template @@ -34,10 +34,10 @@ public interface TemplateEngine extends MessageEncoder { * @return Rendered template. * @throws Exception If something goes wrong. */ - DataBuffer render(Context ctx, ModelAndView modelAndView) throws Exception; + Output render(Context ctx, ModelAndView modelAndView) throws Exception; @Override - default DataBuffer encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + default Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { // initialize flash and session attributes (if any) ctx.flashOrNull(); ctx.sessionOrNull(); diff --git a/jooby/src/main/java/io/jooby/Usage.java b/jooby/src/main/java/io/jooby/Usage.java index ccbe508398..171fe1874c 100644 --- a/jooby/src/main/java/io/jooby/Usage.java +++ b/jooby/src/main/java/io/jooby/Usage.java @@ -23,10 +23,10 @@ public class Usage extends RuntimeException { * Creates a new Usage exception. * * @param message Message. - * @param id Link to detailed section. + * @param id Link to a detailed section. */ public Usage(@NonNull String message, @NonNull String id) { - super( + this( (message + "\nFor more details, please visit: " + System.getProperty("jooby.host", "https://jooby.io") @@ -34,6 +34,14 @@ public Usage(@NonNull String message, @NonNull String id) { + id)); } + protected Usage(@NonNull String message) { + super(message); + } + + public static @NonNull Usage noSession() { + return new Usage("No session available. See https://jooby.io/#session-in-memory-session"); + } + /** * Creates a mvc route missing exception. * diff --git a/jooby/src/main/java/io/jooby/ValueConverter.java b/jooby/src/main/java/io/jooby/ValueConverter.java deleted file mode 100644 index 6d8a87e675..0000000000 --- a/jooby/src/main/java/io/jooby/ValueConverter.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby; - -import java.util.List; - -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.internal.ValueConverters; - -/** - * Value converter for simple values that come from query, path, form, etc... parameters into more - * specific type. - * - *

It is an extension point for {@link Value#to(Class)} calls. - */ -public interface ValueConverter { - /** - * True if the converter applies for the given type. - * - * @param type Conversion type. - * @return True if the converter applies for the given type. - */ - boolean supports(@NonNull Class type); - - /** - * Convert simple to specific type. - * - * @param value Value value. - * @param type Requested type. - * @return Converted value. - */ - Object convert(@NonNull V value, @NonNull Class type); - - /** - * Immutable list of defaults/built-in {@link ValueConverter}. - * - * @return Immutable list of defaults/built-in {@link ValueConverter}. - */ - static List> defaults() { - return ValueConverters.defaultConverters(); - } -} diff --git a/jooby/src/main/java/io/jooby/ValueNode.java b/jooby/src/main/java/io/jooby/ValueNode.java deleted file mode 100644 index b2085f235c..0000000000 --- a/jooby/src/main/java/io/jooby/ValueNode.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby; - -import java.util.Collections; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.function.BiFunction; - -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.exception.MissingValueException; - -/** - * Unified API for HTTP value. This API plays two role: - * - *

- unify access to HTTP values, like query, path, form and header parameter - works as - * validation API, because it is able to check for required and type-safe values - * - *

The value API is composed by three types: - * - *

- Single value - Object value - Sequence of values (array) - * - *

Single value are can be converted to string, int, boolean, enum like values. Object value is a - * key:value structure (like a hash). Sequence of values are index based structure. - * - *

All these 3 types are modeled into a single Value class. At any time you can treat a value as - * 1) single, 2) hash or 3) array of them. - * - * @since 2.0.0 - * @author edgar - */ -public interface ValueNode extends Iterable, Value { - - /** - * Get a value at the given position. - * - * @param index Position. - * @return A value at the given position. - */ - @NonNull ValueNode get(int index); - - /** - * Get a value that matches the given name. - * - * @param name Field name. - * @return Field value. - */ - @NonNull ValueNode get(@NonNull String name); - - /** - * The number of values this one has. For single values size is 0. - * - * @return Number of values. Mainly for array and hash values. - */ - default int size() { - return 0; - } - - /** - * Value iterator. - * - * @return Value iterator. - */ - @NonNull default @Override Iterator iterator() { - return Collections.emptyIterator(); - } - - /** - * Process the given expression and resolve value references. - * - *

{@code
-   * Value value = Value.single("foo", "bar");
-   *
-   * String output = value.resolve("${foo}");
-   * System.out.println(output);
-   * }
- * - * @param expression Text expression. - * @return Resolved text. - */ - @NonNull default String resolve(@NonNull String expression) { - return resolve(expression, "${", "}"); - } - - /** - * Process the given expression and resolve value references. - * - *
{@code
-   * Value value = Value.single("foo", "bar");
-   *
-   * String output = value.resolve("${missing}", true);
-   * System.out.println(output);
-   * }
- * - * @param expression Text expression. - * @param ignoreMissing On missing values, keep the expression as it is. - * @return Resolved text. - */ - @NonNull default String resolve(@NonNull String expression, boolean ignoreMissing) { - return resolve(expression, ignoreMissing, "${", "}"); - } - - /** - * Process the given expression and resolve value references. - * - *
{@code
-   * Value value = Value.single("foo", "bar");
-   *
-   * String output = value.resolve("<%missing%>", "<%", "%>");
-   * System.out.println(output);
-   * }
- * - * @param expression Text expression. - * @param startDelim Start delimiter. - * @param endDelim End delimiter. - * @return Resolved text. - */ - @NonNull default String resolve( - @NonNull String expression, @NonNull String startDelim, @NonNull String endDelim) { - return resolve(expression, false, startDelim, endDelim); - } - - /** - * Process the given expression and resolve value references. - * - *
{@code
-   * Value value = Value.single("foo", "bar");
-   *
-   * String output = value.resolve("<%missing%>", "<%", "%>");
-   * System.out.println(output);
-   * }
- * - * @param expression Text expression. - * @param ignoreMissing On missing values, keep the expression as it is. - * @param startDelim Start delimiter. - * @param endDelim End delimiter. - * @return Resolved text. - */ - @NonNull default String resolve( - @NonNull String expression, - boolean ignoreMissing, - @NonNull String startDelim, - @NonNull String endDelim) { - if (expression.length() == 0) { - return ""; - } - - BiFunction, RuntimeException> err = - (start, ex) -> { - String snapshot = expression.substring(0, start); - int line = Math.max(1, (int) snapshot.chars().filter(ch -> ch == '\n').count()); - int column = start - snapshot.lastIndexOf('\n'); - return ex.apply(line, column); - }; - - StringBuilder buffer = new StringBuilder(); - int offset = 0; - int start = expression.indexOf(startDelim); - while (start >= 0) { - int end = expression.indexOf(endDelim, start + startDelim.length()); - if (end == -1) { - throw err.apply( - start, - (line, column) -> - new IllegalArgumentException( - "found '" - + startDelim - + "' expecting '" - + endDelim - + "' at " - + line - + ":" - + column)); - } - buffer.append(expression.substring(offset, start)); - String key = expression.substring(start + startDelim.length(), end); - String value; - try { - // seek - String[] path = key.split("\\."); - ValueNode src = path[0].equals(name()) ? this : get(path[0]); - for (int i = 1; i < path.length; i++) { - src = src.get(path[i]); - } - value = src.value(); - } catch (MissingValueException x) { - if (ignoreMissing) { - value = expression.substring(start, end + endDelim.length()); - } else { - throw err.apply( - start, - (line, column) -> - new NoSuchElementException( - "Missing " + startDelim + key + endDelim + " at " + line + ":" + column)); - } - } - buffer.append(value); - offset = end + endDelim.length(); - start = expression.indexOf(startDelim, offset); - } - if (buffer.length() == 0) { - return expression; - } - if (offset < expression.length()) { - buffer.append(expression.substring(offset)); - } - return buffer.toString(); - } -} diff --git a/jooby/src/main/java/io/jooby/WebSocket.java b/jooby/src/main/java/io/jooby/WebSocket.java index ca4f1f9a96..6ca0f0f5a6 100644 --- a/jooby/src/main/java/io/jooby/WebSocket.java +++ b/jooby/src/main/java/io/jooby/WebSocket.java @@ -11,7 +11,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; /** * Websocket. Usage: @@ -55,6 +55,10 @@ interface Initializer { void init(@NonNull Context ctx, @NonNull WebSocketConfigurer configurer); } + interface Handler extends Route.Handler { + Initializer getInitializer(); + } + /** On connect callback. */ interface OnConnect { /** @@ -241,11 +245,11 @@ interface WriteCallback { @NonNull WebSocket send(@NonNull ByteBuffer message, @NonNull WriteCallback callback); - default @NonNull WebSocket send(@NonNull DataBuffer message) { + default @NonNull WebSocket send(@NonNull Output message) { return send(message, WriteCallback.NOOP); } - @NonNull WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callback); + @NonNull WebSocket send(@NonNull Output message, @NonNull WriteCallback callback); /** * Send a binary message to client. @@ -293,11 +297,11 @@ interface WriteCallback { @NonNull WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback callback); - default @NonNull WebSocket sendBinary(@NonNull DataBuffer message) { + default @NonNull WebSocket sendBinary(@NonNull Output message) { return sendBinary(message, WriteCallback.NOOP); } - @NonNull WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback); + @NonNull WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback); /** * Encode a value and send a text message to client. diff --git a/jooby/src/main/java/io/jooby/WebSocketMessage.java b/jooby/src/main/java/io/jooby/WebSocketMessage.java index cc0cd35645..f0546fc1e2 100644 --- a/jooby/src/main/java/io/jooby/WebSocketMessage.java +++ b/jooby/src/main/java/io/jooby/WebSocketMessage.java @@ -10,6 +10,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.internal.WebSocketMessageImpl; +import io.jooby.value.Value; /** * Websocket message generated from a {@link WebSocket.OnMessage} callback. Message is a subclass. diff --git a/jooby/src/main/java/io/jooby/annotation/BindParam.java b/jooby/src/main/java/io/jooby/annotation/BindParam.java index 0ee76768e4..7de1e3a966 100644 --- a/jooby/src/main/java/io/jooby/annotation/BindParam.java +++ b/jooby/src/main/java/io/jooby/annotation/BindParam.java @@ -20,7 +20,7 @@ @Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface BindParam { /** - * Class containing the mapping function. If no class is specified it looks at: + * Class containing the mapping function. If no class is specified, it looks at: * *

- Parameter type for {@link #fn()} method. - Or fallback to controller class. * diff --git a/jooby/src/main/java/io/jooby/annotation/EmptyBean.java b/jooby/src/main/java/io/jooby/annotation/EmptyBean.java deleted file mode 100644 index 09f4bac2a8..0000000000 --- a/jooby/src/main/java/io/jooby/annotation/EmptyBean.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface EmptyBean {} diff --git a/jooby/src/main/java/io/jooby/annotation/Header.java b/jooby/src/main/java/io/jooby/annotation/Header.java deleted file mode 100644 index 7e19b8931e..0000000000 --- a/jooby/src/main/java/io/jooby/annotation/Header.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import jakarta.inject.Qualifier; - -/** - * Mark a MVC method parameter as a request header. - * - *

- *   class Resources {
- *
- *     @GET
- *     public void method(@Header String myHeader) {
- *     }
- *   }
- * 
- * - * @author edgar - * @since 0.1.0 - */ -@Qualifier @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.PARAMETER) -public @interface Header { - /** - * @return Header's name. - */ - String value() default ""; -} diff --git a/jooby/src/main/java/io/jooby/annotation/ResultType.java b/jooby/src/main/java/io/jooby/annotation/ResultType.java deleted file mode 100644 index f0f4c73527..0000000000 --- a/jooby/src/main/java/io/jooby/annotation/ResultType.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Hints source code generator (jooby annotation processor) to map/adapt a specific return type to - * use a custom handler. This annotation if only for source code generator process so only applies - * for MVC routes. Example: - * - *
{@code
- * class MyController {
- *   @GET("/")
- *   public MySpecialType hello() {}
- * }
- * }
- * - * Write a code generator: - * - *
{@code
- * @ResultType(types = MySpecialType.class, handler = "customMapping")
- * class MySpecialTypeGenerator {
- *
- *     public static Route.Handler customMapping(Route.Handler handler) {
- *         return myHandler.then(handler);
- *     }
- * }
- * }
- * - * Let jooby annotation processor to know about your handler by setting the jooby.handler - * annotation processor option: jooby.handler=mypackage.MySpecialTypeGenerator - * Generates: - * - *
{@code
- * app.get("/", customMapping(this::hello));
- * }
- * - * @author edgar - * @since 3.2.0 - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface ResultType { - /** - * Custom type that requires special handling. - * - * @return Types. - */ - Class[] types(); - - /** - * Mapping function must be: - * - *
    - *
  • Single argument function of type {@link io.jooby.Route.Handler}. - *
  • Returns type {@link io.jooby.Route.Handler}. - Must be static. - *
- * - * @return Name of mapping function. - */ - String handler(); - - /** - * When true, the handler run on the event loop (when application starts in event loop mode). - * - * @return True, the handler run on the event loop (when application starts in event loop mode). - */ - boolean nonBlocking() default false; -} diff --git a/jooby/src/main/java/io/jooby/buffer/Assert.java b/jooby/src/main/java/io/jooby/buffer/Assert.java deleted file mode 100644 index 424aa97845..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/Assert.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.util.List; -import java.util.Objects; - -class Assert { - public static void notNull(Object charset, String message) { - Objects.requireNonNull(charset, message); - } - - public static void state(boolean value, String message) { - if (!value) { - throw new IllegalStateException(message); - } - } - - public static void isTrue(boolean value, String message) { - if (!value) { - throw new IllegalArgumentException(message); - } - } - - public static void notEmpty(Object[] array, String message) { - if (ObjectUtils.isEmpty(array)) { - throw new IllegalArgumentException(message); - } - } - - public static void notEmpty(List list, String message) { - if (ObjectUtils.isEmpty(list)) { - throw new IllegalArgumentException(message); - } - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/CloseableDataBuffer.java b/jooby/src/main/java/io/jooby/buffer/CloseableDataBuffer.java deleted file mode 100644 index 860d30b9dc..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/CloseableDataBuffer.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -/** - * Extension of {@link DataBuffer} that allows for buffers that can be used in a {@code - * try}-with-resources statement. - * - * @author Arjen Poutsma - * @since 6.0 - */ -public interface CloseableDataBuffer extends DataBuffer, AutoCloseable { - - /** - * Closes this data buffer, freeing any resources. - * - * @throws IllegalStateException if this buffer has already been closed - */ - @Override - void close(); -} diff --git a/jooby/src/main/java/io/jooby/buffer/DataBuffer.java b/jooby/src/main/java/io/jooby/buffer/DataBuffer.java deleted file mode 100644 index d99ed5eb1e..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/DataBuffer.java +++ /dev/null @@ -1,473 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.io.Closeable; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Writer; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.*; -import java.util.Iterator; -import java.util.function.IntPredicate; - -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Context; - -/** - * Basic abstraction over byte buffers. - * - *

{@code DataBuffer}s has a separate {@linkplain #readPosition() read} and {@linkplain - * #writePosition() write} position, as opposed to {@code ByteBuffer}'s single {@linkplain - * ByteBuffer#position() position}. As such, the {@code DataBuffer} does not require a {@linkplain - * ByteBuffer#flip() flip} to read after writing. In general, the following invariant holds for the - * read and write positions, and the capacity: - * - *

- * - * {@code 0} {@code <=} readPosition {@code <=} writePosition {@code <=} - * capacity - * - *
- * - *

The {@linkplain #capacity() capacity} of a {@code DataBuffer} is expanded on demand, similar - * to {@code StringBuilder}. - * - *

The main purpose of the {@code DataBuffer} abstraction is to provide a convenient wrapper - * around {@link ByteBuffer} which is similar to Netty's {@link io.netty.buffer.ByteBuf} but can - * also be used on non-Netty platforms (i.e. Servlet containers). - * - * @author Arjen Poutsma - * @author Brian Clozel - * @since 5.0 - * @see DataBufferFactory - */ -public interface DataBuffer { - - /** - * Return the {@link DataBufferFactory} that created this buffer. - * - * @return the creating buffer factory - */ - DataBufferFactory factory(); - - /** - * Return the index of the first byte in this buffer that matches the given predicate. - * - * @param predicate the predicate to match - * @param fromIndex the index to start the search from - * @return the index of the first byte that matches {@code predicate}; or {@code -1} if none match - */ - int indexOf(IntPredicate predicate, int fromIndex); - - /** - * Return the index of the last byte in this buffer that matches the given predicate. - * - * @param predicate the predicate to match - * @param fromIndex the index to start the search from - * @return the index of the last byte that matches {@code predicate}; or {@code -1} if none match - */ - int lastIndexOf(IntPredicate predicate, int fromIndex); - - /** - * Return the number of bytes that can be read from this data buffer. - * - * @return the readable byte count - */ - int readableByteCount(); - - /** - * Return the number of bytes that can be written to this data buffer. - * - * @return the writable byte count - * @since 5.0.1 - */ - int writableByteCount(); - - /** - * Return the number of bytes that this buffer can contain. - * - * @return the capacity - * @since 5.0.1 - */ - int capacity(); - - DataBuffer duplicate(); - - /** - * Ensure that the current buffer has enough {@link #writableByteCount()} to write the amount of - * data given as an argument. If not, the missing capacity will be added to the buffer. - * - * @param capacity the writable capacity to check for - * @return this buffer - * @since 6.0 - */ - DataBuffer ensureWritable(int capacity); - - /** - * Return the position from which this buffer will read. - * - * @return the read position - * @since 5.0.1 - */ - int readPosition(); - - /** - * Set the position from which this buffer will read. - * - * @param readPosition the new read position - * @return this buffer - * @throws IndexOutOfBoundsException if {@code readPosition} is smaller than 0 or greater than - * {@link #writePosition()} - * @since 5.0.1 - */ - DataBuffer readPosition(int readPosition); - - /** - * Return the position to which this buffer will write. - * - * @return the write position - * @since 5.0.1 - */ - int writePosition(); - - /** - * Set the position to which this buffer will write. - * - * @param writePosition the new write position - * @return this buffer - * @throws IndexOutOfBoundsException if {@code writePosition} is smaller than {@link - * #readPosition()} or greater than {@link #capacity()} - * @since 5.0.1 - */ - DataBuffer writePosition(int writePosition); - - /** - * Read a single byte at the given index from this data buffer. - * - * @param index the index at which the byte will be read - * @return the byte at the given index - * @throws IndexOutOfBoundsException when {@code index} is out of bounds - * @since 5.0.4 - */ - byte getByte(int index); - - /** - * Read a single byte from the current reading position from this data buffer. - * - * @return the byte at this buffer's current reading position - */ - byte read(); - - /** - * Read this buffer's data into the specified destination, starting at the current reading - * position of this buffer. - * - * @param destination the array into which the bytes are to be written - * @return this buffer - */ - DataBuffer read(byte[] destination); - - /** - * Read at most {@code length} bytes of this buffer into the specified destination, starting at - * the current reading position of this buffer. - * - * @param destination the array into which the bytes are to be written - * @param offset the index within {@code destination} of the first byte to be written - * @param length the maximum number of bytes to be written in {@code destination} - * @return this buffer - */ - DataBuffer read(byte[] destination, int offset, int length); - - /** - * Write a single byte into this buffer at the current writing position. - * - * @param b the byte to be written - * @return this buffer - */ - DataBuffer write(byte b); - - /** - * Write the given source into this buffer, starting at the current writing position of this - * buffer. - * - * @param source the bytes to be written into this buffer - * @return this buffer - */ - DataBuffer write(byte[] source); - - /** - * Write at most {@code length} bytes of the given source into this buffer, starting at the - * current writing position of this buffer. - * - * @param source the bytes to be written into this buffer - * @param offset the index within {@code source} to start writing from - * @param length the maximum number of bytes to be written from {@code source} - * @return this buffer - */ - DataBuffer write(byte[] source, int offset, int length); - - /** - * Write one or more {@code DataBuffer}s to this buffer, starting at the current writing position. - * It is the responsibility of the caller to {@linkplain DataBufferUtils#release(DataBuffer) - * release} the given data buffers. - * - * @param buffers the byte buffers to write into this buffer - * @return this buffer - */ - DataBuffer write(DataBuffer... buffers); - - /** - * Write one or more {@link ByteBuffer} to this buffer, starting at the current writing position. - * - * @param buffers the byte buffers to write into this buffer - * @return this buffer - */ - DataBuffer write(ByteBuffer... buffers); - - /** - * Write the given {@code CharSequence} using the given {@code Charset}, starting at the current - * writing position. - * - * @param charSequence the char sequence to write into this buffer - * @param charset the charset to encode the char sequence with - * @return this buffer - * @since 5.1.4 - */ - default DataBuffer write(CharSequence charSequence, Charset charset) { - Assert.notNull(charSequence, "CharSequence must not be null"); - Assert.notNull(charset, "Charset must not be null"); - if (!charSequence.isEmpty()) { - write(CharBuffer.wrap(charSequence), charset); - } - return this; - } - - /** - * Write the given {@code CharBuffer} using the given {@code Charset}, starting at the current - * writing position. - * - * @param src the char sequence to write into this buffer - * @param charset the charset to encode the char sequence with - * @return this buffer - * @since 5.1.4 - */ - default DataBuffer write(CharBuffer src, Charset charset) { - CharsetEncoder encoder = - charset - .newEncoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); - int averageSize = (int) Math.ceil(src.remaining() * encoder.averageBytesPerChar()); - ensureWritable(averageSize); - while (true) { - CoderResult cr; - if (src.hasRemaining()) { - try (ByteBufferIterator iterator = writableByteBuffers()) { - Assert.state(iterator.hasNext(), "No ByteBuffer available"); - ByteBuffer dest = iterator.next(); - cr = encoder.encode(src, dest, true); - if (cr.isUnderflow()) { - cr = encoder.flush(dest); - } - writePosition(writePosition() + dest.position()); - } - } else { - cr = CoderResult.UNDERFLOW; - } - if (cr.isUnderflow()) { - break; - } else if (cr.isOverflow()) { - int maxSize = (int) Math.ceil(src.remaining() * encoder.maxBytesPerChar()); - ensureWritable(maxSize); - } - } - return this; - } - - /** - * Splits this data buffer into two at the given index. - * - *

Data that precedes the {@code index} will be returned in a new buffer, while this buffer - * will contain data that follows after {@code index}. Memory between the two buffers is shared. - * - *

The {@linkplain #readPosition() read} and {@linkplain #writePosition() write} position of - * the returned buffer are truncated to fit within the buffers {@linkplain #capacity() capacity} - * if necessary. The positions of this buffer are set to {@code 0} if they are smaller than {@code - * index}. - * - * @param index the index at which it should be split - * @return a new data buffer, containing the bytes from index {@code 0} to {@code index} - * @since 6.0 - */ - DataBuffer split(int index); - - /** - * Copies this entire data buffer into the given destination {@code ByteBuffer}, beginning at the - * current {@linkplain #readPosition() reading position}, and the current {@linkplain - * ByteBuffer#position() position} of destination byte buffer. - * - * @param dest the destination byte buffer - * @since 6.0.5 - */ - default void toByteBuffer(ByteBuffer dest) { - toByteBuffer(readPosition(), dest, dest.position(), readableByteCount()); - } - - /** - * Copies the given length from this data buffer into the given destination {@code ByteBuffer}, - * beginning at the given source position, and the given destination position in the destination - * byte buffer. - * - * @param srcPos the position of this data buffer from where copying should start - * @param dest the destination byte buffer - * @param destPos the position in {@code dest} to where copying should start - * @param length the amount of data to copy - * @since 6.0.5 - */ - void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length); - - /** - * Returns a closeable iterator over each {@link ByteBuffer} in this data buffer that can be read. - * However, the byte buffers provided can only be used during the iteration. - * - *

Note that the returned iterator must be used in a try-with-resources clause or - * explicitly {@linkplain ByteBufferIterator#close() closed}. - * - * @return a closeable iterator over the readable byte buffers contained in this data buffer - * @since 6.0.5 - */ - ByteBufferIterator readableByteBuffers(); - - /** - * Returns a closeable iterator over each {@link ByteBuffer} in this data buffer that can be - * written to. The byte buffers provided can only be used during the iteration. - * - *

Note that the returned iterator must be used in a try-with-resources clause or - * explicitly {@linkplain ByteBufferIterator#close() closed}. - * - * @return a closeable iterator over the writable byte buffers contained in this data buffer - * @since 6.0.5 - */ - ByteBufferIterator writableByteBuffers(); - - /** - * Expose this buffer's data as an {@link InputStream}. Both data and read position are shared - * between the returned stream and this data buffer. The underlying buffer will - * not be {@linkplain DataBufferUtils#release(DataBuffer) released} when the - * input stream is {@linkplain InputStream#close() closed}. - * - * @return this data buffer as an input stream - * @see #asInputStream(boolean) - */ - default InputStream asInputStream() { - return new DataBufferInputStream(this, false); - } - - /** - * Expose this buffer's data as an {@link InputStream}. Both data and read position are shared - * between the returned stream and this data buffer. - * - * @param releaseOnClose whether the underlying buffer will be {@linkplain - * DataBufferUtils#release(DataBuffer) released} when the input stream is {@linkplain - * InputStream#close() closed}. - * @return this data buffer as an input stream - * @since 5.0.4 - */ - default InputStream asInputStream(boolean releaseOnClose) { - return new DataBufferInputStream(this, releaseOnClose); - } - - /** - * Expose this buffer's data as an {@link OutputStream}. Both data and write position are shared - * between the returned stream and this data buffer. - * - * @return this data buffer as an output stream - */ - default OutputStream asOutputStream() { - return new DataBufferOutputStream(this); - } - - /** - * Expose this buffer's data as an {@link Writer}. Both data and write position are shared between - * the returned stream and this data buffer. Uses UTF-8 charset. - * - * @return this data buffer as an output stream - */ - default Writer asWriter() { - return asWriter(StandardCharsets.UTF_8); - } - - /** - * Expose this buffer's data as an {@link Writer}. Both data and write position are shared between - * the returned stream and this data buffer. - * - * @param charset Charset to use. - * @return this data buffer as an output stream - */ - default Writer asWriter(@NonNull Charset charset) { - return new DataBufferWriter(this, charset); - } - - /** - * Return this buffer's data a String using the specified charset. Default implementation - * delegates to {@code toString(readPosition(), readableByteCount(), charset)}. - * - * @param charset the character set to use - * @return a string representation of all this buffers data - * @since 5.2 - */ - default String toString(Charset charset) { - Assert.notNull(charset, "Charset must not be null"); - return toString(readPosition(), readableByteCount(), charset); - } - - /** - * Return a part of this buffer's data as a String using the specified charset. - * - * @param index the index at which to start the string - * @param length the number of bytes to use for the string - * @param charset the charset to use - * @return a string representation of a part of this buffers data - * @since 5.2 - */ - String toString(int index, int length, Charset charset); - - /** - * Clears this buffer. The position is set to zero, the limit is set to the capacity, and the mark - * is discarded. - * - *

This method does not actually erase the data in the buffer, but it is named as if it did - * because it will most often be used in situations in which that might as well be the case. - * - * @return This buffer - */ - DataBuffer clear(); - - /** - * Send the buffer data to the client. - * - * @param ctx HTTP context. - * @return HTTP context. - */ - Context send(Context ctx); - - /** - * A dedicated iterator type that ensures the lifecycle of iterated {@link ByteBuffer} elements. - * This iterator must be used in a try-with-resources clause or explicitly {@linkplain #close() - * closed}. - * - * @since 6.0.5 - * @see DataBuffer#readableByteBuffers() - * @see DataBuffer#writableByteBuffers() - */ - interface ByteBufferIterator extends Iterator, Closeable { - - @Override - void close(); - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/DataBufferFactory.java b/jooby/src/main/java/io/jooby/buffer/DataBufferFactory.java deleted file mode 100644 index a5326aa642..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/DataBufferFactory.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.nio.ByteBuffer; -import java.util.List; - -/** - * A factory for {@link DataBuffer DataBuffers}, allowing for allocation and wrapping of data - * buffers. - * - * @author Arjen Poutsma - * @since 5.0 - * @see DataBuffer - */ -public interface DataBufferFactory { - /** - * Buffer of a default initial capacity. Default capacity is 1024 bytes. - * - * @return buffer of a default initial capacity. - */ - int getDefaultInitialCapacity(); - - /** - * Set default buffer initial capacity. - * - * @param defaultInitialCapacity Default initial buffer capacity. - * @return This buffer factory. - */ - DataBufferFactory setDefaultInitialCapacity(int defaultInitialCapacity); - - /** - * Allocate a data buffer of a default initial capacity. Depending on the underlying - * implementation and its configuration, this will be heap-based or direct buffer. - * - * @return the allocated buffer - */ - DataBuffer allocateBuffer(); - - /** - * Allocate a data buffer of the given initial capacity. Depending on the underlying - * implementation and its configuration, this will be heap-based or direct buffer. - * - * @param initialCapacity the initial capacity of the buffer to allocate - * @return the allocated buffer - */ - DataBuffer allocateBuffer(int initialCapacity); - - /** - * Wrap the given {@link ByteBuffer} in a {@code DataBuffer}. Unlike {@linkplain - * #allocateBuffer(int) allocating}, wrapping does not use new memory. - * - * @param byteBuffer the NIO byte buffer to wrap - * @return the wrapped buffer - */ - DataBuffer wrap(ByteBuffer byteBuffer); - - /** - * Wrap the given {@code byte} array in a {@code DataBuffer}. Unlike {@linkplain - * #allocateBuffer(int) allocating}, wrapping does not use new memory. - * - * @param bytes the byte array to wrap - * @return the wrapped buffer - */ - DataBuffer wrap(byte[] bytes); - - /** - * Wrap the given {@code byte} array in a {@code DataBuffer}. Unlike {@linkplain - * #allocateBuffer(int) allocating}, wrapping does not use new memory. - * - * @param bytes the byte array to wrap - * @return the wrapped buffer - */ - DataBuffer wrap(byte[] bytes, int offset, int length); - - /** - * Return a new {@code DataBuffer} composed of the {@code dataBuffers} elements joined together. - * Depending on the implementation, the returned buffer may be a single buffer containing all data - * of the provided buffers, or it may be a true composite that contains references to the buffers. - * - *

Note that the given data buffers do not have to be released, as they are - * released as part of the returned composite. - * - * @param dataBuffers the data buffers to be composed - * @return a buffer that is composed of the {@code dataBuffers} argument - * @since 5.0.3 - */ - DataBuffer join(List dataBuffers); - - /** - * Indicates whether this factory allocates direct buffers (i.e. non-heap, native memory). - * - * @return {@code true} if this factory allocates direct buffers; {@code false} otherwise - * @since 6.0 - */ - boolean isDirect(); -} diff --git a/jooby/src/main/java/io/jooby/buffer/DataBufferInputStream.java b/jooby/src/main/java/io/jooby/buffer/DataBufferInputStream.java deleted file mode 100644 index 16087fd7fe..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/DataBufferInputStream.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.io.IOException; -import java.io.InputStream; - -/** - * An {@link InputStream} that reads from a {@link DataBuffer}. - * - * @author Arjen Poutsma - * @since 6.0 - * @see DataBuffer#asInputStream(boolean) - */ -final class DataBufferInputStream extends InputStream { - - private final DataBuffer dataBuffer; - - private final int end; - - private final boolean releaseOnClose; - - private boolean closed; - - private int mark; - - public DataBufferInputStream(DataBuffer dataBuffer, boolean releaseOnClose) { - Assert.notNull(dataBuffer, "DataBuffer must not be null"); - this.dataBuffer = dataBuffer; - int start = this.dataBuffer.readPosition(); - this.end = start + this.dataBuffer.readableByteCount(); - this.mark = start; - this.releaseOnClose = releaseOnClose; - } - - @Override - public int read() throws IOException { - checkClosed(); - if (available() == 0) { - return -1; - } - return this.dataBuffer.read() & 0xFF; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - checkClosed(); - int available = available(); - if (available == 0) { - return -1; - } - len = Math.min(available, len); - this.dataBuffer.read(b, off, len); - return len; - } - - @Override - public boolean markSupported() { - return true; - } - - @Override - public void mark(int readLimit) { - Assert.isTrue(readLimit > 0, "readLimit must be greater than 0"); - this.mark = this.dataBuffer.readPosition(); - } - - @Override - public int available() { - return Math.max(0, this.end - this.dataBuffer.readPosition()); - } - - @Override - public void reset() { - this.dataBuffer.readPosition(this.mark); - } - - @Override - public void close() { - if (this.closed) { - return; - } - if (this.releaseOnClose) { - DataBufferUtils.release(this.dataBuffer); - } - this.closed = true; - } - - private void checkClosed() throws IOException { - if (this.closed) { - throw new IOException("DataBufferInputStream is closed"); - } - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/DataBufferLimitException.java b/jooby/src/main/java/io/jooby/buffer/DataBufferLimitException.java deleted file mode 100644 index 3a12e25adb..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/DataBufferLimitException.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -/** - * Exception that indicates the cumulative number of bytes consumed from a stream of {@link - * DataBuffer DataBuffer}'s exceeded some pre-configured limit. This can be raised when data buffers - * are cached and aggregated, e.g. {@link DataBufferUtils#join}. Or it could also be raised when - * data buffers have been released but a parsed representation is being aggregated, e.g. async - * parsing with Jackson, SSE parsing and aggregating lines per event. - * - * @author Rossen Stoyanchev - * @since 5.1.11 - */ -@SuppressWarnings("serial") -public class DataBufferLimitException extends IllegalStateException { - - public DataBufferLimitException(String message) { - super(message); - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/DataBufferOutputStream.java b/jooby/src/main/java/io/jooby/buffer/DataBufferOutputStream.java deleted file mode 100644 index 2bed80725e..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/DataBufferOutputStream.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * An {@link OutputStream} that writes to a {@link DataBuffer}. - * - * @author Arjen Poutsma - * @since 6.0 - * @see DataBuffer#asOutputStream() - */ -final class DataBufferOutputStream extends OutputStream { - - private final DataBuffer dataBuffer; - - private boolean closed; - - public DataBufferOutputStream(DataBuffer dataBuffer) { - Assert.notNull(dataBuffer, "DataBuffer must not be null"); - this.dataBuffer = dataBuffer; - } - - @Override - public void write(int b) throws IOException { - checkClosed(); - this.dataBuffer.ensureWritable(1); - this.dataBuffer.write((byte) b); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - checkClosed(); - if (len > 0) { - this.dataBuffer.ensureWritable(len); - this.dataBuffer.write(b, off, len); - } - } - - @Override - public void close() { - if (this.closed) { - return; - } - this.closed = true; - } - - private void checkClosed() throws IOException { - if (this.closed) { - throw new IOException("DataBufferOutputStream is closed"); - } - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/DataBufferUtils.java b/jooby/src/main/java/io/jooby/buffer/DataBufferUtils.java deleted file mode 100644 index 8e4f0f8559..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/DataBufferUtils.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.io.OutputStream; -import java.util.concurrent.Executor; -import java.util.concurrent.Flow; -import java.util.function.Consumer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import edu.umd.cs.findbugs.annotations.Nullable; - -public class DataBufferUtils { - private static final Logger logger = LoggerFactory.getLogger(DataBufferUtils.class); - private static final Consumer RELEASE_CONSUMER = DataBufferUtils::release; - private static final int DEFAULT_CHUNK_SIZE = 1024; - - /** - * Release the given data buffer. If it is a {@link PooledDataBuffer} and has been {@linkplain - * PooledDataBuffer#isAllocated() allocated}, this method will call {@link - * PooledDataBuffer#release()}. If it is a {@link CloseableDataBuffer}, this method will call - * {@link CloseableDataBuffer#close()}. - * - * @param dataBuffer the data buffer to release - * @return {@code true} if the buffer was released; {@code false} otherwise. - */ - public static boolean release(@Nullable DataBuffer dataBuffer) { - if (dataBuffer instanceof PooledDataBuffer pooledDataBuffer) { - if (pooledDataBuffer.isAllocated()) { - try { - return pooledDataBuffer.release(); - } catch (IllegalStateException ex) { - if (logger.isDebugEnabled()) { - logger.debug("Failed to release PooledDataBuffer: " + dataBuffer, ex); - } - return false; - } - } - } else if (dataBuffer instanceof CloseableDataBuffer closeableDataBuffer) { - try { - closeableDataBuffer.close(); - return true; - } catch (IllegalStateException ex) { - if (logger.isDebugEnabled()) { - logger.debug("Failed to release CloseableDataBuffer " + dataBuffer, ex); - } - return false; - } - } - return false; - } - - /** - * Retain the given data buffer, if it is a {@link PooledDataBuffer}. - * - * @param dataBuffer the data buffer to retain - * @return the retained buffer - */ - @SuppressWarnings("unchecked") - public static T retain(T dataBuffer) { - if (dataBuffer instanceof PooledDataBuffer pooledDataBuffer) { - return (T) pooledDataBuffer.retain(); - } else { - return dataBuffer; - } - } - - /** - * Associate the given hint with the data buffer if it is a pooled buffer and supports leak - * tracking. - * - * @param dataBuffer the data buffer to attach the hint to - * @param hint the hint to attach - * @return the input buffer - * @since 5.3.2 - */ - @SuppressWarnings("unchecked") - public static T touch(T dataBuffer, Object hint) { - if (dataBuffer instanceof TouchableDataBuffer touchableDataBuffer) { - return (T) touchableDataBuffer.touch(hint); - } else { - return dataBuffer; - } - } - - /** - * Create a new {@code Publisher} based on bytes written to a {@code OutputStream}. - * - *

    - *
  • The parameter {@code outputStreamConsumer} is invoked once per subscription of the - * returned {@code Publisher}, when the first item is {@linkplain - * Flow.Subscription#request(long) requested}. - *
  • {@link OutputStream#write(byte[], int, int) OutputStream.write()} invocations made by - * {@code outputStreamConsumer} are buffered until they exceed the default chunk size of - * 1024, or when the stream is {@linkplain OutputStream#flush() flushed} and then result in - * a {@linkplain Flow.Subscriber#onNext(Object) published} item if there is {@linkplain - * Flow.Subscription#request(long) demand}. - *
  • If there is no demand, {@code OutputStream.write()} will block until there is. - *
  • If the subscription is {@linkplain Flow.Subscription#cancel() cancelled}, {@code - * OutputStream.write()} will throw a {@code IOException}. - *
  • The subscription is {@linkplain Flow.Subscriber#onComplete() completed} when {@code - * outputStreamHandler} completes. - *
  • Any exceptions thrown from {@code outputStreamHandler} will be dispatched to the - * {@linkplain Flow.Subscriber#onError(Throwable) Subscriber}. - *
- * - * @param outputStreamConsumer invoked when the first buffer is requested - * @param executor used to invoke the {@code outputStreamHandler} - * @return a {@code Publisher} based on bytes written by {@code outputStreamHandler} - * @since 6.1 - */ - public static Flow.Publisher outputStreamPublisher( - Consumer outputStreamConsumer, - DataBufferFactory bufferFactory, - Executor executor) { - - return outputStreamPublisher(outputStreamConsumer, bufferFactory, executor, DEFAULT_CHUNK_SIZE); - } - - /** - * Creates a new {@code Publisher} based on bytes written to a {@code OutputStream}. - * - *
    - *
  • The parameter {@code outputStreamConsumer} is invoked once per subscription of the - * returned {@code Publisher}, when the first item is {@linkplain - * Flow.Subscription#request(long) requested}. - *
  • {@link OutputStream#write(byte[], int, int) OutputStream.write()} invocations made by - * {@code outputStreamHandler} are buffered until they reach or exceed {@code chunkSize}, or - * when the stream is {@linkplain OutputStream#flush() flushed} and then result in a - * {@linkplain Flow.Subscriber#onNext(Object) published} item if there is {@linkplain - * Flow.Subscription#request(long) demand}. - *
  • If there is no demand, {@code OutputStream.write()} will block until there is. - *
  • If the subscription is {@linkplain Flow.Subscription#cancel() cancelled}, {@code - * OutputStream.write()} will throw a {@code IOException}. - *
  • The subscription is {@linkplain Flow.Subscriber#onComplete() completed} when {@code - * outputStreamHandler} completes. - *
  • Any exceptions thrown from {@code outputStreamHandler} will be dispatched to the - * {@linkplain Flow.Subscriber#onError(Throwable) Subscriber}. - *
- * - * @param outputStreamConsumer invoked when the first buffer is requested - * @param executor used to invoke the {@code outputStreamHandler} - * @param chunkSize minimum size of the buffer produced by the publisher - * @return a {@code Publisher} based on bytes written by {@code outputStreamHandler} - * @since 6.1 - */ - public static Flow.Publisher outputStreamPublisher( - Consumer outputStreamConsumer, - DataBufferFactory bufferFactory, - Executor executor, - int chunkSize) { - - Assert.notNull(outputStreamConsumer, "OutputStreamConsumer must not be null"); - Assert.notNull(bufferFactory, "BufferFactory must not be null"); - Assert.notNull(executor, "Executor must not be null"); - Assert.isTrue(chunkSize > 0, "Chunk size must be > 0"); - - return new OutputStreamPublisher(outputStreamConsumer, bufferFactory, executor, chunkSize); - } - - /** Return a consumer that calls {@link #release(DataBuffer)} on all passed data buffers. */ - public static Consumer releaseConsumer() { - return RELEASE_CONSUMER; - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/DataBufferWrapper.java b/jooby/src/main/java/io/jooby/buffer/DataBufferWrapper.java deleted file mode 100644 index 789d60b7c2..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/DataBufferWrapper.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.function.IntPredicate; - -import io.jooby.Context; - -/** - * Provides a convenient implementation of the {@link DataBuffer} interface that can be overridden - * to adapt the delegate. - * - *

These methods default to calling through to the wrapped delegate object. - * - * @author Arjen Poutsma - * @since 5.2 - */ -public class DataBufferWrapper implements DataBuffer { - - private final DataBuffer delegate; - - /** - * Create a new {@code DataBufferWrapper} that wraps the given buffer. - * - * @param delegate the buffer to wrap - */ - public DataBufferWrapper(DataBuffer delegate) { - Assert.notNull(delegate, "Delegate must not be null"); - this.delegate = delegate; - } - - /** Return the wrapped delegate. */ - public DataBuffer dataBuffer() { - return this.delegate; - } - - @Override - public DataBufferFactory factory() { - return this.delegate.factory(); - } - - @Override - public int indexOf(IntPredicate predicate, int fromIndex) { - return this.delegate.indexOf(predicate, fromIndex); - } - - @Override - public int lastIndexOf(IntPredicate predicate, int fromIndex) { - return this.delegate.lastIndexOf(predicate, fromIndex); - } - - @Override - public int readableByteCount() { - return this.delegate.readableByteCount(); - } - - @Override - public int writableByteCount() { - return this.delegate.writableByteCount(); - } - - @Override - public int capacity() { - return this.delegate.capacity(); - } - - @Override - public DataBuffer duplicate() { - return new DataBufferWrapper(this.delegate.duplicate()); - } - - @Override - public DataBuffer ensureWritable(int capacity) { - this.delegate.ensureWritable(capacity); - return this; - } - - @Override - public int readPosition() { - return this.delegate.readPosition(); - } - - @Override - public DataBuffer readPosition(int readPosition) { - this.delegate.readPosition(readPosition); - return this; - } - - @Override - public int writePosition() { - return this.delegate.writePosition(); - } - - @Override - public DataBuffer writePosition(int writePosition) { - this.delegate.writePosition(writePosition); - return this; - } - - @Override - public byte getByte(int index) { - return this.delegate.getByte(index); - } - - @Override - public byte read() { - return this.delegate.read(); - } - - @Override - public DataBuffer read(byte[] destination) { - this.delegate.read(destination); - return this; - } - - @Override - public DataBuffer read(byte[] destination, int offset, int length) { - this.delegate.read(destination, offset, length); - return this; - } - - @Override - public DataBuffer write(byte b) { - this.delegate.write(b); - return this; - } - - @Override - public DataBuffer write(byte[] source) { - this.delegate.write(source); - return this; - } - - @Override - public DataBuffer write(byte[] source, int offset, int length) { - this.delegate.write(source, offset, length); - return this; - } - - @Override - public DataBuffer write(DataBuffer... buffers) { - this.delegate.write(buffers); - return this; - } - - @Override - public DataBuffer write(ByteBuffer... buffers) { - this.delegate.write(buffers); - return this; - } - - @Override - public DataBuffer write(CharSequence charSequence, Charset charset) { - this.delegate.write(charSequence, charset); - return this; - } - - @Override - public DataBuffer split(int index) { - this.delegate.split(index); - return this; - } - - @Override - public void toByteBuffer(ByteBuffer dest) { - this.delegate.toByteBuffer(dest); - } - - @Override - public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - this.delegate.toByteBuffer(srcPos, dest, destPos, length); - } - - @Override - public ByteBufferIterator readableByteBuffers() { - return this.delegate.readableByteBuffers(); - } - - @Override - public ByteBufferIterator writableByteBuffers() { - return this.delegate.writableByteBuffers(); - } - - @Override - public InputStream asInputStream() { - return this.delegate.asInputStream(); - } - - @Override - public InputStream asInputStream(boolean releaseOnClose) { - return this.delegate.asInputStream(releaseOnClose); - } - - @Override - public OutputStream asOutputStream() { - return this.delegate.asOutputStream(); - } - - @Override - public String toString(Charset charset) { - return this.delegate.toString(charset); - } - - @Override - public DataBuffer clear() { - delegate.clear(); - return this; - } - - @Override - public Context send(Context ctx) { - this.delegate.send(ctx); - return ctx; - } - - @Override - public String toString(int index, int length, Charset charset) { - return this.delegate.toString(index, length, charset); - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/DefaultDataBuffer.java b/jooby/src/main/java/io/jooby/buffer/DefaultDataBuffer.java deleted file mode 100644 index 2267479f12..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/DefaultDataBuffer.java +++ /dev/null @@ -1,513 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.NoSuchElementException; -import java.util.function.IntPredicate; - -import io.jooby.Context; - -/** - * Default implementation of the {@link DataBuffer} interface that uses a {@link ByteBuffer} - * internally. with separate read and write positions. Constructed using the {@link - * DefaultDataBufferFactory}. - * - *

Inspired by Netty's {@code ByteBuf}. Introduced so that non-Netty runtimes (i.e. Servlet) do - * not require Netty on the classpath. - * - * @author Arjen Poutsma - * @author Juergen Hoeller - * @author Brian Clozel - * @since 5.0 - * @see DefaultDataBufferFactory - */ -public class DefaultDataBuffer implements DataBuffer { - - private static final int MAX_CAPACITY = Integer.MAX_VALUE; - - private static final int CAPACITY_THRESHOLD = 1024 * 1024 * 4; - - private final DefaultDataBufferFactory dataBufferFactory; - - private ByteBuffer byteBuffer; - - private int capacity; - - private int readPosition; - - private int writePosition; - - private DefaultDataBuffer(DefaultDataBufferFactory dataBufferFactory, ByteBuffer byteBuffer) { - Assert.notNull(dataBufferFactory, "DefaultDataBufferFactory must not be null"); - Assert.notNull(byteBuffer, "ByteBuffer must not be null"); - this.dataBufferFactory = dataBufferFactory; - ByteBuffer slice = byteBuffer.slice(); - this.byteBuffer = slice; - this.capacity = slice.remaining(); - } - - static DefaultDataBuffer fromFilledByteBuffer( - DefaultDataBufferFactory dataBufferFactory, ByteBuffer byteBuffer) { - DefaultDataBuffer dataBuffer = new DefaultDataBuffer(dataBufferFactory, byteBuffer); - dataBuffer.writePosition(byteBuffer.remaining()); - return dataBuffer; - } - - static DefaultDataBuffer fromEmptyByteBuffer( - DefaultDataBufferFactory dataBufferFactory, ByteBuffer byteBuffer) { - return new DefaultDataBuffer(dataBufferFactory, byteBuffer); - } - - /** - * Directly exposes the native {@code ByteBuffer} that this buffer is based on. The {@linkplain - * ByteBuffer#position() position} of the returned {@code ByteBuffer} is set to the {@linkplain - * #readPosition() read position}, and the {@linkplain ByteBuffer#limit()} to the {@linkplain - * #writePosition() write position}. - * - * @return the wrapped byte buffer - */ - public ByteBuffer getNativeBuffer() { - return this.byteBuffer.duplicate().position(this.readPosition).limit(this.writePosition); - } - - private void setNativeBuffer(ByteBuffer byteBuffer) { - this.byteBuffer = byteBuffer; - this.capacity = byteBuffer.capacity(); - } - - @Override - public DefaultDataBufferFactory factory() { - return this.dataBufferFactory; - } - - @Override - public int indexOf(IntPredicate predicate, int fromIndex) { - Assert.notNull(predicate, "IntPredicate must not be null"); - if (fromIndex < 0) { - fromIndex = 0; - } else if (fromIndex >= this.writePosition) { - return -1; - } - for (int i = fromIndex; i < this.writePosition; i++) { - byte b = this.byteBuffer.get(i); - if (predicate.test(b)) { - return i; - } - } - return -1; - } - - @Override - public int lastIndexOf(IntPredicate predicate, int fromIndex) { - Assert.notNull(predicate, "IntPredicate must not be null"); - int i = Math.min(fromIndex, this.writePosition - 1); - for (; i >= 0; i--) { - byte b = this.byteBuffer.get(i); - if (predicate.test(b)) { - return i; - } - } - return -1; - } - - @Override - public int readableByteCount() { - return this.writePosition - this.readPosition; - } - - @Override - public int writableByteCount() { - return this.capacity - this.writePosition; - } - - @Override - public int readPosition() { - return this.readPosition; - } - - @Override - public DefaultDataBuffer readPosition(int readPosition) { - assertIndex(readPosition >= 0, "'readPosition' %d must be >= 0", readPosition); - assertIndex( - readPosition <= this.writePosition, - "'readPosition' %d must be <= %d", - readPosition, - this.writePosition); - this.readPosition = readPosition; - return this; - } - - @Override - public int writePosition() { - return this.writePosition; - } - - @Override - public DefaultDataBuffer writePosition(int writePosition) { - assertIndex( - writePosition >= this.readPosition, - "'writePosition' %d must be >= %d", - writePosition, - this.readPosition); - assertIndex( - writePosition <= this.capacity, - "'writePosition' %d must be <= %d", - writePosition, - this.capacity); - this.writePosition = writePosition; - return this; - } - - @Override - public int capacity() { - return this.capacity; - } - - @Override - public DataBuffer duplicate() { - return new DefaultDataBuffer(this.dataBufferFactory, byteBuffer.duplicate()); - } - - private void setCapacity(int newCapacity) { - if (newCapacity < 0) { - throw new IllegalArgumentException( - String.format("'newCapacity' %d must be 0 or higher", newCapacity)); - } - int readPosition = readPosition(); - int writePosition = writePosition(); - int oldCapacity = capacity(); - - if (newCapacity > oldCapacity) { - ByteBuffer oldBuffer = this.byteBuffer; - ByteBuffer newBuffer = allocate(newCapacity, oldBuffer.isDirect()); - oldBuffer.position(0).limit(oldBuffer.capacity()); - newBuffer.position(0).limit(oldBuffer.capacity()); - newBuffer.put(oldBuffer); - newBuffer.clear(); - setNativeBuffer(newBuffer); - } else if (newCapacity < oldCapacity) { - ByteBuffer oldBuffer = this.byteBuffer; - ByteBuffer newBuffer = allocate(newCapacity, oldBuffer.isDirect()); - if (readPosition < newCapacity) { - if (writePosition > newCapacity) { - writePosition = newCapacity; - writePosition(writePosition); - } - oldBuffer.position(readPosition).limit(writePosition); - newBuffer.position(readPosition).limit(writePosition); - newBuffer.put(oldBuffer); - newBuffer.clear(); - } else { - readPosition(newCapacity); - writePosition(newCapacity); - } - setNativeBuffer(newBuffer); - } - } - - @Override - public DataBuffer ensureWritable(int length) { - if (length > writableByteCount()) { - int newCapacity = calculateCapacity(this.writePosition + length); - setCapacity(newCapacity); - } - return this; - } - - private static ByteBuffer allocate(int capacity, boolean direct) { - return (direct ? ByteBuffer.allocateDirect(capacity) : ByteBuffer.allocate(capacity)); - } - - @Override - public byte getByte(int index) { - assertIndex(index >= 0, "index %d must be >= 0", index); - assertIndex( - index <= this.writePosition - 1, "index %d must be <= %d", index, this.writePosition - 1); - return this.byteBuffer.get(index); - } - - @Override - public byte read() { - assertIndex( - this.readPosition <= this.writePosition - 1, - "readPosition %d must be <= %d", - this.readPosition, - this.writePosition - 1); - int pos = this.readPosition; - byte b = this.byteBuffer.get(pos); - this.readPosition = pos + 1; - return b; - } - - @Override - public DefaultDataBuffer read(byte[] destination) { - Assert.notNull(destination, "Byte array must not be null"); - read(destination, 0, destination.length); - return this; - } - - @Override - public DefaultDataBuffer read(byte[] destination, int offset, int length) { - Assert.notNull(destination, "Byte array must not be null"); - assertIndex( - this.readPosition <= this.writePosition - length, - "readPosition %d and length %d should be smaller than writePosition %d", - this.readPosition, - length, - this.writePosition); - - ByteBuffer tmp = this.byteBuffer.duplicate(); - int limit = this.readPosition + length; - tmp.clear().position(this.readPosition).limit(limit); - tmp.get(destination, offset, length); - - this.readPosition += length; - return this; - } - - @Override - public DefaultDataBuffer write(byte b) { - ensureWritable(1); - int pos = this.writePosition; - this.byteBuffer.put(pos, b); - this.writePosition = pos + 1; - return this; - } - - @Override - public DefaultDataBuffer write(byte[] source) { - Assert.notNull(source, "Byte array must not be null"); - write(source, 0, source.length); - return this; - } - - @Override - public DefaultDataBuffer write(byte[] source, int offset, int length) { - Assert.notNull(source, "Byte array must not be null"); - ensureWritable(length); - - ByteBuffer tmp = this.byteBuffer.duplicate(); - int limit = this.writePosition + length; - tmp.clear().position(this.writePosition).limit(limit); - tmp.put(source, offset, length); - - this.writePosition += length; - return this; - } - - @Override - public DefaultDataBuffer write(DataBuffer... dataBuffers) { - if (!ObjectUtils.isEmpty(dataBuffers)) { - ByteBuffer[] byteBuffers = new ByteBuffer[dataBuffers.length]; - for (int i = 0; i < dataBuffers.length; i++) { - byteBuffers[i] = ByteBuffer.allocate(dataBuffers[i].readableByteCount()); - dataBuffers[i].toByteBuffer(byteBuffers[i]); - } - write(byteBuffers); - } - return this; - } - - @Override - public DefaultDataBuffer write(ByteBuffer... buffers) { - if (!ObjectUtils.isEmpty(buffers)) { - int capacity = Arrays.stream(buffers).mapToInt(ByteBuffer::remaining).sum(); - ensureWritable(capacity); - Arrays.stream(buffers).forEach(this::write); - } - return this; - } - - private void write(ByteBuffer source) { - int length = source.remaining(); - ByteBuffer tmp = this.byteBuffer.duplicate(); - int limit = this.writePosition + source.remaining(); - tmp.clear().position(this.writePosition).limit(limit); - tmp.put(source); - this.writePosition += length; - } - - @Override - public DataBuffer split(int index) { - checkIndex(index); - - ByteBuffer split = this.byteBuffer.duplicate().clear().position(0).limit(index).slice(); - - DefaultDataBuffer result = new DefaultDataBuffer(this.dataBufferFactory, split); - result.writePosition = Math.min(this.writePosition, index); - result.readPosition = Math.min(this.readPosition, index); - - this.byteBuffer = - this.byteBuffer - .duplicate() - .clear() - .position(index) - .limit(this.byteBuffer.capacity()) - .slice(); - this.writePosition = Math.max(this.writePosition, index) - index; - this.readPosition = Math.max(this.readPosition, index) - index; - this.capacity = this.byteBuffer.capacity(); - - return result; - } - - @Override - public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - checkIndex(srcPos, length); - Assert.notNull(dest, "Dest must not be null"); - - dest = dest.duplicate().clear(); - dest.put(destPos, this.byteBuffer, srcPos, length); - } - - @Override - public DataBuffer.ByteBufferIterator readableByteBuffers() { - ByteBuffer readOnly = - this.byteBuffer.slice(this.readPosition, readableByteCount()).asReadOnlyBuffer(); - return new ByteBufferIterator(readOnly); - } - - @Override - public DataBuffer.ByteBufferIterator writableByteBuffers() { - ByteBuffer slice = this.byteBuffer.slice(this.writePosition, writableByteCount()); - return new ByteBufferIterator(slice); - } - - @Override - public String toString(int index, int length, Charset charset) { - checkIndex(index, length); - Assert.notNull(charset, "Charset must not be null"); - - byte[] bytes; - int offset; - - if (this.byteBuffer.hasArray()) { - bytes = this.byteBuffer.array(); - offset = this.byteBuffer.arrayOffset() + index; - } else { - bytes = new byte[length]; - offset = 0; - ByteBuffer duplicate = this.byteBuffer.duplicate(); - duplicate.clear().position(index).limit(index + length); - duplicate.get(bytes, 0, length); - } - return new String(bytes, offset, length, charset); - } - - /** - * Calculate the capacity of the buffer. - * - * @see io.netty.buffer.AbstractByteBufAllocator#calculateNewCapacity(int, int) - */ - private int calculateCapacity(int neededCapacity) { - Assert.isTrue(neededCapacity >= 0, "'neededCapacity' must be >= 0"); - - if (neededCapacity == CAPACITY_THRESHOLD) { - return CAPACITY_THRESHOLD; - } else if (neededCapacity > CAPACITY_THRESHOLD) { - int newCapacity = neededCapacity / CAPACITY_THRESHOLD * CAPACITY_THRESHOLD; - if (newCapacity > MAX_CAPACITY - CAPACITY_THRESHOLD) { - newCapacity = MAX_CAPACITY; - } else { - newCapacity += CAPACITY_THRESHOLD; - } - return newCapacity; - } else { - int newCapacity = 64; - while (newCapacity < neededCapacity) { - newCapacity <<= 1; - } - return Math.min(newCapacity, MAX_CAPACITY); - } - } - - @Override - public boolean equals(Object other) { - return (this == other - || (other instanceof DefaultDataBuffer that - && this.readPosition == that.readPosition - && this.writePosition == that.writePosition - && this.byteBuffer.equals(that.byteBuffer))); - } - - @Override - public int hashCode() { - return this.byteBuffer.hashCode(); - } - - @Override - public String toString() { - return String.format( - "DefaultDataBuffer (r: %d, w: %d, c: %d)", - this.readPosition, this.writePosition, this.capacity); - } - - private void checkIndex(int index, int length) { - checkIndex(index); - checkLength(length); - } - - private void checkIndex(int index) { - assertIndex(index >= 0, "index %d must be >= 0", index); - assertIndex(index <= this.capacity, "index %d must be <= %d", index, this.capacity); - } - - private void checkLength(int length) { - assertIndex(length >= 0, "length %d must be >= 0", length); - assertIndex(length <= this.capacity, "length %d must be <= %d", length, this.capacity); - } - - private void assertIndex(boolean expression, String format, Object... args) { - if (!expression) { - String message = String.format(format, args); - throw new IndexOutOfBoundsException(message); - } - } - - @Override - public DataBuffer clear() { - this.byteBuffer.clear(); - return this; - } - - @Override - public Context send(Context ctx) { - ctx.send(this.byteBuffer.slice(this.readPosition, readableByteCount())); - return ctx; - } - - private static final class ByteBufferIterator implements DataBuffer.ByteBufferIterator { - - private final ByteBuffer buffer; - - private boolean hasNext = true; - - public ByteBufferIterator(ByteBuffer buffer) { - this.buffer = buffer; - } - - @Override - public boolean hasNext() { - return this.hasNext; - } - - @Override - public ByteBuffer next() { - if (!this.hasNext) { - throw new NoSuchElementException(); - } else { - this.hasNext = false; - return this.buffer; - } - } - - @Override - public void close() {} - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/DefaultDataBufferFactory.java b/jooby/src/main/java/io/jooby/buffer/DefaultDataBufferFactory.java deleted file mode 100644 index 016818b9db..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/DefaultDataBufferFactory.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.nio.ByteBuffer; -import java.util.List; - -/** - * Default implementation of the {@code DataBufferFactory} interface. Allows for specification of - * the default initial capacity at construction time, as well as whether heap-based or direct - * buffers are to be preferred. - * - * @author Arjen Poutsma - * @since 5.0 - */ -public class DefaultDataBufferFactory implements DataBufferFactory { - - /** - * The default capacity when none is specified. - * - * @see #DefaultDataBufferFactory() - * @see #DefaultDataBufferFactory(boolean) - */ - public static final int DEFAULT_INITIAL_CAPACITY = 1024; - - /** - * Shared instance based on the default constructor. - * - * @since 5.3 - */ - public static final DefaultDataBufferFactory sharedInstance = new DefaultDataBufferFactory(); - - private final boolean preferDirect; - - private int defaultInitialCapacity; - - /** - * Creates a new {@code DefaultDataBufferFactory} with default settings. - * - * @see #sharedInstance - */ - public DefaultDataBufferFactory() { - this(false); - } - - /** - * Creates a new {@code DefaultDataBufferFactory}, indicating whether direct buffers should be - * created by {@link #allocateBuffer()} and {@link #allocateBuffer(int)}. - * - * @param preferDirect {@code true} if direct buffers are to be preferred; {@code false} otherwise - */ - public DefaultDataBufferFactory(boolean preferDirect) { - this(preferDirect, DEFAULT_INITIAL_CAPACITY); - } - - /** - * Creates a new {@code DefaultDataBufferFactory}, indicating whether direct buffers should be - * created by {@link #allocateBuffer()} and {@link #allocateBuffer(int)}, and what the capacity is - * to be used for {@link #allocateBuffer()}. - * - * @param preferDirect {@code true} if direct buffers are to be preferred; {@code false} otherwise - */ - public DefaultDataBufferFactory(boolean preferDirect, int defaultInitialCapacity) { - Assert.isTrue(defaultInitialCapacity > 0, "'defaultInitialCapacity' should be larger than 0"); - this.preferDirect = preferDirect; - this.defaultInitialCapacity = defaultInitialCapacity; - } - - @Override - public int getDefaultInitialCapacity() { - return defaultInitialCapacity; - } - - @Override - public DataBufferFactory setDefaultInitialCapacity(int defaultInitialCapacity) { - this.defaultInitialCapacity = defaultInitialCapacity; - return this; - } - - @Override - public DefaultDataBuffer allocateBuffer() { - return allocateBuffer(this.defaultInitialCapacity); - } - - @Override - public DefaultDataBuffer allocateBuffer(int initialCapacity) { - ByteBuffer byteBuffer = - (this.preferDirect - ? ByteBuffer.allocateDirect(initialCapacity) - : ByteBuffer.allocate(initialCapacity)); - return DefaultDataBuffer.fromEmptyByteBuffer(this, byteBuffer); - } - - @Override - public DefaultDataBuffer wrap(ByteBuffer byteBuffer) { - return DefaultDataBuffer.fromFilledByteBuffer(this, byteBuffer.slice()); - } - - @Override - public DefaultDataBuffer wrap(byte[] bytes) { - return wrap(bytes, 0, bytes.length); - } - - @Override - public DefaultDataBuffer wrap(byte[] bytes, int offset, int length) { - return DefaultDataBuffer.fromFilledByteBuffer(this, ByteBuffer.wrap(bytes, offset, length)); - } - - /** - * {@inheritDoc} - * - *

This implementation creates a single {@link DefaultDataBuffer} to contain the data in {@code - * dataBuffers}. - */ - @Override - public DefaultDataBuffer join(List dataBuffers) { - Assert.notEmpty(dataBuffers, "DataBuffer List must not be empty"); - int capacity = dataBuffers.stream().mapToInt(DataBuffer::readableByteCount).sum(); - DefaultDataBuffer result = allocateBuffer(capacity); - dataBuffers.forEach(result::write); - dataBuffers.forEach(DataBufferUtils::release); - return result; - } - - @Override - public boolean isDirect() { - return this.preferDirect; - } - - @Override - public String toString() { - return "DefaultDataBufferFactory (preferDirect=" + this.preferDirect + ")"; - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/LimitedDataBufferList.java b/jooby/src/main/java/io/jooby/buffer/LimitedDataBufferList.java deleted file mode 100644 index 29d2d89a53..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/LimitedDataBufferList.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.function.Predicate; - -/** - * Custom {@link List} to collect data buffers with and enforce a limit on the total number of bytes - * buffered. For use with "collect" or other buffering operators in declarative APIs, e.g. {@link - * Flux}. - * - *

Adding elements increases the byte count and if the limit is exceeded, {@link - * DataBufferLimitException} is raised. {@link #clear()} resets the count. Remove and set are not - * supported. - * - *

Note: This class does not automatically release the buffers it contains. It - * is usually preferable to use hooks such as {@link Flux#doOnDiscard} that also take care of cancel - * and error signals, or otherwise {@link #releaseAndClear()} can be used. - * - * @author Rossen Stoyanchev - * @since 5.1.11 - */ -@SuppressWarnings("serial") -public class LimitedDataBufferList extends ArrayList { - - private final int maxByteCount; - - private int byteCount; - - public LimitedDataBufferList(int maxByteCount) { - this.maxByteCount = maxByteCount; - } - - @Override - public boolean add(DataBuffer buffer) { - updateCount(buffer.readableByteCount()); - return super.add(buffer); - } - - @Override - public void add(int index, DataBuffer buffer) { - super.add(index, buffer); - updateCount(buffer.readableByteCount()); - } - - @Override - public boolean addAll(Collection collection) { - boolean result = super.addAll(collection); - collection.forEach(buffer -> updateCount(buffer.readableByteCount())); - return result; - } - - @Override - public boolean addAll(int index, Collection collection) { - boolean result = super.addAll(index, collection); - collection.forEach(buffer -> updateCount(buffer.readableByteCount())); - return result; - } - - private void updateCount(int bytesToAdd) { - if (this.maxByteCount < 0) { - return; - } - if (bytesToAdd > Integer.MAX_VALUE - this.byteCount) { - raiseLimitException(); - } else { - this.byteCount += bytesToAdd; - if (this.byteCount > this.maxByteCount) { - raiseLimitException(); - } - } - } - - private void raiseLimitException() { - // Do not release here, it's likely done via doOnDiscard - throw new DataBufferLimitException( - "Exceeded limit on max bytes to buffer : " + this.maxByteCount); - } - - @Override - public DataBuffer remove(int index) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean remove(Object o) { - throw new UnsupportedOperationException(); - } - - @Override - protected void removeRange(int fromIndex, int toIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeAll(Collection c) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeIf(Predicate filter) { - throw new UnsupportedOperationException(); - } - - @Override - public DataBuffer set(int index, DataBuffer element) { - throw new UnsupportedOperationException(); - } - - @Override - public void clear() { - this.byteCount = 0; - super.clear(); - } - - /** - * Shortcut to {@link DataBufferUtils#release release} all data buffers and then {@link #clear()}. - */ - public void releaseAndClear() { - forEach( - buf -> { - try { - DataBufferUtils.release(buf); - } catch (Throwable ex) { - // Keep going.. - } - }); - clear(); - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/ObjectUtils.java b/jooby/src/main/java/io/jooby/buffer/ObjectUtils.java deleted file mode 100644 index 5bf1834370..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/ObjectUtils.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.util.List; - -class ObjectUtils { - - public static boolean isEmpty(Object[] array) { - return (array == null || array.length == 0); - } - - public static boolean isEmpty(List list) { - return (list == null || list.isEmpty()); - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/OutputStreamPublisher.java b/jooby/src/main/java/io/jooby/buffer/OutputStreamPublisher.java deleted file mode 100644 index 989471ec3b..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/OutputStreamPublisher.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Objects; -import java.util.concurrent.Executor; -import java.util.concurrent.Flow; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.LockSupport; -import java.util.function.Consumer; - -/** - * Bridges between {@link OutputStream} and {@link java.util.concurrent.Flow.Publisher - * Publisher<DataBuffer>}. - * - * @author Oleh Dokuka - * @author Arjen Poutsma - * @since 6.1 - */ -final class OutputStreamPublisher implements Flow.Publisher { - - private final Consumer outputStreamConsumer; - - private final DataBufferFactory bufferFactory; - - private final Executor executor; - - private final int chunkSize; - - OutputStreamPublisher( - Consumer outputStreamConsumer, - DataBufferFactory bufferFactory, - Executor executor, - int chunkSize) { - - this.outputStreamConsumer = outputStreamConsumer; - this.bufferFactory = bufferFactory; - this.executor = executor; - this.chunkSize = chunkSize; - } - - @Override - public void subscribe(Flow.Subscriber subscriber) { - // We don't use Assert.notNull(), because a NullPointerException is required - // for Reactive Streams compliance. - Objects.requireNonNull(subscriber, "Subscriber must not be null"); - - OutputStreamSubscription subscription = - new OutputStreamSubscription( - subscriber, this.outputStreamConsumer, this.bufferFactory, this.chunkSize); - - subscriber.onSubscribe(subscription); - this.executor.execute(subscription::invokeHandler); - } - - private static final class OutputStreamSubscription extends OutputStream - implements Flow.Subscription { - - private static final Object READY = new Object(); - - private final Flow.Subscriber actual; - - private final Consumer outputStreamHandler; - - private final DataBufferFactory bufferFactory; - - private final int chunkSize; - - private final AtomicLong requested = new AtomicLong(); - - private final AtomicReference parkedThread = new AtomicReference<>(); - - private volatile Throwable error; - - private long produced; - - OutputStreamSubscription( - Flow.Subscriber actual, - Consumer outputStreamConsumer, - DataBufferFactory bufferFactory, - int chunkSize) { - - this.actual = actual; - this.outputStreamHandler = outputStreamConsumer; - this.bufferFactory = bufferFactory; - this.chunkSize = chunkSize; - } - - @Override - public void write(int b) throws IOException { - checkDemandAndAwaitIfNeeded(); - - DataBuffer next = this.bufferFactory.allocateBuffer(1); - next.write((byte) b); - - this.actual.onNext(next); - - this.produced++; - } - - @Override - public void write(byte[] b) throws IOException { - write(b, 0, b.length); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - checkDemandAndAwaitIfNeeded(); - - DataBuffer next = this.bufferFactory.allocateBuffer(len); - next.write(b, off, len); - - this.actual.onNext(next); - - this.produced++; - } - - private void checkDemandAndAwaitIfNeeded() throws IOException { - long r = this.requested.get(); - - if (isTerminated(r) || isCancelled(r)) { - throw new IOException("Subscription has been terminated"); - } - - long p = this.produced; - if (p == r) { - if (p > 0) { - r = tryProduce(p); - this.produced = 0; - } - - while (true) { - if (isTerminated(r) || isCancelled(r)) { - throw new IOException("Subscription has been terminated"); - } - - if (r != 0) { - return; - } - - await(); - - r = this.requested.get(); - } - } - } - - private void invokeHandler() { - // assume sync write within try-with-resource block - - // use BufferedOutputStream, so that written bytes are buffered - // before publishing as byte buffer - try (OutputStream outputStream = new BufferedOutputStream(this, this.chunkSize)) { - this.outputStreamHandler.accept(outputStream); - } catch (Exception ex) { - long previousState = tryTerminate(); - if (isCancelled(previousState)) { - return; - } - if (isTerminated(previousState)) { - // failure due to illegal requestN - Throwable error = this.error; - if (error != null) { - this.actual.onError(error); - return; - } - } - this.actual.onError(ex); - return; - } - - long previousState = tryTerminate(); - if (isCancelled(previousState)) { - return; - } - if (isTerminated(previousState)) { - // failure due to illegal requestN - Throwable error = this.error; - if (error != null) { - this.actual.onError(error); - return; - } - } - this.actual.onComplete(); - } - - @Override - public void request(long n) { - if (n <= 0) { - this.error = new IllegalArgumentException("request should be a positive number"); - long previousState = tryTerminate(); - if (isTerminated(previousState) || isCancelled(previousState)) { - return; - } - if (previousState > 0) { - // error should eventually be observed and propagated - return; - } - // resume parked thread, so it can observe error and propagate it - resume(); - return; - } - - if (addCap(n) == 0) { - // resume parked thread so it can continue the work - resume(); - } - } - - @Override - public void cancel() { - long previousState = tryCancel(); - if (isCancelled(previousState) || previousState > 0) { - return; - } - - // resume parked thread, so it can be unblocked and close all the resources - resume(); - } - - private void await() { - Thread toUnpark = Thread.currentThread(); - - while (true) { - Object current = this.parkedThread.get(); - if (current == READY) { - break; - } - - if (current != null && current != toUnpark) { - throw new IllegalStateException("Only one (Virtual)Thread can await!"); - } - - if (this.parkedThread.compareAndSet(null, toUnpark)) { - LockSupport.park(); - // we don't just break here because park() can wake up spuriously - // if we got a proper resume, get() == READY and the loop will quit above - } - } - // clear the resume indicator so that the next await call will park without a resume() - this.parkedThread.lazySet(null); - } - - private void resume() { - if (this.parkedThread.get() != READY) { - Object old = this.parkedThread.getAndSet(READY); - if (old != READY) { - LockSupport.unpark((Thread) old); - } - } - } - - private long tryCancel() { - while (true) { - long r = this.requested.get(); - if (isCancelled(r)) { - return r; - } - if (this.requested.compareAndSet(r, Long.MIN_VALUE)) { - return r; - } - } - } - - private long tryTerminate() { - while (true) { - long r = this.requested.get(); - if (isCancelled(r) || isTerminated(r)) { - return r; - } - if (this.requested.compareAndSet(r, Long.MIN_VALUE | Long.MAX_VALUE)) { - return r; - } - } - } - - private long tryProduce(long n) { - while (true) { - long current = this.requested.get(); - if (isTerminated(current) || isCancelled(current)) { - return current; - } - if (current == Long.MAX_VALUE) { - return Long.MAX_VALUE; - } - long update = current - n; - if (update < 0L) { - update = 0L; - } - if (this.requested.compareAndSet(current, update)) { - return update; - } - } - } - - private long addCap(long n) { - while (true) { - long r = this.requested.get(); - if (isTerminated(r) || isCancelled(r) || r == Long.MAX_VALUE) { - return r; - } - long u = addCap(r, n); - if (this.requested.compareAndSet(r, u)) { - return r; - } - } - } - - private static boolean isTerminated(long state) { - return state == (Long.MIN_VALUE | Long.MAX_VALUE); - } - - private static boolean isCancelled(long state) { - return state == Long.MIN_VALUE; - } - - private static long addCap(long a, long b) { - long res = a + b; - if (res < 0L) { - return Long.MAX_VALUE; - } - return res; - } - } -} diff --git a/jooby/src/main/java/io/jooby/buffer/PooledDataBuffer.java b/jooby/src/main/java/io/jooby/buffer/PooledDataBuffer.java deleted file mode 100644 index f71f8f90d7..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/PooledDataBuffer.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -/** - * Extension of {@link DataBuffer} that allows for buffers that share a memory pool. Introduces - * methods for reference counting. - * - * @author Arjen Poutsma - * @since 5.0 - */ -public interface PooledDataBuffer extends TouchableDataBuffer { - - /** - * Return {@code true} if this buffer is allocated; {@code false} if it has been deallocated. - * - * @since 5.1 - */ - boolean isAllocated(); - - /** - * Increase the reference count for this buffer by one. - * - * @return this buffer - */ - PooledDataBuffer retain(); - - /** - * Associate the given hint with the data buffer for debugging purposes. - * - * @return this buffer - * @since 5.3.2 - */ - @Override - PooledDataBuffer touch(Object hint); - - /** - * Decrease the reference count for this buffer by one, and deallocate it once the count reaches - * zero. - * - * @return {@code true} if the buffer was deallocated; {@code false} otherwise - */ - boolean release(); -} diff --git a/jooby/src/main/java/io/jooby/buffer/README b/jooby/src/main/java/io/jooby/buffer/README deleted file mode 100644 index ef007dc163..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/README +++ /dev/null @@ -1,18 +0,0 @@ -Generic abstraction for working with byte buffer implementations. - -Copy from: https://github.com/spring-projects/spring-framework/tree/main/spring-core/src/main/java/org/springframework/core/io/buffer. - -- Copy all package inside io.jooby.byffer -- remove all Assert/ObjectUtils import references -- remove Netty5DataBuffer references -- replace reactive stream classes references by JDK reactive streams -- DataBufferUtils is a limited version of original - remove Deprecated methods -- Remove all deprecated since 6.0 from DataBuffer and DataBufferFactory - - -= NEW BUFFER API - -== JETTY - - https://github.com/jetty/jetty.project/issues/10476 - - https://javadoc.jetty.org/jetty-12/org/eclipse/jetty/util/BufferUtil.html - - https://stackoverflow.com/questions/78659372/how-do-you-set-in-jetty-12-a-max-request-size-programmatically diff --git a/jooby/src/main/java/io/jooby/buffer/TouchableDataBuffer.java b/jooby/src/main/java/io/jooby/buffer/TouchableDataBuffer.java deleted file mode 100644 index 2c27e1548f..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/TouchableDataBuffer.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -/** - * Extension of {@link DataBuffer} that allows for buffers that can be given hints for debugging - * purposes. - * - * @author Arjen Poutsma - * @since 6.0 - */ -public interface TouchableDataBuffer extends DataBuffer { - - /** - * Associate the given hint with the data buffer for debugging purposes. - * - * @return this buffer - */ - TouchableDataBuffer touch(Object hint); -} diff --git a/jooby/src/main/java/io/jooby/buffer/package-info.java b/jooby/src/main/java/io/jooby/buffer/package-info.java deleted file mode 100644 index feb1b7e08f..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/package-info.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * This file is part of Jooby. - * - * It is derived from Spring Framework, originally available at: https://github.com/spring-projects/spring-framework - * - * Spring Framework is licensed under the Apache License, Version 2.0. - * - * Modifications: - * - Code live inside of io.jooby.buffer package - * - Added DataBuffer.duplicate - * - Added DataBufferFactory.wrap(byte[] bytes, int offset, int length) - * - * Jooby is also licensed under the Apache License, Version 2.0. - * - * See the LICENSE file in the root of this repository for details. - */ - -/** - * Generic abstraction for working with byte buffer implementations. - * - *

Copy from - * https://github.com/spring-projects/spring-framework/tree/main/spring-core/src/main/java/org/springframework/core/io/buffer. - * - *

    - *
  • Copy all package inside io.jooby.byffer - *
  • remove all Assert/ObjectUtils import references - *
  • remove Netty5DataBuffer references - *
  • replace reactive stream classes references by JDK reactive streams - *
  • DataBufferUtils is a limited version of original - remove Deprecated methods - *
  • Remove all deprecated since 6.0 from DataBuffer and DataBufferFactory - *
- */ -@edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault -package io.jooby.buffer; diff --git a/jooby/src/main/java/io/jooby/handler/SSLHandler.java b/jooby/src/main/java/io/jooby/handler/SSLHandler.java index db1cbd6cef..e41e29dc3a 100644 --- a/jooby/src/main/java/io/jooby/handler/SSLHandler.java +++ b/jooby/src/main/java/io/jooby/handler/SSLHandler.java @@ -9,6 +9,7 @@ import io.jooby.Context; import io.jooby.Route; import io.jooby.Router; +import io.jooby.ServerOptions; /** * Force SSL handler. Check for non-HTTPs request and force client to use HTTPs by redirecting the @@ -90,8 +91,11 @@ public void apply(@NonNull Context ctx) { buff.append(host); if (host.equals("localhost")) { - int securePort = ctx.getRouter().getServerOptions().getSecurePort(); - buff.append(":").append(securePort); + var server = ctx.require(ServerOptions.class); + Integer securePort = server.getSecurePort(); + if (securePort != null) { + buff.append(":").append(securePort); + } } else { if (port > 0 && port != SECURE_PORT) { buff.append(":").append(port); diff --git a/jooby/src/main/java/io/jooby/internal/ArrayValue.java b/jooby/src/main/java/io/jooby/internal/ArrayValue.java index 60e2090bdc..5aa984398b 100644 --- a/jooby/src/main/java/io/jooby/internal/ArrayValue.java +++ b/jooby/src/main/java/io/jooby/internal/ArrayValue.java @@ -16,20 +16,21 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.Context; -import io.jooby.ValueNode; import io.jooby.exception.MissingValueException; import io.jooby.exception.TypeMismatchException; +import io.jooby.value.ConversionHint; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; -public class ArrayValue implements ValueNode { - private final Context ctx; +public class ArrayValue implements Value { + private final ValueFactory factory; private final String name; - private final List list = new ArrayList<>(5); + private final List list = new ArrayList<>(5); - public ArrayValue(Context ctx, String name) { - this.ctx = ctx; + public ArrayValue(ValueFactory factory, String name) { + this.factory = factory; this.name = name; } @@ -38,7 +39,7 @@ public String name() { return name; } - public ArrayValue add(ValueNode value) { + public ArrayValue add(Value value) { this.list.add(value); return this; } @@ -51,11 +52,11 @@ public ArrayValue add(List values) { } public ArrayValue add(String value) { - return this.add(new SingleValue(ctx, name, value)); + return this.add(new SingleValue(factory, name, value)); } @Override - public @NonNull ValueNode get(int index) { + public @NonNull Value get(int index) { try { return list.get(index); } catch (IndexOutOfBoundsException x) { @@ -64,7 +65,7 @@ public ArrayValue add(String value) { } @Override - public @NonNull ValueNode get(@NonNull String name) { + public @NonNull Value get(@NonNull String name) { return new MissingValue(this.name + "." + name); } @@ -85,18 +86,18 @@ public String toString() { } @Override - public @NonNull Iterator iterator() { + public @NonNull Iterator iterator() { return list.iterator(); } @NonNull @Override public T to(@NonNull Class type) { - return ctx.convert(list.get(0), type); + return factory.convert(type, list.get(0), ConversionHint.Strict); } @Nullable @Override public T toNullable(@NonNull Class type) { - return list.isEmpty() ? null : ctx.convertOrNull(list.get(0), type); + return list.isEmpty() ? null : factory.convert(type, list.get(0), ConversionHint.Nullable); } @NonNull @Override @@ -107,7 +108,7 @@ public List toList(@NonNull Class type) { @NonNull @Override public Optional toOptional(@NonNull Class type) { try { - return Optional.ofNullable(to(type)); + return Optional.ofNullable(toNullable(type)); } catch (MissingValueException x) { return Optional.empty(); } @@ -120,24 +121,39 @@ public Set toSet(@NonNull Class type) { @Override public @NonNull Map> toMultimap() { - List values = new ArrayList<>(); + var values = new ArrayList(); list.forEach(it -> it.toMultimap().values().forEach(values::addAll)); return Map.of(name, values); } @Override public @NonNull List toList() { - return collect(new ArrayList<>(), String.class); + return switch (list.size()) { + case 0 -> List.of(); + case 1 -> List.of(list.get(0).value()); + case 2 -> List.of(list.get(0).value(), list.get(1).value()); + case 3 -> List.of(list.get(0).value(), list.get(1).value(), list.get(2).value()); + default -> collect(new ArrayList<>(list.size()), String.class); + }; } @Override public @NonNull Set toSet() { - return collect(new LinkedHashSet<>(), String.class); + return switch (list.size()) { + case 0 -> Set.of(); + case 1 -> Set.of(list.get(0).value()); + default -> collect(new LinkedHashSet<>(list.size()), String.class); + }; } private > C collect(C collection, Class type) { for (var node : list) { - collection.add(node.to(type)); + if (type == String.class) { + //noinspection unchecked + collection.add((T) node.value()); + } else { + collection.add(node.to(type)); + } } return collection; } diff --git a/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java b/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java index 6ffb551ab6..4ee5407846 100644 --- a/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java +++ b/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java @@ -20,7 +20,6 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.ValueNode; public class ByteArrayBody implements Body { private static final byte[] EMPTY = new byte[0]; @@ -69,16 +68,6 @@ public List toList() { return Collections.singletonList(value()); } - @NonNull @Override - public ValueNode get(int index) { - return index == 0 ? this : get(Integer.toString(index)); - } - - @NonNull @Override - public ValueNode get(@NonNull String name) { - return new MissingValue(name); - } - @Override public String name() { return "body"; diff --git a/jooby/src/main/java/io/jooby/internal/Chi.java b/jooby/src/main/java/io/jooby/internal/Chi.java index 8c977a2485..017eff0a5e 100644 --- a/jooby/src/main/java/io/jooby/internal/Chi.java +++ b/jooby/src/main/java/io/jooby/internal/Chi.java @@ -6,9 +6,9 @@ package io.jooby.internal; import java.util.Arrays; +import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -485,7 +485,7 @@ public StaticRoute get(String path) { } private static class StaticMapN implements StaticMap { - private final Map paths = new ConcurrentHashMap<>(10); + private final Map paths = new HashMap<>(10); public StaticMapN(StaticMap6 staticMap, String path, StaticRoute staticRoute) { put(staticMap.pattern1, staticMap.route1); @@ -515,8 +515,6 @@ private interface MethodMatcher { StaticRouterMatch get(String method); void put(String method, StaticRouterMatch route); - - boolean matches(String method); } private static class SingleMethodMatcher implements MethodMatcher { @@ -531,17 +529,9 @@ public void put(String method, StaticRouterMatch route) { @Override public StaticRouterMatch get(String method) { - if (this.method == method) { - return route; - } return this.method.equals(method) ? route : null; } - @Override - public boolean matches(String method) { - return this.method.equals(method); - } - public void clear() { this.method = null; this.route = null; @@ -549,7 +539,7 @@ public void clear() { } private static class MultipleMethodMatcher implements MethodMatcher { - private final Map methods = new ConcurrentHashMap<>(); + private final Map methods = new HashMap<>(); public MultipleMethodMatcher(SingleMethodMatcher matcher) { methods.put(matcher.method, matcher.route); @@ -565,11 +555,6 @@ public StaticRouterMatch get(String method) { public void put(String method, StaticRouterMatch route) { methods.put(method, route); } - - @Override - public boolean matches(String method) { - return this.methods.containsKey(method); - } } static class StaticRoute { @@ -890,7 +875,7 @@ void setEndpoint(String method, Route route) { Node n = this; // Set the handler for the method type on the node if (n.endpoints == null) { - n.endpoints = new ConcurrentHashMap<>(); + n.endpoints = new HashMap<>(); } // if ((method & mSTUB) == mSTUB) { diff --git a/jooby/src/main/java/io/jooby/internal/FileBody.java b/jooby/src/main/java/io/jooby/internal/FileBody.java index 0e5959d6f2..a77496d5cc 100644 --- a/jooby/src/main/java/io/jooby/internal/FileBody.java +++ b/jooby/src/main/java/io/jooby/internal/FileBody.java @@ -22,7 +22,6 @@ import io.jooby.Context; import io.jooby.MediaType; import io.jooby.SneakyThrows; -import io.jooby.ValueNode; public class FileBody implements Body { private Context ctx; @@ -84,16 +83,6 @@ public List toList() { return Collections.singletonList(value()); } - @Override - public @NonNull ValueNode get(int index) { - return index == 0 ? this : get(Integer.toString(index)); - } - - @NonNull @Override - public ValueNode get(@NonNull String name) { - return new MissingValue(name); - } - @Override public String name() { return "body"; diff --git a/jooby/src/main/java/io/jooby/internal/FlashMapImpl.java b/jooby/src/main/java/io/jooby/internal/FlashMapImpl.java index 4860d89ce9..5b920ed40a 100644 --- a/jooby/src/main/java/io/jooby/internal/FlashMapImpl.java +++ b/jooby/src/main/java/io/jooby/internal/FlashMapImpl.java @@ -16,7 +16,7 @@ import io.jooby.Context; import io.jooby.Cookie; import io.jooby.FlashMap; -import io.jooby.Value; +import io.jooby.value.Value; public class FlashMapImpl extends HashMap implements FlashMap { diff --git a/jooby/src/main/java/io/jooby/internal/HashValue.java b/jooby/src/main/java/io/jooby/internal/HashValue.java index 6297c94aee..3544c56ce0 100644 --- a/jooby/src/main/java/io/jooby/internal/HashValue.java +++ b/jooby/src/main/java/io/jooby/internal/HashValue.java @@ -9,7 +9,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -23,24 +22,25 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.Context; import io.jooby.FileUpload; -import io.jooby.ValueNode; - -public class HashValue implements ValueNode { - protected static final Map EMPTY = Collections.emptyMap(); - private Context ctx; - protected Map hash = EMPTY; +import io.jooby.value.ConversionHint; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; + +public class HashValue implements Value { + protected static final Map EMPTY = Map.of(); + protected final ValueFactory factory; + protected Map hash = EMPTY; private final String name; private boolean arrayLike; - public HashValue(Context ctx, String name) { - this.ctx = ctx; + public HashValue(ValueFactory factory, String name) { + this.factory = factory; this.name = name; } - protected HashValue(Context ctx) { - this.ctx = ctx; + protected HashValue(ValueFactory factory) { + this.factory = factory; this.name = null; } @@ -50,14 +50,14 @@ public String name() { } public void put(String path, String value) { - put(path, Collections.singletonList(value)); + put(path, List.of(value)); } - public void put(String path, ValueNode node) { + public void put(String path, Value node) { put( path, (name, scope) -> { - ValueNode existing = scope.get(name); + var existing = scope.get(name); if (existing == null) { scope.put(name, node); } else { @@ -65,7 +65,7 @@ public void put(String path, ValueNode node) { if (existing instanceof ArrayValue) { list = (ArrayValue) existing; } else { - list = new ArrayValue(ctx, name).add(existing); + list = new ArrayValue(factory, name).add(existing); scope.put(name, list); } list.add(node); @@ -77,48 +77,44 @@ public void put(String path, Collection values) { put( path, (name, scope) -> { - for (String value : values) { - ValueNode existing = scope.get(name); + for (var value : values) { + var existing = scope.get(name); if (existing == null) { - scope.put(name, new SingleValue(ctx, name, decode(value))); + scope.put(name, new SingleValue(factory, name, value)); } else { ArrayValue list; if (existing instanceof ArrayValue) { list = (ArrayValue) existing; } else { - list = new ArrayValue(ctx, name).add(existing); + list = new ArrayValue(factory, name).add(existing); scope.put(name, list); } - list.add(decode(value)); + list.add(value); } } }); } - protected String decode(String value) { - return value; - } - - private void put(String path, BiConsumer> consumer) { + private void put(String path, BiConsumer> consumer) { // Locate node: - int nameStart = 0; - int nameEnd = path.length(); - HashValue target = this; - for (int i = nameStart; i < nameEnd; i++) { - char ch = path.charAt(i); + var nameStart = 0; + var nameEnd = path.length(); + var target = this; + for (var i = nameStart; i < nameEnd; i++) { + var ch = path.charAt(i); if (ch == '.') { - String name = path.substring(nameStart, i); + var name = path.substring(nameStart, i); nameStart = i + 1; target = target.getOrCreateScope(name); } else if (ch == '[') { if (nameStart < i) { - String name = path.substring(nameStart, i); + var name = path.substring(nameStart, i); target = target.getOrCreateScope(name); } nameStart = i + 1; } else if (ch == ']') { if (i + 1 < nameEnd) { - String name = path.substring(nameStart, i); + var name = path.substring(nameStart, i); if (isNumber(name)) { target.useIndexes(); } @@ -129,7 +125,7 @@ private void put(String path, BiConsumer> consume } } } - String key = path.substring(nameStart, nameEnd); + var key = path.substring(nameStart, nameEnd); if (isNumber(key)) { target.useIndexes(); } @@ -142,9 +138,11 @@ private void useIndexes() { return; } this.arrayLike = true; - TreeMap ordered = new TreeMap<>(); - ordered.putAll(hash); - hash.clear(); + var ordered = new TreeMap(); + if (hash != EMPTY) { + ordered.putAll(hash); + hash.clear(); + } this.hash = ordered; } @@ -157,7 +155,7 @@ private boolean isNumber(String value) { return true; } - protected Map hash() { + protected Map hash() { if (hash == EMPTY) { hash = new LinkedHashMap<>(); } @@ -165,11 +163,11 @@ protected Map hash() { } /*package*/ HashValue getOrCreateScope(String name) { - return (HashValue) hash().computeIfAbsent(name, k -> new HashValue(ctx, k)); + return (HashValue) hash().computeIfAbsent(name, k -> new HashValue(factory, k)); } - public @NonNull ValueNode get(@NonNull String name) { - ValueNode value = hash.get(name); + public @NonNull Value get(@NonNull String name) { + var value = hash.get(name); if (value == null) { return new MissingValue(scope(name)); } @@ -181,7 +179,7 @@ private String scope(String name) { } @Override - public @NonNull ValueNode get(int index) { + public @NonNull Value get(int index) { return get(Integer.toString(index)); } @@ -191,13 +189,11 @@ public int size() { @Override public String value() { - StringJoiner joiner = new StringJoiner("&"); + var joiner = new StringJoiner("&"); hash.forEach( (k, v) -> { - Iterator it = v.iterator(); - while (it.hasNext()) { - ValueNode value = it.next(); - String str = + for (var value : v) { + var str = value instanceof FileUpload ? ((FileUpload) value).getFileName() : value.toString(); joiner.add(k + "=" + str); } @@ -206,7 +202,7 @@ public String value() { } @Override - public Iterator iterator() { + public Iterator iterator() { return hash.values().iterator(); } @@ -235,35 +231,34 @@ public Optional toOptional(@NonNull Class type) { if (hash.isEmpty()) { return Optional.empty(); } - return ofNullable(to(type)); + return ofNullable(toNullable(type)); } @NonNull @Override public T to(@NonNull Class type) { - return ctx.convert(this, type); + return factory.convert(type, this); } @Nullable @Override - public final T toNullable(@NonNull Class type) { - return toNullable(ctx, type, allowEmptyBean()); + public T toNullable(@NonNull Class type) { + return toNullable(factory, type); } - protected T toNullable(@NonNull Context ctx, @NonNull Class type, boolean allowEmpty) { - return ValueConverters.convert(this, type, ctx.getRouter(), allowEmpty); + private T toNullable(@NonNull ValueFactory factory, @NonNull Class type) { + return factory.convert(type, this, ConversionHint.Nullable); } @Override public Map> toMultimap() { - Map> result = new LinkedHashMap<>(hash.size()); - Set> entries = hash.entrySet(); - String scope = name == null ? "" : name + "."; - for (Map.Entry entry : entries) { - ValueNode value = entry.getValue(); + var result = new LinkedHashMap>(hash.size()); + var entries = hash.entrySet(); + for (var entry : entries) { + var value = entry.getValue(); value .toMultimap() .forEach( (k, v) -> { - result.put(scope + k, v); + result.put(scope(k), v); }); } return result; @@ -275,7 +270,7 @@ public String toString() { } public void put(Map> headers) { - for (Map.Entry> entry : headers.entrySet()) { + for (var entry : headers.entrySet()) { put(entry.getKey(), entry.getValue()); } } @@ -284,32 +279,28 @@ private > C toCollection(@NonNull Class type, C co if (!hash.isEmpty()) { if (arrayLike) { // indexes access, treat like a list - for (Map.Entry e : hash.entrySet()) { + for (Map.Entry e : hash.entrySet()) { if (e.getKey().chars().allMatch(Character::isDigit)) { // put only [index] where index is a number if (e.getValue() instanceof HashValue node) { - addItem(ctx, node, type, collection); + addItem(factory, node, type, collection); } else { ofNullable(e.getValue().toNullable(type)).ifPresent(collection::add); } } } } else { - addItem(ctx, this, type, collection); + addItem(factory, this, type, collection); } } return collection; } private static void addItem( - Context ctx, HashValue node, Class type, Collection container) { - var item = node.toNullable(ctx, type, false); + ValueFactory factory, HashValue node, Class type, Collection container) { + var item = node.toNullable(factory, type); if (item != null) { container.add(item); } } - - protected boolean allowEmptyBean() { - return false; - } } diff --git a/jooby/src/main/java/io/jooby/internal/HeadContext.java b/jooby/src/main/java/io/jooby/internal/HeadContext.java index dd9a0f3ede..c4dc99aec9 100644 --- a/jooby/src/main/java/io/jooby/internal/HeadContext.java +++ b/jooby/src/main/java/io/jooby/internal/HeadContext.java @@ -19,7 +19,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.*; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class HeadContext extends ForwardingContext { /** @@ -66,8 +66,8 @@ public Context send(@NonNull ByteBuffer data) { } @NonNull @Override - public Context send(@NonNull DataBuffer data) { - ctx.setResponseLength(data.readableByteCount()); + public Context send(@NonNull Output output) { + ctx.setResponseLength(output.size()); checkSizeHeaders(); ctx.send(StatusCode.OK); return this; @@ -186,7 +186,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { + public Sender write(@NonNull Output output, @NonNull Callback callback) { return this; } diff --git a/jooby/src/main/java/io/jooby/internal/HeadersValue.java b/jooby/src/main/java/io/jooby/internal/HeadersValue.java index 6d870be9ba..8ed7148ca2 100644 --- a/jooby/src/main/java/io/jooby/internal/HeadersValue.java +++ b/jooby/src/main/java/io/jooby/internal/HeadersValue.java @@ -11,17 +11,17 @@ import java.util.TreeMap; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Context; -import io.jooby.ValueNode; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; -public class HeadersValue extends HashValue implements ValueNode { +public class HeadersValue extends HashValue implements Value { - public HeadersValue(final Context ctx) { - super(ctx); + public HeadersValue(ValueFactory valueFactory) { + super(valueFactory); } @Override - protected Map hash() { + protected Map hash() { if (hash == EMPTY) { hash = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } @@ -38,9 +38,9 @@ public Map toMap() { @NonNull @Override public Map> toMultimap() { Map> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - Set> entries = hash.entrySet(); - for (Map.Entry entry : entries) { - ValueNode value = entry.getValue(); + Set> entries = hash.entrySet(); + for (Map.Entry entry : entries) { + Value value = entry.getValue(); result.putAll(value.toMultimap()); } return result; diff --git a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java index e6a7e8ca50..4650a6d2f6 100644 --- a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java +++ b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java @@ -9,7 +9,6 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; -import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.*; @@ -21,7 +20,7 @@ import io.jooby.ModelAndView; import io.jooby.StatusCode; import io.jooby.TemplateEngine; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class HttpMessageEncoder implements MessageEncoder { @@ -31,7 +30,7 @@ public class HttpMessageEncoder implements MessageEncoder { public HttpMessageEncoder add(MediaType type, MessageEncoder encoder) { if (encoder instanceof TemplateEngine engine) { - // media type is ignored for template engines. They have a custom object type + // Media type is ignored for template engines. They have a custom object type templateEngineList.add(engine); } else { if (encoders == null) { @@ -43,67 +42,66 @@ public HttpMessageEncoder add(MediaType type, MessageEncoder encoder) { } @Override - public DataBuffer encode(@NonNull Context ctx, @NonNull Object value) throws Exception { - if (value instanceof ModelAndView modelAndView) { - for (TemplateEngine engine : templateEngineList) { + public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + if (value instanceof ModelAndView modelAndView) { + for (var engine : templateEngineList) { if (engine.supports(modelAndView)) { return engine.encode(ctx, modelAndView); } } throw new IllegalArgumentException("No template engine for: " + modelAndView.getView()); } - /** InputStream: */ - if (value instanceof InputStream) { - ctx.send((InputStream) value); + /* InputStream: */ + if (value instanceof InputStream in) { + ctx.send(in); return null; } - /** StatusCode: */ - if (value instanceof StatusCode) { - ctx.send((StatusCode) value); + /* StatusCode: */ + if (value instanceof StatusCode statusCode) { + ctx.send(statusCode); return null; } - /** FileChannel: */ - if (value instanceof FileChannel) { - ctx.send((FileChannel) value); + /* FileChannel: */ + if (value instanceof FileChannel channel) { + ctx.send(channel); return null; } - if (value instanceof File) { - ctx.send(((File) value).toPath()); + if (value instanceof File file) { + ctx.send(file.toPath()); return null; } - if (value instanceof Path) { - ctx.send((Path) value); + if (value instanceof Path path) { + ctx.send(path); return null; } - /** FileDownload: */ - if (value instanceof FileDownload) { - ctx.send((FileDownload) value); + /* FileDownload: */ + if (value instanceof FileDownload download) { + ctx.send(download); return null; } - var bufferFactory = ctx.getBufferFactory(); - /** Strings: */ - if (value instanceof CharSequence) { - return bufferFactory.wrap(value.toString().getBytes(StandardCharsets.UTF_8)); + var outputFactory = ctx.getOutputFactory(); + /* Strings: */ + if (value instanceof CharSequence charSequence) { + return outputFactory.wrap(charSequence.toString()); } if (value instanceof Number) { - return bufferFactory.wrap(value.toString().getBytes(StandardCharsets.UTF_8)); + return outputFactory.wrap(value.toString()); } - /** RawByte: */ - if (value instanceof byte[]) { - return bufferFactory.wrap((byte[]) value); + /* RawByte: */ + if (value instanceof byte[] bytes) { + return outputFactory.wrap(bytes); } - if (value instanceof ByteBuffer) { - ctx.send((ByteBuffer) value); - return null; + if (value instanceof ByteBuffer buffer) { + return outputFactory.wrap(buffer); } if (encoders != null) { // Content negotiation, find best: - List produces = ctx.getRoute().getProduces(); + var produces = ctx.getRoute().getProduces(); if (produces.isEmpty()) { - produces = new ArrayList<>(encoders.keySet()); + produces = encoders.keySet().stream().toList(); } - MediaType type = ctx.accept(produces); - MessageEncoder encoder = encoders.getOrDefault(type, MessageEncoder.TO_STRING); + var type = ctx.accept(produces); + var encoder = encoders.getOrDefault(type, MessageEncoder.TO_STRING); return encoder.encode(ctx, value); } else { return MessageEncoder.TO_STRING.encode(ctx, value); diff --git a/jooby/src/main/java/io/jooby/internal/InputStreamBody.java b/jooby/src/main/java/io/jooby/internal/InputStreamBody.java index 8daf0e33a9..f836397604 100644 --- a/jooby/src/main/java/io/jooby/internal/InputStreamBody.java +++ b/jooby/src/main/java/io/jooby/internal/InputStreamBody.java @@ -23,7 +23,6 @@ import io.jooby.MediaType; import io.jooby.ServerOptions; import io.jooby.SneakyThrows; -import io.jooby.ValueNode; public class InputStreamBody implements Body { private Context ctx; @@ -76,16 +75,6 @@ public String value() { return value(StandardCharsets.UTF_8); } - @NonNull @Override - public ValueNode get(int index) { - return index == 0 ? this : get(Integer.toString(index)); - } - - @NonNull @Override - public ValueNode get(@NonNull String name) { - return new MissingValue(name); - } - @Override public String name() { return "body"; diff --git a/jooby/src/main/java/io/jooby/internal/MemorySessionStore.java b/jooby/src/main/java/io/jooby/internal/MemorySessionStore.java index fe983f5cf6..3f915c13b2 100644 --- a/jooby/src/main/java/io/jooby/internal/MemorySessionStore.java +++ b/jooby/src/main/java/io/jooby/internal/MemorySessionStore.java @@ -12,6 +12,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.Session; import io.jooby.SessionStore; @@ -29,27 +30,27 @@ public MemorySessionStore(SessionToken token, Duration timeout) { } @Override - protected Data getOrCreate(String sessionId, Function factory) { + protected Data getOrCreate(@NonNull String sessionId, @NonNull Function factory) { return sessions.computeIfAbsent(sessionId, factory); } @Override - protected Data getOrNull(String sessionId) { + protected Data getOrNull(@NonNull String sessionId) { return sessions.get(sessionId); } @Override - protected Data remove(String sessionId) { + protected Data remove(@NonNull String sessionId) { return sessions.remove(sessionId); } @Override - protected void put(String sessionId, Data data) { + protected void put(@NonNull String sessionId, @NonNull Data data) { sessions.put(sessionId, data); } @Override - public Session findSession(Context ctx) { + public Session findSession(@NonNull Context ctx) { purge(); return super.findSession(ctx); } diff --git a/jooby/src/main/java/io/jooby/internal/MissingValue.java b/jooby/src/main/java/io/jooby/internal/MissingValue.java index 3cb585fb0c..c9a8ec2138 100644 --- a/jooby/src/main/java/io/jooby/internal/MissingValue.java +++ b/jooby/src/main/java/io/jooby/internal/MissingValue.java @@ -5,20 +5,15 @@ */ package io.jooby.internal; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; +import java.util.*; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.ValueNode; import io.jooby.exception.MissingValueException; +import io.jooby.value.Value; -public class MissingValue implements ValueNode { - private String name; +public class MissingValue implements Value { + private final String name; public MissingValue(String name) { this.name = name; @@ -30,12 +25,22 @@ public String name() { } @Override - public @NonNull ValueNode get(@NonNull String name) { + public @NonNull Value get(@NonNull String name) { return this.name.equals(name) ? this : new MissingValue(this.name + "." + name); } @Override - public @NonNull ValueNode get(int index) { + public int size() { + return 0; + } + + @Override + public @NonNull Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public @NonNull Value get(int index) { return new MissingValue(this.name + "[" + index + "]"); } diff --git a/jooby/src/main/java/io/jooby/internal/MultipartNode.java b/jooby/src/main/java/io/jooby/internal/MultipartNode.java index d16f017b39..90ec58d939 100644 --- a/jooby/src/main/java/io/jooby/internal/MultipartNode.java +++ b/jooby/src/main/java/io/jooby/internal/MultipartNode.java @@ -6,34 +6,33 @@ package io.jooby.internal; import java.util.*; -import java.util.stream.Collectors; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Context; import io.jooby.FileUpload; import io.jooby.Formdata; import io.jooby.SneakyThrows; +import io.jooby.value.ValueFactory; public class MultipartNode extends HashValue implements Formdata { - private Map> files = new HashMap<>(); + private final Map> files = new HashMap<>(); - public MultipartNode(Context ctx) { - super(ctx); + public MultipartNode(ValueFactory valueFactory) { + super(valueFactory); } @Override - public void put(String name, FileUpload file) { + public void put(@NonNull String name, @NonNull FileUpload file) { files.computeIfAbsent(name, k -> new ArrayList<>()).add(file); } @NonNull @Override public List files() { - return files.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); + return files.values().stream().flatMap(Collection::stream).toList(); } @NonNull @Override public List files(@NonNull String name) { - return this.files.getOrDefault(name, Collections.emptyList()); + return this.files.getOrDefault(name, List.of()); } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/internal/MultipleSessionToken.java b/jooby/src/main/java/io/jooby/internal/MultipleSessionToken.java index cb7d9b7247..f2d5a8e7a0 100644 --- a/jooby/src/main/java/io/jooby/internal/MultipleSessionToken.java +++ b/jooby/src/main/java/io/jooby/internal/MultipleSessionToken.java @@ -9,6 +9,7 @@ import java.util.Arrays; import java.util.List; +import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SessionToken; @@ -21,7 +22,7 @@ public MultipleSessionToken(SessionToken... sessionToken) { } @Override - public String findToken(Context ctx) { + public String findToken(@NonNull Context ctx) { for (SessionToken sessionToken : sessionTokens) { String token = sessionToken.findToken(ctx); if (token != null) { @@ -32,12 +33,12 @@ public String findToken(Context ctx) { } @Override - public void saveToken(Context ctx, String token) { + public void saveToken(@NonNull Context ctx, @NonNull String token) { strategy(ctx).forEach(it -> it.saveToken(ctx, token)); } @Override - public void deleteToken(Context ctx, String token) { + public void deleteToken(@NonNull Context ctx, @NonNull String token) { strategy(ctx).forEach(it -> it.deleteToken(ctx, token)); } diff --git a/jooby/src/main/java/io/jooby/internal/MutedServer.java b/jooby/src/main/java/io/jooby/internal/MutedServer.java index af5f64be35..804ae04855 100644 --- a/jooby/src/main/java/io/jooby/internal/MutedServer.java +++ b/jooby/src/main/java/io/jooby/internal/MutedServer.java @@ -16,6 +16,7 @@ import io.jooby.LoggingService; import io.jooby.Server; import io.jooby.ServerOptions; +import io.jooby.output.OutputFactory; public class MutedServer implements Server { private Server delegate; @@ -30,6 +31,11 @@ private MutedServer(Server server, LoggingService loggingService, List m this.mute = mute; } + @NonNull @Override + public OutputFactory getOutputFactory() { + return delegate.getOutputFactory(); + } + /** * Muted a server when need it. * diff --git a/jooby/src/main/java/io/jooby/internal/ParamLookupImpl.java b/jooby/src/main/java/io/jooby/internal/ParamLookupImpl.java index 317262aab2..118c285e96 100644 --- a/jooby/src/main/java/io/jooby/internal/ParamLookupImpl.java +++ b/jooby/src/main/java/io/jooby/internal/ParamLookupImpl.java @@ -11,7 +11,7 @@ import io.jooby.Context; import io.jooby.ParamLookup; import io.jooby.ParamSource; -import io.jooby.Value; +import io.jooby.value.Value; public class ParamLookupImpl implements ParamLookup.Stage { diff --git a/jooby/src/main/java/io/jooby/internal/QueryStringValue.java b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java index 366262c16b..164321d37f 100644 --- a/jooby/src/main/java/io/jooby/internal/QueryStringValue.java +++ b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java @@ -6,28 +6,21 @@ package io.jooby.internal; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Context; import io.jooby.QueryString; +import io.jooby.value.ConversionHint; +import io.jooby.value.ValueFactory; public class QueryStringValue extends HashValue implements QueryString { - private String queryString; + private final String queryString; - public QueryStringValue(Context ctx, String queryString) { - super(ctx); + public QueryStringValue(ValueFactory valueFactory, String queryString) { + super(valueFactory); this.queryString = queryString; } - protected boolean allowEmptyBean() { - return true; - } - @Override - protected T toNullable(@NonNull Context ctx, @NonNull Class type, boolean allowEmpty) { - // NOTE: 2.x backward compatible. Make sure Query object are almost always created - // GET /search? - // with class Search (q="*") - // so q is defaulted to "*" - return ValueConverters.convert(this, type, ctx.getRouter(), allowEmpty); + public @NonNull T toEmpty(@NonNull Class type) { + return factory.convert(type, this, ConversionHint.Empty); } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 435755e244..e67d9c5a18 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -14,7 +14,6 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; -import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; @@ -23,8 +22,6 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; -import java.util.ServiceLoader; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.function.BiConsumer; @@ -41,14 +38,13 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.*; -import io.jooby.buffer.DataBufferFactory; -import io.jooby.buffer.DefaultDataBufferFactory; import io.jooby.exception.RegistryException; import io.jooby.exception.StatusCodeException; import io.jooby.internal.handler.ServerSentEventHandler; import io.jooby.internal.handler.WebSocketHandler; +import io.jooby.output.OutputFactory; import io.jooby.problem.ProblemDetailsHandler; -import jakarta.inject.Provider; +import io.jooby.value.ValueFactory; public class RouterImpl implements Router { @@ -136,7 +132,7 @@ public Stack executor(Executor executor) { private List routes = new ArrayList<>(); - private HttpMessageEncoder encoder = new HttpMessageEncoder(); + private final HttpMessageEncoder encoder = new HttpMessageEncoder(); private String basePath; @@ -152,33 +148,28 @@ public Stack executor(Executor executor) { private ServiceRegistry services = new ServiceRegistryImpl(); - private SessionStore sessionStore = SessionStore.memory(); + private SessionStore sessionStore = SessionStore.UNSUPPORTED; private Cookie flashCookie = new Cookie("jooby.flash").setHttpOnly(true); - private LinkedList converters; - - private List beanConverters; - private ContextInitializer preDispatchInitializer; private ContextInitializer postDispatchInitializer; - private Set routerOptions = EnumSet.of(RouterOption.RESET_HEADERS_ON_ERROR); - private DataBufferFactory bufferFactory; - private boolean trustProxy; - - private boolean contextAsService; + private RouterOptions routerOptions = RouterOptions.defaults(); private boolean started; private boolean stopped; + private ValueFactory valueFactory = new ValueFactory(); + + private OutputFactory outputFactory; + + private ServerOptions serverOptions; + public RouterImpl() { stack.addLast(new Stack(chi, null)); - - converters = new LinkedList<>(ValueConverters.defaultConverters()); - beanConverters = new ArrayList<>(3); } @NonNull @Override @@ -202,19 +193,19 @@ public Map getAttributes() { } @NonNull @Override - public Set getRouterOptions() { + public RouterOptions getRouterOptions() { return routerOptions; } @NonNull @Override - public Router setRouterOptions(@NonNull RouterOption... options) { - Stream.of(options).forEach(routerOptions::add); + public Router setRouterOptions(@NonNull RouterOptions options) { + this.routerOptions = options; return this; } @NonNull @Override public Router setContextPath(@NonNull String basePath) { - if (routes.size() > 0) { + if (!routes.isEmpty()) { throw new IllegalStateException("Base path must be set before adding any routes."); } this.basePath = Router.leadingSlash(basePath); @@ -236,11 +227,6 @@ public List getRoutes() { return routes; } - @Override - public boolean isTrustProxy() { - return trustProxy; - } - @Override public boolean isStarted() { return started; @@ -251,36 +237,31 @@ public boolean isStopped() { return stopped; } - @NonNull @Override - public Router setTrustProxy(boolean trustProxy) { - this.trustProxy = trustProxy; + private void configureTrustProxy(boolean trustProxy) { if (trustProxy) { addPreDispatchInitializer(ContextInitializer.PROXY_PEER_ADDRESS); } else { removePreDispatchInitializer(ContextInitializer.PROXY_PEER_ADDRESS); } - return this; } @NonNull @Override - public RouteSet domain(@NonNull String domain, @NonNull Runnable body) { + public Route.Set domain(@NonNull String domain, @NonNull Runnable body) { return mount(domainPredicate(domain), body); } @NonNull @Override - public Router domain(@NonNull String domain, @NonNull Router subrouter) { + public Route.Set domain(@NonNull String domain, @NonNull Router subrouter) { return mount(domainPredicate(domain), subrouter); } @NonNull @Override - public RouteSet mount(@NonNull Predicate predicate, @NonNull Runnable body) { - var routeSet = new RouteSet(); + public Route.Set mount(@NonNull Predicate predicate, @NonNull Runnable body) { var tree = new Chi(); putPredicate(predicate, tree); int start = this.routes.size(); newStack(tree, "/", body); - routeSet.setRoutes(this.routes.subList(start, this.routes.size())); - return routeSet; + return new Route.Set(this.routes.subList(start, this.routes.size())); } public Router install( @@ -300,11 +281,11 @@ public Router install( } @NonNull @Override - public Router mount(@NonNull Predicate predicate, @NonNull Router subrouter) { - /** Override services: */ + public Route.Set mount(@NonNull Predicate predicate, @NonNull Router subrouter) { + /* Override services: */ overrideAll(this, subrouter); - /** Routes: */ - mount( + /* Routes: */ + return mount( predicate, () -> { for (Route route : subrouter.getRoutes()) { @@ -312,45 +293,25 @@ public Router mount(@NonNull Predicate predicate, @NonNull Router subro copy(route, newRoute); } }); - return this; } @NonNull @Override - public Router mount(@NonNull String path, @NonNull Router router) { + public Route.Set mount(@NonNull String path, @NonNull Router router) { + int start = this.routes.size(); /** Override services: */ overrideAll(this, router); /** Merge error handler: */ mergeErrorHandler(router); /** Routes: */ copyRoutes(path, router); - return this; + return new Route.Set(this.routes.subList(start, this.routes.size())); } @NonNull @Override - public Router mount(@NonNull Router router) { + public Route.Set mount(@NonNull Router router) { return mount("/", router); } - @NonNull @Override - public Router mvc(@NonNull MvcExtension router) { - throw new UnsupportedOperationException(); - } - - @NonNull @Override - public Router mvc(@NonNull Object router) { - throw new UnsupportedOperationException(); - } - - @NonNull @Override - public Router mvc(@NonNull Class router) { - throw new UnsupportedOperationException(); - } - - @NonNull @Override - public Router mvc(@NonNull Class router, @NonNull Provider provider) { - throw new UnsupportedOperationException(); - } - @NonNull @Override public Router encoder(@NonNull MessageEncoder encoder) { this.encoder.add(MediaType.all, encoder); @@ -374,23 +335,6 @@ public Executor getWorker() { return worker; } - @NonNull @Override - public DataBufferFactory getBufferFactory() { - if (bufferFactory == null) { - bufferFactory = - ServiceLoader.load(DataBufferFactory.class) - .findFirst() - .orElse(DefaultDataBufferFactory.sharedInstance); - } - return bufferFactory; - } - - @NonNull @Override - public Router setBufferFactory(@NonNull DataBufferFactory bufferFactory) { - this.bufferFactory = bufferFactory; - return this; - } - @NonNull @Override public Router setWorker(Executor worker) { ForwardingExecutor workerRef = (ForwardingExecutor) this.worker; @@ -442,17 +386,15 @@ public Router dispatch(@NonNull Executor executor, @NonNull Runnable action) { } @NonNull @Override - public RouteSet routes(@NonNull Runnable action) { + public Route.Set routes(@NonNull Runnable action) { return path("/", action); } @Override - @NonNull public RouteSet path(@NonNull String pattern, @NonNull Runnable action) { - RouteSet routeSet = new RouteSet(); + @NonNull public Route.Set path(@NonNull String pattern, @NonNull Runnable action) { int start = this.routes.size(); newStack(chi, pattern, action); - routeSet.setRoutes(this.routes.subList(start, this.routes.size())); - return routeSet; + return new Route.Set(this.routes.subList(start, this.routes.size())); } @NonNull @Override @@ -467,35 +409,33 @@ public Router setSessionStore(SessionStore sessionStore) { } @NonNull @Override - public Router converter(ValueConverter converter) { - if (converter instanceof BeanConverter) { - beanConverters.add((BeanConverter) converter); - } else { - converters.addFirst(converter); - } - return this; + public ValueFactory getValueFactory() { + return valueFactory; } @NonNull @Override - public List getConverters() { - return converters; + public Router setValueFactory(@NonNull ValueFactory valueFactory) { + this.valueFactory = valueFactory; + return this; } @NonNull @Override - public List getBeanConverters() { - return beanConverters; + public OutputFactory getOutputFactory() { + return outputFactory; + } + + public void setOutputFactory(@NonNull OutputFactory outputFactory) { + this.outputFactory = outputFactory; } @NonNull @Override public Route ws(@NonNull String pattern, @NonNull WebSocket.Initializer handler) { - return route(WS, pattern, new WebSocketHandler(handler)).setHandle(handler); + return route(WS, pattern, new WebSocketHandler(handler)); } @NonNull @Override public Route sse(@NonNull String pattern, @NonNull ServerSentEmitter.Handler handler) { - return route(SSE, pattern, new ServerSentEventHandler(handler)) - .setHandle(handler) - .setExecutorKey("worker"); + return route(SSE, pattern, new ServerSentEventHandler(handler)).setExecutorKey("worker"); } @Override @@ -513,9 +453,9 @@ private Route newRoute( pathBuilder.append(pattern); /** Filter: */ - List decoratorList = stack.stream().flatMap(Stack::toFilter).toList(); - Route.Filter decorator = - decoratorList.stream().reduce(null, (it, next) -> it == null ? next : it.then(next)); + List filters = stack.stream().flatMap(Stack::toFilter).toList(); + Route.Filter filter = + filters.stream().reduce(null, (it, next) -> it == null ? next : it.then(next)); /** After: */ Route.After after = @@ -527,15 +467,17 @@ private Route newRoute( String safePattern = pathBuilder.toString(); Route route = new Route(method, safePattern, handler); route.setPathKeys(Router.pathKeys(safePattern)); - route.setAfter(after); - route.setFilter(decorator); + if (after != null) { + route.setAfter(after); + } + route.setFilter(filter); route.setEncoder(encoder); route.setDecoders(decoders); - decoratorList.forEach(it -> it.setRoute(route)); + filters.forEach(it -> it.setRoute(route)); handler.setRoute(route); - Stack stack = this.stack.peekLast(); + var stack = this.stack.peekLast(); if (stack.executor != null) { routeExecutor.put(route, stack.executor); } @@ -543,7 +485,7 @@ private Route newRoute( String finalPattern = basePath == null ? safePattern : new PathBuilder(basePath, safePattern).toString(); - if (routerOptions.contains(RouterOption.IGNORE_CASE)) { + if (routerOptions.isIgnoreCase()) { finalPattern = finalPattern.toLowerCase(); } @@ -553,10 +495,8 @@ private Route newRoute( for (String routePattern : Router.expandOptionalVariables(asciiPattern)) { if (route.getMethod().equals(WS)) { tree.insert(GET, routePattern, route); - // route.setReturnType(Context.class); } else if (route.getMethod().equals(SSE)) { tree.insert(GET, routePattern, route); - // route.setReturnType(Context.class); } else { tree.insert(route.getMethod(), routePattern, route); @@ -595,6 +535,8 @@ private void pureAscii(String pattern, Consumer consumer) { @NonNull public Router start(@NonNull Jooby app, @NonNull Server server) { started = true; + configureTrustProxy(routerOptions.isTrustProxy()); + setContextAsService(); var globalErrHandler = defineGlobalErrorHandler(app); if (err == null) { err = globalErrHandler; @@ -648,13 +590,13 @@ private void pureAscii(String pattern, Consumer consumer) { ((Chi) chi).setEncoder(encoder); /** router options: */ - if (routerOptions.contains(RouterOption.IGNORE_CASE)) { + if (routerOptions.isIgnoreCase()) { chi = new RouteTreeLowerCasePath(chi); } - if (routerOptions.contains(RouterOption.IGNORE_TRAILING_SLASH)) { + if (routerOptions.isIgnoreTrailingSlash()) { chi = new RouteTreeIgnoreTrailingSlash(chi); } - if (routerOptions.contains(RouterOption.NORMALIZE_SLASH)) { + if (routerOptions.isNormalizeSlash()) { chi = new RouteTreeNormPath(chi); } @@ -833,7 +775,12 @@ public Router setFlashCookie(@NonNull Cookie flashCookie) { @NonNull @Override public ServerOptions getServerOptions() { - throw new UnsupportedOperationException(); + return serverOptions; + } + + public void setServerOptions(ServerOptions serverOptions) { + this.serverOptions = serverOptions; + services.put(ServerOptions.class, serverOptions); } @NonNull @Override @@ -854,21 +801,9 @@ public Router setCurrentUser(@NonNull Function provider) { return this; } - @NonNull @Override - public Router setContextAsService(boolean contextAsService) { - if (this.contextAsService == contextAsService) { - return this; - } - - this.contextAsService = contextAsService; - - if (contextAsService) { - addPostDispatchInitializer(ContextAsServiceInitializer.INSTANCE); - getServices().put(Context.class, ContextAsServiceInitializer.INSTANCE); - } else { - removePostDispatchInitializer(ContextAsServiceInitializer.INSTANCE); - getServices().put(Context.class, (Provider) null); - } + private Router setContextAsService() { + addPostDispatchInitializer(ContextAsServiceInitializer.INSTANCE); + getServices().put(Context.class, ContextAsServiceInitializer.INSTANCE); return this; } @@ -931,8 +866,6 @@ private void copy(Route src, Route it) { it.setFilter(filter); it.setAfter(after); it.setEncoder(src.getEncoder()); - // it.setReturnType(src.getReturnType()); - it.setHandle(src.getHandle()); it.setProduces(src.getProduces()); it.setConsumes(src.getConsumes()); it.setAttributes(src.getAttributes()); diff --git a/jooby/src/main/java/io/jooby/internal/SessionImpl.java b/jooby/src/main/java/io/jooby/internal/SessionImpl.java index b45f84e07e..ccec6d784d 100644 --- a/jooby/src/main/java/io/jooby/internal/SessionImpl.java +++ b/jooby/src/main/java/io/jooby/internal/SessionImpl.java @@ -14,8 +14,7 @@ import io.jooby.Context; import io.jooby.Session; import io.jooby.SessionStore; -import io.jooby.Value; -import io.jooby.ValueNode; +import io.jooby.value.Value; public class SessionImpl implements Session { @@ -78,21 +77,27 @@ public Session setId(@Nullable String id) { @Override public @NonNull Value get(@NonNull String name) { - return Value.create(ctx, name, attributes.get(name)); + return Value.create(ctx.getValueFactory(), name, attributes.get(name)); } @Override - public @NonNull Session put(@NonNull String name, String value) { + public @NonNull Session put(@NonNull String name, @NonNull String value) { attributes.put(name, value); updateState(); return this; } + public @NonNull Session put(@NonNull String name, Object value) { + attributes.put(name, value.toString()); + return this; + } + @Override - public @NonNull ValueNode remove(@NonNull String name) { - String value = attributes.remove(name); + public @NonNull Value remove(@NonNull String name) { + var value = get(name); + attributes.remove(name); updateState(); - return value == null ? Value.missing(name) : Value.value(ctx, name, value); + return value; } @Override @@ -106,7 +111,7 @@ public Session setId(@Nullable String id) { } @NonNull @Override - public Session setCreationTime(Instant creationTime) { + public Session setCreationTime(@NonNull Instant creationTime) { this.creationTime = creationTime; return this; } @@ -122,7 +127,7 @@ public Session setCreationTime(Instant creationTime) { return this; } - @Override + @NonNull @Override public Session clear() { attributes.clear(); updateState(); @@ -136,7 +141,7 @@ public void destroy() { store(ctx).deleteSession(ctx, this); } - @Override + @NonNull @Override public Session renewId() { store(ctx).renewSessionId(ctx, this); updateState(); diff --git a/jooby/src/main/java/io/jooby/internal/SingleValue.java b/jooby/src/main/java/io/jooby/internal/SingleValue.java index 6fd0c38b2b..306b44db3f 100644 --- a/jooby/src/main/java/io/jooby/internal/SingleValue.java +++ b/jooby/src/main/java/io/jooby/internal/SingleValue.java @@ -5,10 +5,6 @@ */ package io.jooby.internal; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; - import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -18,19 +14,20 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.Context; -import io.jooby.ValueNode; +import io.jooby.value.ConversionHint; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; -public class SingleValue implements ValueNode { +public class SingleValue implements Value { - private final Context ctx; + private final ValueFactory factory; private final String name; - private String value; + private final String value; - public SingleValue(Context ctx, String name, String value) { - this.ctx = ctx; + public SingleValue(ValueFactory factory, String name, String value) { + this.factory = factory; this.name = name; this.value = value; } @@ -41,12 +38,12 @@ public String name() { } @Override - public @NonNull ValueNode get(int index) { - return index == 0 ? this : get(Integer.toString(index)); + public @NonNull Value get(int index) { + return get(Integer.toString(index)); } @Override - public @NonNull ValueNode get(@NonNull String name) { + public @NonNull Value get(@NonNull String name) { return new MissingValue(this.name + "." + name); } @@ -56,7 +53,7 @@ public int size() { } @Override - public String value() { + public @NonNull String value() { return value; } @@ -66,8 +63,8 @@ public String toString() { } @Override - public Iterator iterator() { - return Collections.singletonList(this).iterator(); + public @NonNull Iterator iterator() { + return List.of((Value) this).iterator(); } @NonNull @Override @@ -82,31 +79,31 @@ public Set toSet(@NonNull Class type) { @NonNull @Override public Optional toOptional(@NonNull Class type) { - return Optional.of(to(type)); + return Optional.ofNullable(toNullable(type)); } @NonNull @Override public T to(@NonNull Class type) { - return ctx.convert(this, type); + return factory.convert(type, this); } @Nullable @Override public T toNullable(@NonNull Class type) { - return ctx.convertOrNull(this, type); + return factory.convert(type, this, ConversionHint.Nullable); } @Override public Map> toMultimap() { - return singletonMap(name, singletonList(value)); + return Map.of(name, List.of(value)); } @Override public List toList() { - return singletonList(value); + return List.of(value); } @Override public Set toSet() { - return singleton(value); + return Set.of(value); } } diff --git a/jooby/src/main/java/io/jooby/internal/UrlParser.java b/jooby/src/main/java/io/jooby/internal/UrlParser.java index 74ef40fe62..e103e1cfec 100644 --- a/jooby/src/main/java/io/jooby/internal/UrlParser.java +++ b/jooby/src/main/java/io/jooby/internal/UrlParser.java @@ -13,18 +13,18 @@ import java.nio.charset.CoderResult; import java.nio.charset.StandardCharsets; -import io.jooby.Context; import io.jooby.QueryString; import io.jooby.SneakyThrows; +import io.jooby.value.ValueFactory; public final class UrlParser { private static final char SPACE = 0x20; - public static QueryString queryString(Context ctx, String queryString) { + public static QueryString queryString(ValueFactory valueFactory, String queryString) { if (queryString == null || queryString.length() == 0) { - return new QueryStringValue(ctx, ""); + return new QueryStringValue(valueFactory, ""); } - QueryStringValue result = new QueryStringValue(ctx, "?" + queryString); + QueryStringValue result = new QueryStringValue(valueFactory, "?" + queryString); decodeParams(result, queryString, 0, StandardCharsets.UTF_8, 1024); return result; } diff --git a/jooby/src/main/java/io/jooby/internal/ValueConverters.java b/jooby/src/main/java/io/jooby/internal/ValueConverters.java deleted file mode 100644 index 4057ed5a58..0000000000 --- a/jooby/src/main/java/io/jooby/internal/ValueConverters.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal; - -import java.lang.reflect.Type; -import java.util.*; - -import io.jooby.BeanConverter; -import io.jooby.Router; -import io.jooby.ValueConverter; -import io.jooby.ValueNode; -import io.jooby.internal.converter.BuiltinConverter; -import io.jooby.internal.converter.ReflectiveBeanConverter; -import io.jooby.internal.converter.StringConstructorConverter; -import io.jooby.internal.converter.ValueOfConverter; -import io.jooby.internal.reflect.$Types; - -public class ValueConverters { - - public static List> defaultConverters() { - var converters = new ArrayList>(List.of(BuiltinConverter.values())); - converters.add(new ValueOfConverter()); - converters.add(new StringConstructorConverter()); - return converters; - } - - public static T convert(ValueNode value, Type type, Router router) { - Class rawType = $Types.getRawType(type); - if (List.class.isAssignableFrom(rawType)) { - return (T) Collections.singletonList(convert(value, $Types.parameterizedType0(type), router)); - } - if (Set.class.isAssignableFrom(rawType)) { - return (T) Collections.singleton(convert(value, $Types.parameterizedType0(type), router)); - } - if (Optional.class.isAssignableFrom(rawType)) { - return (T) Optional.ofNullable(convert(value, $Types.parameterizedType0(type), router)); - } - return convert(value, rawType, router, false); - } - - public static T convert(ValueNode value, Class type, Router router, boolean allowEmptyBean) { - if (type == String.class) { - return (T) value.valueOrNull(); - } - if (type == int.class) { - return (T) Integer.valueOf(value.intValue()); - } - if (type == long.class) { - return (T) Long.valueOf(value.longValue()); - } - if (type == float.class) { - return (T) Float.valueOf(value.floatValue()); - } - if (type == double.class) { - return (T) Double.valueOf(value.doubleValue()); - } - if (type == boolean.class) { - return (T) Boolean.valueOf(value.booleanValue()); - } - if (type == byte.class) { - return (T) Byte.valueOf(value.byteValue()); - } - if (Enum.class.isAssignableFrom(type)) { - return (T) enumValue(value, type); - } - // Wrapper - if (type == Integer.class) { - return (T) (value.isMissing() ? null : Integer.valueOf(value.intValue())); - } - if (type == Long.class) { - return (T) (value.isMissing() ? null : Long.valueOf(value.longValue())); - } - if (type == Float.class) { - return (T) (value.isMissing() ? null : Float.valueOf(value.floatValue())); - } - if (type == Double.class) { - return (T) (value.isMissing() ? null : Double.valueOf(value.doubleValue())); - } - if (type == Byte.class) { - return (T) (value.isMissing() ? null : Byte.valueOf(value.byteValue())); - } - - if (value.isSingle()) { - for (ValueConverter converter : router.getConverters()) { - if (converter.supports(type)) { - return (T) converter.convert(value, type); - } - } - } else if (value.isObject()) { - for (BeanConverter converter : router.getBeanConverters()) { - if (converter.supports(type)) { - return (T) converter.convert(value, type); - } - } - } - // Fallback: - ReflectiveBeanConverter reflective = new ReflectiveBeanConverter(); - return (T) reflective.convert(value, type, allowEmptyBean); - } - - private static Object enumValue(ValueNode value, Class type) { - try { - return Enum.valueOf(type, value.value().toUpperCase()); - } catch (IllegalArgumentException x) { - String name = value.value(); - // Fallback: Ignore case: - Set enums = EnumSet.allOf(type); - for (Enum e : enums) { - if (e.name().equalsIgnoreCase(name)) { - return e; - } - } - throw x; - } - } -} diff --git a/jooby/src/main/java/io/jooby/internal/WebSocketSender.java b/jooby/src/main/java/io/jooby/internal/WebSocketSender.java index fe98fdc1cc..f2c98791b0 100644 --- a/jooby/src/main/java/io/jooby/internal/WebSocketSender.java +++ b/jooby/src/main/java/io/jooby/internal/WebSocketSender.java @@ -19,7 +19,7 @@ import io.jooby.MediaType; import io.jooby.StatusCode; import io.jooby.WebSocket; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class WebSocketSender extends ForwardingContext implements DefaultContext { @@ -68,12 +68,12 @@ public Context send(@NonNull ByteBuffer data) { return this; } - @NonNull @Override - public Context send(@NonNull DataBuffer data) { + @Override + public Context send(@NonNull Output output) { if (binary) { - ws.sendBinary(data, callback); + ws.sendBinary(output, callback); } else { - ws.send(data, callback); + ws.send(output, callback); } return this; } diff --git a/jooby/src/main/java/io/jooby/internal/converter/FromStringConverter.java b/jooby/src/main/java/io/jooby/internal/converter/FromStringConverter.java deleted file mode 100644 index f275921046..0000000000 --- a/jooby/src/main/java/io/jooby/internal/converter/FromStringConverter.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.converter; - -import java.lang.reflect.Executable; -import java.lang.reflect.InvocationTargetException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiFunction; - -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.SneakyThrows; -import io.jooby.Value; -import io.jooby.ValueConverter; - -public abstract class FromStringConverter - implements ValueConverter, BiFunction, E, E> { - - private final Map, E> cache = new ConcurrentHashMap<>(); - - @Override - public boolean supports(@NonNull Class type) { - return cache.compute(type, this) != null; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - try { - return invoke(cache.compute(type, this), value.value()); - } catch (InvocationTargetException x) { - throw SneakyThrows.propagate(x.getTargetException()); - } catch (IllegalAccessException | InstantiationException x) { - throw SneakyThrows.propagate(x); - } - } - - @Override - public E apply(Class type, E executable) { - if (executable == null) { - return mappingMethod(type); - } - return executable; - } - - protected abstract Object invoke(E executable, String value) - throws InvocationTargetException, IllegalAccessException, InstantiationException; - - protected abstract E mappingMethod(Class type); -} diff --git a/jooby/src/main/java/io/jooby/internal/converter/StringConstructorConverter.java b/jooby/src/main/java/io/jooby/internal/converter/StringConstructorConverter.java deleted file mode 100644 index 24174d8e9b..0000000000 --- a/jooby/src/main/java/io/jooby/internal/converter/StringConstructorConverter.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.converter; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - -public class StringConstructorConverter extends FromStringConverter> { - @Override - protected Object invoke(Constructor executable, String value) - throws InvocationTargetException, IllegalAccessException, InstantiationException { - return executable.newInstance(value); - } - - @Override - protected Constructor mappingMethod(Class type) { - try { - return type.getDeclaredConstructor(String.class); - } catch (NoSuchMethodException e) { - return null; - } - } -} diff --git a/jooby/src/main/java/io/jooby/internal/converter/ValueOfConverter.java b/jooby/src/main/java/io/jooby/internal/converter/ValueOfConverter.java deleted file mode 100644 index 11577ccaa2..0000000000 --- a/jooby/src/main/java/io/jooby/internal/converter/ValueOfConverter.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.converter; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - -public class ValueOfConverter extends FromStringConverter { - - @Override - protected Object invoke(Method executable, String value) - throws InvocationTargetException, IllegalAccessException { - return executable.invoke(null, value); - } - - @Override - protected Method mappingMethod(Class type) { - try { - Method valueOf = type.getDeclaredMethod("valueOf", String.class); - if (Modifier.isStatic(valueOf.getModifiers()) && Modifier.isPublic(valueOf.getModifiers())) { - return valueOf; - } - return null; - } catch (NoSuchMethodException x) { - return null; - } - } -} diff --git a/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java b/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java index c60139fae5..e7e600c56d 100644 --- a/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java +++ b/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java @@ -11,11 +11,10 @@ import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.MessageEncoder; import io.jooby.Route; import io.jooby.Sender; import io.jooby.Server; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class ChunkedSubscriber implements Flow.Subscriber { @@ -39,12 +38,12 @@ public void onSubscribe(Flow.Subscription subscription) { public void onNext(Object item) { try { - Route route = ctx.getRoute(); - Route.After after = route.getAfter(); + var route = ctx.getRoute(); + var after = route.getAfter(); if (after != null) { after.apply(ctx, item, null); } - MessageEncoder encoder = route.getEncoder(); + var encoder = route.getEncoder(); var data = encoder.encode(ctx, item); if (responseType == null) { @@ -117,10 +116,10 @@ public void onComplete() { sender().close(); } - private static DataBuffer prepend(Context ctx, DataBuffer data, byte c) { - var buffer = ctx.getBufferFactory().allocateBuffer(); + private static Output prepend(Context ctx, Output data, byte c) { + var buffer = ctx.getOutputFactory().newComposite(); buffer.write(c); - buffer.write(data); + data.transferTo(buffer::write); return buffer; } diff --git a/jooby/src/main/java/io/jooby/internal/handler/WebSocketHandler.java b/jooby/src/main/java/io/jooby/internal/handler/WebSocketHandler.java index 2ca1e44e22..bec0afdc75 100644 --- a/jooby/src/main/java/io/jooby/internal/handler/WebSocketHandler.java +++ b/jooby/src/main/java/io/jooby/internal/handler/WebSocketHandler.java @@ -7,17 +7,20 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; -import io.jooby.Route; import io.jooby.StatusCode; import io.jooby.WebSocket; -public class WebSocketHandler implements Route.Handler { +public class WebSocketHandler implements WebSocket.Handler { private WebSocket.Initializer handler; public WebSocketHandler(WebSocket.Initializer handler) { this.handler = handler; } + public WebSocket.Initializer getInitializer() { + return handler; + } + @NonNull @Override public Object apply(@NonNull Context ctx) { boolean webSocket = ctx.header("Upgrade").value("").equalsIgnoreCase("WebSocket"); diff --git a/jooby/src/main/java/io/jooby/internal/output/CompositeOutput.java b/jooby/src/main/java/io/jooby/internal/output/CompositeOutput.java new file mode 100644 index 0000000000..dfd6241587 --- /dev/null +++ b/jooby/src/main/java/io/jooby/internal/output/CompositeOutput.java @@ -0,0 +1,91 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.output; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.SneakyThrows; +import io.jooby.output.BufferedOutput; + +/** + * Merge buffers into one. + * + * @author edgar + * @since 4.0.0 + */ +public class CompositeOutput implements BufferedOutput { + private final List chunks = new ArrayList<>(); + private int size = 0; + + @Override + public int size() { + return size; + } + + @Override + public BufferedOutput write(byte b) { + addChunk(ByteBuffer.wrap(new byte[] {b})); + return this; + } + + @Override + public BufferedOutput write(byte[] source) { + addChunk(ByteBuffer.wrap(source)); + return this; + } + + @Override + public BufferedOutput write(byte[] source, int offset, int length) { + addChunk(ByteBuffer.wrap(source, offset, length)); + return this; + } + + @Override + public BufferedOutput clear() { + chunks.forEach(ByteBuffer::clear); + chunks.clear(); + return this; + } + + /** + * Expensive operation. + * + * @return A byte buffer. + */ + @Override + public ByteBuffer asByteBuffer() { + var buf = ByteBuffer.allocate(size); + for (ByteBuffer chunk : chunks) { + buf.put(chunk.duplicate()); + } + buf.flip(); + return buf; + } + + @Override + public void transferTo(@NonNull SneakyThrows.Consumer consumer) { + chunks.forEach(consumer); + } + + @Override + public String toString() { + return "chunks=" + chunks.size() + ", size=" + size; + } + + @Override + public void send(Context ctx) { + ctx.send(chunks.toArray(new ByteBuffer[0])); + } + + private void addChunk(ByteBuffer chunk) { + chunks.add(chunk); + size += chunk.remaining(); + } +} diff --git a/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java new file mode 100644 index 0000000000..aeea74deec --- /dev/null +++ b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java @@ -0,0 +1,57 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.output; + +import java.io.IOException; +import java.io.OutputStream; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.output.BufferedOutput; +import io.jooby.output.Output; + +/** + * An {@link OutputStream} that writes to a {@link Output}. + * + * @see BufferedOutput#asOutputStream() + */ +public class OutputOutputStream extends OutputStream { + + private final BufferedOutput output; + + private boolean closed; + + public OutputOutputStream(@NonNull BufferedOutput output) { + this.output = output; + } + + @Override + public void write(int b) throws IOException { + checkClosed(); + this.output.write((byte) b); + } + + @Override + public void write(@NonNull byte[] b, int off, int len) throws IOException { + checkClosed(); + if (len > 0) { + this.output.write(b, off, len); + } + } + + @Override + public void close() throws IOException { + if (this.closed) { + return; + } + this.closed = true; + } + + private void checkClosed() throws IOException { + if (this.closed) { + throw new IOException("OutputStream is closed"); + } + } +} diff --git a/jooby/src/main/java/io/jooby/buffer/DataBufferWriter.java b/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java similarity index 69% rename from jooby/src/main/java/io/jooby/buffer/DataBufferWriter.java rename to jooby/src/main/java/io/jooby/internal/output/OutputWriter.java index 308fc2b6b5..c0279a1eba 100644 --- a/jooby/src/main/java/io/jooby/buffer/DataBufferWriter.java +++ b/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.buffer; +package io.jooby.internal.output; import java.io.IOException; import java.io.Writer; @@ -11,14 +11,15 @@ import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.output.BufferedOutput; -class DataBufferWriter extends Writer { - private final DataBuffer dataBuffer; +public class OutputWriter extends Writer { + private final BufferedOutput output; private final Charset charset; private boolean closed; - public DataBufferWriter(DataBuffer dataBuffer, Charset charset) { - this.dataBuffer = dataBuffer; + public OutputWriter(@NonNull BufferedOutput output, @NonNull Charset charset) { + this.output = output; this.charset = charset; } @@ -36,26 +37,26 @@ public void write(@NonNull char[] source) throws IOException { @Override public void write(@NonNull char[] source, int off, int len) throws IOException { checkClosed(); - dataBuffer.write(CharBuffer.wrap(source, off, len), charset); + output.write(CharBuffer.wrap(source, off, len), charset); } @Override public void write(@NonNull String source) throws IOException { checkClosed(); - dataBuffer.write(CharBuffer.wrap(source), charset); + output.write(source, charset); } @Override public void write(@NonNull String source, int off, int len) throws IOException { checkClosed(); - dataBuffer.write(CharBuffer.wrap(source, off, off + len), charset); + output.write(CharBuffer.wrap(source, off, off + len), charset); } @Override public void flush() throws IOException {} @Override - public void close() { + public void close() throws IOException { if (this.closed) { return; } @@ -64,7 +65,7 @@ public void close() { private void checkClosed() throws IOException { if (this.closed) { - throw new IOException("DataBufferWriter is closed"); + throw new IOException("Writer is closed"); } } } diff --git a/jooby/src/main/java/io/jooby/internal/output/StaticOutput.java b/jooby/src/main/java/io/jooby/internal/output/StaticOutput.java new file mode 100644 index 0000000000..2dc2620875 --- /dev/null +++ b/jooby/src/main/java/io/jooby/internal/output/StaticOutput.java @@ -0,0 +1,55 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.output; + +import java.nio.ByteBuffer; +import java.util.function.Supplier; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.SneakyThrows; +import io.jooby.output.Output; + +public class StaticOutput implements Output { + + private final int size; + private final Supplier provider; + + public StaticOutput(int size, Supplier provider) { + this.size = size; + this.provider = provider; + } + + public StaticOutput(ByteBuffer byteBuffer) { + this(byteBuffer.remaining(), () -> byteBuffer); + } + + @Override + public int size() { + return size; + } + + @Override + public void transferTo(@NonNull SneakyThrows.Consumer consumer) { + consumer.accept(asByteBuffer()); + } + + @Override + public ByteBuffer asByteBuffer() { + var buffer = provider.get(); + return buffer.slice().asReadOnlyBuffer(); + } + + @Override + public String toString() { + return "size=" + size(); + } + + @Override + public void send(Context ctx) { + ctx.send(provider.get()); + } +} diff --git a/jooby/src/main/java/io/jooby/internal/output/WrappedOutput.java b/jooby/src/main/java/io/jooby/internal/output/WrappedOutput.java new file mode 100644 index 0000000000..10e8b18297 --- /dev/null +++ b/jooby/src/main/java/io/jooby/internal/output/WrappedOutput.java @@ -0,0 +1,49 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.output; + +import java.nio.ByteBuffer; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.SneakyThrows; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; + +/** This is part of {@link OutputFactory#getContextFactory()}. */ +public class WrappedOutput implements Output { + + private final ByteBuffer buffer; + + public WrappedOutput(ByteBuffer buffer) { + this.buffer = buffer; + } + + @Override + public int size() { + return buffer.remaining(); + } + + @Override + public void transferTo(@NonNull SneakyThrows.Consumer consumer) { + consumer.accept(asByteBuffer()); + } + + @Override + public ByteBuffer asByteBuffer() { + return buffer.slice().asReadOnlyBuffer(); + } + + @Override + public String toString() { + return "size=" + size(); + } + + @Override + public void send(Context ctx) { + ctx.send(buffer); + } +} diff --git a/jooby/src/main/java/io/jooby/internal/output/package-info.java b/jooby/src/main/java/io/jooby/internal/output/package-info.java new file mode 100644 index 0000000000..881ac8de5c --- /dev/null +++ b/jooby/src/main/java/io/jooby/internal/output/package-info.java @@ -0,0 +1,4 @@ +@ReturnValuesAreNonnullByDefault +package io.jooby.internal.output; + +import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault; diff --git a/jooby/src/main/java/io/jooby/output/BufferedOutput.java b/jooby/src/main/java/io/jooby/output/BufferedOutput.java new file mode 100644 index 0000000000..2c5ec732ef --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/BufferedOutput.java @@ -0,0 +1,158 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.output; + +import java.io.OutputStream; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.internal.output.OutputOutputStream; +import io.jooby.internal.output.OutputWriter; + +/** + * Buffered output. + * + *

The capacity of a {@code BufferedOutput} is expanded on demand, similar to {@code + * StringBuilder}. + * + *

The main purpose of the {@code BufferedOutput} abstraction is to provide a convenient wrapper + * around {@link ByteBuffer} which is similar to Netty's {@code ByteBuf} but can also be used on + * non-Netty platforms. + * + * @author edgar + * @since 4.0.0 + */ +public interface BufferedOutput extends Output { + /** + * This output as an output stream. Changes made to the output stream are reflected in this + * output. + * + * @return An output stream. + */ + default OutputStream asOutputStream() { + return new OutputOutputStream(this); + } + + /** + * This output as a writer. Changes made to the writer are reflected in this output. Bytes are + * written using the {@link StandardCharsets#UTF_8} charset. + * + * @return An output stream. + */ + default Writer asWriter() { + return asWriter(StandardCharsets.UTF_8); + } + + /** + * This output as a writer. Changes made to the writer are reflected in this output. + * + * @param charset Charset to use. + * @return An output stream. + */ + default Writer asWriter(@NonNull Charset charset) { + return new OutputWriter(this, charset); + } + + /** + * Write a single byte into this buffer at the current writing position. + * + * @param b the byte to be written + * @return this output + */ + BufferedOutput write(byte b); + + /** + * Write the given source into this buffer, starting at the current writing position of this + * buffer. + * + * @param source the bytes to be written into this buffer + * @return this output + */ + BufferedOutput write(byte[] source); + + /** + * Write at most {@code length} bytes of the given source into this buffer, starting at the + * current writing position of this buffer. + * + * @param source the bytes to be written into this buffer + * @param offset the index within {@code source} to start writing from + * @param length the maximum number of bytes to be written from {@code source} + * @return this output + */ + BufferedOutput write(byte[] source, int offset, int length); + + /** + * Write the given {@code String} using {@code UTF-8}, starting at the current writing position. + * + * @param source the char sequence to write into this buffer + * @return this output + */ + default BufferedOutput write(@NonNull String source) { + return write(source, StandardCharsets.UTF_8); + } + + /** + * Write the given {@code String} using the given {@code Charset}, starting at the current writing + * position. + * + * @param source the char sequence to write into this buffer + * @param charset the charset to encode the char sequence with + * @return this output + */ + default BufferedOutput write(@NonNull String source, @NonNull Charset charset) { + if (!source.isEmpty()) { + return write(source.getBytes(charset)); + } + return this; + } + + /** + * Write the given source into this buffer, starting at the current writing position of this + * buffer. + * + * @param source the bytes to be written into this buffer + * @return this output + */ + default BufferedOutput write(@NonNull ByteBuffer source) { + if (source.hasArray()) { + return write(source.array(), source.arrayOffset() + source.position(), source.remaining()); + } else { + var bytes = new byte[source.remaining()]; + source.get(bytes); + return write(bytes); + } + } + + /** + * Write the given source into this buffer, starting at the current writing position of this + * buffer. + * + * @param source the bytes to be written into this buffer + * @param charset Charset. + * @return this output + */ + default BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { + if (!source.isEmpty()) { + return write(charset.encode(source)); + } + return this; + } + + /** + * Clears this buffer. The position is set to zero, the limit is set to the capacity, and the mark + * is discarded. + * + *

This method does not erase the data in the buffer, but it is named as if it did because it + * will most often be used in situations in which that might as well be the case. + * + * @return This output. + */ + BufferedOutput clear(); +} diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferedOutput.java b/jooby/src/main/java/io/jooby/output/ByteBufferedOutput.java new file mode 100644 index 0000000000..e3448a8b4a --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/ByteBufferedOutput.java @@ -0,0 +1,192 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.output; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.List; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.SneakyThrows; + +/** + * Default implementation of {@link BufferedOutput}. + * + * @author edgar + * @since 4.0.0 + */ +public class ByteBufferedOutput implements BufferedOutput { + private static final int MAX_CAPACITY = Integer.MAX_VALUE; + + private static final int CAPACITY_THRESHOLD = 1024 * 1024 * 4; + + private ByteBuffer buffer; + + private int readPosition; + + private int writePosition; + + public ByteBufferedOutput(boolean direct, int capacity) { + this.buffer = allocate(capacity, direct); + } + + @Override + public int size() { + return this.writePosition - this.readPosition; + } + + @Override + public void transferTo(@NonNull SneakyThrows.Consumer consumer) { + consumer.accept(asByteBuffer()); + } + + @Override + public @NonNull Iterator iterator() { + return List.of(asByteBuffer()).iterator(); + } + + @Override + public @NonNull ByteBuffer asByteBuffer() { + return this.buffer.slice(this.readPosition, size()).asReadOnlyBuffer(); + } + + @Override + public BufferedOutput write(byte b) { + ensureWritable(1); + this.buffer.put(this.writePosition, b); + this.writePosition += 1; + return this; + } + + @Override + public BufferedOutput write(byte[] source) { + return write(source, 0, source.length); + } + + @Override + public BufferedOutput write(byte[] source, int offset, int length) { + ensureWritable(length); + + var tmp = this.buffer.duplicate(); + int limit = this.writePosition + length; + tmp.clear().position(this.writePosition).limit(limit); + tmp.put(source, offset, length); + + this.writePosition += length; + return this; + } + + @Override + public BufferedOutput write(@NonNull ByteBuffer source) { + ensureWritable(source.remaining()); + var length = source.remaining(); + var tmp = this.buffer.duplicate(); + var limit = this.writePosition + source.remaining(); + tmp.clear().position(this.writePosition).limit(limit); + tmp.put(source); + this.writePosition += length; + return this; + } + + @Override + public BufferedOutput clear() { + this.writePosition = 0; + this.readPosition = 0; + this.buffer.clear(); + return this; + } + + @Override + public void send(Context ctx) { + ctx.send(this.buffer.slice(this.readPosition, size())); + } + + @Override + public String toString() { + return "readPosition=" + + this.readPosition + + ", writePosition=" + + this.writePosition + + ", size=" + + this.size() + + ", capacity=" + + this.buffer.capacity(); + } + + private int writableByteCount() { + return this.buffer.capacity() - this.writePosition; + } + + /** Calculate the capacity of the buffer. */ + private int calculateCapacity(int neededCapacity) { + if (neededCapacity == CAPACITY_THRESHOLD) { + return CAPACITY_THRESHOLD; + } else if (neededCapacity > CAPACITY_THRESHOLD) { + int newCapacity = neededCapacity / CAPACITY_THRESHOLD * CAPACITY_THRESHOLD; + if (newCapacity > MAX_CAPACITY - CAPACITY_THRESHOLD) { + newCapacity = MAX_CAPACITY; + } else { + newCapacity += CAPACITY_THRESHOLD; + } + return newCapacity; + } else { + int newCapacity = 64; + while (newCapacity < neededCapacity) { + newCapacity <<= 1; + } + return newCapacity; + } + } + + private void setCapacity(int newCapacity) { + if (newCapacity < 0) { + throw new IllegalArgumentException( + String.format("'newCapacity' %d must be 0 or higher", newCapacity)); + } + var readPosition = this.readPosition; + var writePosition = this.writePosition; + var oldCapacity = this.buffer.capacity(); + + if (newCapacity > oldCapacity) { + var oldBuffer = this.buffer; + var newBuffer = allocate(newCapacity, oldBuffer.isDirect()); + oldBuffer.position(0).limit(oldBuffer.capacity()); + newBuffer.position(0).limit(oldBuffer.capacity()); + newBuffer.put(oldBuffer); + newBuffer.clear(); + this.buffer = newBuffer; + } else if (newCapacity < oldCapacity) { + var oldBuffer = this.buffer; + var newBuffer = allocate(newCapacity, oldBuffer.isDirect()); + if (readPosition < newCapacity) { + if (writePosition > newCapacity) { + writePosition = newCapacity; + this.writePosition = writePosition; + } + oldBuffer.position(readPosition).limit(writePosition); + newBuffer.position(readPosition).limit(writePosition); + newBuffer.put(oldBuffer); + newBuffer.clear(); + } else { + this.readPosition = newCapacity; + this.writePosition = newCapacity; + } + this.buffer = newBuffer; + } + } + + private void ensureWritable(int length) { + if (length > writableByteCount()) { + int newCapacity = calculateCapacity(this.writePosition + length); + setCapacity(newCapacity); + } + } + + private static ByteBuffer allocate(int capacity, boolean direct) { + return direct ? ByteBuffer.allocateDirect(capacity) : ByteBuffer.allocate(capacity); + } +} diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferedOutputFactory.java b/jooby/src/main/java/io/jooby/output/ByteBufferedOutputFactory.java new file mode 100644 index 0000000000..d967cbe845 --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/ByteBufferedOutputFactory.java @@ -0,0 +1,101 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.output; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.internal.output.CompositeOutput; +import io.jooby.internal.output.StaticOutput; +import io.jooby.internal.output.WrappedOutput; + +/** + * An output factory backed by {@link ByteBuffer}. + * + * @author edgar + * @since 4.0.0 + */ +public class ByteBufferedOutputFactory implements OutputFactory { + + private static class ContextOutputFactory extends ByteBufferedOutputFactory { + public ContextOutputFactory(OutputOptions options) { + super(options); + } + + @Override + public Output wrap(@NonNull ByteBuffer buffer) { + return new WrappedOutput(buffer); + } + + @Override + public Output wrap(@NonNull String value, @NonNull Charset charset) { + return new WrappedOutput(charset.encode(value)); + } + + @Override + public Output wrap(@NonNull byte[] bytes) { + return new WrappedOutput(ByteBuffer.wrap(bytes)); + } + + @Override + public Output wrap(@NonNull byte[] bytes, int offset, int length) { + return new WrappedOutput(ByteBuffer.wrap(bytes, offset, length)); + } + } + + private OutputOptions options; + + public ByteBufferedOutputFactory(OutputOptions options) { + this.options = options; + } + + @Override + public OutputOptions getOptions() { + return options; + } + + @Override + public OutputFactory setOptions(@NonNull OutputOptions options) { + this.options = options; + return this; + } + + @Override + public BufferedOutput allocate(boolean direct, int size) { + return new ByteBufferedOutput(direct, size); + } + + @Override + public BufferedOutput newComposite() { + return new CompositeOutput(); + } + + @Override + public Output wrap(@NonNull String value, @NonNull Charset charset) { + return new StaticOutput(ByteBuffer.wrap(value.getBytes(charset))); + } + + @Override + public Output wrap(@NonNull ByteBuffer buffer) { + return new StaticOutput(buffer.remaining(), () -> buffer); + } + + @Override + public Output wrap(@NonNull byte[] bytes) { + return new StaticOutput(bytes.length, () -> ByteBuffer.wrap(bytes)); + } + + @Override + public Output wrap(@NonNull byte[] bytes, int offset, int length) { + return new StaticOutput(length - offset, () -> ByteBuffer.wrap(bytes, offset, length)); + } + + @Override + public OutputFactory getContextFactory() { + return new ContextOutputFactory(options); + } +} diff --git a/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java b/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java new file mode 100644 index 0000000000..d843f65f15 --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.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.output; + +import java.nio.ByteBuffer; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Delegate/forwarding class for output factory. + * + * @author edgar + * @since 4.0.0 + */ +public abstract class ForwardingOutputFactory implements OutputFactory { + + protected final OutputFactory delegate; + + public ForwardingOutputFactory(@NonNull OutputFactory delegate) { + this.delegate = delegate; + } + + @Override + public OutputOptions getOptions() { + return delegate.getOptions(); + } + + @Override + public OutputFactory setOptions(@NonNull OutputOptions options) { + delegate.setOptions(options); + return this; + } + + @Override + public BufferedOutput allocate(int size) { + return delegate.allocate(size); + } + + @Override + public BufferedOutput allocate(boolean direct, int size) { + return delegate.allocate(direct, size); + } + + @Override + public BufferedOutput newComposite() { + return delegate.newComposite(); + } + + @Override + public Output wrap(@NonNull ByteBuffer buffer) { + return delegate.wrap(buffer); + } + + @Override + public Output wrap(@NonNull byte[] bytes) { + return delegate.wrap(bytes); + } + + @Override + public Output wrap(@NonNull byte[] bytes, int offset, int length) { + return delegate.wrap(bytes, offset, length); + } +} diff --git a/jooby/src/main/java/io/jooby/output/Output.java b/jooby/src/main/java/io/jooby/output/Output.java new file mode 100644 index 0000000000..3bee9efb5b --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/Output.java @@ -0,0 +1,63 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.output; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.SneakyThrows; + +/** + * Output used to support multiple implementations like byte array, byte buffer, netty buffers. + * + * @author edgar + * @since 4.0.0 + * @see BufferedOutput + */ +public interface Output { + + /** + * A read-only view as {@link ByteBuffer}. + * + * @return A read-only byte buffer. + */ + ByteBuffer asByteBuffer(); + + /** + * Transfers the entire buffered output (one or multiple buffers) to a consumer. {@link + * ByteBuffer} are read-only. + * + * @param consumer Consumer. + */ + void transferTo(@NonNull SneakyThrows.Consumer consumer); + + /** + * An iterator over read-only byte buffers. + * + * @return An iterator over read-only byte buffers. + */ + default Iterator iterator() { + var list = new ArrayList(); + transferTo(list::add); + return list.iterator(); + } + + /** + * Total size in number of bytes of the output. + * + * @return Total size in number of bytes of the output. + */ + int size(); + + /** + * Send the output to the client. + * + * @param ctx Context. + */ + void send(io.jooby.Context ctx); +} diff --git a/jooby/src/main/java/io/jooby/output/OutputFactory.java b/jooby/src/main/java/io/jooby/output/OutputFactory.java new file mode 100644 index 0000000000..c7a60177ff --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/OutputFactory.java @@ -0,0 +1,128 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.output; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Factory class for {@link Output}. + * + * @author edgar + * @since 4.0.0 + */ +public interface OutputFactory { + + /** + * Default output factory, backed by {@link ByteBuffer}. + * + * @return Default output factory. + */ + static OutputFactory create(@NonNull OutputOptions options) { + return new ByteBufferedOutputFactory(options); + } + + /** + * Default output factory, backed by {@link ByteBuffer}. + * + * @return Default output factory. + */ + static OutputFactory create() { + return create(new OutputOptions()); + } + + OutputOptions getOptions(); + + OutputFactory setOptions(@NonNull OutputOptions options); + + /** + * Creates a new byte buffered output. + * + * @param direct True for direct buffers. + * @param size Output size. + * @return A byte buffered output. + */ + BufferedOutput allocate(boolean direct, int size); + + /** + * Creates a new byte buffered output. + * + * @param size Output size. + * @return A byte buffered output. + */ + default BufferedOutput allocate(int size) { + return allocate(getOptions().isDirectBuffers(), size); + } + + default BufferedOutput allocate() { + return allocate(getOptions().isDirectBuffers(), getOptions().getSize()); + } + + /** + * A virtual buffer which shows multiple buffers as a single merged buffer. Useful for chunk of + * data. + * + * @return A new composite buffer. + */ + BufferedOutput newComposite(); + + /** + * Readonly buffer created from string utf-8 bytes. + * + * @param value String. + * @return Readonly buffer. + */ + default Output wrap(String value) { + return wrap(value, StandardCharsets.UTF_8); + } + + /** + * Readonly buffer created from string. + * + * @param value String. + * @param charset Charset to use. + * @return Readonly buffer. + */ + default Output wrap(@NonNull String value, @NonNull Charset charset) { + return wrap(value.getBytes(charset)); + } + + /** + * Readonly buffer created from buffer. + * + * @param buffer Input buffer. + * @return Readonly buffer. + */ + Output wrap(@NonNull ByteBuffer buffer); + + /** + * Readonly buffer created from byte array. + * + * @param bytes Byte array. + * @return Readonly buffer. + */ + Output wrap(@NonNull byte[] bytes); + + /** + * Readonly buffer created from byte array. + * + * @param bytes Byte array. + * @param offset Array offset. + * @param length Length. + * @return Readonly buffer. + */ + Output wrap(@NonNull byte[] bytes, int offset, int length); + + /** + * Special implementation when output factory is requested from {@link io.jooby.Context}. + * + * @return Same or custom implementation. + */ + OutputFactory getContextFactory(); +} diff --git a/jooby/src/main/java/io/jooby/output/OutputOptions.java b/jooby/src/main/java/io/jooby/output/OutputOptions.java new file mode 100644 index 0000000000..b7d0b506c1 --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/OutputOptions.java @@ -0,0 +1,84 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.output; + +public class OutputOptions { + private int size; + + private boolean directBuffers; + + public OutputOptions() { + long maxMemory = Runtime.getRuntime().maxMemory(); + // smaller than 64mb of ram we use 512b buffers + if (maxMemory < 64 * 1024 * 1024) { + // use 512b buffers + directBuffers = false; + size = 512; + // 128mb + } else if (maxMemory < 128 * 1024 * 1024) { + // use 1k buffers + directBuffers = true; + size = 1024; + } else if (maxMemory < 512 * 1024 * 1024) { + // use 4k buffers + directBuffers = true; + size = 4096; + } else { + // use 16k buffers for best performance + // as 16k is generally the max amount of data that can be sent in a single write() call + directBuffers = true; + size = + 1024 * 16 - 20; // the 20 is to allow some space for protocol headers, see UNDERTOW-1209 + } + } + + /** + * Heap buffer of 512kb initial size. + * + * @return Heap buffer of 512kb initial size. + */ + public static OutputOptions small() { + return new OutputOptions().setDirectBuffers(false).setSize(512); + } + + public static OutputOptions defaults() { + return new OutputOptions(); + } + + /** + * Default and initial buffer size. + * + * @return Buffer size. + */ + public int getSize() { + return size; + } + + /** + * Set default and initial buffer size. + * + * @param size The default initial buffer size. + * @return This options. + */ + public OutputOptions setSize(int size) { + this.size = size; + return this; + } + + public boolean isDirectBuffers() { + return directBuffers; + } + + public OutputOptions setDirectBuffers(boolean directBuffers) { + this.directBuffers = directBuffers; + return this; + } + + @Override + public String toString() { + return "{size: " + size + ", direct: " + directBuffers + '}'; + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/buffer/package-info.java b/jooby/src/main/java/io/jooby/output/package-info.java similarity index 67% rename from modules/jooby-netty/src/main/java/io/jooby/netty/buffer/package-info.java rename to jooby/src/main/java/io/jooby/output/package-info.java index e892a0274d..6dbbe0ce94 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/buffer/package-info.java +++ b/jooby/src/main/java/io/jooby/output/package-info.java @@ -1,2 +1,2 @@ @edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault -package io.jooby.netty.buffer; +package io.jooby.output; diff --git a/jooby/src/main/java/io/jooby/validation/ValidationContext.java b/jooby/src/main/java/io/jooby/validation/ValidationContext.java index 90599b2b91..f242a79691 100644 --- a/jooby/src/main/java/io/jooby/validation/ValidationContext.java +++ b/jooby/src/main/java/io/jooby/validation/ValidationContext.java @@ -15,6 +15,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.*; +import io.jooby.value.ConversionHint; +import io.jooby.value.Value; /** * Wrap a context and run {@link BeanValidator#validate(Context, Object)} over HTTP request objects. @@ -24,23 +26,28 @@ */ public class ValidationContext extends ForwardingContext { - public static class ValidatedValue extends ForwardingValueNode { + public static class ValidatedValue extends ForwardingValue { protected final Context ctx; - public ValidatedValue(Context ctx, ValueNode delegate) { + public ValidatedValue(Context ctx, Value delegate) { super(delegate); this.ctx = ctx; } @NonNull @Override public T to(@NonNull Class type) { - // Call nullable version to let bean validator to run - return BeanValidator.apply(ctx, super.toNullable(type)); + return validate(type); + } + + protected T validate(@NonNull Class type) { + // Call empty version to let bean validator to run + return BeanValidator.apply( + ctx, ctx.getValueFactory().convert(type, this, ConversionHint.Empty)); } @Nullable @Override public T toNullable(@NonNull Class type) { - return BeanValidator.apply(ctx, super.toNullable(type)); + return validate(type); } @NonNull @Override @@ -101,6 +108,11 @@ public ValidatedQueryString(Context ctx, QueryString delegate) { super(ctx, delegate); } + @Override + public @NonNull T toEmpty(@NonNull Class type) { + return validate(type); + } + @NonNull @Override public String queryString() { return ((QueryString) delegate).queryString(); @@ -113,7 +125,7 @@ public ValidatedFormdata(Context ctx, Formdata delegate) { } @Override - public void put(@NonNull String path, @NonNull ValueNode value) { + public void put(@NonNull String path, @NonNull Value value) { ((Formdata) delegate).put(path, value); } @@ -168,7 +180,7 @@ public T body(@NonNull Class type) { } @NonNull @Override - public ValueNode path() { + public Value path() { return new ValidatedValue(ctx, super.path()); } @@ -179,7 +191,7 @@ public Body body() { @NonNull @Override public T query(@NonNull Class type) { - return query().to(type); + return query().toEmpty(type); } @NonNull @Override @@ -198,7 +210,7 @@ public Formdata form() { } @NonNull @Override - public ValueNode header() { + public Value header() { return new ValidatedValue(ctx, super.header()); } } diff --git a/jooby/src/main/java/io/jooby/value/ConversionHint.java b/jooby/src/main/java/io/jooby/value/ConversionHint.java new file mode 100644 index 0000000000..c9c62cb1af --- /dev/null +++ b/jooby/src/main/java/io/jooby/value/ConversionHint.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.value; + +/** + * Instructs how a {@link Value} must be converted to the requested type. The hint is applied on all + * the built-in converters. Custom converters might or might not follow the conversion hint. + * + * @author edgar + * @since 4.0.0 + */ +public enum ConversionHint { + /** + * Always produces an instance of the required type and make sure at least one value matches the + * output type as property or constructor argument. If nothing matches a {@link + * io.jooby.exception.TypeMismatchException} will be thrown. + */ + Strict, + /** + * If no value matches the output type property or constructor argument, this produces a + * null output. + */ + Nullable, + /** + * Produces an instance of the required type even if no value matches the output property or + * constructor argument. + */ + Empty, +} diff --git a/jooby/src/main/java/io/jooby/value/Converter.java b/jooby/src/main/java/io/jooby/value/Converter.java new file mode 100644 index 0000000000..19aa4ac002 --- /dev/null +++ b/jooby/src/main/java/io/jooby/value/Converter.java @@ -0,0 +1,29 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.value; + +import java.lang.reflect.Type; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Value converter for values that come from config, query, path, form, path parameters into more + * specific type. + * + * @author edgar. + * @since 4.0.0 + */ +public interface Converter { + /** + * Convert a {@link Value} specific type. + * + * @param type Requested type. + * @param value Value value. + * @param hint Requested hint. + * @return Converted value. + */ + Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint); +} diff --git a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java b/jooby/src/main/java/io/jooby/value/ReflectiveBeanConverter.java similarity index 52% rename from jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java rename to jooby/src/main/java/io/jooby/value/ReflectiveBeanConverter.java index 207487b963..933051068a 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java +++ b/jooby/src/main/java/io/jooby/value/ReflectiveBeanConverter.java @@ -3,105 +3,136 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.internal.converter; +package io.jooby.value; import static io.jooby.SneakyThrows.propagate; -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Parameter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.*; +import java.util.*; import java.util.function.Consumer; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.FileUpload; import io.jooby.Formdata; import io.jooby.Usage; -import io.jooby.Value; -import io.jooby.ValueNode; -import io.jooby.annotation.EmptyBean; import io.jooby.exception.BadRequestException; import io.jooby.exception.ProvisioningException; +import io.jooby.exception.TypeMismatchException; import io.jooby.internal.reflect.$Types; import jakarta.inject.Inject; import jakarta.inject.Named; -public class ReflectiveBeanConverter { +/** + * Creates an object from {@link Value}. Value might come from HTTP Context (Query, Path, Form, + * etc.) or from configuration value. + * + *

This is the fallback/default converter for a JavaBeans object. + * + * @author edgar + * @since 1.0.0 + */ +public class ReflectiveBeanConverter implements Converter { private record Setter(Method method, Object arg) { - public void invoke(Object instance) throws InvocationTargetException, IllegalAccessException { - method.invoke(instance, arg); + public void invoke(MethodHandles.Lookup lookup, Object instance) throws Throwable { + var handle = lookup.unreflect(method); + handle.invoke(instance, arg); } } - ; private static final String AMBIGUOUS_CONSTRUCTOR = "Ambiguous constructor found. Expecting a single constructor or only one annotated with " + Inject.class.getName(); - private static final Object[] NO_ARGS = new Object[0]; + private final MethodHandles.Lookup lookup; + + /** + * Creates a new instance using a lookup. + * + * @param lookup Method handle lookup. + */ + public ReflectiveBeanConverter(MethodHandles.Lookup lookup) { + this.lookup = lookup; + } - public Object convert(@NonNull ValueNode node, @NonNull Class type, boolean allowEmptyBean) { + /** + * Convert a value into a JavaBean object. + * + *

Selected constructor follows one of these rules: + * + *

    + *
  • It is the default (no args) constructor. + *
  • There is only when constructor. If the constructor has non-null arguments a {@link + * ProvisioningException} will be thrown when {@link Value} fails to resolve the non-null + * argument + *
  • There are multiple constructor but only one is annotated with {@link Inject}. If the + * constructor has non-null arguments a {@link ProvisioningException} will be thrown when + * {@link Value} fails to resolve the non-null argument + *
+ * + *

Any other value is matched against a setter like method. Method might or might not be + * prefixed with set. + * + *

Argument might be annotated with nullable like annotations. Optionally with {@link Named} + * annotation for non-standard Java Names. + * + * @param type Requested type. + * @param value Value value. + * @param hint Requested hint. + * @return Object instance. + * @throws TypeMismatchException when convert returns null and hint is set to {@link + * ConversionHint#Strict}. + * @throws ProvisioningException when convert target type constructor requires a non-null value + * and value is missing or null. + */ + @Override + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) + throws TypeMismatchException, ProvisioningException { + var rawType = $Types.parameterizedType0(type); + var allowEmptyBean = hint == ConversionHint.Empty; try { - return newInstance(type, node, allowEmptyBean); - } catch (InstantiationException | IllegalAccessException | NoSuchMethodException x) { - throw propagate(x); + var constructors = rawType.getConstructors(); + Set state = new HashSet<>(); + Constructor constructor; + if (constructors.length == 0) { + //noinspection unchecked + constructor = rawType.getDeclaredConstructor(); + } else { + constructor = selectConstructor(constructors); + } + var args = inject(value, constructor, state::add); + var setters = setters(rawType, value, state); + Object instance; + if (!allowEmptyBean && state.stream().allMatch(Value::isMissing)) { + instance = null; + } else { + var handle = lookup.unreflectConstructor(constructor); + instance = handle.invokeWithArguments(args); + for (var setter : setters) { + setter.invoke(lookup, instance); + } + } + if (instance == null && hint == ConversionHint.Strict) { + throw new TypeMismatchException(value.name(), type); + } + return instance; } catch (InvocationTargetException x) { throw propagate(x.getCause()); + } catch (Throwable x) { + throw propagate(x); } } - private static Object newInstance(Class type, ValueNode node, boolean allowEmptyBean) - throws IllegalAccessException, - InstantiationException, - InvocationTargetException, - NoSuchMethodException { - Constructor[] constructors = type.getConstructors(); - Set state = new HashSet<>(); - Constructor constructor; - if (constructors.length == 0) { - constructor = type.getDeclaredConstructor(); - } else { - constructor = selectConstructor(constructors); - } - Object[] args = - constructor.getParameterCount() == 0 ? NO_ARGS : inject(node, constructor, state::add); - List setters = setters(type, node, state); - if (!allowEmptyBean(type, allowEmptyBean) && state.stream().allMatch(Value::isMissing)) { - return null; - } - var instance = constructor.newInstance(args); - for (Setter setter : setters) { - setter.invoke(instance); - } - return instance; - } - - private static boolean allowEmptyBean(Class type, boolean defaults) { - return type.getAnnotation(EmptyBean.class) != null || defaults; - } - - private static Constructor selectConstructor(Constructor[] constructors) { + private static Constructor selectConstructor(Constructor[] constructors) { if (constructors.length == 1) { return constructors[0]; } else { - Constructor injectConstructor = null; - Constructor defaultConstructor = null; - for (Constructor constructor : constructors) { + Constructor injectConstructor = null; + Constructor defaultConstructor = null; + for (var constructor : constructors) { if (Modifier.isPublic(constructor.getModifiers())) { - Annotation inject = constructor.getAnnotation(Inject.class); + var inject = constructor.getAnnotation(Inject.class); if (inject == null) { if (constructor.getParameterCount() == 0) { defaultConstructor = constructor; @@ -115,7 +146,7 @@ private static Constructor selectConstructor(Constructor[] constructors) { } } } - Constructor result = injectConstructor == null ? defaultConstructor : injectConstructor; + var result = injectConstructor == null ? defaultConstructor : injectConstructor; if (result == null) { throw new IllegalStateException(AMBIGUOUS_CONSTRUCTOR); } @@ -123,30 +154,29 @@ private static Constructor selectConstructor(Constructor[] constructors) { } } - public static Object[] inject(ValueNode scope, Executable method, Consumer state) { - Parameter[] parameters = method.getParameters(); + public static List inject(Value scope, Executable method, Consumer state) { + var parameters = method.getParameters(); if (parameters.length == 0) { - return NO_ARGS; + return List.of(); } - Object[] args = new Object[parameters.length]; - for (int i = 0; i < parameters.length; i++) { - Parameter parameter = parameters[i]; - String name = paramName(parameter); - ValueNode param = scope.get(name); + var args = new ArrayList<>(parameters.length); + for (var parameter : parameters) { + var name = parameterName(parameter); + var param = scope.get(name); var arg = value(parameter, scope, param); if (arg == null) { state.accept(Value.missing(name)); } else { state.accept(param); } - args[i] = arg; + args.add(arg); } return args; } - private static String paramName(Parameter parameter) { - Named named = parameter.getAnnotation(Named.class); - if (named != null && named.value().length() > 0) { + private static String parameterName(Parameter parameter) { + var named = parameter.getAnnotation(Named.class); + if (named != null && !named.value().isEmpty()) { return named.value(); } if (parameter.isNamePresent()) { @@ -161,9 +191,9 @@ private static String paramName(Parameter parameter) { * @param node Root node. * @return Names, including file names. */ - private static Set names(ValueNode node) { + private static Set names(Value node) { Set names = new LinkedHashSet<>(); - for (ValueNode item : node) { + for (var item : node) { names.add(item.name()); } if (node instanceof Formdata) { @@ -174,17 +204,17 @@ private static Set names(ValueNode node) { return names; } - private static List setters(Class type, ValueNode node, Set nodes) { + private static List setters(Class type, Value node, Set nodes) { var methods = type.getMethods(); var result = new ArrayList(); for (String name : names(node)) { - ValueNode value = node.get(name); + var value = node.get(name); if (nodes.add(value)) { - Method method = findSetter(methods, name); + var method = findSetter(methods, name); if (method != null) { - Parameter parameter = method.getParameters()[0]; + var parameter = method.getParameters()[0]; try { - Object arg = value(parameter, node, value); + var arg = value(parameter, node, value); result.add(new Setter(method, arg)); } catch (ProvisioningException x) { throw x; @@ -199,12 +229,13 @@ private static List setters(Class type, ValueNode node, Set n return result; } - private static Object value(Parameter parameter, ValueNode node, ValueNode value) { + @SuppressWarnings("unchecked") + private static Object value(Parameter parameter, Value node, Value value) { try { if (isFileUpload(node, parameter)) { - Formdata formdata = (Formdata) node; + var formdata = (Formdata) node; if (Set.class.isAssignableFrom(parameter.getType())) { - return new HashSet<>(formdata.files(value.name())); + return new LinkedHashSet<>(formdata.files(value.name())); } else if (Collection.class.isAssignableFrom(parameter.getType())) { return formdata.files(value.name()); } else if (Optional.class.isAssignableFrom(parameter.getType())) { @@ -224,7 +255,7 @@ private static Object value(Parameter parameter, ValueNode node, ValueNode value if (isNullable(parameter)) { if (value.isSingle()) { var str = value.valueOrNull(); - if (str == null || str.length() == 0) { + if (str == null || str.isEmpty()) { // treat empty values as null return null; } @@ -254,7 +285,7 @@ private static boolean isNullable(Parameter parameter) { private static boolean hasAnnotation(AnnotatedElement element, String... names) { var nameList = List.of(names); - for (Annotation annotation : element.getAnnotations()) { + for (var annotation : element.getAnnotations()) { if (nameList.stream() .anyMatch(name -> annotation.annotationType().getSimpleName().endsWith(name))) { return true; @@ -263,19 +294,19 @@ private static boolean hasAnnotation(AnnotatedElement element, String... names) return false; } - private static boolean isFileUpload(ValueNode node, Parameter parameter) { + private static boolean isFileUpload(Value node, Parameter parameter) { return (node instanceof Formdata) && isFileUpload(parameter.getType()) || isFileUpload($Types.parameterizedType0(parameter.getParameterizedType())); } - private static boolean isFileUpload(Class type) { + private static boolean isFileUpload(Class type) { return FileUpload.class == type; } private static Method findSetter(Method[] methods, String name) { var setter = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1); var candidates = new LinkedList(); - for (Method method : methods) { + for (var method : methods) { if ((method.getName().equals(name) || method.getName().equals(setter)) && method.getParameterCount() == 1) { if (method.getName().startsWith("set")) { diff --git a/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java b/jooby/src/main/java/io/jooby/value/StandardConverter.java similarity index 58% rename from jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java rename to jooby/src/main/java/io/jooby/value/StandardConverter.java index 9676b397b6..595853257b 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java +++ b/jooby/src/main/java/io/jooby/value/StandardConverter.java @@ -3,11 +3,14 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.internal.converter; +package io.jooby.value; +import static java.lang.Double.parseDouble; +import static java.lang.Long.parseLong; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.NANOSECONDS; +import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; import java.net.MalformedURLException; @@ -27,41 +30,140 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.SneakyThrows; import io.jooby.StatusCode; -import io.jooby.Value; -import io.jooby.ValueConverter; -public enum BuiltinConverter implements ValueConverter { +public enum StandardConverter implements Converter { + String { + @Override + protected void add(ValueFactory factory) { + factory.put(java.lang.String.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { + return value.valueOrNull(); + } + }, + Int { + @Override + protected void add(ValueFactory factory) { + factory.put(int.class, this); + factory.put(Integer.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { + if (type == int.class) { + return value.intValue(); + } + return value.isMissing() ? null : value.intValue(); + } + }, + Long { + @Override + protected void add(ValueFactory factory) { + factory.put(long.class, this); + factory.put(Long.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { + if (type == long.class) { + return value.longValue(); + } + return value.isMissing() ? null : value.longValue(); + } + }, + Float { + @Override + protected void add(ValueFactory factory) { + factory.put(float.class, this); + factory.put(Float.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { + if (type == float.class) { + return value.floatValue(); + } + return value.isMissing() ? null : value.floatValue(); + } + }, + Double { + @Override + protected void add(ValueFactory factory) { + factory.put(double.class, this); + factory.put(Double.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { + if (type == double.class) { + return value.doubleValue(); + } + return value.isMissing() ? null : value.doubleValue(); + } + }, + Boolean { + @Override + protected void add(ValueFactory factory) { + factory.put(boolean.class, this); + factory.put(Boolean.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { + if (type == boolean.class) { + return value.booleanValue(); + } + return value.isMissing() ? null : value.booleanValue(); + } + }, + Byte { + @Override + protected void add(ValueFactory factory) { + factory.put(byte.class, this); + factory.put(Byte.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { + if (type == byte.class) { + return value.byteValue(); + } + return value.isMissing() ? null : value.byteValue(); + } + }, BigDecimal { @Override - public boolean supports(@NonNull Class type) { - return type == BigDecimal.class; + protected void add(ValueFactory factory) { + factory.put(BigDecimal.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return new BigDecimal(value.value()); } }, BigInteger { @Override - public boolean supports(@NonNull Class type) { - return type == BigInteger.class; + protected void add(ValueFactory factory) { + factory.put(BigInteger.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return new BigInteger(value.value()); } }, Charset { @Override - public boolean supports(@NonNull Class type) { - return type == Charset.class; + protected void add(ValueFactory factory) { + factory.put(Charset.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - String charset = value.value(); + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { + var charset = value.value(); return switch (charset.toLowerCase()) { case "utf-8" -> StandardCharsets.UTF_8; case "us-ascii" -> StandardCharsets.US_ASCII; @@ -75,15 +177,15 @@ public boolean supports(@NonNull Class type) { }, Date { @Override - public boolean supports(@NonNull Class type) { - return type == Date.class; + protected void add(ValueFactory factory) { + factory.put(Date.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { // must be millis - return new Date(Long.parseLong(value.value())); + return new Date(parseLong(value.value())); } catch (NumberFormatException x) { // must be YYYY-MM-dd var date = java.time.LocalDate.parse(value.value(), DateTimeFormatter.ISO_LOCAL_DATE); @@ -93,12 +195,12 @@ public boolean supports(@NonNull Class type) { }, Duration { @Override - public boolean supports(@NonNull Class type) { - return type == Duration.class; + protected void add(ValueFactory factory) { + factory.put(Duration.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { return java.time.Duration.parse(value.value()); } catch (DateTimeParseException x) { @@ -146,10 +248,10 @@ private static long parseDuration(String value) { // if the string is purely digits, parse as an integer to avoid possible precision loss; // otherwise as a double. if (numberString.matches("[+-]?[0-9]+")) { - return units.toNanos(Long.parseLong(numberString)); + return units.toNanos(parseLong(numberString)); } else { long nanosInUnit = units.toNanos(1); - return (long) (Double.parseDouble(numberString) * nanosInUnit); + return (long) (parseDouble(numberString) * nanosInUnit); } } catch (NumberFormatException e) { throw new DateTimeParseException( @@ -159,14 +261,14 @@ private static long parseDuration(String value) { }, Period { @Override - public boolean supports(@NonNull Class type) { - return type == Period.class; + protected void add(ValueFactory factory) { + factory.put(Period.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { - return java.time.Period.from((Duration) Duration.convert(value, type)); + return java.time.Period.from((Duration) Duration.convert(type, value, hint)); } catch (DateTimeException x) { return parsePeriod(value.value()); } @@ -226,14 +328,14 @@ private static Period periodOf(int n, ChronoUnit unit) { }, Instant { @Override - public boolean supports(@NonNull Class type) { - return type == Instant.class; + protected void add(ValueFactory factory) { + factory.put(Instant.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { - return java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); + return java.time.Instant.ofEpochMilli(parseLong(value.value())); } catch (NumberFormatException x) { return DateTimeFormatter.ISO_INSTANT.parse(value.value(), java.time.Instant::from); } @@ -241,15 +343,15 @@ public boolean supports(@NonNull Class type) { }, LocalDate { @Override - public boolean supports(@NonNull Class type) { - return type == LocalDate.class; + protected void add(ValueFactory factory) { + factory.put(LocalDate.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { // must be millis - var instant = java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); + var instant = java.time.Instant.ofEpochMilli(parseLong(value.value())); return instant.atZone(java.time.ZoneId.systemDefault()).toLocalDate(); } catch (NumberFormatException x) { // must be YYYY-MM-dd @@ -259,15 +361,15 @@ public boolean supports(@NonNull Class type) { }, LocalDateTime { @Override - public boolean supports(@NonNull Class type) { - return type == LocalDateTime.class; + protected void add(ValueFactory factory) { + factory.put(LocalDateTime.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { // must be millis - var instant = java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); + var instant = java.time.Instant.ofEpochMilli(parseLong(value.value())); return instant.atZone(java.time.ZoneId.systemDefault()).toLocalDateTime(); } catch (NumberFormatException x) { // must be YYYY-MM-dd @@ -277,34 +379,34 @@ public boolean supports(@NonNull Class type) { }, StatusCode { @Override - public boolean supports(@NonNull Class type) { - return type == StatusCode.class; + protected void add(ValueFactory factory) { + factory.put(StatusCode.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return io.jooby.StatusCode.valueOf(value.intValue()); } }, TimeZone { @Override - public boolean supports(@NonNull Class type) { - return type == TimeZone.class; + protected void add(ValueFactory factory) { + factory.put(TimeZone.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return java.util.TimeZone.getTimeZone(value.value()); } }, URI { @Override - public boolean supports(@NonNull Class type) { - return type == URI.class || type == URL.class; + protected void add(ValueFactory factory) { + factory.put(URI.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { var uri = java.net.URI.create(value.value()); if (type == URL.class) { @@ -318,23 +420,23 @@ public boolean supports(@NonNull Class type) { }, UUID { @Override - public boolean supports(@NonNull Class type) { - return type == UUID.class; + protected void add(ValueFactory factory) { + factory.put(UUID.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return java.util.UUID.fromString(value.value()); } }, ZoneId { @Override - public boolean supports(@NonNull Class type) { - return type == ZoneId.class; + protected void add(ValueFactory factory) { + factory.put(ZoneId.class, this); } @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { var zoneId = value.value(); return java.time.ZoneId.of(java.time.ZoneId.SHORT_IDS.getOrDefault(zoneId, zoneId)); } @@ -349,4 +451,12 @@ private static String getUnits(String s) { } return s.substring(i + 1); } + + protected abstract void add(ValueFactory factory); + + public static void register(ValueFactory factory) { + for (var converter : values()) { + converter.add(factory); + } + } } diff --git a/jooby/src/main/java/io/jooby/Value.java b/jooby/src/main/java/io/jooby/value/Value.java similarity index 60% rename from jooby/src/main/java/io/jooby/Value.java rename to jooby/src/main/java/io/jooby/value/Value.java index 4ce8fc3406..3edd77fad0 100644 --- a/jooby/src/main/java/io/jooby/Value.java +++ b/jooby/src/main/java/io/jooby/value/Value.java @@ -3,23 +3,21 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby; +package io.jooby.value; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeParseException; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; +import java.util.function.BiFunction; import java.util.function.Function; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import io.jooby.Context; +import io.jooby.Formdata; +import io.jooby.SneakyThrows; import io.jooby.exception.MissingValueException; import io.jooby.exception.TypeMismatchException; import io.jooby.internal.ArrayValue; @@ -48,7 +46,179 @@ * @since 2.0.0 * @author edgar */ -public interface Value { +public interface Value extends Iterable { + /** + * Get a value at the given position. + * + * @param index Position. + * @return A value at the given position. + */ + Value get(int index); + + /** + * Get a value that matches the given name. + * + * @param name Field name. + * @return Field value. + */ + Value get(@NonNull String name); + + /** + * The number of values this one has. For single values size is 0. + * + * @return Number of values. Mainly for array and hash values. + */ + int size(); + + /** + * Value iterator. + * + * @return Value iterator. + */ + Iterator iterator(); + + /** + * Process the given expression and resolve value references. + * + *
{@code
+   * Value value = Value.single("foo", "bar");
+   *
+   * String output = value.resolve("${foo}");
+   * System.out.println(output);
+   * }
+ * + * @param expression Text expression. + * @return Resolved text. + */ + default String resolve(@NonNull String expression) { + return resolve(expression, "${", "}"); + } + + /** + * Process the given expression and resolve value references. + * + *
{@code
+   * Value value = Value.single("foo", "bar");
+   *
+   * String output = value.resolve("${missing}", true);
+   * System.out.println(output);
+   * }
+ * + * @param expression Text expression. + * @param ignoreMissing On missing values, keep the expression as it is. + * @return Resolved text. + */ + default String resolve(@NonNull String expression, boolean ignoreMissing) { + return resolve(expression, ignoreMissing, "${", "}"); + } + + /** + * Process the given expression and resolve value references. + * + *
{@code
+   * Value value = Value.single("foo", "bar");
+   *
+   * String output = value.resolve("<%missing%>", "<%", "%>");
+   * System.out.println(output);
+   * }
+ * + * @param expression Text expression. + * @param startDelim Start delimiter. + * @param endDelim End delimiter. + * @return Resolved text. + */ + default String resolve( + @NonNull String expression, @NonNull String startDelim, @NonNull String endDelim) { + return resolve(expression, false, startDelim, endDelim); + } + + /** + * Process the given expression and resolve value references. + * + *
{@code
+   * Value value = Value.single("foo", "bar");
+   *
+   * String output = value.resolve("<%missing%>", "<%", "%>");
+   * System.out.println(output);
+   * }
+ * + * @param expression Text expression. + * @param ignoreMissing On missing values, keep the expression as it is. + * @param startDelim Start delimiter. + * @param endDelim End delimiter. + * @return Resolved text. + */ + default String resolve( + @NonNull String expression, + boolean ignoreMissing, + @NonNull String startDelim, + @NonNull String endDelim) { + if (expression.isEmpty()) { + return ""; + } + + BiFunction, RuntimeException> err = + (start, ex) -> { + String snapshot = expression.substring(0, start); + int line = Math.max(1, (int) snapshot.chars().filter(ch -> ch == '\n').count()); + int column = start - snapshot.lastIndexOf('\n'); + return ex.apply(line, column); + }; + + StringBuilder buffer = new StringBuilder(); + int offset = 0; + int start = expression.indexOf(startDelim); + while (start >= 0) { + int end = expression.indexOf(endDelim, start + startDelim.length()); + if (end == -1) { + throw err.apply( + start, + (line, column) -> + new IllegalArgumentException( + "found '" + + startDelim + + "' expecting '" + + endDelim + + "' at " + + line + + ":" + + column)); + } + buffer.append(expression.substring(offset, start)); + String key = expression.substring(start + startDelim.length(), end); + String value; + try { + // seek + String[] path = key.split("\\."); + var src = path[0].equals(name()) ? this : get(path[0]); + for (int i = 1; i < path.length; i++) { + src = src.get(path[i]); + } + value = src.value(); + } catch (MissingValueException x) { + if (ignoreMissing) { + value = expression.substring(start, end + endDelim.length()); + } else { + throw err.apply( + start, + (line, column) -> + new NoSuchElementException( + "Missing " + startDelim + key + endDelim + " at " + line + ":" + column)); + } + } + buffer.append(value); + offset = end + endDelim.length(); + start = expression.indexOf(startDelim, offset); + } + if (buffer.isEmpty()) { + return expression; + } + if (offset < expression.length()) { + buffer.append(expression.substring(offset)); + } + return buffer.toString(); + } + /** * Convert this value to long (if possible). * @@ -62,7 +232,7 @@ default long longValue() { LocalDateTime date = LocalDateTime.parse(value(), Context.RFC1123); Instant instant = date.toInstant(ZoneOffset.UTC); return instant.toEpochMilli(); - } catch (DateTimeParseException expected) { + } catch (DateTimeParseException ignored) { } throw new TypeMismatchException(name(), long.class, x); } @@ -219,7 +389,7 @@ default boolean booleanValue(boolean defaultValue) { * @param defaultValue Default value. * @return Convert this value to String (if possible) or fallback to given value when missing. */ - @NonNull default String value(@NonNull String defaultValue) { + default String value(@NonNull String defaultValue) { try { return value(); } catch (MissingValueException x) { @@ -243,7 +413,7 @@ default boolean booleanValue(boolean defaultValue) { * @param Target type. * @return Converted value. */ - @NonNull default T value(@NonNull SneakyThrows.Function fn) { + default T value(@NonNull SneakyThrows.Function fn) { return fn.apply(value()); } @@ -252,21 +422,21 @@ default boolean booleanValue(boolean defaultValue) { * * @return String value. */ - @NonNull String value(); + String value(); /** * Get list of values. * * @return List of values. */ - @NonNull List toList(); + List toList(); /** * Get set of values. * * @return set of values. */ - @NonNull Set toSet(); + Set toSet(); /** * Convert this value to an Enum. @@ -275,7 +445,7 @@ default boolean booleanValue(boolean defaultValue) { * @param Enum type. * @return Enum. */ - @NonNull default > T toEnum(@NonNull SneakyThrows.Function fn) { + default > T toEnum(@NonNull SneakyThrows.Function fn) { return toEnum(fn, String::toUpperCase); } @@ -287,7 +457,7 @@ default boolean booleanValue(boolean defaultValue) { * @param Enum type. * @return Enum. */ - @NonNull default > T toEnum( + default > T toEnum( @NonNull SneakyThrows.Function fn, @NonNull Function nameProvider) { return fn.apply(nameProvider.apply(value())); @@ -298,7 +468,7 @@ default boolean booleanValue(boolean defaultValue) { * * @return Value or empty optional. */ - @NonNull default Optional toOptional() { + default Optional toOptional() { try { return Optional.of(value()); } catch (MissingValueException x) { @@ -352,9 +522,9 @@ default boolean isObject() { } /** - * Name of this value or null. + * Name of this value or empty string for root hash. * - * @return Name of this value or null. + * @return Name of this value or empty string for root hash. */ @Nullable String name(); @@ -365,9 +535,9 @@ default boolean isObject() { * @param Item type. * @return Value or empty optional. */ - @NonNull default Optional toOptional(@NonNull Class type) { + default Optional toOptional(@NonNull Class type) { try { - return Optional.ofNullable(to(type)); + return Optional.ofNullable(toNullable(type)); } catch (MissingValueException x) { return Optional.empty(); } @@ -380,8 +550,8 @@ default boolean isObject() { * @param Item type. * @return List of items. */ - @NonNull default List toList(@NonNull Class type) { - return Collections.singletonList(to(type)); + default List toList(@NonNull Class type) { + return List.of(to(type)); } /** @@ -391,19 +561,21 @@ default boolean isObject() { * @param Item type. * @return Set of items. */ - @NonNull default Set toSet(@NonNull Class type) { - return Collections.singleton(to(type)); + default Set toSet(@NonNull Class type) { + return Set.of(to(type)); } /** * Convert this value to the given type. Support values are single-value, array-value and * object-value. Object-value can be converted to a JavaBean type. * + *

At least one of the property of the node must match a target type property. + * * @param type Type to convert. * @param Element type. * @return Instance of the type. */ - @NonNull T to(@NonNull Class type); + T to(@NonNull Class type); /** * Convert this value to the given type. Support values are single-value, array-value and @@ -420,14 +592,14 @@ default boolean isObject() { * * @return Value as multi-value map. */ - @NonNull Map> toMultimap(); + Map> toMultimap(); /** * Value as single-value map. * * @return Value as single-value map. */ - default @NonNull Map toMap() { + default Map toMap() { Map map = new LinkedHashMap<>(); toMultimap().forEach((k, v) -> map.put(k, v.get(0))); return map; @@ -444,34 +616,34 @@ default boolean isObject() { * @param name Name of missing value. * @return Missing value. */ - static @NonNull ValueNode missing(@NonNull String name) { + static Value missing(@NonNull String name) { return new MissingValue(name); } /** * Creates a single value. * - * @param ctx Current context. + * @param valueFactory Current context. * @param name Name of value. * @param value Value. * @return Single value. */ - static @NonNull ValueNode value( - @NonNull Context ctx, @NonNull String name, @NonNull String value) { - return new SingleValue(ctx, name, value); + static Value value( + @NonNull ValueFactory valueFactory, @NonNull String name, @NonNull String value) { + return new SingleValue(valueFactory, name, value); } /** * Creates a sequence/array of values. * - * @param ctx Current context. + * @param valueFactory Current context. * @param name Name of array. * @param values Field values. * @return Array value. */ - static @NonNull ValueNode array( - @NonNull Context ctx, @NonNull String name, @NonNull List values) { - return new ArrayValue(ctx, name).add(values); + static Value array( + @NonNull ValueFactory valueFactory, @NonNull String name, @NonNull List values) { + return new ArrayValue(valueFactory, name).add(values); } /** @@ -480,20 +652,20 @@ default boolean isObject() { *

- For null/empty values. It produces a missing value. - For single element (size==1). It * produces a single value - For multi-value elements (size>1). It produces an array value. * - * @param ctx Current context. + * @param valueFactory Current context. * @param name Field name. * @param values Field values. * @return A value. */ - static @NonNull ValueNode create( - Context ctx, @NonNull String name, @Nullable List values) { - if (values == null || values.size() == 0) { + static Value create( + @NonNull ValueFactory valueFactory, @NonNull String name, @Nullable List values) { + if (values == null || values.isEmpty()) { return missing(name); } if (values.size() == 1) { - return value(ctx, name, values.get(0)); + return value(valueFactory, name, values.get(0)); } - return new ArrayValue(ctx, name).add(values); + return new ArrayValue(valueFactory, name).add(values); } /** @@ -502,27 +674,29 @@ default boolean isObject() { *

- For null/empty values. It produces a missing value. - For single element (size==1). It * produces a single value * - * @param ctx Current context. + * @param valueFactory Current context. * @param name Field name. * @param value Field values. * @return A value. */ - static @NonNull ValueNode create(Context ctx, @NonNull String name, @Nullable String value) { + static Value create( + @NonNull ValueFactory valueFactory, @NonNull String name, @Nullable String value) { if (value == null) { return missing(name); } - return value(ctx, name, value); + return value(valueFactory, name, value); } /** * Create a hash/object value using the map values. * - * @param ctx Current context. + * @param valueFactory Current context. * @param values Map values. * @return A hash/object value. */ - static @NonNull ValueNode hash(Context ctx, @NonNull Map> values) { - HashValue node = new HashValue(ctx, null); + static Value hash( + @NonNull ValueFactory valueFactory, @NonNull Map> values) { + var node = new HashValue(valueFactory, null); node.put(values); return node; } @@ -530,22 +704,23 @@ default boolean isObject() { /** * Creates a formdata. * - * @param ctx Current context. + * @param valueFactory Current context. * @return A hash/object value. */ - static @NonNull Formdata formdata(Context ctx) { - return new MultipartNode(ctx); + static Formdata formdata(@NonNull ValueFactory valueFactory) { + return new MultipartNode(valueFactory); } /** * Create a hash/object value using the map values. * - * @param ctx Current context. + * @param valueFactory Current context. * @param values Map values. * @return A hash/object value. */ - static @NonNull ValueNode headers(Context ctx, @NonNull Map> values) { - HeadersValue node = new HeadersValue(ctx); + static Value headers( + @NonNull ValueFactory valueFactory, @NonNull Map> values) { + var node = new HeadersValue(valueFactory); node.put(values); return node; } diff --git a/jooby/src/main/java/io/jooby/value/ValueFactory.java b/jooby/src/main/java/io/jooby/value/ValueFactory.java new file mode 100644 index 0000000000..85bbe75761 --- /dev/null +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -0,0 +1,239 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.value; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Type; +import java.util.*; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import io.jooby.SneakyThrows; +import io.jooby.exception.ProvisioningException; +import io.jooby.exception.TypeMismatchException; +import io.jooby.internal.reflect.$Types; + +/** + * Keep track of existing {@link Converter} and convert values to a more concrete type. This class + * resolve all the toXXX calls from {@link Value}. + * + *

    + *
  • {@link Value#to(Class)}: convert to request type using {@link ConversionHint#Strict} + *
  • {@link Value#toNullable(Class)}: convert to request type using {@link + * ConversionHint#Nullable} + *
  • {@link io.jooby.QueryString#toEmpty(Class)}: convert to request type using {@link + * ConversionHint#Empty} + *
+ * + * @author edgar + * @since 4.0.0 + */ +public class ValueFactory { + + private final Map converterMap = new HashMap<>(); + + private ConversionHint defaultHint = ConversionHint.Strict; + + private MethodHandles.Lookup lookup; + + private Converter fallback; + + /** + * Creates a new instance. + * + * @param lookup Lookup to use. + */ + public ValueFactory(@NonNull MethodHandles.Lookup lookup) { + this.lookup = lookup; + this.fallback = new ReflectiveBeanConverter(lookup); + StandardConverter.register(this); + } + + /** Creates a new instance with public lookup. */ + public ValueFactory() { + this(MethodHandles.publicLookup()); + } + + /** + * Set lookup to use. Required by: + * + *
    + *
  • valueOf(String) converter + *
  • constructor(String) converter + *
  • fallback/reflective bean converter + *
+ * + * @param lookup Look up to use. + * @return This instance. + */ + public @NonNull ValueFactory lookup(@NonNull MethodHandles.Lookup lookup) { + this.lookup = lookup; + this.fallback = new ReflectiveBeanConverter(lookup); + return this; + } + + /** + * Set default conversion hint to use. Defaults is {@link ConversionHint#Strict}. + * + * @param defaultHint Default conversion hint. + * @return This instance. + */ + public @NonNull ValueFactory hint(@NonNull ConversionHint defaultHint) { + this.defaultHint = defaultHint; + return this; + } + + /** + * Get a converter for the given type. This is an exact lookup, no inheritance rule applies here. + * + * @param type The requested type. + * @return A converter or null. + */ + public @Nullable Converter get(Type type) { + return converterMap.get(type); + } + + /** + * Set a custom converter for type. + * + * @param type Target type. + * @param converter Converter. + * @return This instance. + */ + public @NonNull ValueFactory put(@NonNull Type type, @NonNull Converter converter) { + converterMap.put(type, converter); + return this; + } + + /** + * Convert a value to target type using the default {@link #hint(ConversionHint)}. Conversion + * steps: + * + *
    + *
  • Find a converter by type and use it. If no converter is found: + *
  • Find a factory method valueOf(String) for {@link Value#isSingle()} values + * and use it. If no converter is found: + *
  • Find a constructor(String) for {@link Value#isSingle()} values. If no + * converter is found: + *
  • Fallback to reflective converter. + *
+ * + * @param type Target type. + * @param value Value. + * @param Target type. + * @return New instance. + * @throws TypeMismatchException when convert returns null and hint is set to {@link + * ConversionHint#Strict}. + * @throws ProvisioningException when convert target type constructor requires a non-null value + * and value is missing or null. + */ + public T convert(@NonNull Type type, @NonNull Value value) + throws TypeMismatchException, ProvisioningException { + return convert(type, value, defaultHint); + } + + /** + * Convert a value to target type using a hint. Conversion steps: + * + *
    + *
  • Find a converter by type and use it. If no converter is found: + *
  • Find a factory method valueOf(String) for {@link Value#isSingle()} values + * and use it. If no converter is found: + *
  • Find a constructor(String) for {@link Value#isSingle()} values. If no + * converter is found: + *
  • Fallback to reflective converter. + *
+ * + * @param type Target type. + * @param value Value. + * @param hint Conversion hint. + * @param Target type. + * @return New instance. + * @throws TypeMismatchException when convert returns null and hint is set to {@link + * ConversionHint#Strict}. + * @throws ProvisioningException when convert target type constructor requires a non-null value + * and value is missing or null. + */ + public T convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) + throws TypeMismatchException, ProvisioningException { + T result = convertInternal(type, value, hint); + if (result == null && hint == ConversionHint.Strict) { + throw new TypeMismatchException(value.name(), type); + } + return result; + } + + @SuppressWarnings("unchecked") + private T convertInternal( + @NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { + var converter = converterMap.get(type); + if (converter != null) { + // Specific converter at type level. + return (T) converter.convert(type, value, hint); + } + var rawType = $Types.getRawType(type); + // Is it a container? + if (List.class.isAssignableFrom(rawType)) { + return (T) List.of(convert($Types.parameterizedType0(type), value)); + } else if (Set.class.isAssignableFrom(rawType)) { + return (T) Set.of(convert($Types.parameterizedType0(type), value)); + } else if (Optional.class.isAssignableFrom(rawType)) { + return (T) Optional.of(convert($Types.parameterizedType0(type), value)); + } else { + // dynamic conversion + if (Enum.class.isAssignableFrom(rawType)) { + return (T) enumValue(value, (Class) rawType); + } + if (!value.isObject()) { + // valueOf only works on non-object either a single or array[0] + var valueOf = valueOf(rawType); + if (valueOf != null) { + try { + return (T) valueOf.invoke(value.value()); + } catch (Throwable ex) { + throw SneakyThrows.propagate(ex); + } + } + } + // anything else fallback to reflective + return (T) fallback.convert(type, value, hint); + } + } + + private MethodHandle valueOf(Class rawType) { + try { + // Factory method first + return lookup.findStatic(rawType, "valueOf", MethodType.methodType(rawType, String.class)); + } catch (NoSuchMethodException ignored) { + try { + // Fallback to constructor + return lookup.findConstructor(rawType, MethodType.methodType(void.class, String.class)); + } catch (NoSuchMethodException inner) { + return null; + } catch (IllegalAccessException inner) { + throw SneakyThrows.propagate(inner); + } + } catch (IllegalAccessException cause) { + throw SneakyThrows.propagate(cause); + } + } + + private static > Object enumValue(Value node, Class type) { + var name = node.value(); + try { + return Enum.valueOf(type, name.toUpperCase()); + } catch (IllegalArgumentException x) { + for (var e : EnumSet.allOf(type)) { + if (e.name().equalsIgnoreCase(name)) { + return e; + } + } + throw x; + } + } +} diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index b3c120d7aa..87b3707006 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -8,17 +8,17 @@ module io.jooby { exports io.jooby; exports io.jooby.annotation; - exports io.jooby.buffer; exports io.jooby.exception; exports io.jooby.handler; exports io.jooby.validation; exports io.jooby.problem; + exports io.jooby.value; + exports io.jooby.output; + exports io.jooby.internal.output; - uses io.jooby.MvcFactory; uses io.jooby.Server; uses io.jooby.SslProvider; uses io.jooby.LoggingService; - uses io.jooby.buffer.DataBufferFactory; /* * True core deps diff --git a/jooby/src/test/java/io/jooby/Issue2525.java b/jooby/src/test/java/io/jooby/Issue2525.java index d4e5e2fe4c..455b319b23 100644 --- a/jooby/src/test/java/io/jooby/Issue2525.java +++ b/jooby/src/test/java/io/jooby/Issue2525.java @@ -7,6 +7,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.lang.reflect.Type; +import java.util.Map; import java.util.Objects; import java.util.function.Consumer; @@ -14,19 +16,17 @@ import org.junit.jupiter.api.Test; import io.jooby.internal.UrlParser; -import io.jooby.internal.ValueConverterHelper; +import io.jooby.value.ConversionHint; +import io.jooby.value.Converter; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; import jakarta.inject.Inject; public class Issue2525 { - public class VC2525 implements ValueConverter { + public class VC2525 implements Converter { @Override - public boolean supports(@NotNull Class type) { - return type == MyID2525.class; - } - - @Override - public Object convert(@NotNull Value value, @NotNull Class type) { + public Object convert(@NotNull Type type, @NotNull Value value, @NotNull ConversionHint hint) { return new MyID2525(value.value()); } } @@ -99,7 +99,7 @@ public void shouldBeMoreClever() { queryString -> { assertEquals("MyID:1234", queryString.get("id").to(MyID2525.class).toString()); }, - new VC2525()); + Map.of(MyID2525.class, new VC2525())); queryString( "a=1&b=2&foo.a=3&foo.b=4", queryString -> { @@ -126,9 +126,14 @@ public void shouldBeMoreClever() { }); } + private void queryString(String queryString, Consumer consumer) { + queryString(queryString, consumer, Map.of()); + } + private void queryString( - String queryString, Consumer consumer, ValueConverter... converter) { - consumer.accept( - UrlParser.queryString(ValueConverterHelper.testContext(converter), queryString)); + String queryString, Consumer consumer, Map converters) { + var factory = new ValueFactory(); + converters.forEach(factory::put); + consumer.accept(UrlParser.queryString(factory, queryString)); } } diff --git a/jooby/src/test/java/io/jooby/Issue3607.java b/jooby/src/test/java/io/jooby/Issue3607.java index 2cb31658af..3b2d539eb9 100644 --- a/jooby/src/test/java/io/jooby/Issue3607.java +++ b/jooby/src/test/java/io/jooby/Issue3607.java @@ -9,22 +9,24 @@ import org.junit.jupiter.api.Test; -import io.jooby.buffer.DataBuffer; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; public class Issue3607 { private static class TemplateEngineImpl implements TemplateEngine { @Override - public DataBuffer render(Context ctx, ModelAndView modelAndView) throws Exception { + public Output render(Context ctx, ModelAndView modelAndView) throws Exception { // do nothing - return DefaultDataBufferFactory.sharedInstance.wrap(new byte[0]); + return ctx.getOutputFactory().wrap(new byte[0]); } } @Test public void shouldNotGenerateEmptyFlashMap() throws Exception { var ctx = mock(Context.class); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small())); var templateEngine = new TemplateEngineImpl(); templateEngine.encode(ctx, ModelAndView.map("index.html")); diff --git a/jooby/src/test/java/io/jooby/Issue3653.java b/jooby/src/test/java/io/jooby/Issue3653.java index b7054a0bb2..822029bb18 100644 --- a/jooby/src/test/java/io/jooby/Issue3653.java +++ b/jooby/src/test/java/io/jooby/Issue3653.java @@ -13,13 +13,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.jooby.output.OutputFactory; + public class Issue3653 { private static final ServerOptions defaultOptions = new ServerOptions(); private static class TestServer extends Server.Base { - @Override + @NotNull @Override + public OutputFactory getOutputFactory() { + return null; + } + protected ServerOptions defaultOptions() { return defaultOptions; } diff --git a/jooby/src/test/java/io/jooby/ServerOptionsTest.java b/jooby/src/test/java/io/jooby/ServerOptionsTest.java index 3c34c9a784..e32b896fca 100644 --- a/jooby/src/test/java/io/jooby/ServerOptionsTest.java +++ b/jooby/src/test/java/io/jooby/ServerOptionsTest.java @@ -7,6 +7,7 @@ import static com.typesafe.config.ConfigValueFactory.fromAnyRef; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -36,12 +37,11 @@ public void shouldParseFromConfig() { assertEquals(9443, options.getSecurePort()); assertEquals(4, options.getIoThreads()); assertEquals("Test", options.getServer()); - assertEquals(1024, options.getBufferSize()); assertEquals(8, options.getCompressionLevel()); assertEquals(2048, options.getMaxRequestSize()); assertEquals(32, options.getWorkerThreads()); assertEquals("0.0.0.0", options.getHost()); - assertEquals(true, options.isHttpsOnly()); + assertTrue(options.isHttpsOnly()); } @Test diff --git a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java index a763db115d..bd04cfdb86 100644 --- a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java +++ b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java @@ -13,69 +13,74 @@ import org.junit.jupiter.api.Test; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; public class ServerSentMessageTest { @Test public void shouldFormatMessage() throws Exception { - String data = "some"; - Context ctx = mock(Context.class); + var data = "some"; + var ctx = mock(Context.class); - var bufferFactory = new DefaultDataBufferFactory(); - when(ctx.getBufferFactory()).thenReturn(bufferFactory); - MessageEncoder encoder = mock(MessageEncoder.class); + var bufferFactory = OutputFactory.create(OutputOptions.small()); + when(ctx.getOutputFactory()).thenReturn(bufferFactory); + var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) .thenReturn(bufferFactory.wrap(data.getBytes(StandardCharsets.UTF_8))); - Route route = mock(Route.class); + var route = mock(Route.class); when(route.getEncoder()).thenReturn(encoder); when(ctx.getRoute()).thenReturn(route); - ServerSentMessage message = new ServerSentMessage(data); - assertEquals("data: " + data + "\n\n", message.encode(ctx).toString(StandardCharsets.UTF_8)); + var message = new ServerSentMessage(data); + assertEquals( + "data: " + data + "\n\n", + StandardCharsets.UTF_8.decode(message.encode(ctx).asByteBuffer()).toString()); } @Test public void shouldFormatMultiLineMessage() throws Exception { - String data = "line 1\n line ,a .. 2\nline ...abc 3"; - Context ctx = mock(Context.class); + var data = "line 1\n line ,a .. 2\nline ...abc 3"; + var ctx = mock(Context.class); - var bufferFactory = new DefaultDataBufferFactory(); - when(ctx.getBufferFactory()).thenReturn(bufferFactory); - MessageEncoder encoder = mock(MessageEncoder.class); + var bufferFactory = OutputFactory.create(OutputOptions.small()); + when(ctx.getOutputFactory()).thenReturn(bufferFactory); + var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) .thenReturn(bufferFactory.wrap(data.getBytes(StandardCharsets.UTF_8))); - Route route = mock(Route.class); + var route = mock(Route.class); when(route.getEncoder()).thenReturn(encoder); when(ctx.getRoute()).thenReturn(route); - ServerSentMessage message = new ServerSentMessage(data); + var message = new ServerSentMessage(data); assertEquals( "data: line 1\ndata: line ,a .. 2\ndata: line ...abc 3\n\n", - message.encode(ctx).toString(StandardCharsets.UTF_8)); + StandardCharsets.UTF_8.decode(message.encode(ctx).asByteBuffer()).toString()); } @Test public void shouldFormatMessageEndingWithNL() throws Exception { - String data = "line 1\n"; - Context ctx = mock(Context.class); + var data = "line 1\n"; + var ctx = mock(Context.class); - var bufferFactory = new DefaultDataBufferFactory(); - when(ctx.getBufferFactory()).thenReturn(bufferFactory); - MessageEncoder encoder = mock(MessageEncoder.class); + var bufferFactory = OutputFactory.create(OutputOptions.small()); + when(ctx.getOutputFactory()).thenReturn(bufferFactory); + var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) .thenReturn(bufferFactory.wrap(data.getBytes(StandardCharsets.UTF_8))); - Route route = mock(Route.class); + var route = mock(Route.class); when(route.getEncoder()).thenReturn(encoder); when(ctx.getRoute()).thenReturn(route); - ServerSentMessage message = new ServerSentMessage(data); - assertEquals("data: " + data + "\n\n", message.encode(ctx).toString(StandardCharsets.UTF_8)); + var message = new ServerSentMessage(data); + assertEquals( + "data: " + data + "\n\n", + StandardCharsets.UTF_8.decode(message.encode(ctx).asByteBuffer()).toString()); } } diff --git a/jooby/src/test/java/io/jooby/ValueLikeTest.java b/jooby/src/test/java/io/jooby/ValueLikeTest.java new file mode 100644 index 0000000000..6d285b3774 --- /dev/null +++ b/jooby/src/test/java/io/jooby/ValueLikeTest.java @@ -0,0 +1,36 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; + +public class ValueLikeTest { + + private ValueFactory factory = new ValueFactory(); + + @Test + public void firstElement() { + var single = Value.value(factory, "name", "a"); + var array = Value.array(factory, "name", List.of("a", "b", "c")); + var hash = Value.hash(factory, Map.of("a", List.of("1"), "b", List.of("2"), "c", List.of("3"))); + // check(node -> { + // assertEquals("a", node.value()); + // assertEquals("a", node.get(0).value()); + // assertEquals("a", node.toList().get(0)); + // }, single, array, hash); + } + + private void check(SneakyThrows.Consumer consumer, Value... values) { + Stream.of(values).forEach(consumer); + } +} diff --git a/jooby/src/test/java/io/jooby/ValueResolveTest.java b/jooby/src/test/java/io/jooby/ValueResolveTest.java index 36eefa5d2a..fcc79ef96c 100644 --- a/jooby/src/test/java/io/jooby/ValueResolveTest.java +++ b/jooby/src/test/java/io/jooby/ValueResolveTest.java @@ -12,12 +12,13 @@ import org.junit.jupiter.api.Test; import io.jooby.internal.HashValue; +import io.jooby.value.Value; public class ValueResolveTest { @Test public void resolveOne() { - ValueNode value = Value.value(null, "foo", "bar"); + Value value = Value.value(null, "foo", "bar"); assertEquals("bar", value.resolve("${foo}")); assertEquals("- bar", value.resolve("- ${foo}")); assertEquals("bar-", value.resolve("${foo}-")); @@ -52,13 +53,13 @@ public void resolveComplexWithRoot() { @Test public void resolveMissing() { try { - ValueNode value = Value.value(null, "x", "y"); + Value value = Value.value(null, "x", "y"); assertEquals("bar", value.resolve("${foo}")); } catch (NoSuchElementException x) { assertEquals("Missing ${foo} at 1:1", x.getMessage()); } - ValueNode value = Value.value(null, "x", "y"); + Value value = Value.value(null, "x", "y"); assertEquals("${foo}", value.resolve("${foo}", true)); } } diff --git a/jooby/src/test/java/io/jooby/ValueTest.java b/jooby/src/test/java/io/jooby/ValueTest.java index f36abc32d8..7fd530018c 100644 --- a/jooby/src/test/java/io/jooby/ValueTest.java +++ b/jooby/src/test/java/io/jooby/ValueTest.java @@ -23,7 +23,8 @@ import io.jooby.exception.BadRequestException; import io.jooby.exception.MissingValueException; import io.jooby.internal.UrlParser; -import io.jooby.internal.ValueConverterHelper; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; public class ValueTest { @@ -230,7 +231,8 @@ public void bracketNotation() { @Test public void arrayArity() { assertEquals("1", Value.value(null, "a", "1").value()); - assertEquals("1", Value.value(null, "a", "1").get(0).value()); + + assertThrows(MissingValueException.class, () -> Value.value(null, "a", "1").get(0).value()); assertEquals(1, Value.value(null, "a", "1").size()); queryString( "a=1&a=2", @@ -303,7 +305,7 @@ public void verifyIllegalAccess() { queryString -> { assertThrows( MissingValueException.class, () -> queryString.get("foo").get("missing").value()); - assertEquals("bar", queryString.get("foo").get(0).value()); + assertThrows(MissingValueException.class, () -> queryString.get("foo").get(0).value()); }); /** Missing Property: */ @@ -514,7 +516,9 @@ public static void assertMessage( } } + private final ValueFactory valueFactory = new ValueFactory(); + private void queryString(String queryString, Consumer consumer) { - consumer.accept(UrlParser.queryString(ValueConverterHelper.testContext(), queryString)); + consumer.accept(UrlParser.queryString(valueFactory, queryString)); } } diff --git a/jooby/src/test/java/io/jooby/ValueToBeanTest.java b/jooby/src/test/java/io/jooby/ValueToBeanTest.java index 63a1631d56..9bd5de4e83 100644 --- a/jooby/src/test/java/io/jooby/ValueToBeanTest.java +++ b/jooby/src/test/java/io/jooby/ValueToBeanTest.java @@ -16,7 +16,7 @@ import org.junit.jupiter.api.Test; import io.jooby.internal.UrlParser; -import io.jooby.internal.ValueConverterHelper; +import io.jooby.value.ValueFactory; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -549,7 +549,9 @@ public void constructorAndMixed() { }); } + private final ValueFactory valueFactory = new ValueFactory(); + private void queryString(String queryString, Consumer consumer) { - consumer.accept(UrlParser.queryString(ValueConverterHelper.testContext(), queryString)); + consumer.accept(UrlParser.queryString(valueFactory, queryString)); } } diff --git a/jooby/src/test/java/io/jooby/internal/ChiTest.java b/jooby/src/test/java/io/jooby/internal/ChiTest.java index 1c5d497ac9..cd3ae45f34 100644 --- a/jooby/src/test/java/io/jooby/internal/ChiTest.java +++ b/jooby/src/test/java/io/jooby/internal/ChiTest.java @@ -33,6 +33,20 @@ public void routeOverride() { assertEquals(bar, result.route()); } + @Test + public void staticMap6() { + Chi router = new Chi(); + router.insert(route("GET", "/1", stringHandler("1"))); + router.insert(route("GET", "/2", stringHandler("2"))); + router.insert(route("GET", "/3", stringHandler("3"))); + router.insert(route("GET", "/4", stringHandler("4"))); + router.insert(route("GET", "/5", stringHandler("5"))); + router.insert(route("GET", "/6", stringHandler("6"))); + + Router.Match result = router.find("GET", "/1"); + assertTrue(result.matches()); + } + @Test public void routeCase() { Chi router = new Chi(); diff --git a/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java b/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java index 899ac2a942..bbd8860128 100644 --- a/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java +++ b/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java @@ -14,8 +14,9 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import io.jooby.Value; -import io.jooby.internal.converter.BuiltinConverter; +import io.jooby.value.ConversionHint; +import io.jooby.value.StandardConverter; +import io.jooby.value.Value; public class DurationConverterTest { @@ -53,11 +54,13 @@ public void convertPeriod() { } private Duration duration(String value) { - return (Duration) BuiltinConverter.Duration.convert(value(value), Duration.class); + return (Duration) + StandardConverter.Duration.convert(Duration.class, value(value), ConversionHint.Strict); } private Period period(String value) { - return (Period) BuiltinConverter.Period.convert(value(value), Period.class); + return (Period) + StandardConverter.Period.convert(Period.class, value(value), ConversionHint.Strict); } private Value value(String value) { diff --git a/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java b/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java deleted file mode 100644 index 9dc0e05973..0000000000 --- a/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; - -import org.mockito.stubbing.Answer; - -import io.jooby.BeanConverter; -import io.jooby.Context; -import io.jooby.Router; -import io.jooby.ValueConverter; -import io.jooby.ValueNode; -import io.jooby.exception.TypeMismatchException; - -public class ValueConverterHelper { - - public static Context testContext(ValueConverter... converters) { - List beans = new ArrayList<>(); - List simple = new ArrayList<>(ValueConverters.defaultConverters()); - Stream.of(converters).filter(it -> (!(it instanceof BeanConverter))).forEach(simple::add); - Stream.of(converters) - .filter(it -> (it instanceof BeanConverter)) - .forEach(it -> beans.add((BeanConverter) it)); - - Context ctx = mock(Context.class); - Router router = mock(Router.class); - when(router.getConverters()).thenReturn(simple); - when(router.getBeanConverters()).thenReturn(beans); - when(ctx.getRouter()).thenReturn(router); - when(ctx.convert(any(), any())) - .then( - (Answer) - invocation -> { - ValueNode value = invocation.getArgument(0); - Class type = invocation.getArgument(1); - var result = ValueConverters.convert(value, type, router); - if (result == null) { - throw new TypeMismatchException(value.name(), type); - } - return result; - }); - when(ctx.convertOrNull(any(), any())) - .then( - (Answer) - invocation -> { - ValueNode value = invocation.getArgument(0); - Class type = invocation.getArgument(1); - return ValueConverters.convert(value, type, router); - }); - return ctx; - } -} diff --git a/jooby/src/test/java/io/jooby/output/ByteBufferedOutputTest.java b/jooby/src/test/java/io/jooby/output/ByteBufferedOutputTest.java new file mode 100644 index 0000000000..7ff3a01a04 --- /dev/null +++ b/jooby/src/test/java/io/jooby/output/ByteBufferedOutputTest.java @@ -0,0 +1,39 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.output; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.ReadOnlyBufferException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +public class ByteBufferedOutputTest { + + @Test + public void shouldReadMultipleTimes() { + var factory = OutputFactory.create(new OutputOptions().setSize(4).setDirectBuffers(false)); + var output = factory.allocate(); + output.write("hello"); + output.write((byte) 32); + output.write("world"); + assertEquals("hello world", StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString()); + assertEquals("hello world", StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString()); + } + + @Test + public void shouldCheckReadOnlyBuffer() { + var factory = OutputFactory.create(new OutputOptions().setSize(4).setDirectBuffers(false)); + var output = factory.allocate(); + output.write("hello"); + assertThrows( + ReadOnlyBufferException.class, + () -> output.asByteBuffer().put("world".getBytes(StandardCharsets.UTF_8))); + assertEquals("hello", StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString()); + } +} diff --git a/jooby/src/test/java/io/jooby/buffer/Issue3434.java b/jooby/src/test/java/io/jooby/output/Issue3434.java similarity index 95% rename from jooby/src/test/java/io/jooby/buffer/Issue3434.java rename to jooby/src/test/java/io/jooby/output/Issue3434.java index a1874b9844..047afd27ae 100644 --- a/jooby/src/test/java/io/jooby/buffer/Issue3434.java +++ b/jooby/src/test/java/io/jooby/output/Issue3434.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.buffer; +package io.jooby.output; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -17,7 +17,7 @@ import io.jooby.SneakyThrows; public class Issue3434 { - DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + OutputFactory factory = OutputFactory.create(OutputOptions.small()); @Test void shouldWriteCharBufferOnBufferWriter() throws IOException { @@ -142,10 +142,10 @@ void shouldWriteCharBufferOnBufferWriter() throws IOException { private String writeCharSequence(Charset charset, SneakyThrows.Consumer writer) throws IOException { - var buffer = factory.allocateBuffer(); + var buffer = factory.allocate(); try (var out = buffer.asWriter(charset)) { writer.accept(out); - return buffer.toString(charset); + return charset.decode(buffer.asByteBuffer()).toString(); } } } diff --git a/jooby/src/test/java/io/jooby/output/OutputTest.java b/jooby/src/test/java/io/jooby/output/OutputTest.java new file mode 100644 index 0000000000..bfad698a61 --- /dev/null +++ b/jooby/src/test/java/io/jooby/output/OutputTest.java @@ -0,0 +1,115 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.output; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import io.jooby.SneakyThrows; +import io.jooby.internal.output.CompositeOutput; +import io.jooby.internal.output.StaticOutput; + +public class OutputTest { + + @Test + public void bufferedOutput() { + output( + buffered -> { + var buffer = ByteBuffer.wrap(". New Output API!!".getBytes(StandardCharsets.UTF_8)); + buffered.write("Hello".getBytes(StandardCharsets.UTF_8)); + buffered.write(" "); + buffered.write("World!"); + assertEquals( + "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); + assertEquals(12, buffered.size()); + assertEquals( + "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); + buffered.write(buffer); + assertEquals( + "Hello World!. New Output API!!", + StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); + assertEquals(30, buffered.size()); + }, + new ByteBufferedOutput(false, 3)); + + output( + buffered -> { + var buffer = ByteBuffer.wrap(". New Output API!!".getBytes(StandardCharsets.UTF_8)); + buffered.write(" Hello World! ".getBytes(StandardCharsets.UTF_8), 1, 12); + assertEquals( + "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); + assertEquals(12, buffered.size()); + assertEquals( + "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); + buffered.write(buffer); + assertEquals( + "Hello World!. New Output API!!", + StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); + assertEquals(30, buffered.size()); + }, + new ByteBufferedOutput(false, 255)); + + output( + buffered -> { + var bytes = "xxHello World!xx".getBytes(StandardCharsets.UTF_8); + buffered.write(bytes, 2, bytes.length - 4); + assertEquals( + "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); + assertEquals(12, buffered.size()); + }, + new ByteBufferedOutput(false, 255)); + + output( + buffered -> { + buffered.write((byte) 'A'); + assertEquals("A", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); + assertEquals(1, buffered.size()); + }, + new ByteBufferedOutput(false, 255)); + } + + private void output(SneakyThrows.Consumer consumer, BufferedOutput... buffers) { + Stream.of(buffers).forEach(consumer); + } + + @Test + public void chunkedOutput() { + output( + chunked -> { + var buffer = ByteBuffer.allocateDirect(6); + buffer.put("rld!".getBytes(StandardCharsets.UTF_8)); + buffer.flip(); + chunked.write((byte) 'H'); + assertEquals(1, chunked.size()); + chunked.write("ello".getBytes(StandardCharsets.UTF_8)); + assertEquals(5, chunked.size()); + chunked.write(" "); + assertEquals(6, chunked.size()); + chunked.write(" Wor".getBytes(StandardCharsets.UTF_8), 1, 2); + assertEquals(8, chunked.size()); + chunked.write(buffer); + assertEquals(12, chunked.size()); + assertEquals( + "Hello World!", StandardCharsets.UTF_8.decode(chunked.asByteBuffer()).toString()); + assertEquals(12, chunked.size()); + }, + new CompositeOutput()); + } + + @Test + public void wrapOutput() throws IOException { + var bytes = "xxHello World!xx".getBytes(StandardCharsets.UTF_8); + var output = new StaticOutput(ByteBuffer.wrap(bytes, 2, bytes.length - 4)); + assertEquals("Hello World!", StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString()); + assertEquals(12, output.size()); + } +} diff --git a/jooby/src/test/java/io/jooby/value/ValueHintTest.java b/jooby/src/test/java/io/jooby/value/ValueHintTest.java new file mode 100644 index 0000000000..b70733b561 --- /dev/null +++ b/jooby/src/test/java/io/jooby/value/ValueHintTest.java @@ -0,0 +1,59 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.value; + +import static io.jooby.internal.UrlParser.queryString; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import io.jooby.exception.ProvisioningException; +import io.jooby.exception.TypeMismatchException; + +public class ValueHintTest { + + public record Search(String q, String fq, Integer start, Integer end) {} + + public record SearchPrimitive(String q, String fq, int start, int end) {} + + @Test + public void queryHint() { + var factory = new ValueFactory(); + var node = queryString(factory, ""); + var throwable = assertThrows(TypeMismatchException.class, () -> node.to(Search.class)); + assertEquals( + "Cannot convert value: 'null', to: 'io.jooby.value.ValueHintTest$Search'", + throwable.getMessage()); + // Nullable + assertNull(node.toNullable(Search.class)); + // Empty + var search = node.toEmpty(Search.class); + // There is no match still query produces an empty instance + assertNotNull(search); + assertNull(search.q()); + assertNull(search.fq()); + assertNull(search.start); + assertNull(search.end); + // Strict + assertThrows(TypeMismatchException.class, () -> factory.convert(Search.class, node)); + // Nullable + assertNull(factory.convert(Search.class, node, ConversionHint.Nullable)); + // Default instance + assertNotNull(factory.convert(Search.class, node, ConversionHint.Empty)); + } + + @Test + public void queryWithNonNullProperties() { + var factory = new ValueFactory(); + var node = queryString(factory, ""); + var throwable = assertThrows(ProvisioningException.class, () -> node.to(SearchPrimitive.class)); + assertEquals( + "Unable to provision parameter: 'start: int', require by: constructor" + + " io.jooby.value.ValueHintTest.SearchPrimitive(java.lang.String, java.lang.String," + + " int, int)", + throwable.getMessage()); + } +} 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 a59f968558..de5ac10670 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 @@ -31,12 +31,9 @@ import io.jooby.internal.apt.*; @SupportedOptions({ - HANDLER, DEBUG, INCREMENTAL, - SERVICES, MVC_METHOD, - RETURN_TYPE, ROUTER_PREFIX, ROUTER_SUFFIX, SKIP_ATTRIBUTE_ANNOTATIONS @@ -44,14 +41,11 @@ @SupportedSourceVersion(SourceVersion.RELEASE_17) public class JoobyProcessor extends AbstractProcessor { public interface Options { - String HANDLER = "jooby.handler"; String DEBUG = "jooby.debug"; String ROUTER_PREFIX = "jooby.routerPrefix"; String ROUTER_SUFFIX = "jooby.routerSuffix"; String INCREMENTAL = "jooby.incremental"; - String RETURN_TYPE = "jooby.returnType"; String MVC_METHOD = "jooby.mvcMethod"; - String SERVICES = "jooby.services"; String SKIP_ATTRIBUTE_ANNOTATIONS = "jooby.skipAttributeAnnotations"; static boolean boolOpt(ProcessingEnvironment environment, String option, boolean defaultValue) { @@ -101,9 +95,6 @@ public boolean process(Set annotations, RoundEnvironment if (roundEnv.processingOver()) { context.debug("Output:"); context.getRouters().forEach(it -> context.debug(" %s.java", it.getGeneratedType())); - if (context.generateServices()) { - doServices(context.getProcessingEnvironment().getFiler(), context.getRouters()); - } return false; } else { var routeMap = buildRouteRegistry(annotations, roundEnv); @@ -328,10 +319,7 @@ public Set getSupportedOptions() { // more then one originating element is passed to the Filer // API on writing the resource file - isolating mode does not // allow this. - options.add( - String.format( - "org.gradle.annotation.processing.%s", - context.generateServices() ? "aggregating" : "isolating")); + options.add("org.gradle.annotation.processing.isolating"); } return options; diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcContext.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcContext.java index 0a3e24e953..6a4a441a9c 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcContext.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcContext.java @@ -9,11 +9,9 @@ import java.io.StringWriter; import java.util.*; import java.util.function.BiConsumer; -import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.*; -import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; @@ -24,19 +22,15 @@ * processor options. */ public class MvcContext { - private record ResultType(String type, String handler, boolean nonBlocking) {} - private final ProcessingEnvironment processingEnvironment; private final boolean debug; private final boolean incremental; - private final boolean services; private final String routerPrefix; private final String routerSuffix; private final BiConsumer output; private final List routers = new ArrayList<>(); - private final boolean returnType; private final boolean mvcMethod; - private final Map handler = new HashMap<>(); + private final Map reactiveTypeMap = new HashMap<>(); public MvcContext( ProcessingEnvironment processingEnvironment, BiConsumer output) { @@ -44,105 +38,31 @@ public MvcContext( this.output = output; this.debug = Options.boolOpt(processingEnvironment, Options.DEBUG, false); this.incremental = Options.boolOpt(processingEnvironment, Options.INCREMENTAL, true); - this.returnType = Options.boolOpt(processingEnvironment, Options.RETURN_TYPE, false); - this.mvcMethod = Options.boolOpt(processingEnvironment, Options.MVC_METHOD, true); - this.services = Options.boolOpt(processingEnvironment, Options.SERVICES, true); + this.mvcMethod = Options.boolOpt(processingEnvironment, Options.MVC_METHOD, false); this.routerPrefix = Options.string(processingEnvironment, Options.ROUTER_PREFIX, ""); this.routerSuffix = Options.string(processingEnvironment, Options.ROUTER_SUFFIX, "_"); - computeResultTypes(processingEnvironment, handler::put); + computeReactiveTypes(processingEnvironment, reactiveTypeMap::put); debug("Incremental annotation processing is turned %s.", incremental ? "ON" : "OFF"); - debug("Generation of service provider configuration is turned %s.", services ? "ON" : "OFF"); } - private void computeResultTypes( - ProcessingEnvironment processingEnvironment, BiConsumer consumer) { - var handler = - new HashSet<>( - Set.of( - "io.jooby.ReactiveSupport", - "io.jooby.mutiny.Mutiny", - "io.jooby.reactor.Reactor", - "io.jooby.rxjava3.Reactivex")); - handler.addAll(Options.stringListOpt(processingEnvironment, Options.HANDLER)); - handler.stream() - .map(type -> processingEnvironment.getElementUtils().getTypeElement(type)) - .filter(Objects::nonNull) + private void computeReactiveTypes( + ProcessingEnvironment processingEnvironment, BiConsumer consumer) { + ReactiveType.supportedTypes() .forEach( - it -> { - var annotation = - AnnotationSupport.findAnnotationByName(it, "io.jooby.annotation.ResultType"); - if (annotation != null) { - var handlerFunction = - AnnotationSupport.findAnnotationValue(annotation, "handler"::equals).get(0); - boolean nonBlocking = - AnnotationSupport.findAnnotationValue(annotation, "nonBlocking"::equals) - .stream() - .findFirst() - .map(Boolean::valueOf) - .orElse(Boolean.FALSE); - ResultType entry; - var i = handlerFunction.lastIndexOf('.'); - if (i > 0) { - var container = handlerFunction.substring(0, i); - var fn = handlerFunction.substring(i + 1); - entry = new ResultType(container, fn, nonBlocking); - } else { - entry = new ResultType(it.asType().toString(), handlerFunction, nonBlocking); - } - var functions = - it.getEnclosedElements().stream() - .filter(ExecutableElement.class::isInstance) - .map(ExecutableElement.class::cast) - .filter(m -> entry.handler.equals(m.getSimpleName().toString())) - .toList(); - if (functions.isEmpty()) { - throw new IllegalArgumentException( - "Method not found: " + entry.type + "." + entry.handler); - } else { - var args = - functions.stream() - .filter( - m -> - !m.getParameters().isEmpty() - && m.getParameters() - .get(0) - .asType() - .toString() - .equals("io.jooby.Route.Handler")) - .findFirst() - .orElseThrow( - () -> - new IllegalArgumentException( - "Signature doesn't match: " - + functions - + " must be: " - + functions.stream() - .map( - e -> - e.getSimpleName() - + "(io.jooby.Route.Handler)") - .collect(Collectors.joining(", ", "[", "]")))); - if (!args.getReturnType().toString().equals("io.jooby.Route.Handler")) { - throw new IllegalArgumentException( - "Method returns type not supported: " - + args - + ": " - + args.getReturnType() - + " must be: " - + args - + ": io.jooby.Route.Handler"); - } - if (!args.getModifiers().contains(Modifier.STATIC)) { - throw new IllegalArgumentException("Method must be static: " + args); - } - } - var types = - AnnotationSupport.findAnnotationValue( - annotation, "types"::equals, value -> (DeclaredType) value.getValue()); - for (var type : types) { - superTypes(type.asElement()).forEach(t -> consumer.accept(t, entry)); - } + reactiveType -> { + var handlerType = + processingEnvironment + .getElementUtils() + .getTypeElement(reactiveType.handlerType()); + if (handlerType != null) { + // Handler Type is on classpath + reactiveType.reactiveTypes().stream() + .map(it -> processingEnvironment.getElementUtils().getTypeElement(it)) + .forEach( + it -> { + superTypes(it).forEach(t -> consumer.accept(t, reactiveType)); + }); } }); } @@ -204,11 +124,11 @@ public String pipeline(TypeMirror returnType, String handlerReference) { public boolean nonBlocking(TypeMirror returnType) { var entry = findMappingHandler(returnType); - return entry != null && entry.nonBlocking; + return entry != null; } - private ResultType findMappingHandler(TypeMirror type) { - for (var e : handler.entrySet()) { + private ReactiveType findMappingHandler(TypeMirror type) { + for (var e : reactiveTypeMap.entrySet()) { var that = e.getKey(); if (type.toString().equals(that.toString()) || processingEnvironment.getTypeUtils().isAssignable(type, that.asType())) { @@ -243,18 +163,10 @@ public Set superTypes(Element owner) { return result; } - public boolean generateServices() { - return services; - } - public boolean generateMvcMethod() { return mvcMethod; } - public boolean generateReturnType() { - return returnType; - } - public boolean isIncremental() { return incremental; } @@ -294,7 +206,7 @@ public void generateStaticImports(MvcRouter mvcRouter, BiConsumer generateMapping(boolean kt) { .toSourceCode(kt, this, 2) .ifPresent( attributes -> block.add(statement(indent(2), ".setAttributes(", attributes, ")"))); - if (context.generateReturnType()) { - /* returnType */ - block.add(statement(indent(2), ".setReturnType(", returnType.toSourceCode(kt), ")")); - } var lineSep = lastLine ? lineSeparator() : lineSeparator() + lineSeparator(); if (context.generateMvcMethod()) { /* mvcMethod */ @@ -158,7 +154,7 @@ public List generateMapping(boolean kt) { ".setMvcMethod(", kt ? "" : "new ", "io.jooby.Route.MvcMethod(", - router.getTargetType().getQualifiedName().toString(), + router.getTargetType().getSimpleName().toString(), clazz(kt), ", ", string(getMethodName()), diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRouter.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRouter.java index 49456e62b6..9804777e15 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRouter.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRouter.java @@ -205,6 +205,7 @@ private StringBuilder constructors(String generatedName, boolean kt) { constructor( generatedName, kt, + kt ? ":" : null, buffer, List.of(), (output, params) -> { @@ -221,6 +222,7 @@ private StringBuilder constructors(String generatedName, boolean kt) { constructor( generatedName, kt, + kt ? ":" : null, buffer, List.of(), (output, params) -> { @@ -243,6 +245,7 @@ private StringBuilder constructors(String generatedName, boolean kt) { constructor( generatedName, kt, + kt ? ":" : null, buffer, constructor.getParameters().stream() .map(it -> Map.entry(it.asType(), it.getSimpleName().toString())) @@ -262,30 +265,27 @@ private StringBuilder constructors(String generatedName, boolean kt) { constructor( generatedName, true, + "{", buffer, List.of(Map.entry("kotlin.reflect.KClass<" + targetType + ">", "type")), (output, params) -> { - // this(java.util.function.Function { ctx: - // io.jooby.Context -> ctx.require<${className}>(type.java) }) output - .append("this(java.util.function.Function { ctx: io.jooby.Context -> ") - .append("ctx.require<") + .append("setup { ctx -> ctx.require<") .append(targetType) .append(">(type.java)") - .append(" })") + .append(" }") .append(System.lineSeparator()); }); } else { constructor( generatedName, false, + null, buffer, List.of(Map.entry("Class<" + targetType + ">", "type")), (output, params) -> { output - .append("this(") + .append("setup(") .append("ctx -> ctx.require(type)") .append(")") .append(";") @@ -308,6 +308,7 @@ private static Predicate hasInjectAnnotation() { private void constructor( String generatedName, boolean kt, + String ktBody, StringBuilder buffer, List> parameters, BiConsumer>> body) { @@ -334,10 +335,10 @@ private void constructor( buffer.append(" {").append(System.lineSeparator()); buffer.append(indent(6)); } else { - buffer.append(" : "); + buffer.append(" ").append(ktBody).append(" "); } body.accept(buffer, parameters); - if (!kt) { + if (!kt || "{".equals(ktBody)) { buffer.append(indent(4)).append("}"); } buffer.append(System.lineSeparator()).append(System.lineSeparator()); diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParameterGenerator.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParameterGenerator.java index de6cdaab30..9f7167599a 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParameterGenerator.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ParameterGenerator.java @@ -221,6 +221,29 @@ public String toSourceCode( return Map.entry(convertMethod, type); }); if (paramSource.isEmpty() && BUILT_IN.stream().noneMatch(it -> toValue.getValue().is(it))) { + var useEmpty = this == QueryParam && toValue.getKey().equals("toNullable"); + String valueToBean; + String toMethod = toValue.getKey(); + if (useEmpty) { + valueToBean = + CodeBlock.of( + method, + "(", + CodeBlock.type(kt, toValue.getValue().getName()), + CodeBlock.clazz(kt), + ")"); + toMethod = "toEmpty"; + } else { + valueToBean = + CodeBlock.of( + method, + "().", + toValue.getKey(), + "(", + CodeBlock.type(kt, toValue.getValue().getName()), + CodeBlock.clazz(kt), + ")"); + } // for unsupported types, we check if node with matching name is present, if not we fallback // to entire scope converter if (kt) { @@ -238,18 +261,13 @@ public String toSourceCode( "(", CodeBlock.string(name), ").isMissing()) ctx.", - method, - "().", - toValue.getKey(), - "(", - CodeBlock.type(kt, toValue.getValue().getName()), - CodeBlock.clazz(kt), - ") else ctx.", + valueToBean, + " else ctx.", method, "(", CodeBlock.string(name), ").", - toValue.getKey(), + toMethod, "(", CodeBlock.type(kt, toValue.getValue().getName()), CodeBlock.clazz(kt), @@ -262,13 +280,8 @@ public String toSourceCode( "(", CodeBlock.string(name), ").isMissing() ? ctx.", - method, - "().", - toValue.getKey(), - "(", - CodeBlock.type(kt, toValue.getValue().getName()), - CodeBlock.clazz(kt), - ") : ctx.", + valueToBean, + " : ctx.", method, "(", CodeBlock.string(name), diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ReactiveType.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ReactiveType.java new file mode 100644 index 0000000000..149eb34b9d --- /dev/null +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/ReactiveType.java @@ -0,0 +1,59 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.apt; + +import java.util.List; +import java.util.Set; + +public class ReactiveType { + + private final String handlerType; + private final String handler; + private final Set reactiveTypes; + + private ReactiveType(String handlerType, String handler, Set reactiveTypes) { + this.handlerType = handlerType; + this.handler = handler; + this.reactiveTypes = reactiveTypes; + } + + public Set reactiveTypes() { + return reactiveTypes; + } + + public String handlerType() { + return handlerType; + } + + public String handler() { + return handler; + } + + public static List supportedTypes() { + return List.of( + new ReactiveType( + "io.jooby.ReactiveSupport", + "concurrent", + Set.of("java.util.concurrent.Flow", "java.util.concurrent.CompletionStage")), + new ReactiveType( + "io.jooby.mutiny.Mutiny", + "mutiny", + Set.of("io.smallrye.mutiny.Uni", "io.smallrye.mutiny.Multi")), + new ReactiveType( + "io.jooby.reactor.Reactor", + "reactor", + Set.of("reactor.core.publisher.Flux", "reactor.core.publisher.Mono")), + new ReactiveType( + "io.jooby.rxjava3.Reactivex", + "rx", + Set.of( + "io.reactivex.rxjava3.core.Flowable", + "io.reactivex.rxjava3.core.Maybe", + "io.reactivex.rxjava3.core.Observable", + "io.reactivex.rxjava3.core.Single", + "io.reactivex.rxjava3.disposables.Disposable"))); + } +} diff --git a/modules/jooby-apt/src/main/resources/io/jooby/internal/apt/Source.java b/modules/jooby-apt/src/main/resources/io/jooby/internal/apt/Source.java index 8beea1cc6c..8b498bbfdf 100644 --- a/modules/jooby-apt/src/main/resources/io/jooby/internal/apt/Source.java +++ b/modules/jooby-apt/src/main/resources/io/jooby/internal/apt/Source.java @@ -1,18 +1,22 @@ package ${packageName}; ${imports} @io.jooby.annotation.Generated(${className}.class) -public class ${generatedClassName} implements io.jooby.MvcExtension { - protected final java.util.function.Function factory; +public class ${generatedClassName} implements io.jooby.Extension { + protected java.util.function.Function factory; ${constructors} public ${generatedClassName}(${className} instance) { - this(ctx -> instance); + setup(ctx -> instance); } - public ${generatedClassName}(java.util.function.Supplier<${className}> provider) { - this(ctx -> provider.get()); + public ${generatedClassName}(io.jooby.SneakyThrows.Supplier<${className}> provider) { + setup(ctx -> provider.get()); } - public ${generatedClassName}(java.util.function.Function factory) { + public ${generatedClassName}(io.jooby.SneakyThrows.Function, ${className}> provider) { + setup(ctx -> provider.apply(${className}.class)); + } + + private void setup(java.util.function.Function factory) { this.factory = factory; } diff --git a/modules/jooby-apt/src/main/resources/io/jooby/internal/apt/Source.kt b/modules/jooby-apt/src/main/resources/io/jooby/internal/apt/Source.kt index c41cc475c4..cb1dfca701 100644 --- a/modules/jooby-apt/src/main/resources/io/jooby/internal/apt/Source.kt +++ b/modules/jooby-apt/src/main/resources/io/jooby/internal/apt/Source.kt @@ -1,11 +1,20 @@ package ${packageName} ${imports} @io.jooby.annotation.Generated(${className}::class) -open class ${generatedClassName}(protected val factory: java.util.function.Function) : io.jooby.MvcExtension { +open class ${generatedClassName} : io.jooby.Extension { + private lateinit var factory: java.util.function.Function + ${constructors} - constructor(instance: ${className}) : this(java.util.function.Function { instance }) + constructor(instance: ${className}) { setup { instance } } + + constructor(provider: io.jooby.SneakyThrows.Supplier<${className}>) { setup { provider.get() } } + + constructor(provider: (Class<${className}>) -> ${className}) { setup { provider(${className}::class.java) } } - constructor(provider: java.util.function.Supplier<${className}?>) : this(java.util.function.Function { provider.get()!! }) + constructor(provider: io.jooby.SneakyThrows.Function, ${className}>) { setup { provider.apply(${className}::class.java) } } + private fun setup(factory: java.util.function.Function) { + this.factory = factory + } ${methods} } diff --git a/modules/jooby-apt/src/test/java/io/jooby/apt/MockContextHelper.java b/modules/jooby-apt/src/test/java/io/jooby/apt/MockContextHelper.java index 616c5e584a..f702e442b0 100644 --- a/modules/jooby-apt/src/test/java/io/jooby/apt/MockContextHelper.java +++ b/modules/jooby-apt/src/test/java/io/jooby/apt/MockContextHelper.java @@ -6,32 +6,33 @@ package io.jooby.apt; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; +import java.util.Map; -import io.jooby.BeanConverter; import io.jooby.Router; -import io.jooby.ValueConverter; import io.jooby.test.MockContext; +import io.jooby.value.Converter; +import io.jooby.value.ValueFactory; public class MockContextHelper { - public static MockContext mockContext(ValueConverter... converters) { - List beans = new ArrayList<>(); - List simple = new ArrayList<>(ValueConverter.defaults()); - Stream.of(converters).filter(it -> (!(it instanceof BeanConverter))).forEach(simple::add); - Stream.of(converters) - .filter(it -> (it instanceof BeanConverter)) - .forEach(it -> beans.add((BeanConverter) it)); + public static MockContext mockContext() { + return mockContext(Map.of()); + } - Router router = mock(Router.class); - when(router.getConverters()).thenReturn(simple); - when(router.getBeanConverters()).thenReturn(beans); + public static MockContext mockContext(Map, Converter> converters) { + // List beans = new ArrayList<>(); + // List simple = new ArrayList<>(ValueConverter.defaults()); + // Stream.of(converters).filter(it -> (!(it instanceof BeanConverter))).forEach(simple::add); + // Stream.of(converters) + // .filter(it -> (it instanceof BeanConverter)) + // .forEach(it -> beans.add((BeanConverter) it)); + Router router = mock(Router.class); + var factory = new ValueFactory(); + converters.forEach(factory::put); MockContext ctx = new MockContext(); + ctx.setValueFactory(factory); ctx.setRouter(router); return ctx; } diff --git a/modules/jooby-apt/src/test/java/io/jooby/apt/ProcessorRunner.java b/modules/jooby-apt/src/test/java/io/jooby/apt/ProcessorRunner.java index 1cf3617846..49bc7e7bdb 100644 --- a/modules/jooby-apt/src/test/java/io/jooby/apt/ProcessorRunner.java +++ b/modules/jooby-apt/src/test/java/io/jooby/apt/ProcessorRunner.java @@ -125,7 +125,7 @@ public ProcessorRunner withRouter(SneakyThrows.Consumer2 throws Exception { var classLoader = processor.createClassLoader(); var factoryName = classLoader.getClassName(); - var factoryClass = (Class) classLoader.loadClass(factoryName); + var factoryClass = (Class) classLoader.loadClass(factoryName); var constructor = factoryClass.getDeclaredConstructor(); var extension = constructor.newInstance(); var application = new Jooby(); diff --git a/modules/jooby-apt/src/test/java/source/ParamSourceCheckerContext.java b/modules/jooby-apt/src/test/java/source/ParamSourceCheckerContext.java index 7336a49c68..668414af50 100644 --- a/modules/jooby-apt/src/test/java/source/ParamSourceCheckerContext.java +++ b/modules/jooby-apt/src/test/java/source/ParamSourceCheckerContext.java @@ -9,8 +9,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.ParamSource; -import io.jooby.Value; import io.jooby.test.MockContext; +import io.jooby.value.Value; public class ParamSourceCheckerContext extends MockContext { diff --git a/modules/jooby-apt/src/test/java/source/Routes.java b/modules/jooby-apt/src/test/java/source/Routes.java index 93737dfac6..cf7af8c5ab 100644 --- a/modules/jooby-apt/src/test/java/source/Routes.java +++ b/modules/jooby-apt/src/test/java/source/Routes.java @@ -5,13 +5,10 @@ */ package source; -import static org.junit.jupiter.api.Assertions.assertEquals; - import java.util.Arrays; import java.util.List; import io.jooby.Context; -import io.jooby.Reified; import io.jooby.annotation.GET; import io.jooby.annotation.POST; import io.jooby.annotation.Path; @@ -21,25 +18,21 @@ public class Routes { @GET public String doIt(Context ctx) { - assertEquals(String.class, ctx.getRoute().getReturnType()); return ctx.getRequestPath(); } @GET("/subpath") public List subpath(Context ctx) { - assertEquals(Reified.list(String.class).getType(), ctx.getRoute().getReturnType()); return Arrays.asList(ctx.getRequestPath()); } @GET("/object") public Object object(Context ctx) { - assertEquals(Object.class, ctx.getRoute().getReturnType()); return ctx; } @POST("/post") public JavaBeanParam post(Context ctx) { - assertEquals(JavaBeanParam.class, ctx.getRoute().getReturnType()); return new JavaBeanParam(); } diff --git a/modules/jooby-apt/src/test/java/tests/HandlerCompilerTest.java b/modules/jooby-apt/src/test/java/tests/HandlerCompilerTest.java index f8fec07fc7..a5c773a396 100644 --- a/modules/jooby-apt/src/test/java/tests/HandlerCompilerTest.java +++ b/modules/jooby-apt/src/test/java/tests/HandlerCompilerTest.java @@ -153,7 +153,7 @@ public void formParam() throws Exception { MockRouter router = new MockRouter(app); MockContext ctx = MockContextHelper.mockContext(); - Formdata formdata = Formdata.create(ctx); + Formdata formdata = Formdata.create(ctx.getValueFactory()); formdata.put("name", "yo"); assertEquals("yo", router.get("/p/formParam", ctx.setForm(formdata)).value()); diff --git a/modules/jooby-apt/src/test/java/tests/Issue1525.java b/modules/jooby-apt/src/test/java/tests/Issue1525.java index 8282986683..2732248551 100644 --- a/modules/jooby-apt/src/test/java/tests/Issue1525.java +++ b/modules/jooby-apt/src/test/java/tests/Issue1525.java @@ -21,11 +21,11 @@ public void routeClassAttributes() throws Exception { app -> { Route route0 = app.getRoutes().get(0); assertEquals(1, route0.getAttributes().size(), route0.getAttributes().toString()); - assertEquals("Admin", route0.attribute("roleAnnotation")); + assertEquals("Admin", route0.getAttribute("roleAnnotation")); Route route1 = app.getRoutes().get(1); assertEquals(1, route1.getAttributes().size(), route1.getAttributes().toString()); - assertEquals("User", route1.attribute("roleAnnotation")); + assertEquals("User", route1.getAttribute("roleAnnotation")); }); } } diff --git a/modules/jooby-apt/src/test/java/tests/Issue1527.java b/modules/jooby-apt/src/test/java/tests/Issue1527.java index 83502ae101..4de1eb9829 100644 --- a/modules/jooby-apt/src/test/java/tests/Issue1527.java +++ b/modules/jooby-apt/src/test/java/tests/Issue1527.java @@ -24,18 +24,19 @@ public void annotation() throws Exception { app -> { Route route0 = app.getRoutes().get(0); assertEquals(2, route0.getAttributes().size(), route0.getAttributes().toString()); - assertEquals(Controller1527.Role.ADMIN, route0.attribute("requireRole")); - assertEquals(Arrays.asList(TopEnum.FOO), route0.attribute("topAnnotation")); + assertEquals(Controller1527.Role.ADMIN, route0.getAttribute("requireRole")); + assertEquals(Arrays.asList(TopEnum.FOO), route0.getAttribute("topAnnotation")); Route route1 = app.getRoutes().get(1); assertEquals(1, route1.getAttributes().size(), route1.getAttributes().toString()); assertEquals( - Arrays.asList(TopEnum.BAR, TopEnum.FOO), route1.attribute("topAnnotation")); + Arrays.asList(TopEnum.BAR, TopEnum.FOO), route1.getAttribute("topAnnotation")); Route route2 = app.getRoutes().get(2); assertEquals(2, route2.getAttributes().size(), route2.getAttributes().toString()); - assertEquals(Arrays.asList(TopEnum.FOO), route2.attribute("topAnnotation")); - assertEquals(Arrays.asList("a", "b", "c"), route2.attribute("stringArrayAnnotation")); + assertEquals(Arrays.asList(TopEnum.FOO), route2.getAttribute("topAnnotation")); + assertEquals( + Arrays.asList("a", "b", "c"), route2.getAttribute("stringArrayAnnotation")); }); } } diff --git a/modules/jooby-apt/src/test/java/tests/ModuleCompilerTest.java b/modules/jooby-apt/src/test/java/tests/ModuleCompilerTest.java index f7071fbb0d..535b558950 100644 --- a/modules/jooby-apt/src/test/java/tests/ModuleCompilerTest.java +++ b/modules/jooby-apt/src/test/java/tests/ModuleCompilerTest.java @@ -39,7 +39,6 @@ import source.MinRoute; import source.NoPathRoute; import source.ParamSourceCheckerContext; -import source.PrimitiveReturnType; import source.RouteAttributes; import source.RouteDispatch; import source.RouteWithMimeTypes; @@ -261,16 +260,6 @@ public void noTopLevel() throws Exception { }); } - @Test - public void setPrimitiveReturnType() throws Exception { - new ProcessorRunner(new PrimitiveReturnType(), Map.of("jooby.returnType", true)) - .withRouter( - app -> { - Route route = app.getRoutes().get(0); - assertEquals(int.class, route.getReturnType()); - }); - } - @Test public void routeAttributes() throws Exception { new ProcessorRunner(new RouteAttributes()) @@ -278,18 +267,18 @@ public void routeAttributes() throws Exception { app -> { Route route = app.getRoutes().get(0); assertEquals(12, route.getAttributes().size(), route.getAttributes().toString()); - assertEquals("string", route.attribute("someAnnotation")); - assertEquals(Integer.valueOf(5), route.attribute("someAnnotation.i")); - assertEquals(Long.valueOf(200), route.attribute("someAnnotation.l")); - assertEquals(Float.valueOf(8), route.attribute("someAnnotation.f")); - assertEquals(Double.valueOf(99), route.attribute("someAnnotation.d")); - assertEquals(Integer.class, route.attribute("someAnnotation.type")); - assertEquals(true, route.attribute("someAnnotation.bool")); - assertEquals(Character.valueOf('X'), route.attribute("someAnnotation.c")); - assertEquals(Short.MIN_VALUE, (short) route.attribute("someAnnotation.s")); - assertEquals(Arrays.asList("a", "b"), route.attribute("someAnnotation.values")); - assertEquals("User", route.attribute("roleAnnotation")); - Map link = route.attribute("someAnnotation.annotation"); + assertEquals("string", route.getAttribute("someAnnotation")); + assertEquals(Integer.valueOf(5), route.getAttribute("someAnnotation.i")); + assertEquals(Long.valueOf(200), route.getAttribute("someAnnotation.l")); + assertEquals(Float.valueOf(8), route.getAttribute("someAnnotation.f")); + assertEquals(Double.valueOf(99), route.getAttribute("someAnnotation.d")); + assertEquals(Integer.class, route.getAttribute("someAnnotation.type")); + assertEquals(true, route.getAttribute("someAnnotation.bool")); + assertEquals(Character.valueOf('X'), route.getAttribute("someAnnotation.c")); + assertEquals(Short.MIN_VALUE, (short) route.getAttribute("someAnnotation.s")); + assertEquals(Arrays.asList("a", "b"), route.getAttribute("someAnnotation.values")); + assertEquals("User", route.getAttribute("roleAnnotation")); + Map link = route.getAttribute("someAnnotation.annotation"); assertNotNull(link); assertEquals("link", link.get("LinkAnnotation")); List array = (List) link.get("LinkAnnotation.array"); diff --git a/modules/jooby-apt/src/test/java/tests/i1807/Issue1807.java b/modules/jooby-apt/src/test/java/tests/i1807/Issue1807.java index 89bcf75aaa..e227dd7d74 100644 --- a/modules/jooby-apt/src/test/java/tests/i1807/Issue1807.java +++ b/modules/jooby-apt/src/test/java/tests/i1807/Issue1807.java @@ -12,10 +12,10 @@ import org.junit.jupiter.api.Test; import io.jooby.Formdata; -import io.jooby.ValueNode; import io.jooby.apt.ProcessorRunner; import io.jooby.test.MockContext; import io.jooby.test.MockRouter; +import io.jooby.value.Value; public class Issue1807 { @@ -27,7 +27,7 @@ public void shouldGenerateValidByteCode() throws Exception { Word1807 word = new Word1807(); MockRouter router = new MockRouter(app); Formdata formdata = mock(Formdata.class); - ValueNode missing = mock(ValueNode.class); + Value missing = mock(Value.class); when(missing.isMissing()).thenReturn(true); when(formdata.get("data")).thenReturn(missing); when(formdata.to(Word1807.class)).thenReturn(word); diff --git a/modules/jooby-apt/src/test/java/tests/i1814/C1814.java b/modules/jooby-apt/src/test/java/tests/i1814/C1814.java index a6046763c7..8b2d8fcf4f 100644 --- a/modules/jooby-apt/src/test/java/tests/i1814/C1814.java +++ b/modules/jooby-apt/src/test/java/tests/i1814/C1814.java @@ -5,13 +5,10 @@ */ package tests.i1814; -import static org.junit.jupiter.api.Assertions.assertEquals; - import java.util.Collections; import java.util.List; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Reified; import io.jooby.Route; import io.jooby.annotation.GET; import io.jooby.annotation.QueryParam; @@ -19,7 +16,6 @@ public class C1814 { @GET("/1814") public List getUsers(@QueryParam @NonNull String type, Route route) { - assertEquals(Reified.list(U1814.class).getType(), route.getReturnType()); return Collections.singletonList(new U1814(type)); } } diff --git a/modules/jooby-apt/src/test/java/tests/i2325/Issue2325.java b/modules/jooby-apt/src/test/java/tests/i2325/Issue2325.java index 0bc61f76bb..8f03ca5a94 100644 --- a/modules/jooby-apt/src/test/java/tests/i2325/Issue2325.java +++ b/modules/jooby-apt/src/test/java/tests/i2325/Issue2325.java @@ -19,7 +19,8 @@ public void shouldFavorNamedParamWithCustomConverter() throws Exception { new ProcessorRunner(new C2325()) .withRouter( app -> { - app.converter(new VC2325()); + var factory = app.getValueFactory(); + factory.put(MyID2325.class, new VC2325()); MockRouter router = new MockRouter(app); MockContext ctx = new MockContext(); ctx.setQueryString("?myId=1234_TODO"); @@ -32,7 +33,8 @@ public void shouldNotFavorObjectConverterWhenNamedArgIsMissing() throws Exceptio new ProcessorRunner(new C2325()) .withRouter( app -> { - app.converter(new VC2325()); + var factory = app.getValueFactory(); + factory.put(MyID2325.class, new VC2325()); MockRouter router = new MockRouter(app); MockContext ctx = new MockContext(); assertEquals("MyID:{}", router.get("/2325", ctx).value().toString()); diff --git a/modules/jooby-apt/src/test/java/tests/i2325/VC2325.java b/modules/jooby-apt/src/test/java/tests/i2325/VC2325.java index 6bf0ed2c4b..625c6c9499 100644 --- a/modules/jooby-apt/src/test/java/tests/i2325/VC2325.java +++ b/modules/jooby-apt/src/test/java/tests/i2325/VC2325.java @@ -5,17 +5,17 @@ */ package tests.i2325; -import io.jooby.Value; -import io.jooby.ValueConverter; +import java.lang.reflect.Type; -public class VC2325 implements ValueConverter { - @Override - public boolean supports(Class type) { - return type == MyID2325.class; - } +import org.jetbrains.annotations.NotNull; + +import io.jooby.value.ConversionHint; +import io.jooby.value.Converter; +import io.jooby.value.Value; +public class VC2325 implements Converter { @Override - public Object convert(Value value, Class type) { + public Object convert(@NotNull Type type, @NotNull Value value, @NotNull ConversionHint hint) { return new MyID2325(value.value()); } } diff --git a/modules/jooby-apt/src/test/java/tests/i2405/Converter2405.java b/modules/jooby-apt/src/test/java/tests/i2405/Converter2405.java index 449f5627f5..67012df1a4 100644 --- a/modules/jooby-apt/src/test/java/tests/i2405/Converter2405.java +++ b/modules/jooby-apt/src/test/java/tests/i2405/Converter2405.java @@ -5,17 +5,19 @@ */ package tests.i2405; -import io.jooby.Value; -import io.jooby.ValueConverter; +import java.lang.reflect.Type; -public class Converter2405 implements ValueConverter { - @Override - public boolean supports(Class type) { - return type == Bean2405.class; - } +import org.jetbrains.annotations.NotNull; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.value.ConversionHint; +import io.jooby.value.Converter; +import io.jooby.value.Value; + +public class Converter2405 implements Converter { @Override - public Object convert(Value value, Class type) { + public Object convert(@NotNull Type type, @NotNull Value value, @NonNull ConversionHint hint) { return new Bean2405(value.value()); } } diff --git a/modules/jooby-apt/src/test/java/tests/i2405/Issue2405.java b/modules/jooby-apt/src/test/java/tests/i2405/Issue2405.java index e92ec2d4e3..3cde1de105 100644 --- a/modules/jooby-apt/src/test/java/tests/i2405/Issue2405.java +++ b/modules/jooby-apt/src/test/java/tests/i2405/Issue2405.java @@ -19,7 +19,8 @@ public void shouldGenerateUniqueNames() throws Exception { new ProcessorRunner(new C2405()) .withRouter( app -> { - app.converter(new Converter2405()); + var factory = app.getValueFactory(); + factory.put(Bean2405.class, new Converter2405()); MockRouter router = new MockRouter(app); assertEquals( "foo", diff --git a/modules/jooby-apt/src/test/java/tests/i2525/VC2525.java b/modules/jooby-apt/src/test/java/tests/i2525/VC2525.java deleted file mode 100644 index 02bd272c78..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i2525/VC2525.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package tests.i2525; - -import io.jooby.Value; -import io.jooby.ValueConverter; -import tests.i2325.MyID2325; - -public class VC2525 implements ValueConverter { - @Override - public boolean supports(Class type) { - return type == MyID2325.class; - } - - @Override - public Object convert(Value value, Class type) { - return new MyID2325(value.value()); - } -} diff --git a/modules/jooby-apt/src/test/java/tests/i3422/C3422.java b/modules/jooby-apt/src/test/java/tests/i3422/C3422.java deleted file mode 100644 index 480694ba01..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i3422/C3422.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package tests.i3422; - -import io.jooby.annotation.GET; - -public class C3422 { - - @GET("/3422") - public ReactiveType reactiveType() { - return new ReactiveType(); - } -} diff --git a/modules/jooby-apt/src/test/java/tests/i3422/Issue3422.java b/modules/jooby-apt/src/test/java/tests/i3422/Issue3422.java deleted file mode 100644 index ee5e51404a..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i3422/Issue3422.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package tests.i3422; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import io.jooby.apt.ProcessorRunner; - -public class Issue3422 { - - @Test - public void generateCustomHandlerFunction() throws Exception { - new ProcessorRunner(new C3422(), Map.of("jooby.handler", ReactiveTypeGenerator.class.getName())) - .withRouter( - (app, source) -> { - assertTrue(source.toString().contains(", toReactive(this::reactiveType)")); - }); - } -} diff --git a/modules/jooby-apt/src/test/java/tests/i3422/ReactiveType.java b/modules/jooby-apt/src/test/java/tests/i3422/ReactiveType.java deleted file mode 100644 index bdf0911566..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i3422/ReactiveType.java +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package tests.i3422; - -public class ReactiveType {} diff --git a/modules/jooby-apt/src/test/java/tests/i3422/ReactiveTypeGenerator.java b/modules/jooby-apt/src/test/java/tests/i3422/ReactiveTypeGenerator.java deleted file mode 100644 index bfee76e5be..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i3422/ReactiveTypeGenerator.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package tests.i3422; - -import io.jooby.Route; -import io.jooby.annotation.ResultType; - -@ResultType(types = ReactiveType.class, handler = "toReactive") -public class ReactiveTypeGenerator { - - public static Route.Handler toReactive(Route.Handler next) { - return next; - } -} diff --git a/modules/jooby-apt/src/test/java/tests/i3460/Issue3460.java b/modules/jooby-apt/src/test/java/tests/i3460/Issue3460.java index 54828935b1..a6b33547f2 100644 --- a/modules/jooby-apt/src/test/java/tests/i3460/Issue3460.java +++ b/modules/jooby-apt/src/test/java/tests/i3460/Issue3460.java @@ -18,7 +18,7 @@ public void shouldNotUseJakartaProvider() throws Exception { new ProcessorRunner(new C3460()) .withRouter( (app, source) -> { - assertTrue(source.toString().contains("C3460_(java.util.function.Supplier<")); + assertTrue(source.toString().contains("C3460_(io.jooby.SneakyThrows.Supplier<")); }); } } diff --git a/modules/jooby-apt/src/test/java/tests/i3476/Issue3476.java b/modules/jooby-apt/src/test/java/tests/i3476/Issue3476.java index 4b2ca3e504..df72c7f467 100644 --- a/modules/jooby-apt/src/test/java/tests/i3476/Issue3476.java +++ b/modules/jooby-apt/src/test/java/tests/i3476/Issue3476.java @@ -25,7 +25,7 @@ public void shouldGenerateGenerics() throws Exception { .toString() .contains( "c.box(ctx.query(\"box\").isMissing() ?" - + " ctx.query().toNullable(tests.i3476.Box.class) :" + + " ctx.query(tests.i3476.Box.class) :" + " ctx.query(\"box\").toNullable(tests.i3476.Box.class))")); }); } diff --git a/modules/jooby-apt/src/test/java/tests/i3490/Box3490.java b/modules/jooby-apt/src/test/java/tests/i3490/Box3490.java deleted file mode 100644 index 92009117b9..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i3490/Box3490.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package tests.i3490; - -public class Box3490 { - private T value; - - public Box3490(T value) {} - - public T getValue() { - return value; - } -} diff --git a/modules/jooby-apt/src/test/java/tests/i3490/C3490.java b/modules/jooby-apt/src/test/java/tests/i3490/C3490.java deleted file mode 100644 index 0c87316f79..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i3490/C3490.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package tests.i3490; - -import io.jooby.annotation.GET; -import io.jooby.annotation.QueryParam; - -public class C3490 { - - @GET("/3490") - public Box3490 get(@QueryParam int id) { - return null; - } -} diff --git a/modules/jooby-apt/src/test/java/tests/i3490/Issue3490.java b/modules/jooby-apt/src/test/java/tests/i3490/Issue3490.java deleted file mode 100644 index 62e6d2c3ec..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i3490/Issue3490.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package tests.i3490; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import io.jooby.apt.ProcessorRunner; - -public class Issue3490 { - - @Test - public void shouldNotGeneratePrimitiveOnKotlinGenerics() throws IOException { - new ProcessorRunner(new C3490(), Map.of("jooby.returnType", true)) - .withSourceCode( - true, - source -> { - assertTrue( - source.contains( - ".setReturnType(io.jooby.Reified.getParameterized(tests.i3490.Box3490::class.java," - + " Integer::class.java).getType())")); - }); - } -} diff --git a/modules/jooby-apt/src/test/java/tests/i3567/Issue3567.java b/modules/jooby-apt/src/test/java/tests/i3567/Issue3567.java index 6f473366d9..5a50083a0a 100644 --- a/modules/jooby-apt/src/test/java/tests/i3567/Issue3567.java +++ b/modules/jooby-apt/src/test/java/tests/i3567/Issue3567.java @@ -24,7 +24,7 @@ public void shouldSupportGoogleInjectAnnotation() throws Exception { source -> { assertTrue(source.contains("this(C3567.class);")); assertTrue(source.contains("public C3567_(Class type) {")); - assertTrue(source.contains("this(ctx -> ctx.require(type));")); + assertTrue(source.contains("setup(ctx -> ctx.require(type));")); }); } } diff --git a/modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java b/modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java index e9b59c0a09..095e42d558 100644 --- a/modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java +++ b/modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java @@ -23,7 +23,7 @@ public void generate_validation_forBean() throws Exception { source.contains( "c.validateQueryBean(io.jooby.validation.BeanValidator.apply(ctx," + " ctx.query(\"bean\").isMissing() ?" - + " ctx.query().toNullable(tests.validation.Bean.class) :" + + " ctx.query(tests.validation.Bean.class) :" + " ctx.query(\"bean\").toNullable(tests.validation.Bean.class)))")); assertTrue( diff --git a/modules/jooby-avaje-jsonb/pom.xml b/modules/jooby-avaje-jsonb/pom.xml index c67f6c8727..191458e180 100644 --- a/modules/jooby-avaje-jsonb/pom.xml +++ b/modules/jooby-avaje-jsonb/pom.xml @@ -61,5 +61,19 @@ mockito-core test + + + org.openjdk.jmh + jmh-core + 1.37 + test + + + + org.openjdk.jmh + jmh-generator-annprocess + 1.37 + test + diff --git a/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java b/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java index 4ad5977ffc..706e737b77 100644 --- a/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java +++ b/modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java @@ -18,8 +18,8 @@ import io.jooby.MessageDecoder; import io.jooby.MessageEncoder; import io.jooby.ServiceRegistry; -import io.jooby.buffer.DataBuffer; -import io.jooby.internal.avaje.jsonb.DataBufferJsonOutput; +import io.jooby.internal.avaje.jsonb.BufferedJsonOutput; +import io.jooby.output.Output; /** * JSON module using Avaje-JsonB: message; + + private OutputFactory factory; + private ThreadLocal cache = + ThreadLocal.withInitial( + () -> { + return factory.allocate(1024); + }); + + @Setup + public void setup() { + message = Map.of("id", 98, "value", "Hello World"); + jsonb = Jsonb.builder().build(); + factory = OutputFactory.create(OutputOptions.small()); + } + + @Benchmark + public void withJsonBuffer() { + factory.wrap(jsonb.toJsonBytes(message)); + } + + @Benchmark + public void withCachedBufferedOutput() { + var buffer = cache.get().clear(); + jsonb.toJson(message, jsonb.writer(new BufferedJsonOutput(buffer))); + } + + @Benchmark + public void withBufferedOutput() { + var buffer = factory.allocate(1024); + jsonb.toJson(message, jsonb.writer(new BufferedJsonOutput(buffer))); + } + + @Benchmark + public void withCompositeOutput() { + var buffer = factory.newComposite(); + jsonb.toJson(message, jsonb.writer(new BufferedJsonOutput(buffer))); + } +} diff --git a/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbModuleTest.java b/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbModuleTest.java index 9bfa4528e8..cc5f8f3142 100644 --- a/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbModuleTest.java +++ b/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbModuleTest.java @@ -38,6 +38,6 @@ void encode() { var ctx = new MockContext(); var o = List.of(1, 2, 3); var json = decoder.encode(ctx, o); - assertEquals("[1,2,3]", json.toString(StandardCharsets.UTF_8)); + assertEquals("[1,2,3]", StandardCharsets.UTF_8.decode(json.asByteBuffer()).toString()); } } diff --git a/modules/jooby-caffeine/src/main/java/io/jooby/caffeine/CaffeineSessionStore.java b/modules/jooby-caffeine/src/main/java/io/jooby/caffeine/CaffeineSessionStore.java index 01161897bb..e13eaf9875 100644 --- a/modules/jooby-caffeine/src/main/java/io/jooby/caffeine/CaffeineSessionStore.java +++ b/modules/jooby-caffeine/src/main/java/io/jooby/caffeine/CaffeineSessionStore.java @@ -40,8 +40,8 @@ public class CaffeineSessionStore extends SessionStore.InMemory { * * @param cache Cache. */ - public CaffeineSessionStore(@NonNull Cache cache) { - super(SessionToken.cookieId(SessionToken.SID)); + public CaffeineSessionStore(@NonNull SessionToken token, @NonNull Cache cache) { + super(token); this.cache = cache; } @@ -50,14 +50,14 @@ public CaffeineSessionStore(@NonNull Cache cache) { * * @param timeout Session timeout. */ - public CaffeineSessionStore(@NonNull Duration timeout) { - super(SessionToken.cookieId(SessionToken.SID)); + public CaffeineSessionStore(@NonNull SessionToken token, @NonNull Duration timeout) { + super(token); this.cache = Caffeine.newBuilder().expireAfterAccess(timeout).build(); } /** Creates a new session store with timeout of 30 minutes. */ - public CaffeineSessionStore() { - this(Duration.ofMinutes(DEFAULT_TIMEOUT)); + public CaffeineSessionStore(@NonNull SessionToken token) { + this(token, Duration.ofMinutes(DEFAULT_TIMEOUT)); } @Override diff --git a/modules/jooby-freemarker/src/main/java/io/jooby/freemarker/FreemarkerTemplateEngine.java b/modules/jooby-freemarker/src/main/java/io/jooby/freemarker/FreemarkerTemplateEngine.java index 4318f21487..8036288d63 100644 --- a/modules/jooby-freemarker/src/main/java/io/jooby/freemarker/FreemarkerTemplateEngine.java +++ b/modules/jooby-freemarker/src/main/java/io/jooby/freemarker/FreemarkerTemplateEngine.java @@ -15,7 +15,7 @@ import io.jooby.Context; import io.jooby.ModelAndView; import io.jooby.TemplateEngine; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; class FreemarkerTemplateEngine implements TemplateEngine { @@ -33,8 +33,8 @@ public List extensions() { } @Override - public DataBuffer render(Context ctx, ModelAndView modelAndView) throws Exception { - var buffer = ctx.getBufferFactory().allocateBuffer(); + public Output render(Context ctx, ModelAndView modelAndView) throws Exception { + var buffer = ctx.getOutputFactory().allocate(); var template = freemarker.getTemplate(modelAndView.getView()); var writer = buffer.asWriter(); var wrapper = freemarker.getObjectWrapper(); diff --git a/modules/jooby-freemarker/src/test/java/io/jooby/freemarker/FreemarkerModuleTest.java b/modules/jooby-freemarker/src/test/java/io/jooby/freemarker/FreemarkerModuleTest.java index f514dc50d0..6e092231c9 100644 --- a/modules/jooby-freemarker/src/test/java/io/jooby/freemarker/FreemarkerModuleTest.java +++ b/modules/jooby-freemarker/src/test/java/io/jooby/freemarker/FreemarkerModuleTest.java @@ -74,7 +74,9 @@ public void render() throws Exception { engine.render( ctx, ModelAndView.map("index.ftl").put("user", new User("foo", "bar")).put("sign", "!")); - assertEquals("Hello foo bar var!", output.toString(StandardCharsets.UTF_8).trim()); + assertEquals( + "Hello foo bar var!", + StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString().trim()); } @Test @@ -97,33 +99,42 @@ public void renderWithLocale() throws Exception { assertEquals( "friday", - engine - .render(ctx, ModelAndView.map("locales.ftl").put("someDate", nextFriday)) - .toString(StandardCharsets.UTF_8) + StandardCharsets.UTF_8 + .decode( + engine + .render(ctx, ModelAndView.map("locales.ftl").put("someDate", nextFriday)) + .asByteBuffer()) + .toString() .trim() .toLowerCase()); assertEquals( "friday", - engine - .render( - ctx, - ModelAndView.map("locales.ftl") - .put("someDate", nextFriday) - .setLocale(new Locale("en", "GB"))) - .toString(StandardCharsets.UTF_8) + StandardCharsets.UTF_8 + .decode( + engine + .render( + ctx, + ModelAndView.map("locales.ftl") + .put("someDate", nextFriday) + .setLocale(new Locale("en", "GB"))) + .asByteBuffer()) + .toString() .trim() .toLowerCase()); assertEquals( "freitag", - engine - .render( - ctx, - ModelAndView.map("locales.ftl") - .put("someDate", nextFriday) - .setLocale(Locale.GERMAN)) - .toString(StandardCharsets.UTF_8) + StandardCharsets.UTF_8 + .decode( + engine + .render( + ctx, + ModelAndView.map("locales.ftl") + .put("someDate", nextFriday) + .setLocale(Locale.GERMAN)) + .asByteBuffer()) + .toString() .trim() .toLowerCase()); } @@ -133,8 +144,7 @@ public void publicField() throws Exception { Configuration freemarker = FreemarkerModule.create() .build(new Environment(getClass().getClassLoader(), ConfigFactory.empty(), "test")); - FreemarkerTemplateEngine engine = - new FreemarkerTemplateEngine(freemarker, Arrays.asList(".ftl")); + FreemarkerTemplateEngine engine = new FreemarkerTemplateEngine(freemarker, List.of(".ftl")); MockContext ctx = new MockContext().setRouter(new Jooby().setLocales(singletonList(Locale.ENGLISH))); ctx.getAttributes().put("local", "var"); @@ -142,7 +152,9 @@ public void publicField() throws Exception { engine.render( ctx, ModelAndView.map("index.ftl").put("user", new MyModel("foo", "bar")).put("sign", "!")); - assertEquals("Hello foo bar var!", output.toString(StandardCharsets.UTF_8).trim()); + assertEquals( + "Hello foo bar var!", + StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString().trim()); } @Test @@ -159,6 +171,6 @@ public void customTemplatePath() throws Exception { new MockContext().setRouter(new Jooby().setLocales(singletonList(Locale.ENGLISH))); ctx.getAttributes().put("local", "var"); var output = engine.render(ctx, ModelAndView.map("index.ftl")); - assertEquals("var", output.toString(StandardCharsets.UTF_8).trim()); + assertEquals("var", StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString().trim()); } } diff --git a/modules/jooby-graphiql/pom.xml b/modules/jooby-graphiql/pom.xml index ed4e199f00..9ba3c8e558 100644 --- a/modules/jooby-graphiql/pom.xml +++ b/modules/jooby-graphiql/pom.xml @@ -35,29 +35,6 @@ - - - - - - - - - - - - - - - - - - - - - - - org.apache.maven.plugins maven-antrun-plugin diff --git a/modules/jooby-gson/src/main/java/io/jooby/gson/GsonModule.java b/modules/jooby-gson/src/main/java/io/jooby/gson/GsonModule.java index 6bc068feb7..f9b51bcee7 100644 --- a/modules/jooby-gson/src/main/java/io/jooby/gson/GsonModule.java +++ b/modules/jooby-gson/src/main/java/io/jooby/gson/GsonModule.java @@ -21,7 +21,7 @@ import io.jooby.MediaType; import io.jooby.MessageDecoder; import io.jooby.MessageEncoder; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; /** * JSON module using Gson: https://github.com/google/gson. @@ -107,8 +107,8 @@ public Object decode(@NonNull Context ctx, @NonNull Type type) throws Exception } @NonNull @Override - public DataBuffer encode(@NonNull Context ctx, @NonNull Object value) { - var buffer = ctx.getBufferFactory().allocateBuffer(); + public Output encode(@NonNull Context ctx, @NonNull Object value) { + var buffer = ctx.getOutputFactory().allocate(); ctx.setDefaultResponseType(MediaType.json); gson.toJson(value, buffer.asWriter()); return buffer; diff --git a/modules/jooby-gson/src/test/java/io/jooby/gson/Issue3434.java b/modules/jooby-gson/src/test/java/io/jooby/gson/Issue3434.java index c0015f4336..a6351fb657 100644 --- a/modules/jooby-gson/src/test/java/io/jooby/gson/Issue3434.java +++ b/modules/jooby-gson/src/test/java/io/jooby/gson/Issue3434.java @@ -15,7 +15,8 @@ import com.google.gson.GsonBuilder; import io.jooby.Context; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; public class Issue3434 { String text = @@ -139,12 +140,14 @@ public class Issue3434 { @Test void shouldEncodeUsingBufferWriter() { var gson = new GsonBuilder().create(); - var factory = new DefaultDataBufferFactory(); + var factory = OutputFactory.create(OutputOptions.small()); var ctx = mock(Context.class); - when(ctx.getBufferFactory()).thenReturn(factory); + when(ctx.getOutputFactory()).thenReturn(factory); var encoder = new GsonModule(); var result = encoder.encode(ctx, new Bean3434(text)); - assertEquals(gson.toJson(new Bean3434(text)), result.toString(StandardCharsets.UTF_8)); + assertEquals( + gson.toJson(new Bean3434(text)), + StandardCharsets.UTF_8.decode(result.asByteBuffer()).toString()); } } diff --git a/modules/jooby-handlebars/src/main/java/io/jooby/internal/handlebars/HandlebarsTemplateEngine.java b/modules/jooby-handlebars/src/main/java/io/jooby/internal/handlebars/HandlebarsTemplateEngine.java index 3852e980ef..8eed83b35e 100644 --- a/modules/jooby-handlebars/src/main/java/io/jooby/internal/handlebars/HandlebarsTemplateEngine.java +++ b/modules/jooby-handlebars/src/main/java/io/jooby/internal/handlebars/HandlebarsTemplateEngine.java @@ -14,7 +14,7 @@ import io.jooby.Context; import io.jooby.ModelAndView; import io.jooby.TemplateEngine; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class HandlebarsTemplateEngine implements TemplateEngine { @@ -35,14 +35,14 @@ public List extensions() { } @Override - public DataBuffer render(Context ctx, ModelAndView modelAndView) throws Exception { + public Output render(Context ctx, ModelAndView modelAndView) throws Exception { var template = handlebars.compile(modelAndView.getView()); var engineModel = com.github.jknack.handlebars.Context.newBuilder(modelAndView.getModel()) .resolver(resolvers) .build() .data(ctx.getAttributes()); - var buffer = ctx.getBufferFactory().allocateBuffer(); + var buffer = ctx.getOutputFactory().allocate(); template.apply(engineModel, buffer.asWriter()); return buffer; } diff --git a/modules/jooby-handlebars/src/test/java/io/jooby/handlebars/HandlebarsModuleTest.java b/modules/jooby-handlebars/src/test/java/io/jooby/handlebars/HandlebarsModuleTest.java index f52339c227..5320f24835 100644 --- a/modules/jooby-handlebars/src/test/java/io/jooby/handlebars/HandlebarsModuleTest.java +++ b/modules/jooby-handlebars/src/test/java/io/jooby/handlebars/HandlebarsModuleTest.java @@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.Test; @@ -57,7 +58,9 @@ public void render() throws Exception { engine.render( ctx, ModelAndView.map("index.hbs").put("user", new User("foo", "bar")).put("sign", "!")); - assertEquals("Hello foo bar var!", output.toString(StandardCharsets.UTF_8).trim()); + assertEquals( + "Hello foo bar var!", + StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString().trim()); } @Test @@ -70,13 +73,15 @@ public void renderFileSystem() throws Exception { new HandlebarsTemplateEngine( handlebars, ValueResolver.defaultValueResolvers().toArray(new ValueResolver[0]), - Arrays.asList(".hbs")); + List.of(".hbs")); MockContext ctx = new MockContext(); ctx.getAttributes().put("local", "var"); var output = engine.render( ctx, ModelAndView.map("index.hbs").put("user", new User("foo", "bar")).put("sign", "!")); - assertEquals("Hello foo bar var!", output.toString(StandardCharsets.UTF_8).trim()); + assertEquals( + "Hello foo bar var!", + StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString().trim()); } } diff --git a/modules/jooby-hibernate-validator/pom.xml b/modules/jooby-hibernate-validator/pom.xml index fd17f1043a..941202a862 100644 --- a/modules/jooby-hibernate-validator/pom.xml +++ b/modules/jooby-hibernate-validator/pom.xml @@ -22,19 +22,13 @@ org.hibernate.validator hibernate-validator - 8.0.2.Final - - - - jakarta.el - jakarta.el-api - 6.0.1 + 9.0.1.Final org.glassfish.expressly expressly - 5.0.0 + 6.0.0 diff --git a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java index 2deca950b5..54674f4eed 100644 --- a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java +++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java @@ -71,21 +71,6 @@ public HibernateValidatorModule() { this(byProvider(HibernateValidator.class).configure()); } - /** - * Setups a configurer callback. - * - * @param configurer Configurer callback. - * @return This module. - * @deprecated Use {@link - * HibernateValidatorModule#HibernateValidatorModule(HibernateValidatorConfiguration)} - */ - @Deprecated - public HibernateValidatorModule doWith( - @NonNull final Consumer configurer) { - this.configurer = configurer; - return this; - } - /** * Overrides the default status code for the errors produced by validation. Default code is * UNPROCESSABLE_ENTITY(422) diff --git a/modules/jooby-jackson/pom.xml b/modules/jooby-jackson/pom.xml index b16c61f880..8eb7d91451 100644 --- a/modules/jooby-jackson/pom.xml +++ b/modules/jooby-jackson/pom.xml @@ -65,5 +65,19 @@ test + + org.openjdk.jmh + jmh-core + 1.37 + test + + + + org.openjdk.jmh + jmh-generator-annprocess + 1.37 + test + + diff --git a/modules/jooby-jackson/src/main/java/io/jooby/jackson/JacksonModule.java b/modules/jooby-jackson/src/main/java/io/jooby/jackson/JacksonModule.java index 4ca1b02163..2cd82c291c 100644 --- a/modules/jooby-jackson/src/main/java/io/jooby/jackson/JacksonModule.java +++ b/modules/jooby-jackson/src/main/java/io/jooby/jackson/JacksonModule.java @@ -32,7 +32,7 @@ import io.jooby.MessageEncoder; import io.jooby.ServiceRegistry; import io.jooby.StatusCode; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; /** * JSON module using Jackson: https://jooby.io/modules/jackson. @@ -153,11 +153,11 @@ public void install(@NonNull Jooby application) { } @Override - public DataBuffer encode(@NonNull Context ctx, @NonNull Object value) throws Exception { - var buffer = ctx.getBufferFactory().allocateBuffer(); + public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + var factory = ctx.getOutputFactory(); ctx.setDefaultResponseType(mediaType); - mapper.writer().writeValue(buffer.asOutputStream(), value); - return buffer; + // let jackson uses his own cache, so wrap the bytes + return factory.wrap(mapper.writeValueAsBytes(value)); } @Override diff --git a/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java b/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java new file mode 100644 index 0000000000..3c8f5dbba5 --- /dev/null +++ b/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java @@ -0,0 +1,55 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.jackson; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jooby.output.BufferedOutput; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; + +@Fork(5) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 10, time = 1) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Benchmark) +public class JacksonBench { + private ObjectMapper mapper; + private Map message; + + private OutputFactory factory; + private ThreadLocal cache = ThreadLocal.withInitial(() -> factory.allocate(1024)); + + @Setup + public void setup() { + message = Map.of("id", 98, "value", "Hello World"); + mapper = new ObjectMapper(); + factory = OutputFactory.create(OutputOptions.small()); + } + + @Benchmark + public void bytes() throws JsonProcessingException { + mapper.writeValueAsBytes(message); + } + + @Benchmark + public void wrapBytes() throws JsonProcessingException { + factory.wrap(mapper.writeValueAsBytes(message)); + } + + @Benchmark + public void output() throws IOException { + var buffer = cache.get().clear(); + mapper.writeValue(buffer.asOutputStream(), message); + } +} diff --git a/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonJsonModuleTest.java b/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonJsonModuleTest.java index c7272fbeab..e4bf60dac4 100644 --- a/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonJsonModuleTest.java +++ b/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonJsonModuleTest.java @@ -21,19 +21,20 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; public class JacksonJsonModuleTest { @Test public void renderJson() throws Exception { Context ctx = mock(Context.class); - when(ctx.getBufferFactory()).thenReturn(new DefaultDataBufferFactory()); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small())); JacksonModule jackson = new JacksonModule(new ObjectMapper()); var buffer = jackson.encode(ctx, mapOf("k", "v")); - assertEquals("{\"k\":\"v\"}", buffer.toString(StandardCharsets.UTF_8)); + assertEquals("{\"k\":\"v\"}", StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); verify(ctx).setDefaultResponseType(MediaType.json); } @@ -57,12 +58,14 @@ public void parseJson() throws Exception { @Test public void renderXml() throws Exception { Context ctx = mock(Context.class); - when(ctx.getBufferFactory()).thenReturn(new DefaultDataBufferFactory()); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small())); JacksonModule jackson = new JacksonModule(new XmlMapper()); var buffer = jackson.encode(ctx, mapOf("k", "v")); - assertEquals("v", buffer.toString(StandardCharsets.UTF_8)); + assertEquals( + "v", + StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); verify(ctx).setDefaultResponseType(MediaType.xml); } diff --git a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyCallbacks.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyCallbacks.java index afe0eef0f6..432fe708de 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyCallbacks.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyCallbacks.java @@ -6,11 +6,12 @@ package io.jooby.internal.jetty; import java.nio.ByteBuffer; +import java.util.Iterator; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.Callback; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class JettyCallbacks { public static class ByteBufferArrayCallback implements Callback { @@ -44,17 +45,17 @@ public void failed(Throwable x) { } } - public static class DataBufferCallback implements Callback { + public static class OutputCallback implements Callback { private final Response response; private final Callback cb; - private final DataBuffer.ByteBufferIterator it; + private final Iterator it; private boolean closeOnLast; - public DataBufferCallback(Response response, Callback cb, DataBuffer buffer) { + public OutputCallback(Response response, Callback cb, Output buffer) { this.response = response; this.cb = cb; - this.it = buffer.readableByteBuffers(); + this.it = buffer.iterator(); } public void send(boolean closeOnLast) { @@ -72,11 +73,7 @@ public void send(boolean closeOnLast) { } private void sendLast(boolean last, ByteBuffer buffer) { - try { - response.write(last, buffer, cb); - } finally { - it.close(); - } + response.write(last, buffer, cb); } @Override @@ -86,17 +83,12 @@ public void succeeded() { @Override public void failed(Throwable x) { - try { - cb.failed(x); - } finally { - it.close(); - } + cb.failed(x); } } - public static DataBufferCallback fromDataBuffer( - Response response, Callback cb, DataBuffer buffer) { - return new DataBufferCallback(response, cb, buffer); + public static OutputCallback fromOutput(Response response, Callback cb, Output output) { + return new OutputCallback(response, cb, output); } public static ByteBufferArrayCallback fromByteBufferArray( diff --git a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java index 6350054d9a..dcbf817346 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import org.eclipse.jetty.http.*; @@ -44,6 +45,7 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Fields; +import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.websocket.server.ServerWebSocketContainer; import org.slf4j.Logger; @@ -61,7 +63,6 @@ import io.jooby.QueryString; import io.jooby.Route; import io.jooby.Router; -import io.jooby.RouterOption; import io.jooby.Sender; import io.jooby.Server; import io.jooby.ServerSentEmitter; @@ -69,10 +70,9 @@ import io.jooby.SessionStore; import io.jooby.SneakyThrows; import io.jooby.StatusCode; -import io.jooby.Value; -import io.jooby.ValueNode; import io.jooby.WebSocket; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; +import io.jooby.value.Value; public class JettyContext implements DefaultContext, Callback { private final int bufferSize; @@ -83,7 +83,7 @@ public class JettyContext implements DefaultContext, Callback { private QueryString query; private Formdata formdata; private List files; - private ValueNode headers; + private Value headers; private Map pathMap = Collections.EMPTY_MAP; private Map attributes = new HashMap<>(); private Router router; @@ -204,7 +204,7 @@ public Context setPathMap(Map pathMap) { @NonNull @Override public QueryString query() { if (query == null) { - query = QueryString.create(this, request.getHttpURI().getQuery()); + query = QueryString.create(getValueFactory(), request.getHttpURI().getQuery()); } return query; } @@ -212,7 +212,7 @@ public QueryString query() { @NonNull @Override public Formdata form() { if (formdata == null) { - formdata = Formdata.create(this); + formdata = Formdata.create(getValueFactory()); formParam(request, formdata); @@ -233,8 +233,26 @@ public Formdata form() { parser.setMaxMemoryFileSize(bufferSize); parser.setMaxLength(maxRequestSize); // Convert the request content into parts. - var parts = parser.parse(request).get(); - for (var part : parts) { + var futureParts = new CompletableFuture(); + var formCallback = + new Promise.Invocable() { + @Override + public void succeeded(MultiPartFormData.Parts result) { + futureParts.complete(result); + } + + @Override + public void failed(Throwable x) { + futureParts.completeExceptionally(x); + } + + @Override + public InvocationType getInvocationType() { + return InvocationType.NON_BLOCKING; + } + }; + parser.parse(request, formCallback); + for (var part : futureParts.get()) { if (part.getFileName() != null) { String name = part.getName(); formdata.put(name, register(new JettyFileUpload(router.getTmpdir(), part))); @@ -253,17 +271,17 @@ public Formdata form() { @NonNull @Override public Value header(@NonNull String name) { - return Value.create(this, name, request.getHeaders().getValuesList(name)); + return Value.create(getValueFactory(), name, request.getHeaders().getValuesList(name)); } @NonNull @Override - public ValueNode header() { + public Value header() { if (headers == null) { Map> headerMap = new LinkedHashMap<>(); for (HttpField header : request.getHeaders()) { headerMap.put(header.getName(), header.getValueList()); } - headers = Value.headers(this, headerMap); + headers = Value.headers(getValueFactory(), headerMap); } return headers; } @@ -518,8 +536,8 @@ public Context send(@NonNull String data, @NonNull Charset charset) { } @NonNull @Override - public Context send(@NonNull DataBuffer data) { - data.send(this); + public Context send(@NonNull Output output) { + output.send(this); return this; } @@ -589,7 +607,7 @@ public boolean isResponseStarted() { @Override public boolean getResetHeadersOnError() { return resetHeadersOnError == null - ? getRouter().getRouterOptions().contains(RouterOption.RESET_HEADERS_ON_ERROR) + ? getRouter().getRouterOptions().isResetHeadersOnError() : resetHeadersOnError; } diff --git a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettySender.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettySender.java index 31bf66913f..c8f235041f 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettySender.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettySender.java @@ -5,16 +5,15 @@ */ package io.jooby.internal.jetty; -import static io.jooby.internal.jetty.JettyCallbacks.fromDataBuffer; +import static io.jooby.internal.jetty.JettyCallbacks.fromOutput; import java.nio.ByteBuffer; import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.Callback; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Sender; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class JettySender implements Sender { private final JettyContext ctx; @@ -32,8 +31,8 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { - fromDataBuffer(response, toJettyCallback(ctx, callback), data).send(false); + public Sender write(@NonNull Output output, @NonNull Callback callback) { + fromOutput(response, toJettyCallback(ctx, callback), output).send(false); return this; } diff --git a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyServerSentEmitter.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyServerSentEmitter.java index 719634ae17..6cf1ecee0a 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyServerSentEmitter.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyServerSentEmitter.java @@ -5,7 +5,7 @@ */ package io.jooby.internal.jetty; -import static io.jooby.internal.jetty.JettyCallbacks.fromDataBuffer; +import static io.jooby.internal.jetty.JettyCallbacks.fromOutput; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -63,7 +63,7 @@ public Context getContext() { @NonNull @Override public ServerSentEmitter send(@NonNull ServerSentMessage data) { if (isOpen()) { - fromDataBuffer(response, this, data.encode(jetty)).send(false); + fromOutput(response, this, data.encode(jetty)).send(false); } return this; } diff --git a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyWebSocket.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyWebSocket.java index c340966710..d38cee1e0e 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyWebSocket.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyWebSocket.java @@ -34,7 +34,7 @@ import io.jooby.WebSocketCloseStatus; import io.jooby.WebSocketConfigurer; import io.jooby.WebSocketMessage; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class JettyWebSocket implements Session.Listener, WebSocketConfigurer, WebSocket { @@ -287,17 +287,18 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback } @NonNull @Override - public WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callback) { + public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage( - (remote, writeCallback) -> remote.sendText(message.toString(UTF_8), writeCallback), + (remote, writeCallback) -> + remote.sendText(UTF_8.decode(message.asByteBuffer()).toString(), writeCallback), new WriteCallbackAdaptor(this, callback)); } @NonNull @Override - public WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback) { + public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage( (remote, writeCallback) -> - new WebSocketDataBufferCallback(writeCallback, message, remote::sendBinary).send(), + new WebSocketOutputCallback(writeCallback, message, remote::sendBinary).send(), new WriteCallbackAdaptor(this, callback)); } diff --git a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketDataBufferCallback.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketOutputCallback.java similarity index 61% rename from modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketDataBufferCallback.java rename to modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketOutputCallback.java index 8ae2f82eb4..1f11df0989 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketDataBufferCallback.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketOutputCallback.java @@ -6,30 +6,29 @@ package io.jooby.internal.jetty; import java.nio.ByteBuffer; +import java.util.Iterator; import org.eclipse.jetty.websocket.api.Callback; import io.jooby.SneakyThrows.Consumer2; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; -public class WebSocketDataBufferCallback implements Callback { +public class WebSocketOutputCallback implements Callback { - private final DataBuffer.ByteBufferIterator it; + private final Iterator it; private final Callback cb; private Consumer2 sender; - public WebSocketDataBufferCallback( - Callback cb, DataBuffer buffer, Consumer2 sender) { + public WebSocketOutputCallback( + Callback cb, Output buffer, Consumer2 sender) { this.cb = cb; - this.it = buffer.readableByteBuffers(); + this.it = buffer.iterator(); this.sender = sender; } public void send() { if (it.hasNext()) { sender.accept(it.next(), this); - } else { - it.close(); } } @@ -44,10 +43,6 @@ public void succeed() { @Override public void fail(Throwable x) { - try { - cb.fail(x); - } finally { - it.close(); - } + cb.fail(x); } } diff --git a/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java b/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java index 26903c4463..3c6d23cea0 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java @@ -20,6 +20,7 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.util.DecoratedObjectFactory; +import org.eclipse.jetty.util.ProcessorUtils; import org.eclipse.jetty.util.compression.CompressionPool; import org.eclipse.jetty.util.compression.DeflaterPool; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -36,6 +37,7 @@ import io.jooby.internal.jetty.JettyHttpExpectAndContinueHandler; import io.jooby.internal.jetty.PrefixHandler; import io.jooby.internal.jetty.http2.JettyHttp2Configurer; +import io.jooby.output.OutputFactory; /** * Web server implementation using Jetty. @@ -45,8 +47,19 @@ */ public class JettyServer extends io.jooby.Server.Base { + private static final String NAME = "jetty"; private static final int THREADS = 200; + static { + int cpus = ProcessorUtils.availableProcessors(); + var ioThreads = Math.max(1, Math.min(cpus / 2, THREADS / 16)); + System.setProperty("server.ioThreads", ioThreads + ""); + System.setProperty("server.workerThreads", THREADS + ""); + System.setProperty("server.name", NAME); + } + + private OutputFactory outputFactory; + private Server server; private ThreadPool threadPool; @@ -55,29 +68,37 @@ public class JettyServer extends io.jooby.Server.Base { private Consumer httpConfigurer; - // TODO: integrate buffer factory with Jetty. - // private DataBufferFactory bufferFactory; + public JettyServer(@NonNull ServerOptions options, @NonNull QueuedThreadPool threadPool) { + setOptions(options); + this.threadPool = threadPool; + } public JettyServer(@NonNull QueuedThreadPool threadPool) { this.threadPool = threadPool; } + public JettyServer(@NonNull ServerOptions options) { + setOptions(options); + } + public JettyServer() {} @NonNull @Override - public JettyServer setOptions(@NonNull ServerOptions options) { - super.setOptions(options.setWorkerThreads(options.getWorkerThreads(THREADS))); - return this; + public OutputFactory getOutputFactory() { + if (outputFactory == null) { + this.outputFactory = OutputFactory.create(getOptions().getOutput()); + } + return outputFactory; } - @Override - protected ServerOptions defaultOptions() { - return new ServerOptions().setServer("jetty").setWorkerThreads(THREADS); + public JettyServer setOutputFactory(OutputFactory outputFactory) { + this.outputFactory = outputFactory; + return this; } @NonNull @Override public String getName() { - return "jetty"; + return NAME; } /** @@ -103,6 +124,10 @@ public io.jooby.Server start(@NonNull Jooby... application) { "org.eclipse.jetty.server.Request.maxFormContentSize", Long.toString(options.getMaxRequestSize())); + for (var app : applications) { + app.getServices().put(ServerOptions.class, options); + app.getServices().put(io.jooby.Server.class, this); + } addShutdownHook(); if (threadPool == null) { @@ -122,8 +147,8 @@ public io.jooby.Server start(@NonNull Jooby... application) { var httpConf = new HttpConfiguration(); httpConf.setUriCompliance(UriCompliance.LEGACY); - httpConf.setOutputBufferSize(options.getBufferSize()); - httpConf.setOutputAggregationSize(options.getBufferSize()); + httpConf.setOutputBufferSize(options.getOutput().getSize()); + httpConf.setOutputAggregationSize(options.getOutput().getSize()); httpConf.setSendXPoweredBy(false); httpConf.setSendDateHeader(options.getDefaultHeaders()); httpConf.setSendServerVersion(false); @@ -274,7 +299,7 @@ private List> createHandler( new JettyHandler( invocationType, applications.get(0), - options.getBufferSize(), + options.getOutput().getSize(), options.getMaxRequestSize(), options.getDefaultHeaders()); if (options.isExpectContinue() == Boolean.TRUE) { diff --git a/modules/jooby-jstachio/src/main/java/io/jooby/jstachio/JStachioMessageEncoder.java b/modules/jooby-jstachio/src/main/java/io/jooby/jstachio/JStachioMessageEncoder.java index 911599fa27..2249d8deac 100644 --- a/modules/jooby-jstachio/src/main/java/io/jooby/jstachio/JStachioMessageEncoder.java +++ b/modules/jooby-jstachio/src/main/java/io/jooby/jstachio/JStachioMessageEncoder.java @@ -8,13 +8,14 @@ import java.io.IOException; import java.util.function.BiFunction; +import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.MessageEncoder; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; import io.jstach.jstachio.JStachio; import io.jstach.jstachio.output.ByteBufferEncodedOutput; -class JStachioMessageEncoder extends JStachioRenderer implements MessageEncoder { +class JStachioMessageEncoder extends JStachioRenderer implements MessageEncoder { public JStachioMessageEncoder( JStachio jstachio, @@ -24,7 +25,7 @@ public JStachioMessageEncoder( } @Override - public DataBuffer encode(Context ctx, Object value) throws Exception { + public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { if (supportsType(value.getClass())) { return render(ctx, value); } @@ -32,7 +33,7 @@ public DataBuffer encode(Context ctx, Object value) throws Exception { } @Override - DataBuffer extractOutput(Context ctx, ByteBufferEncodedOutput stream) throws IOException { - return ctx.getBufferFactory().wrap(stream.asByteBuffer()); + Output extractOutput(Context ctx, ByteBufferEncodedOutput stream) throws IOException { + return ctx.getOutputFactory().wrap(stream.asByteBuffer()); } } diff --git a/modules/jooby-jstachio/src/main/java/io/jooby/jstachio/JStachioModule.java b/modules/jooby-jstachio/src/main/java/io/jooby/jstachio/JStachioModule.java index 90273fc240..5661e24643 100644 --- a/modules/jooby-jstachio/src/main/java/io/jooby/jstachio/JStachioModule.java +++ b/modules/jooby-jstachio/src/main/java/io/jooby/jstachio/JStachioModule.java @@ -63,19 +63,6 @@ public class JStachioModule implements Extension { return this; } - /** - * Allow simple reuse of raw byte buffers. It is usually used through ThreadLocal - * variables. - * - * @param reuseBuffer True for reuse the buffer. Default is: false - * @return This module. - * @deprecated - */ - @Deprecated - public JStachioModule reuseBuffer(boolean reuseBuffer) { - return this; - } - /** * JStachio will by default bind {@linkplain Context#getAttributes() Context attributes} to * @context. This configuration option allows fetching context keys from something diff --git a/modules/jooby-jte/src/main/java/io/jooby/internal/jte/DataBufferOutput.java b/modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.java similarity index 75% rename from modules/jooby-jte/src/main/java/io/jooby/internal/jte/DataBufferOutput.java rename to modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.java index 2d2dd38e95..6129c812dc 100644 --- a/modules/jooby-jte/src/main/java/io/jooby/internal/jte/DataBufferOutput.java +++ b/modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.java @@ -9,13 +9,13 @@ import java.nio.charset.Charset; import gg.jte.TemplateOutput; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.BufferedOutput; -public class DataBufferOutput implements TemplateOutput { - private final DataBuffer buffer; +public class BufferedTemplateOutput implements TemplateOutput { + private final BufferedOutput buffer; private final Charset charset; - public DataBufferOutput(DataBuffer buffer, Charset charset) { + public BufferedTemplateOutput(BufferedOutput buffer, Charset charset) { this.buffer = buffer; this.charset = charset; } diff --git a/modules/jooby-jte/src/main/java/io/jooby/internal/jte/JteModelEncoder.java b/modules/jooby-jte/src/main/java/io/jooby/internal/jte/JteModelEncoder.java index 3d7a682afd..00e8a43dc8 100644 --- a/modules/jooby-jte/src/main/java/io/jooby/internal/jte/JteModelEncoder.java +++ b/modules/jooby-jte/src/main/java/io/jooby/internal/jte/JteModelEncoder.java @@ -11,15 +11,14 @@ import edu.umd.cs.findbugs.annotations.Nullable; import gg.jte.models.runtime.JteModel; import io.jooby.Context; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class JteModelEncoder implements io.jooby.MessageEncoder { @Nullable @Override - public DataBuffer encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { if (value instanceof JteModel jte) { - var buffer = ctx.getBufferFactory().allocateBuffer(); - var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); - jte.render(output); + var buffer = ctx.getOutputFactory().allocate(); + jte.render(new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8)); return buffer; } return null; diff --git a/modules/jooby-jte/src/main/java/io/jooby/jte/JteTemplateEngine.java b/modules/jooby-jte/src/main/java/io/jooby/jte/JteTemplateEngine.java index 2fbab3bc56..18c8894062 100644 --- a/modules/jooby-jte/src/main/java/io/jooby/jte/JteTemplateEngine.java +++ b/modules/jooby-jte/src/main/java/io/jooby/jte/JteTemplateEngine.java @@ -14,8 +14,8 @@ import io.jooby.Context; import io.jooby.MapModelAndView; import io.jooby.ModelAndView; -import io.jooby.buffer.DataBuffer; -import io.jooby.internal.jte.DataBufferOutput; +import io.jooby.internal.jte.BufferedTemplateOutput; +import io.jooby.output.Output; class JteTemplateEngine implements io.jooby.TemplateEngine { private final TemplateEngine jte; @@ -32,9 +32,9 @@ public List extensions() { } @Override - public DataBuffer render(Context ctx, ModelAndView modelAndView) { - var buffer = ctx.getBufferFactory().allocateBuffer(); - var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); + public Output render(Context ctx, ModelAndView modelAndView) { + var buffer = ctx.getOutputFactory().allocate(); + var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); var attributes = ctx.getAttributes(); if (modelAndView instanceof MapModelAndView mapModelAndView) { var mapModel = new HashMap(); diff --git a/modules/jooby-jte/src/test/java/io/jooby/internal/jte/BufferedTemplateOutputTest.java b/modules/jooby-jte/src/test/java/io/jooby/internal/jte/BufferedTemplateOutputTest.java new file mode 100644 index 0000000000..16f80889a9 --- /dev/null +++ b/modules/jooby-jte/src/test/java/io/jooby/internal/jte/BufferedTemplateOutputTest.java @@ -0,0 +1,45 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.jte; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; + +public class BufferedTemplateOutputTest { + + @Test + public void checkWriteContent() { + var factory = OutputFactory.create(OutputOptions.small()); + var buffer = factory.allocate(); + var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); + output.writeContent("Hello"); + assertEquals("Hello", StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); + } + + @Test + public void checkWriteContentSubstring() { + var factory = OutputFactory.create(OutputOptions.small()); + var buffer = factory.allocate(); + var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); + output.writeContent(" Hello World! ", 1, " Hello World! ".length() - 2); + assertEquals("Hello World", StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); + } + + @Test + public void checkWriteBinaryContent() { + var factory = OutputFactory.create(OutputOptions.small()); + var buffer = factory.allocate(); + var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); + output.writeBinaryContent("Hello".getBytes(StandardCharsets.UTF_8)); + assertEquals("Hello", StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); + } +} diff --git a/modules/jooby-jte/src/test/java/io/jooby/internal/jte/DataBufferOutputTest.java b/modules/jooby-jte/src/test/java/io/jooby/internal/jte/DataBufferOutputTest.java deleted file mode 100644 index 9205930bf1..0000000000 --- a/modules/jooby-jte/src/test/java/io/jooby/internal/jte/DataBufferOutputTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.jte; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; - -import io.jooby.buffer.DefaultDataBufferFactory; - -public class DataBufferOutputTest { - - @Test - public void checkWriteContent() { - var factory = new DefaultDataBufferFactory(); - var buffer = factory.allocateBuffer(); - var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); - output.writeContent("Hello"); - assertEquals("Hello", buffer.toString(StandardCharsets.UTF_8)); - } - - @Test - public void checkWriteContentSubstring() { - var factory = new DefaultDataBufferFactory(); - var buffer = factory.allocateBuffer(); - var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); - output.writeContent(" Hello World! ", 1, " Hello World! ".length() - 2); - assertEquals("Hello World", buffer.toString(StandardCharsets.UTF_8)); - } - - @Test - public void checkWriteBinaryContent() { - var factory = new DefaultDataBufferFactory(); - var buffer = factory.allocateBuffer(); - var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); - output.writeBinaryContent("Hello".getBytes(StandardCharsets.UTF_8)); - assertEquals("Hello", buffer.toString(StandardCharsets.UTF_8)); - } -} diff --git a/modules/jooby-jte/src/test/java/io/jooby/jte/Issue3599.java b/modules/jooby-jte/src/test/java/io/jooby/jte/Issue3599.java index b1f637b353..9772aba573 100644 --- a/modules/jooby-jte/src/test/java/io/jooby/jte/Issue3599.java +++ b/modules/jooby-jte/src/test/java/io/jooby/jte/Issue3599.java @@ -19,23 +19,23 @@ import io.jooby.Context; import io.jooby.MapModelAndView; import io.jooby.ModelAndView; -import io.jooby.buffer.DataBuffer; -import io.jooby.buffer.DataBufferFactory; +import io.jooby.output.BufferedOutput; +import io.jooby.output.OutputFactory; public class Issue3599 { @Test public void shouldNotCallObjectMethodOnMapModels() { - var bufferFactory = mock(DataBufferFactory.class); - var buffer = mock(DataBuffer.class); - when(bufferFactory.allocateBuffer()).thenReturn(buffer); + var bufferFactory = mock(OutputFactory.class); + var buffer = mock(BufferedOutput.class); + when(bufferFactory.allocate()).thenReturn(buffer); var attributes = Map.of("foo", 1); var mapModel = new HashMap(); mapModel.put("param1", new Issue3599()); var ctx = mock(Context.class); - when(ctx.getBufferFactory()).thenReturn(bufferFactory); + when(ctx.getOutputFactory()).thenReturn(bufferFactory); when(ctx.getAttributes()).thenReturn(attributes); var jteParams = new HashMap(); @@ -53,15 +53,15 @@ public void shouldNotCallObjectMethodOnMapModels() { @Test public void shouldCallObjectMethodOnObjectModels() { - var bufferFactory = mock(DataBufferFactory.class); - var buffer = mock(DataBuffer.class); - when(bufferFactory.allocateBuffer()).thenReturn(buffer); + var bufferFactory = mock(OutputFactory.class); + var buffer = mock(BufferedOutput.class); + when(bufferFactory.allocate()).thenReturn(buffer); var attributes = Map.of("foo", 1); var model = new Issue3599(); var ctx = mock(Context.class); - when(ctx.getBufferFactory()).thenReturn(bufferFactory); + when(ctx.getOutputFactory()).thenReturn(bufferFactory); when(ctx.getAttributes()).thenReturn(attributes); var jte = mock(TemplateEngine.class); diff --git a/modules/jooby-jte/src/test/java/io/jooby/jte/Issue3602.java b/modules/jooby-jte/src/test/java/io/jooby/jte/Issue3602.java index bc96966467..0358c2bb88 100644 --- a/modules/jooby-jte/src/test/java/io/jooby/jte/Issue3602.java +++ b/modules/jooby-jte/src/test/java/io/jooby/jte/Issue3602.java @@ -15,21 +15,21 @@ import gg.jte.TemplateOutput; import gg.jte.models.runtime.JteModel; import io.jooby.Context; -import io.jooby.buffer.DataBuffer; -import io.jooby.buffer.DataBufferFactory; import io.jooby.internal.jte.JteModelEncoder; +import io.jooby.output.BufferedOutput; +import io.jooby.output.OutputFactory; public class Issue3602 { @Test public void shouldRenderJteModel() throws Exception { - var bufferFactory = mock(DataBufferFactory.class); - var buffer = mock(DataBuffer.class); - when(bufferFactory.allocateBuffer()).thenReturn(buffer); + var bufferFactory = mock(OutputFactory.class); + var buffer = mock(BufferedOutput.class); + when(bufferFactory.allocate()).thenReturn(buffer); var attributes = Map.of("foo", 1); var ctx = mock(Context.class); - when(ctx.getBufferFactory()).thenReturn(bufferFactory); + when(ctx.getOutputFactory()).thenReturn(bufferFactory); when(ctx.getAttributes()).thenReturn(attributes); var model = mock(JteModel.class); diff --git a/modules/jooby-jwt/src/main/java/io/jooby/jwt/JwtSessionStore.java b/modules/jooby-jwt/src/main/java/io/jooby/jwt/JwtSessionStore.java index 8cccdb6270..732ae2e600 100644 --- a/modules/jooby-jwt/src/main/java/io/jooby/jwt/JwtSessionStore.java +++ b/modules/jooby-jwt/src/main/java/io/jooby/jwt/JwtSessionStore.java @@ -51,34 +51,25 @@ public class JwtSessionStore implements SessionStore { private final SessionStore store; - /** - * Creates a JSON Web Token session store. It uses a cookie token: {@link SessionToken#SID}. - * - * @param key Secret key. - */ - public JwtSessionStore(@NonNull String key) { - this(key, SessionToken.signedCookie(SessionToken.SID)); - } - /** * Creates a JSON Web Token session store. Session token is usually a {@link * SessionToken#signedCookie(Cookie)}, {@link SessionToken#header(String)} or combination of both. * - * @param key Secret key. * @param token Session token. + * @param key Secret key. */ - public JwtSessionStore(@NonNull String key, @NonNull SessionToken token) { - this(Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8)), token); + public JwtSessionStore(@NonNull SessionToken token, @NonNull String key) { + this(token, Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8))); } /** * Creates a JSON Web Token session store. Session token is usually a {@link * SessionToken#signedCookie(Cookie)}, {@link SessionToken#header(String)} or combination of both. * - * @param key Secret key. * @param token Session token. + * @param key Secret key. */ - public JwtSessionStore(@NonNull SecretKey key, @NonNull SessionToken token) { + public JwtSessionStore(@NonNull SessionToken token, @NonNull SecretKey key) { this.store = SessionStore.signed(token, decoder(key), encoder(key)); } diff --git a/modules/jooby-kotlin/src/main/kotlin/io/jooby/kt/CoroutineRouter.kt b/modules/jooby-kotlin/src/main/kotlin/io/jooby/kt/CoroutineRouter.kt index b0b89c7dd3..ac70c52888 100644 --- a/modules/jooby-kotlin/src/main/kotlin/io/jooby/kt/CoroutineRouter.kt +++ b/modules/jooby-kotlin/src/main/kotlin/io/jooby/kt/CoroutineRouter.kt @@ -141,28 +141,26 @@ class CoroutineRouter(val coroutineStart: CoroutineStart, val router: Router) { route(OPTIONS, pattern, handler) fun route(method: String, pattern: String, handler: suspend HandlerContext.() -> Any): Route = - router - .route(method, pattern) { ctx -> - val handlerContext = HandlerContext(ctx) - launch(handlerContext) { + router.route(method, pattern) { ctx -> + val handlerContext = HandlerContext(ctx) + launch(handlerContext) { + try { + val result = handler(handlerContext) + ctx.route.after?.apply(ctx, result, null) + if (result != ctx && !ctx.isResponseStarted) { + ctx.render(result) + } + } catch (cause: Throwable) { try { - val result = handler(handlerContext) - ctx.route.after?.apply(ctx, result, null) - if (result != ctx && !ctx.isResponseStarted) { - ctx.render(result) - } - } catch (cause: Throwable) { - try { - ctx.route.after?.apply(ctx, null, cause) - } finally { - errorHandler.invoke(ErrorHandlerContext(ctx, cause, router.errorCode(cause))) - } + ctx.route.after?.apply(ctx, null, cause) + } finally { + errorHandler.invoke(ErrorHandlerContext(ctx, cause, router.errorCode(cause))) } } - // Return context to mark as handled - ctx } - .setHandle(handler) + // Return context to mark as handled + ctx + } internal fun launch(handlerContext: HandlerContext, block: suspend CoroutineScope.() -> Unit) { // Global catch-all exception handler diff --git a/modules/jooby-kotlin/src/main/kotlin/io/jooby/kt/Kooby.kt b/modules/jooby-kotlin/src/main/kotlin/io/jooby/kt/Kooby.kt index 6485c57e15..895adf2364 100644 --- a/modules/jooby-kotlin/src/main/kotlin/io/jooby/kt/Kooby.kt +++ b/modules/jooby-kotlin/src/main/kotlin/io/jooby/kt/Kooby.kt @@ -18,15 +18,13 @@ import io.jooby.Jooby import io.jooby.QueryString import io.jooby.Registry import io.jooby.Route -import io.jooby.RouteSet +import io.jooby.Route.Set import io.jooby.Router -import io.jooby.RouterOption +import io.jooby.RouterOptions import io.jooby.Server -import io.jooby.ServerOptions import io.jooby.ServiceRegistry -import io.jooby.Value -import io.jooby.ValueNode import io.jooby.handler.Cors +import io.jooby.value.Value import java.util.function.Supplier import kotlin.reflect.KClass import kotlin.reflect.KProperty @@ -84,7 +82,7 @@ fun ServiceRegistry.putIfAbsent(klass: KClass, service: T): T? { } /** Value: */ -inline operator fun ValueNode.getValue(thisRef: Any?, property: KProperty<*>): T { +inline operator fun Value.getValue(thisRef: Any?, property: KProperty<*>): T { return this.get(property.name).to(T::class.java) } @@ -145,20 +143,6 @@ open class Kooby() : Jooby() { this.init() } - @RouterDsl - @Deprecated(message = "Use mvc(io.jooby.MvcExtension)") - fun mvc(router: KClass): Kooby { - super.mvc(router.java) - return this - } - - @RouterDsl - @Deprecated(message = "Use mvc(io.jooby.MvcExtension)") - fun mvc(router: KClass, provider: () -> T): Kooby { - super.mvc(router.java, provider) - return this - } - @RouterDsl fun use(handler: FilterContext.() -> Any): Kooby { super.use { next -> Route.Handler { ctx -> FilterContext(ctx, next).handler() } } @@ -178,12 +162,12 @@ open class Kooby() : Jooby() { } @RouterDsl - override fun path(pattern: String, action: Runnable): RouteSet { + override fun path(pattern: String, action: Runnable): Set { return super.path(pattern, action) } @RouterDsl - override fun routes(action: Runnable): RouteSet { + override fun routes(action: Runnable): Set { return super.routes(action) } @@ -279,7 +263,7 @@ open class Kooby() : Jooby() { router.block() routes.subList(from, routes.size).forEach { it.setNonBlocking(true) - it.attribute("coroutine", true) + it.setAttribute("coroutine", true) } return router } @@ -291,7 +275,7 @@ open class Kooby() : Jooby() { @RouterDsl fun route(method: String, pattern: String, handler: HandlerContext.() -> Any): Route { - return super.route(method, pattern) { ctx -> handler(HandlerContext(ctx)) }.setHandle(handler) + return super.route(method, pattern) { ctx -> handler(HandlerContext(ctx)) } } @RouterDsl @@ -305,16 +289,8 @@ open class Kooby() : Jooby() { } @OptionsDsl - fun serverOptions(configurer: ServerOptions.() -> Unit): Kooby { - val options = ServerOptions() - configurer(options) - setServerOptions(options) - return this - } - - @OptionsDsl - fun routerOptions(vararg option: RouterOption): Kooby { - this.setRouterOptions(*option) + fun routerOptions(options: RouterOptions): Kooby { + this.setRouterOptions(options) return this } diff --git a/modules/jooby-kotlin/src/test/kotlin/io/jooby/kt/Idioms.kt b/modules/jooby-kotlin/src/test/kotlin/io/jooby/kt/Idioms.kt index ccddc75a5b..c9eb0bd9ca 100644 --- a/modules/jooby-kotlin/src/test/kotlin/io/jooby/kt/Idioms.kt +++ b/modules/jooby-kotlin/src/test/kotlin/io/jooby/kt/Idioms.kt @@ -6,17 +6,15 @@ package io.jooby.kt import io.jooby.Jooby -import io.jooby.RouterOption.IGNORE_CASE -import io.jooby.RouterOption.IGNORE_TRAILING_SLASH +import io.jooby.RouterOptions import io.jooby.ServiceKey -import io.jooby.SslOptions import java.nio.file.Paths import java.time.Duration import kotlinx.coroutines.delay /** - * Kotlin DLS in action, this class does nothing but we need it to make sure Kotlin version compiles - * sucessfully. + * Kotlin DLS in action, this class does nothing, but we need it to make sure Kotlin version + * compiles successfully. */ class Idioms : Kooby({ @@ -35,21 +33,7 @@ class Idioms : services.getOrNull(Jooby::class) - /** Options: */ - serverOptions { - bufferSize = 8194 - ioThreads = 8 - compressionLevel = 6 - defaultHeaders = false - maxRequestSize = 8000 - port = 8080 - server = "server" - workerThreads = 99 - securePort = 8443 - ssl = SslOptions().apply { type = "PKCS12" } - } - - routerOptions(IGNORE_CASE, IGNORE_TRAILING_SLASH) + routerOptions(RouterOptions().ignoreCase(true).ignoreTrailingSlash(true)) setHiddenMethod { ctx -> ctx.header("").toOptional() } @@ -83,7 +67,7 @@ class Idioms : ctx } - get("/attributes") { "some" }.attribute("k", "v") + get("/attributes") { "some" }.setAttribute("k", "v") /** Router DSL: */ before { ctx.path() } diff --git a/modules/jooby-kotlin/src/test/kotlin/io/jooby/kt/KotlinIdiomTest.kt b/modules/jooby-kotlin/src/test/kotlin/io/jooby/kt/KotlinIdiomTest.kt deleted file mode 100644 index 0621b80a63..0000000000 --- a/modules/jooby-kotlin/src/test/kotlin/io/jooby/kt/KotlinIdiomTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.kt - -import org.junit.jupiter.api.Test - -class KotlinIdiomTest { - - @Test - fun idioms() { - Idioms() - } - - @Test - fun apps() { - App() - } -} diff --git a/modules/jooby-mutiny/src/main/java/io/jooby/mutiny/Mutiny.java b/modules/jooby-mutiny/src/main/java/io/jooby/mutiny/Mutiny.java index 3f9aa03d37..ff8ff96bc3 100644 --- a/modules/jooby-mutiny/src/main/java/io/jooby/mutiny/Mutiny.java +++ b/modules/jooby-mutiny/src/main/java/io/jooby/mutiny/Mutiny.java @@ -12,7 +12,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.Route; -import io.jooby.annotation.ResultType; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; @@ -21,10 +20,6 @@ * * @author edgar */ -@ResultType( - types = {Uni.class, Mutiny.class}, - handler = "mutiny", - nonBlocking = true) public class Mutiny { private static final Route.Filter MUTINY = diff --git a/modules/jooby-netty/pom.xml b/modules/jooby-netty/pom.xml index 20ae2e15c8..018866ea7d 100644 --- a/modules/jooby-netty/pom.xml +++ b/modules/jooby-netty/pom.xml @@ -81,6 +81,21 @@ ${slf4j.version} test + + + org.openjdk.jmh + jmh-core + 1.37 + test + + + + org.openjdk.jmh + jmh-generator-annprocess + 1.37 + test + + diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledFullHttpResponse.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledFullHttpResponse.java deleted file mode 100644 index f270e5437f..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledFullHttpResponse.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.netty; - -import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.http.*; - -/** - * Helper wrapper class which allows to assemble a LastHttpContent and a HttpResponse into one - * "packet" and so more efficient write it through the pipeline. - * - * @author Norman Maurer - */ -class AssembledFullHttpResponse extends AssembledHttpResponse implements FullHttpResponse { - - private HttpHeaders trailingHeaders; - - public AssembledFullHttpResponse( - boolean head, - HttpVersion version, - HttpResponseStatus status, - HttpHeaders headers, - ByteBuf buf, - HttpHeaders trailingHeaders) { - super(head, version, status, headers, buf); - this.trailingHeaders = trailingHeaders; - } - - @Override - public HttpHeaders trailingHeaders() { - return trailingHeaders; - } - - @Override - public AssembledFullHttpResponse setStatus(HttpResponseStatus status) { - super.setStatus(status); - return this; - } - - @Override - public AssembledFullHttpResponse retain(int increment) { - super.retain(increment); - return this; - } - - @Override - public AssembledFullHttpResponse retain() { - super.retain(); - return this; - } - - @Override - public AssembledFullHttpResponse duplicate() { - super.duplicate(); - return this; - } - - @Override - public AssembledFullHttpResponse copy() { - super.copy(); - return this; - } - - @Override - public AssembledFullHttpResponse retainedDuplicate() { - super.retainedDuplicate(); - return this; - } - - @Override - public AssembledFullHttpResponse replace(ByteBuf content) { - super.replace(content); - return this; - } - - @Override - public AssembledFullHttpResponse setProtocolVersion(HttpVersion version) { - super.setProtocolVersion(version); - return this; - } - - @Override - public AssembledFullHttpResponse touch() { - super.touch(); - return this; - } - - @Override - public AssembledFullHttpResponse touch(Object hint) { - super.touch(hint); - return this; - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledHttpResponse.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledHttpResponse.java deleted file mode 100644 index 7f8dc44166..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledHttpResponse.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.netty; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.handler.codec.DecoderResult; -import io.netty.handler.codec.http.*; - -/** - * Helper wrapper class which allows to assemble a HttpContent and a HttpResponse into one "packet" - * and so more efficient write it through the pipeline. - * - * @author Norman Maurer - */ -class AssembledHttpResponse implements HttpResponse, HttpContent { - - private boolean head; - private HttpResponseStatus status; - private HttpVersion version; - private HttpHeaders headers; - private final ByteBuf content; - private DecoderResult result = DecoderResult.SUCCESS; - - AssembledHttpResponse( - boolean head, HttpVersion version, HttpResponseStatus status, HttpHeaders headers) { - this(head, version, status, headers, Unpooled.EMPTY_BUFFER); - } - - AssembledHttpResponse( - boolean head, - HttpVersion version, - HttpResponseStatus status, - HttpHeaders headers, - ByteBuf content) { - this.head = head; - this.status = status; - this.version = version; - this.headers = headers; - this.content = content; - } - - boolean head() { - return head; - } - - @Override - public HttpContent copy() { - throw new UnsupportedOperationException(); - } - - @Override - public HttpContent duplicate() { - throw new UnsupportedOperationException(); - } - - @Override - public HttpContent retainedDuplicate() { - throw new UnsupportedOperationException(); - } - - @Override - public HttpContent replace(ByteBuf content) { - throw new UnsupportedOperationException(); - } - - @Override - public AssembledHttpResponse retain() { - content.retain(); - return this; - } - - @Override - public AssembledHttpResponse retain(int increment) { - content.retain(increment); - return this; - } - - @Override - public HttpResponseStatus getStatus() { - return status; - } - - @Override - public AssembledHttpResponse setStatus(HttpResponseStatus status) { - this.status = status; - return this; - } - - @Override - public AssembledHttpResponse setProtocolVersion(HttpVersion version) { - this.version = version; - return this; - } - - @Override - public HttpVersion getProtocolVersion() { - return version; - } - - @Override - public HttpVersion protocolVersion() { - return version; - } - - @Override - public HttpResponseStatus status() { - return status; - } - - @Override - public AssembledHttpResponse touch() { - content.touch(); - return this; - } - - @Override - public AssembledHttpResponse touch(Object hint) { - content.touch(hint); - return this; - } - - @Override - public DecoderResult decoderResult() { - return result; - } - - @Override - public HttpHeaders headers() { - return headers; - } - - @Override - public DecoderResult getDecoderResult() { - return result; - } - - @Override - public void setDecoderResult(DecoderResult result) { - this.result = result; - } - - @Override - public ByteBuf content() { - return content; - } - - @Override - public int refCnt() { - return content.refCnt(); - } - - @Override - public boolean release() { - return content.release(); - } - - @Override - public boolean release(int decrement) { - return content.release(decrement); - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledLastHttpContent.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledLastHttpContent.java deleted file mode 100644 index 2f5dfe7259..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledLastHttpContent.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.netty; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.DefaultByteBufHolder; -import io.netty.handler.codec.DecoderResult; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.LastHttpContent; - -/** - * Helper wrapper class which allows to assemble a ByteBuf and a HttpHeaders into one "packet" and - * so more efficient write it through the pipeline. - * - * @author Norman Maurer - */ -class AssembledLastHttpContent extends DefaultByteBufHolder implements LastHttpContent { - - private final HttpHeaders trailingHeaders; - private DecoderResult result; - - AssembledLastHttpContent(ByteBuf buf, HttpHeaders trailingHeaders) { - this(buf, trailingHeaders, DecoderResult.SUCCESS); - } - - AssembledLastHttpContent(ByteBuf buf, HttpHeaders trailingHeaders, DecoderResult result) { - super(buf); - this.trailingHeaders = trailingHeaders; - this.result = result; - } - - @Override - public HttpHeaders trailingHeaders() { - return trailingHeaders; - } - - @Override - public LastHttpContent copy() { - throw new UnsupportedOperationException(); - } - - @Override - public LastHttpContent retain(int increment) { - super.retain(increment); - return this; - } - - @Override - public LastHttpContent retain() { - super.retain(); - return this; - } - - @Override - public LastHttpContent duplicate() { - throw new UnsupportedOperationException(); - } - - @Override - public LastHttpContent replace(ByteBuf content) { - throw new UnsupportedOperationException(); - } - - @Override - public LastHttpContent retainedDuplicate() { - throw new UnsupportedOperationException(); - } - - @Override - public DecoderResult decoderResult() { - return result; - } - - @Override - public DecoderResult getDecoderResult() { - return result; - } - - @Override - public void setDecoderResult(DecoderResult result) { - this.result = result; - } - - @Override - public AssembledLastHttpContent touch() { - super.touch(); - return this; - } - - @Override - public AssembledLastHttpContent touch(Object hint) { - super.touch(hint); - return this; - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/HeadersMultiMap.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/HeadersMultiMap.java index 3308db9fbe..c45c894c1c 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/HeadersMultiMap.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/HeadersMultiMap.java @@ -17,7 +17,6 @@ import io.netty.handler.codec.DateFormatter; import io.netty.handler.codec.http.DefaultHttpHeadersFactory; import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpHeadersFactory; import io.netty.util.AsciiString; import io.netty.util.CharsetUtil; @@ -61,20 +60,6 @@ private static CharSequence toValidCharSequence(Object value) { } } - public static HttpHeadersFactory httpHeadersFactory() { - return new HttpHeadersFactory() { - @Override - public HttpHeaders newHeaders() { - return new HeadersMultiMap(); - } - - @Override - public HttpHeaders newEmptyHeaders() { - return new HeadersMultiMap(); - } - }; - } - @Override public int size() { return names().size(); @@ -505,8 +490,10 @@ static void encoderHeader(CharSequence name, CharSequence value, ByteBuf buf) { } private static void writeAscii(ByteBuf buf, int offset, CharSequence value) { - if (value instanceof AsciiString) { - ByteBufUtil.copy((AsciiString) value, 0, buf, offset, value.length()); + if (value instanceof NettyString netty) { + buf.setBytes(offset, netty.bytes, 0, netty.bytes.length); + } else if (value instanceof AsciiString ascii) { + buf.setBytes(offset, ascii.array(), ascii.arrayOffset(), value.length()); } else { buf.setCharSequence(offset, value, CharsetUtil.US_ASCII); } diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBody.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBody.java index e470e57a66..10408cf992 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBody.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBody.java @@ -24,14 +24,12 @@ import io.jooby.Context; import io.jooby.MediaType; import io.jooby.SneakyThrows; -import io.jooby.Value; -import io.jooby.ValueNode; import io.netty.handler.codec.http.multipart.HttpData; public class NettyBody implements Body { private final Context ctx; private final HttpData data; - private long length; + private final long length; public NettyBody(Context ctx, HttpData data, long contentLength) { this.ctx = ctx; @@ -83,16 +81,6 @@ public String value() { return value(StandardCharsets.UTF_8); } - @NonNull @Override - public ValueNode get(int index) { - return index == 0 ? this : get(Integer.toString(index)); - } - - @NonNull @Override - public ValueNode get(@NonNull String name) { - return Value.missing(name); - } - @Override public String name() { return "body"; diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufOutput.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufOutput.java new file mode 100644 index 0000000000..c3cf434f43 --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufOutput.java @@ -0,0 +1,68 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.netty; + +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.output.BufferedOutput; +import io.netty.buffer.ByteBuf; + +public class NettyByteBufOutput implements BufferedOutput, NettyByteBufRef { + + private final ByteBuf buffer; + + protected NettyByteBufOutput(ByteBuf buffer) { + this.buffer = buffer; + } + + @Override + @NonNull public BufferedOutput write(byte b) { + buffer.writeByte(b); + return this; + } + + @Override + @NonNull public BufferedOutput write(byte[] source) { + buffer.writeBytes(source); + return this; + } + + @Override + @NonNull public BufferedOutput write(byte[] source, int offset, int length) { + this.buffer.writeBytes(source, offset, length); + return this; + } + + @Override + @NonNull public BufferedOutput write(@NonNull String source, @NonNull Charset charset) { + this.buffer.writeBytes(source.getBytes(charset)); + return this; + } + + @Override + @NonNull public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { + this.buffer.writeBytes(charset.encode(source)); + return this; + } + + @Override + @NonNull public BufferedOutput clear() { + this.buffer.clear(); + return this; + } + + @Override + public int size() { + return buffer.readableBytes(); + } + + @NonNull @Override + public ByteBuf byteBuf() { + return buffer; + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufRef.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufRef.java new file mode 100644 index 0000000000..fce4d73384 --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufRef.java @@ -0,0 +1,46 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.netty; + +import java.nio.ByteBuffer; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.SneakyThrows; +import io.jooby.output.Output; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +public interface NettyByteBufRef extends Output { + @NonNull ByteBuf byteBuf(); + + @Override + default void transferTo(@NonNull SneakyThrows.Consumer consumer) { + consumer.accept(asByteBuffer()); + } + + @Override + @NonNull default ByteBuffer asByteBuffer() { + return byteBuf().slice().nioBuffer().asReadOnlyBuffer(); + } + + default void send(Context ctx) { + if (ctx.getClass() == NettyContext.class) { + var buf = byteBuf(); + ((NettyContext) ctx).send(buf, Integer.toString(buf.readableBytes())); + } else { + ctx.send(asByteBuffer()); + } + } + + static ByteBuf byteBuf(Output output) { + if (output instanceof NettyByteBufRef netty) { + return netty.byteBuf(); + } else { + return Unpooled.wrappedBuffer(output.asByteBuffer()); + } + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java index fd278d45d1..042ab1fa47 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java @@ -5,7 +5,7 @@ */ package io.jooby.internal.netty; -import static io.netty.buffer.Unpooled.copiedBuffer; +import static io.jooby.internal.netty.NettyHeadersFactory.HEADERS; import static io.netty.buffer.Unpooled.wrappedBuffer; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; @@ -32,7 +32,6 @@ import java.nio.charset.Charset; import java.security.cert.Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -48,29 +47,11 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.Body; -import io.jooby.ByteRange; -import io.jooby.CompletionListeners; -import io.jooby.Context; +import io.jooby.*; import io.jooby.Cookie; -import io.jooby.DefaultContext; import io.jooby.FileUpload; -import io.jooby.Formdata; -import io.jooby.MediaType; -import io.jooby.QueryString; -import io.jooby.Route; -import io.jooby.Router; -import io.jooby.RouterOption; -import io.jooby.Sender; -import io.jooby.Server; -import io.jooby.ServerSentEmitter; -import io.jooby.Session; -import io.jooby.SneakyThrows; -import io.jooby.StatusCode; -import io.jooby.Value; -import io.jooby.ValueNode; -import io.jooby.WebSocket; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; +import io.jooby.value.Value; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; @@ -94,12 +75,11 @@ import io.netty.util.IllegalReferenceCountException; public class NettyContext implements DefaultContext, ChannelFutureListener { - private static final HttpHeaders NO_TRAILING = EmptyHttpHeaders.INSTANCE; private static final String STREAM_ID = "x-http2-stream-id"; private String streamId; - HeadersMultiMap setHeaders = new HeadersMultiMap(); + HeadersMultiMap setHeaders = HEADERS.newHeaders(); private int bufferSize; InterfaceHttpPostRequestDecoder decoder; DefaultHttpDataFactory httpDataFactory; @@ -113,7 +93,7 @@ public class NettyContext implements DefaultContext, ChannelFutureListener { private QueryString query; private Formdata formdata; private List files; - private ValueNode headers; + private Value headers; private Map pathMap = Collections.EMPTY_MAP; private MediaType responseType; private Map attributes; @@ -240,7 +220,7 @@ public QueryString query() { if (query == null) { String uri = req.uri(); int q = uri.indexOf('?'); - query = QueryString.create(this, q >= 0 ? uri.substring(q + 1) : null); + query = QueryString.create(getValueFactory(), q >= 0 ? uri.substring(q + 1) : null); } return query; } @@ -248,7 +228,7 @@ public QueryString query() { @NonNull @Override public Formdata form() { if (formdata == null) { - formdata = Formdata.create(this); + formdata = Formdata.create(getValueFactory()); decodeForm(formdata); } return formdata; @@ -256,7 +236,7 @@ public Formdata form() { @NonNull @Override public Value header(@NonNull String name) { - return Value.create(this, name, req.headers().getAll(name)); + return Value.create(getValueFactory(), name, req.headers().getAll(name)); } @NonNull @Override @@ -305,7 +285,7 @@ public List getClientCertificates() { SslHandler sslHandler = (SslHandler) ctx.channel().pipeline().get("ssl"); if (sslHandler != null) { try { - return Arrays.asList(sslHandler.engine().getSession().getPeerCertificates()); + return List.of(sslHandler.engine().getSession().getPeerCertificates()); } catch (SSLPeerUnverifiedException x) { throw SneakyThrows.propagate(x); } @@ -339,7 +319,7 @@ public Context setPort(int port) { } @NonNull @Override - public ValueNode header() { + public Value header() { if (headers == null) { Map> headerMap = new LinkedHashMap<>(); HttpHeaders headers = req.headers(); @@ -347,7 +327,7 @@ public ValueNode header() { for (String name : names) { headerMap.put(name, headers.getAll(name)); } - this.headers = Value.headers(this, headerMap); + this.headers = Value.headers(getValueFactory(), headerMap); } return headers; } @@ -437,15 +417,13 @@ public Context upgrade(WebSocket.Initializer handler) { @NonNull @Override public Context upgrade(@NonNull ServerSentEmitter.Handler handler) { responseStarted = true; - ctx.writeAndFlush(new AssembledHttpResponse(route.isHttpHead(), HTTP_1_1, status, setHeaders)); + ctx.writeAndFlush(new DefaultHttpResponse(HTTP_1_1, status, setHeaders)); - // ctx.executor().execute(() -> { try { handler.handle(new NettyServerSentEmitter(this)); } catch (Throwable x) { sendError(x); } - // }); return this; } @@ -554,7 +532,7 @@ public PrintWriter responseWriter(MediaType type, Charset charset) { @NonNull @Override public Sender responseSender() { prepareChunked(); - ctx.write(new AssembledHttpResponse(route.isHttpHead(), HTTP_1_1, status, setHeaders)); + ctx.write(new DefaultHttpResponse(HTTP_1_1, status, setHeaders)); return new NettySender(this, ctx); } @@ -565,12 +543,12 @@ public OutputStream responseStream() { @NonNull @Override public Context send(@NonNull String data) { - return send(copiedBuffer(data, UTF_8)); + return send(data, UTF_8); } @Override public final Context send(String data, Charset charset) { - return send(copiedBuffer(data, charset)); + return send(wrappedBuffer(data.getBytes(charset))); } @Override @@ -593,19 +571,21 @@ public final Context send(ByteBuffer data) { return send(wrappedBuffer(data)); } - @NonNull @Override - public Context send(@NonNull DataBuffer data) { - data.send(this); + @Override + @NonNull public Context send(@NonNull Output output) { + output.send(this); return this; } - public Context send(@NonNull ByteBuf data) { + private Context send(@NonNull ByteBuf data) { + return send(data, Integer.toString(data.readableBytes())); + } + + Context send(@NonNull ByteBuf data, CharSequence contentLength) { try { responseStarted = true; - setHeaders.set(CONTENT_LENGTH, Integer.toString(data.readableBytes())); - var response = - new AssembledFullHttpResponse( - route.isHttpHead(), HTTP_1_1, status, setHeaders, data, NO_TRAILING); + setHeaders.set(CONTENT_LENGTH, contentLength); + var response = new DefaultFullHttpResponse(HTTP_1_1, status, data, setHeaders, NO_TRAILING); if (ctx.channel().eventLoop().inEventLoop()) { needsFlush = true; ctx.write(response, promise(this)); @@ -630,7 +610,7 @@ public void flush() { public Context send(@NonNull ReadableByteChannel channel) { try { prepareChunked(); - var rsp = new AssembledHttpResponse(route.isHttpHead(), HTTP_1_1, status, setHeaders); + var rsp = new DefaultHttpResponse(HTTP_1_1, status, setHeaders); int bufferSize = contentLength > 0 ? (int) contentLength : this.bufferSize; ctx.channel() .eventLoop() @@ -661,7 +641,7 @@ public Context send(@NonNull InputStream in) { prepareChunked(); ChunkedStream chunkedStream = new ChunkedStream(range.apply(in), bufferSize); - var rsp = new AssembledHttpResponse(route.isHttpHead(), HTTP_1_1, status, setHeaders); + var rsp = new DefaultHttpResponse(HTTP_1_1, status, setHeaders); responseStarted = true; ctx.channel() .eventLoop() @@ -690,7 +670,7 @@ public Context send(@NonNull FileChannel file) { ByteRange range = ByteRange.parse(req.headers().get(RANGE), len).apply(this); - var rsp = new AssembledHttpResponse(route.isHttpHead(), HTTP_1_1, status, setHeaders); + var rsp = new DefaultHttpResponse(HTTP_1_1, status, setHeaders); responseStarted = true; if (preferChunked()) { @@ -747,8 +727,8 @@ public boolean isResponseStarted() { @Override public boolean getResetHeadersOnError() { return resetHeadersOnError == null - ? getRouter().getRouterOptions().contains(RouterOption.RESET_HEADERS_ON_ERROR) - : resetHeadersOnError.booleanValue(); + ? getRouter().getRouterOptions().isResetHeadersOnError() + : resetHeadersOnError; } @Override @@ -766,8 +746,8 @@ public Context send(StatusCode statusCode) { setHeaders.set(CONTENT_LENGTH, "0"); } var rsp = - new AssembledFullHttpResponse( - route.isHttpHead(), HTTP_1_1, status, setHeaders, Unpooled.EMPTY_BUFFER, NO_TRAILING); + new DefaultFullHttpResponse( + HTTP_1_1, status, Unpooled.EMPTY_BUFFER, setHeaders, NO_TRAILING); if (ctx.channel().eventLoop().inEventLoop()) { needsFlush = true; ctx.write(rsp, promise(this)); @@ -870,10 +850,7 @@ void destroy(Throwable cause) { private NettyOutputStream newOutputStream() { prepareChunked(); return new NettyOutputStream( - this, - ctx, - bufferSize, - new AssembledHttpResponse(route.isHttpHead(), HTTP_1_1, status, setHeaders)); + this, ctx, bufferSize, new DefaultHttpResponse(HTTP_1_1, status, setHeaders)); } private FileUpload register(FileUpload upload) { @@ -951,6 +928,6 @@ private void ifStreamId(String streamId) { } private boolean isGzip() { - return getRouter().getServerOptions().getCompressionLevel() != null; + return require(ServerOptions.class).getCompressionLevel() != null; } } diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyDateService.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyDateService.java index a7a6cbc6c7..ab7842ea2e 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyDateService.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyDateService.java @@ -11,24 +11,22 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import io.netty.util.AsciiString; - public class NettyDateService implements Runnable { private static final int DATE_INTERVAL = 1000; - private AsciiString date; + private CharSequence date; public NettyDateService(ScheduledExecutorService scheduler) { scheduler.scheduleAtFixedRate(this, 0, DATE_INTERVAL, TimeUnit.MILLISECONDS); } - public AsciiString date() { + public CharSequence date() { return this.date; } @Override public void run() { this.date = - new AsciiString( + new NettyString( DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC))); } } diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java index 261592f720..90fd036c5b 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java @@ -22,11 +22,14 @@ import io.netty.handler.codec.http.multipart.*; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.timeout.IdleStateEvent; -import io.netty.util.AsciiString; public class NettyHandler extends ChannelInboundHandlerAdapter { private final Logger log = LoggerFactory.getLogger(NettyServer.class); - private static final AsciiString server = AsciiString.cached("N"); + private static final CharSequence server = NettyString.of("N"); + public static final CharSequence CONTENT_TYPE = NettyString.of("content-type"); + public static final CharSequence TEXT_PLAIN = NettyString.of("text/plain"); + public static final CharSequence DATE = NettyString.of("date"); + public static final CharSequence SERVER = NettyString.of("server"); private final NettyDateService serverDate; private final List applications; private Router router; @@ -66,12 +69,12 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { context = new NettyContext(ctx, req, app, path, bufferSize, http2); if (defaultHeaders) { - context.setHeaders.set(HttpHeaderNames.DATE, serverDate.date()); - context.setHeaders.set(HttpHeaderNames.SERVER, server); + context.setHeaders.set(DATE, serverDate.date()); + context.setHeaders.set(SERVER, server); } - context.setHeaders.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN); + context.setHeaders.set(CONTENT_TYPE, TEXT_PLAIN); - if (req.method().equals(HttpMethod.GET)) { + if (req.method() == HttpMethod.GET) { router.match(context).execute(context); } else { // possibly body: diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHeadersFactory.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHeadersFactory.java new file mode 100644 index 0000000000..9c7fe0ee43 --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHeadersFactory.java @@ -0,0 +1,23 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.netty; + +import io.netty.handler.codec.http.HttpHeadersFactory; + +public class NettyHeadersFactory implements HttpHeadersFactory { + + public static NettyHeadersFactory HEADERS = new NettyHeadersFactory(); + + @Override + public HeadersMultiMap newHeaders() { + return new HeadersMultiMap(); + } + + @Override + public HeadersMultiMap newEmptyHeaders() { + return new HeadersMultiMap(); + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.java new file mode 100644 index 0000000000..da789b1fa3 --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.java @@ -0,0 +1,112 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.netty; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.output.BufferedOutput; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.ResourceLeakDetector; + +public class NettyOutputFactory implements OutputFactory { + + private static final String LEAK_DETECTION = "io.netty.leakDetection.level"; + + static { + System.setProperty( + LEAK_DETECTION, + System.getProperty(LEAK_DETECTION, ResourceLeakDetector.Level.DISABLED.name())); + ResourceLeakDetector.setLevel( + ResourceLeakDetector.Level.valueOf(System.getProperty(LEAK_DETECTION))); + } + + private static class NettyContextOutputFactory extends NettyOutputFactory { + public NettyContextOutputFactory(ByteBufAllocator allocator, OutputOptions options) { + super(allocator, options); + } + + @Override + @NonNull public Output wrap(@NonNull String value, @NonNull Charset charset) { + return new NettyWrappedOutput(Unpooled.wrappedBuffer(value.getBytes(charset))); + } + + @Override + @NonNull public Output wrap(@NonNull ByteBuffer buffer) { + return new NettyWrappedOutput(Unpooled.wrappedBuffer(buffer)); + } + + @Override + @NonNull public Output wrap(@NonNull byte[] bytes) { + return new NettyWrappedOutput(Unpooled.wrappedBuffer(bytes)); + } + + @Override + @NonNull public Output wrap(@NonNull byte[] bytes, int offset, int length) { + return new NettyWrappedOutput(Unpooled.wrappedBuffer(bytes, offset, length)); + } + } + + private final ByteBufAllocator allocator; + private OutputOptions options; + + public NettyOutputFactory(ByteBufAllocator allocator, OutputOptions options) { + this.allocator = allocator; + this.options = options; + } + + public ByteBufAllocator getAllocator() { + return allocator; + } + + @Override + @NonNull public OutputOptions getOptions() { + return options; + } + + @Override + @NonNull public OutputFactory setOptions(@NonNull OutputOptions options) { + this.options = options; + return this; + } + + @Override + public @NonNull BufferedOutput allocate(boolean direct, int size) { + return new NettyByteBufOutput( + direct ? this.allocator.directBuffer(size) : this.allocator.heapBuffer(size)); + } + + @Override + @NonNull public Output wrap(@NonNull ByteBuffer buffer) { + return new NettyOutputStatic(buffer.remaining(), () -> Unpooled.wrappedBuffer(buffer)); + } + + @Override + @NonNull public Output wrap(@NonNull byte[] bytes) { + return new NettyOutputStatic(bytes.length, () -> Unpooled.wrappedBuffer(bytes)); + } + + @Override + @NonNull public Output wrap(@NonNull byte[] bytes, int offset, int length) { + return new NettyOutputStatic( + length - offset, () -> Unpooled.wrappedBuffer(bytes, offset, length)); + } + + @Override + @NonNull public BufferedOutput newComposite() { + return new NettyByteBufOutput(allocator.compositeBuffer(48)); + } + + @Override + @NonNull public OutputFactory getContextFactory() { + return new NettyContextOutputFactory(allocator, options); + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputStatic.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputStatic.java new file mode 100644 index 0000000000..337b9c8d44 --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputStatic.java @@ -0,0 +1,42 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.netty; + +import java.util.function.Supplier; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.netty.buffer.ByteBuf; + +public class NettyOutputStatic implements NettyByteBufRef { + private final Supplier provider; + private final int size; + private final NettyString contentLength; + + protected NettyOutputStatic(int length, Supplier provider) { + this.provider = provider; + this.size = length; + this.contentLength = new NettyString(Integer.toString(length)); + } + + @Override + public int size() { + return size; + } + + @NonNull public ByteBuf byteBuf() { + return provider.get(); + } + + @Override + public void send(Context ctx) { + if (ctx.getClass() == NettyContext.class) { + ((NettyContext) ctx).send(provider.get(), contentLength); + } else { + ctx.send(asByteBuffer()); + } + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputStream.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputStream.java index 0e3bc4eb03..46bc39db39 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputStream.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputStream.java @@ -23,12 +23,12 @@ public class NettyOutputStream extends OutputStream { private final ChannelHandlerContext context; private final ChannelFutureListener closeListener; private HttpResponse headers; - private AtomicBoolean closed = new AtomicBoolean(false); + private final AtomicBoolean closed = new AtomicBoolean(false); public NettyOutputStream( NettyContext ctx, ChannelHandlerContext context, int bufferSize, HttpResponse headers) { this.ctx = ctx; - this.buffer = context.alloc().buffer(0, bufferSize); + this.buffer = context.alloc().heapBuffer(bufferSize, bufferSize); this.context = context; this.headers = headers; this.closeListener = ctx; @@ -36,12 +36,7 @@ public NettyOutputStream( @Override public void write(int b) { - writeHeaders(); - - if (buffer.maxWritableBytes() < 1) { - flush(null, null); - } - buffer.writeByte(b); + write(new byte[] {(byte) b}, 0, 1); } @Override diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyRequestDecoder.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyRequestDecoder.java index f87a24d9d7..dea62c988b 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyRequestDecoder.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyRequestDecoder.java @@ -9,6 +9,11 @@ public class NettyRequestDecoder extends HttpRequestDecoder { + private static final String GET = HttpMethod.GET.name(); + private static final String POST = HttpMethod.POST.name(); + private static final String PUT = HttpMethod.PUT.name(); + private static final String DELETE = HttpMethod.DELETE.name(); + public NettyRequestDecoder(HttpDecoderConfig config) { super(config); } @@ -24,16 +29,16 @@ protected HttpMessage createMessage(String[] initialLine) throws Exception { private static HttpMethod valueOf(String name) { // fast-path - if (name == HttpMethod.GET.name()) { + if (name == GET) { return HttpMethod.GET; } - if (name == HttpMethod.POST.name()) { + if (name == POST) { return HttpMethod.POST; } - if (name == HttpMethod.DELETE.name()) { + if (name == DELETE) { return HttpMethod.DELETE; } - if (name == HttpMethod.PUT.name()) { + if (name == PUT) { return HttpMethod.PUT; } // "slow"-path: ensure method is on upper case diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.java index 41e45f6d96..56ff78c423 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.java @@ -6,8 +6,8 @@ package io.jooby.internal.netty; import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponseEncoder; public class NettyResponseEncoder extends HttpResponseEncoder { @Override @@ -18,25 +18,4 @@ protected void encodeHeaders(HttpHeaders headers, ByteBuf buf) { super.encodeHeaders(headers, buf); } } - - @Override - public boolean acceptOutboundMessage(Object msg) throws Exception { - // fast-path singleton(s) - if (msg == Unpooled.EMPTY_BUFFER || msg == LastHttpContent.EMPTY_LAST_CONTENT) { - return true; - } - // JDK type checks vs non-implemented interfaces costs O(N), where - // N is the number of interfaces already implemented by the concrete type that's being tested. - // !(msg instanceof HttpRequest) is supposed to always be true (and meaning that msg isn't a - // HttpRequest), - // but sadly was part of the original behaviour of this method and cannot be removed. - // We place here exact checks vs DefaultHttpResponse and DefaultFullHttpResponse because bad - // users can - // extends such types and make them to implement HttpRequest (non-sense, but still possible). - final Class msgClass = msg.getClass(); - if (msgClass == AssembledFullHttpResponse.class || msgClass == AssembledHttpResponse.class) { - return true; - } - return super.acceptOutboundMessage(msg) && !(msg instanceof HttpRequest); - } } diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettySender.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettySender.java index f7fd05c325..d7ef176dbc 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettySender.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettySender.java @@ -5,10 +5,11 @@ */ package io.jooby.internal.netty; +import static io.jooby.internal.netty.NettyByteBufRef.byteBuf; + import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Sender; -import io.jooby.buffer.DataBuffer; -import io.jooby.netty.buffer.NettyDataBuffer; +import io.jooby.output.Output; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; @@ -34,9 +35,9 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { + public Sender write(@NonNull Output output, @NonNull Callback callback) { context - .writeAndFlush(new DefaultHttpContent(((NettyDataBuffer) data).getNativeBuffer())) + .writeAndFlush(new DefaultHttpContent(byteBuf(output))) .addListener(newChannelFutureListener(ctx, callback)); return this; } diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyServerSentEmitter.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyServerSentEmitter.java index fab7e4cdb2..838baf5d78 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyServerSentEmitter.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyServerSentEmitter.java @@ -5,6 +5,8 @@ */ package io.jooby.internal.netty; +import static io.jooby.internal.netty.NettyByteBufRef.byteBuf; + import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -18,7 +20,6 @@ import io.jooby.ServerSentEmitter; import io.jooby.ServerSentMessage; import io.jooby.SneakyThrows; -import io.jooby.netty.buffer.NettyDataBuffer; import io.netty.channel.EventLoop; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; @@ -64,9 +65,8 @@ public Context getContext() { @NonNull @Override public ServerSentEmitter send(ServerSentMessage data) { if (checkOpen()) { - // TODO: FIX usage of new DataBuffer - var nettyDataBuffer = (NettyDataBuffer) data.encode(netty); - netty.ctx.writeAndFlush(nettyDataBuffer.getNativeBuffer()).addListener(this); + var output = data.encode(netty); + netty.ctx.writeAndFlush(byteBuf(output)).addListener(this); } else { log.warn("server-sent-event closed: {}", id); } diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyString.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyString.java new file mode 100644 index 0000000000..4d8b2fa62a --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyString.java @@ -0,0 +1,45 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.netty; + +import java.nio.charset.StandardCharsets; + +import edu.umd.cs.findbugs.annotations.NonNull; + +public class NettyString implements CharSequence { + + final byte[] bytes; + private final String value; + + public NettyString(String value) { + this.value = value; + this.bytes = value.getBytes(StandardCharsets.US_ASCII); + } + + public static NettyString of(String value) { + return new NettyString(value); + } + + @Override + public int length() { + return value.length(); + } + + @Override + public char charAt(int index) { + return value.charAt(index); + } + + @Override + @NonNull public CharSequence subSequence(int start, int end) { + return value.subSequence(start, end); + } + + @Override + @NonNull public String toString() { + return value; + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWebSocket.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWebSocket.java index 21d572294e..233d281319 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWebSocket.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWebSocket.java @@ -5,6 +5,8 @@ */ package io.jooby.internal.netty; +import static io.jooby.internal.netty.NettyByteBufRef.byteBuf; + import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -26,8 +28,7 @@ import io.jooby.WebSocketCloseStatus; import io.jooby.WebSocketConfigurer; import io.jooby.WebSocketMessage; -import io.jooby.buffer.DataBuffer; -import io.jooby.netty.buffer.NettyDataBuffer; +import io.jooby.output.Output; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; @@ -139,13 +140,13 @@ public WebSocket sendBinary(@NonNull byte[] message, @NonNull WriteCallback call } @NonNull @Override - public WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback) { - return sendMessage(((NettyDataBuffer) message).getNativeBuffer(), true, callback); + public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { + return sendMessage(byteBuf(message), false, callback); } @NonNull @Override - public WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callback) { - return sendMessage(((NettyDataBuffer) message).getNativeBuffer(), false, callback); + public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { + return sendMessage(byteBuf(message), true, callback); } @Override diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWrappedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWrappedOutput.java new file mode 100644 index 0000000000..f91017428c --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWrappedOutput.java @@ -0,0 +1,26 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.netty; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.buffer.ByteBuf; + +public class NettyWrappedOutput implements NettyByteBufRef { + private final ByteBuf buffer; + + protected NettyWrappedOutput(ByteBuf buffer) { + this.buffer = buffer; + } + + @Override + public int size() { + return buffer.readableBytes(); + } + + @NonNull public ByteBuf byteBuf() { + return buffer; + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java b/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java index eaea22c579..3e74d96507 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java @@ -6,6 +6,7 @@ package io.jooby.netty; import static io.jooby.ServerOptions._4KB; +import static io.jooby.internal.netty.NettyHeadersFactory.HEADERS; import static java.util.concurrent.Executors.newFixedThreadPool; import java.net.BindException; @@ -23,7 +24,7 @@ import io.jooby.SslOptions; import io.jooby.exception.StartupException; import io.jooby.internal.netty.*; -import io.jooby.netty.buffer.NettyDataBufferFactory; +import io.jooby.output.OutputFactory; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelOption; @@ -44,6 +45,13 @@ * @since 2.0.0 */ public class NettyServer extends Server.Base { + + private static final String NAME = "netty"; + + static { + System.setProperty("server.name", NAME); + } + private static final int _50 = 50; private static final int _100 = 100; @@ -53,19 +61,17 @@ public class NettyServer extends Server.Base { private ScheduledExecutorService dateLoop; private ExecutorService worker; - private NettyDataBufferFactory bufferFactory; private List applications; + private NettyOutputFactory outputFactory; /** * Creates a server. * - * @param bufferFactory Byte buffer allocator. * @param worker Thread-pool to use. */ - public NettyServer( - @NonNull NettyDataBufferFactory bufferFactory, @NonNull ExecutorService worker) { + public NettyServer(@NonNull ServerOptions options, @NonNull ExecutorService worker) { + super.setOptions(options); this.worker = worker; - this.bufferFactory = bufferFactory; } /** @@ -77,32 +83,24 @@ public NettyServer(@NonNull ExecutorService worker) { this.worker = worker; } - /** - * Creates a server. - * - * @param bufferFactory Byte buffer allocator. - */ - public NettyServer(@NonNull NettyDataBufferFactory bufferFactory) { - this.bufferFactory = bufferFactory; - } - /** Creates a server. */ - public NettyServer() {} - - @Override - public @NonNull NettyServer setOptions(@NonNull ServerOptions options) { + public NettyServer(@NonNull ServerOptions options) { super.setOptions(options); - return this; } - @Override - protected ServerOptions defaultOptions() { - return new ServerOptions().setServer("netty"); + public NettyServer() {} + + @NonNull @Override + public OutputFactory getOutputFactory() { + if (outputFactory == null) { + outputFactory = new NettyOutputFactory(ByteBufAllocator.DEFAULT, getOptions().getOutput()); + } + return outputFactory; } @NonNull @Override public String getName() { - return "netty"; + return NAME; } @NonNull @Override @@ -112,16 +110,15 @@ public Server start(@NonNull Jooby... application) { var portInUse = options.getPort(); try { this.applications = List.of(application); - boolean single = applications.size() == 1; /* Worker: Application blocking code */ if (worker == null) { worker = newFixedThreadPool(options.getWorkerThreads(), new DefaultThreadFactory("worker")); } - if (bufferFactory == null) { - bufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); - } // Make sure context use same buffer factory - applications.forEach(app -> app.setBufferFactory(bufferFactory)); + for (var app : applications) { + app.getServices().put(ServerOptions.class, options); + app.getServices().put(Server.class, this); + } addShutdownHook(); @@ -139,7 +136,8 @@ public Server start(@NonNull Jooby... application) { this.dateLoop = Executors.newSingleThreadScheduledExecutor(); var dateService = new NettyDateService(dateLoop); - var allocator = bufferFactory.getByteBufAllocator(); + var outputFactory = (NettyOutputFactory) getOutputFactory(); + var allocator = outputFactory.getAllocator(); var http2 = options.isHttp2() == Boolean.TRUE; /* Bootstrap: */ if (!options.isHttpsOnly()) { @@ -198,22 +196,20 @@ private ClientAuth toClientAuth(SslOptions.ClientAuth clientAuth) { private NettyPipeline newPipeline( ServerOptions options, SslContext sslContext, NettyDateService dateService, boolean http2) { - var bufferSize = options.getBufferSize(); - var headersFactory = HeadersMultiMap.httpHeadersFactory(); var decoderConfig = new HttpDecoderConfig() .setMaxInitialLineLength(_4KB) .setMaxHeaderSize(options.getMaxHeaderSize()) - .setMaxChunkSize(bufferSize) - .setHeadersFactory(headersFactory) - .setTrailersFactory(headersFactory); + .setMaxChunkSize(options.getOutput().getSize()) + .setHeadersFactory(HEADERS) + .setTrailersFactory(HEADERS); return new NettyPipeline( sslContext, dateService, decoderConfig, applications, options.getMaxRequestSize(), - bufferSize, + options.getOutput().getSize(), options.getDefaultHeaders(), http2, options.isExpectContinue() == Boolean.TRUE, diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/buffer/NettyDataBuffer.java b/modules/jooby-netty/src/main/java/io/jooby/netty/buffer/NettyDataBuffer.java deleted file mode 100644 index af2760c252..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/buffer/NettyDataBuffer.java +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.netty.buffer; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.function.IntPredicate; - -import io.jooby.Context; -import io.jooby.buffer.DataBuffer; -import io.jooby.buffer.PooledDataBuffer; -import io.jooby.internal.netty.NettyContext; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; - -/** - * Implementation of the {@code DataBuffer} interface that wraps a Netty 4 {@link ByteBuf}. - * Typically constructed with {@link NettyDataBufferFactory}. - * - * @author Arjen Poutsma - * @author Brian Clozel - * @since 5.0 - */ -public class NettyDataBuffer implements PooledDataBuffer { - - private ByteBuf byteBuf; - - private final NettyDataBufferFactory dataBufferFactory; - - /** - * Create a new {@code NettyDataBuffer} based on the given {@code ByteBuff}. - * - * @param byteBuf the buffer to base this buffer on - */ - NettyDataBuffer(ByteBuf byteBuf, NettyDataBufferFactory dataBufferFactory) { - Objects.requireNonNull(byteBuf, "ByteBuf must not be null"); - Objects.requireNonNull(dataBufferFactory, "NettyDataBufferFactory must not be null"); - this.byteBuf = byteBuf; - this.dataBufferFactory = dataBufferFactory; - } - - /** - * Directly exposes the native {@code ByteBuf} that this buffer is based on. - * - * @return the wrapped byte buffer - */ - public ByteBuf getNativeBuffer() { - return this.byteBuf; - } - - @Override - public NettyDataBufferFactory factory() { - return this.dataBufferFactory; - } - - @Override - public int indexOf(IntPredicate predicate, int fromIndex) { - Objects.requireNonNull(predicate, "IntPredicate must not be null"); - if (fromIndex < 0) { - fromIndex = 0; - } else if (fromIndex >= this.byteBuf.writerIndex()) { - return -1; - } - int length = this.byteBuf.writerIndex() - fromIndex; - return this.byteBuf.forEachByte(fromIndex, length, predicate.negate()::test); - } - - @Override - public int lastIndexOf(IntPredicate predicate, int fromIndex) { - Objects.requireNonNull(predicate, "IntPredicate must not be null"); - if (fromIndex < 0) { - return -1; - } - fromIndex = Math.min(fromIndex, this.byteBuf.writerIndex() - 1); - return this.byteBuf.forEachByteDesc(0, fromIndex + 1, predicate.negate()::test); - } - - @Override - public int readableByteCount() { - return this.byteBuf.readableBytes(); - } - - @Override - public int writableByteCount() { - return this.byteBuf.writableBytes(); - } - - @Override - public int readPosition() { - return this.byteBuf.readerIndex(); - } - - @Override - public NettyDataBuffer readPosition(int readPosition) { - this.byteBuf.readerIndex(readPosition); - return this; - } - - @Override - public int writePosition() { - return this.byteBuf.writerIndex(); - } - - @Override - public NettyDataBuffer writePosition(int writePosition) { - this.byteBuf.writerIndex(writePosition); - return this; - } - - @Override - public byte getByte(int index) { - return this.byteBuf.getByte(index); - } - - @Override - public int capacity() { - return this.byteBuf.capacity(); - } - - @Override - public NettyDataBuffer duplicate() { - return new NettyDataBuffer(byteBuf.duplicate(), dataBufferFactory); - } - - @Override - public NettyDataBuffer ensureWritable(int capacity) { - this.byteBuf.ensureWritable(capacity); - return this; - } - - @Override - public byte read() { - return this.byteBuf.readByte(); - } - - @Override - public NettyDataBuffer read(byte[] destination) { - this.byteBuf.readBytes(destination); - return this; - } - - @Override - public NettyDataBuffer read(byte[] destination, int offset, int length) { - this.byteBuf.readBytes(destination, offset, length); - return this; - } - - @Override - public NettyDataBuffer write(byte b) { - this.byteBuf.writeByte(b); - return this; - } - - @Override - public NettyDataBuffer write(byte[] source) { - this.byteBuf.writeBytes(source); - return this; - } - - @Override - public NettyDataBuffer write(byte[] source, int offset, int length) { - this.byteBuf.writeBytes(source, offset, length); - return this; - } - - @Override - public NettyDataBuffer write(DataBuffer... dataBuffers) { - if (hasNettyDataBuffers(dataBuffers)) { - ByteBuf[] nativeBuffers = new ByteBuf[dataBuffers.length]; - for (int i = 0; i < dataBuffers.length; i++) { - nativeBuffers[i] = ((NettyDataBuffer) dataBuffers[i]).getNativeBuffer(); - } - write(nativeBuffers); - } else { - ByteBuffer[] byteBuffers = new ByteBuffer[dataBuffers.length]; - for (int i = 0; i < dataBuffers.length; i++) { - byteBuffers[i] = ByteBuffer.allocate(dataBuffers[i].readableByteCount()); - dataBuffers[i].toByteBuffer(byteBuffers[i]); - } - write(byteBuffers); - } - return this; - } - - private static boolean hasNettyDataBuffers(DataBuffer[] buffers) { - for (DataBuffer buffer : buffers) { - if (!(buffer instanceof NettyDataBuffer)) { - return false; - } - } - return true; - } - - @Override - public NettyDataBuffer write(ByteBuffer... buffers) { - for (ByteBuffer buffer : buffers) { - this.byteBuf.writeBytes(buffer); - } - return this; - } - - /** - * Writes one or more Netty {@link ByteBuf ByteBufs} to this buffer, starting at the current - * writing position. - * - * @param byteBufs the buffers to write into this buffer - * @return this buffer - */ - public NettyDataBuffer write(ByteBuf... byteBufs) { - for (ByteBuf byteBuf : byteBufs) { - this.byteBuf.writeBytes(byteBuf); - } - return this; - } - - @Override - public DataBuffer write(CharSequence charSequence, Charset charset) { - Objects.requireNonNull(charSequence, "CharSequence must not be null"); - Objects.requireNonNull(charset, "Charset must not be null"); - if (StandardCharsets.UTF_8.equals(charset)) { - ByteBufUtil.writeUtf8(this.byteBuf, charSequence); - } else if (StandardCharsets.US_ASCII.equals(charset)) { - ByteBufUtil.writeAscii(this.byteBuf, charSequence); - } else { - return PooledDataBuffer.super.write(charSequence, charset); - } - return this; - } - - @Override - public NettyDataBuffer split(int index) { - ByteBuf split = this.byteBuf.retainedSlice(0, index); - int writerIndex = this.byteBuf.writerIndex(); - int readerIndex = this.byteBuf.readerIndex(); - - split.writerIndex(Math.min(writerIndex, index)); - split.readerIndex(Math.min(readerIndex, index)); - - this.byteBuf = this.byteBuf.slice(index, this.byteBuf.capacity() - index); - this.byteBuf.writerIndex(Math.max(writerIndex, index) - index); - this.byteBuf.readerIndex(Math.max(readerIndex, index) - index); - - return new NettyDataBuffer(split, this.dataBufferFactory); - } - - @Override - public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - Objects.requireNonNull(dest, "Dest must not be null"); - - dest = dest.duplicate().clear(); - dest.put(destPos, this.byteBuf.nioBuffer(srcPos, length), 0, length); - } - - @Override - public DataBuffer.ByteBufferIterator readableByteBuffers() { - ByteBuffer[] readable = - this.byteBuf.nioBuffers(this.byteBuf.readerIndex(), this.byteBuf.readableBytes()); - return new ByteBufferIterator(readable, true); - } - - @Override - public DataBuffer.ByteBufferIterator writableByteBuffers() { - ByteBuffer[] writable = - this.byteBuf.nioBuffers(this.byteBuf.writerIndex(), this.byteBuf.writableBytes()); - return new ByteBufferIterator(writable, false); - } - - @Override - public String toString(Charset charset) { - Objects.requireNonNull(charset, "Charset must not be null"); - return this.byteBuf.toString(charset); - } - - @Override - public String toString(int index, int length, Charset charset) { - Objects.requireNonNull(charset, "Charset must not be null"); - return this.byteBuf.toString(index, length, charset); - } - - @Override - public DataBuffer clear() { - this.byteBuf.clear(); - return this; - } - - @Override - public Context send(Context ctx) { - ((NettyContext) ctx).send(this.byteBuf); - return ctx; - } - - @Override - public boolean isAllocated() { - return this.byteBuf.refCnt() > 0; - } - - @Override - public PooledDataBuffer retain() { - return new NettyDataBuffer(this.byteBuf.retain(), this.dataBufferFactory); - } - - @Override - public PooledDataBuffer touch(Object hint) { - this.byteBuf.touch(hint); - return this; - } - - @Override - public boolean release() { - return this.byteBuf.release(); - } - - @Override - public boolean equals(Object other) { - return (this == other - || (other instanceof NettyDataBuffer that && this.byteBuf.equals(that.byteBuf))); - } - - @Override - public int hashCode() { - return this.byteBuf.hashCode(); - } - - @Override - public String toString() { - return this.byteBuf.toString(); - } - - private static final class ByteBufferIterator implements DataBuffer.ByteBufferIterator { - - private final ByteBuffer[] byteBuffers; - - private final boolean readOnly; - - private int cursor = 0; - - public ByteBufferIterator(ByteBuffer[] byteBuffers, boolean readOnly) { - this.byteBuffers = byteBuffers; - this.readOnly = readOnly; - } - - @Override - public boolean hasNext() { - return this.cursor < this.byteBuffers.length; - } - - @Override - public ByteBuffer next() { - int index = this.cursor; - if (index < this.byteBuffers.length) { - this.cursor = index + 1; - ByteBuffer next = this.byteBuffers[index]; - return this.readOnly ? next.asReadOnlyBuffer() : next; - } else { - throw new NoSuchElementException(); - } - } - - @Override - public void close() {} - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/buffer/NettyDataBufferFactory.java b/modules/jooby-netty/src/main/java/io/jooby/netty/buffer/NettyDataBufferFactory.java deleted file mode 100644 index 9963362b3e..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/buffer/NettyDataBufferFactory.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.netty.buffer; - -import static java.util.Objects.requireNonNull; - -import java.nio.ByteBuffer; -import java.util.List; - -import io.jooby.buffer.DataBuffer; -import io.jooby.buffer.DataBufferFactory; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.CompositeByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.util.ResourceLeakDetector; - -/** - * Implementation of the {@code DataBufferFactory} interface based on a Netty 4 {@link - * ByteBufAllocator}. - * - * @author Arjen Poutsma - * @author Juergen Hoeller - * @since 5.0 - * @see io.netty.buffer.PooledByteBufAllocator - * @see io.netty.buffer.UnpooledByteBufAllocator - */ -public class NettyDataBufferFactory implements DataBufferFactory { - - private static final String LEAK_DETECTION = "io.netty.leakDetection.level"; - - static { - System.setProperty( - LEAK_DETECTION, - System.getProperty(LEAK_DETECTION, ResourceLeakDetector.Level.DISABLED.name())); - ResourceLeakDetector.setLevel( - ResourceLeakDetector.Level.valueOf(System.getProperty(LEAK_DETECTION))); - } - - private final ByteBufAllocator byteBufAllocator; - private int defaultInitialCapacity = 1024; - - /** - * Create a new {@code NettyDataBufferFactory} based on the given factory. - * - * @param byteBufAllocator the factory to use - * @see io.netty.buffer.PooledByteBufAllocator - * @see io.netty.buffer.UnpooledByteBufAllocator - */ - public NettyDataBufferFactory(ByteBufAllocator byteBufAllocator) { - requireNonNull(byteBufAllocator, "ByteBufAllocator must not be null"); - this.byteBufAllocator = byteBufAllocator; - } - - public NettyDataBufferFactory() { - this(ByteBufAllocator.DEFAULT); - } - - /** Return the {@code ByteBufAllocator} used by this factory. */ - public ByteBufAllocator getByteBufAllocator() { - return this.byteBufAllocator; - } - - @Override - public int getDefaultInitialCapacity() { - return defaultInitialCapacity; - } - - @Override - public NettyDataBufferFactory setDefaultInitialCapacity(int defaultInitialCapacity) { - this.defaultInitialCapacity = defaultInitialCapacity; - return this; - } - - @Override - public NettyDataBuffer allocateBuffer() { - return allocateBuffer(defaultInitialCapacity); - } - - @Override - public NettyDataBuffer allocateBuffer(int initialCapacity) { - ByteBuf byteBuf = this.byteBufAllocator.buffer(initialCapacity); - return new NettyDataBuffer(byteBuf, this); - } - - @Override - public NettyDataBuffer wrap(ByteBuffer byteBuffer) { - ByteBuf byteBuf = Unpooled.wrappedBuffer(byteBuffer); - return new NettyDataBuffer(byteBuf, this); - } - - @Override - public DataBuffer wrap(byte[] bytes) { - ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes); - return new NettyDataBuffer(byteBuf, this); - } - - @Override - public DataBuffer wrap(byte[] bytes, int offset, int length) { - ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes, offset, length); - return new NettyDataBuffer(byteBuf, this); - } - - /** - * Wrap the given Netty {@link ByteBuf} in a {@code NettyDataBuffer}. - * - * @param byteBuf the Netty byte buffer to wrap - * @return the wrapped buffer - */ - public NettyDataBuffer wrap(ByteBuf byteBuf) { - byteBuf.touch(); - return new NettyDataBuffer(byteBuf, this); - } - - /** - * {@inheritDoc} - * - *

This implementation uses Netty's {@link CompositeByteBuf}. - */ - @Override - public DataBuffer join(List dataBuffers) { - requireNonNull(dataBuffers, "DataBuffer List must not be empty"); - int bufferCount = dataBuffers.size(); - if (bufferCount == 1) { - return dataBuffers.get(0); - } - CompositeByteBuf composite = this.byteBufAllocator.compositeBuffer(bufferCount); - for (DataBuffer dataBuffer : dataBuffers) { - if (!(dataBuffer instanceof NettyDataBuffer)) { - throw new IllegalArgumentException(""); - } - composite.addComponent(true, ((NettyDataBuffer) dataBuffer).getNativeBuffer()); - } - return new NettyDataBuffer(composite, this); - } - - @Override - public boolean isDirect() { - return this.byteBufAllocator.isDirectBufferPooled(); - } - - /** - * Return the given Netty {@link DataBuffer} as a {@link ByteBuf}. - * - *

Returns the {@linkplain NettyDataBuffer#getNativeBuffer() native buffer} if {@code - * dataBuffer} is a {@link NettyDataBuffer}; returns {@link Unpooled#wrappedBuffer(ByteBuffer)} - * otherwise. - * - * @param dataBuffer the {@code DataBuffer} to return a {@code ByteBuf} for - * @return the netty {@code ByteBuf} - */ - public static ByteBuf toByteBuf(DataBuffer dataBuffer) { - if (dataBuffer instanceof NettyDataBuffer nettyDataBuffer) { - return nettyDataBuffer.getNativeBuffer(); - } else { - ByteBuffer byteBuffer = ByteBuffer.allocate(dataBuffer.readableByteCount()); - dataBuffer.toByteBuffer(byteBuffer); - return Unpooled.wrappedBuffer(byteBuffer); - } - } - - @Override - public String toString() { - return "NettyDataBufferFactory (" + this.byteBufAllocator + ")"; - } -} diff --git a/modules/jooby-netty/src/main/java/module-info.java b/modules/jooby-netty/src/main/java/module-info.java index adb13bc87a..0831e5136c 100644 --- a/modules/jooby-netty/src/main/java/module-info.java +++ b/modules/jooby-netty/src/main/java/module-info.java @@ -4,14 +4,11 @@ * Copyright 2014 Edgar Espina */ import io.jooby.Server; -import io.jooby.buffer.DataBufferFactory; import io.jooby.netty.NettyServer; -import io.jooby.netty.buffer.NettyDataBufferFactory; /** Netty module. */ module io.jooby.netty { exports io.jooby.netty; - exports io.jooby.netty.buffer; requires io.jooby; requires static com.github.spotbugs.annotations; @@ -31,6 +28,4 @@ provides Server with NettyServer; - provides DataBufferFactory with - NettyDataBufferFactory; } diff --git a/modules/jooby-netty/src/main/resources/META-INF/services/io.jooby.buffer.DataBufferFactory b/modules/jooby-netty/src/main/resources/META-INF/services/io.jooby.buffer.DataBufferFactory deleted file mode 100644 index f73eeeb63d..0000000000 --- a/modules/jooby-netty/src/main/resources/META-INF/services/io.jooby.buffer.DataBufferFactory +++ /dev/null @@ -1 +0,0 @@ -io.jooby.netty.buffer.NettyDataBufferFactory diff --git a/modules/jooby-netty/src/test/java/io/jooby/internal/netty/Issue3554.java b/modules/jooby-netty/src/test/java/io/jooby/internal/netty/Issue3554.java index 9eab26e27d..e0e33f07cb 100644 --- a/modules/jooby-netty/src/test/java/io/jooby/internal/netty/Issue3554.java +++ b/modules/jooby-netty/src/test/java/io/jooby/internal/netty/Issue3554.java @@ -29,7 +29,7 @@ public void shouldCloseOutputStreamOnce() throws IOException { when(buffer.readableBytes()).thenReturn(0); var bufferAllocator = mock(ByteBufAllocator.class); - when(bufferAllocator.buffer(0, 1024)).thenReturn(buffer); + when(bufferAllocator.heapBuffer(1024, 1024)).thenReturn(buffer); var future = mock(ChannelFuture.class); var channelContext = mock(ChannelHandlerContext.class); diff --git a/modules/jooby-netty/src/test/java/io/jooby/netty/ByteBufBench.java b/modules/jooby-netty/src/test/java/io/jooby/netty/ByteBufBench.java new file mode 100644 index 0000000000..f031dc5c3e --- /dev/null +++ b/modules/jooby-netty/src/test/java/io/jooby/netty/ByteBufBench.java @@ -0,0 +1,48 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.netty; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; + +import io.netty.buffer.Unpooled; + +@Fork(5) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 10, time = 1) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Benchmark) +public class ByteBufBench { + private String string; + + @Setup + public void setup() { + string = "Hello World!"; + } + + @Benchmark + public void copiedBufferUtf8() { + Unpooled.copiedBuffer(string, StandardCharsets.UTF_8); + } + + @Benchmark + public void copiedBufferUSAscii() { + Unpooled.copiedBuffer(string, StandardCharsets.US_ASCII); + } + + @Benchmark + public void stringUtf8Bytes() { + Unpooled.wrappedBuffer(string.getBytes(StandardCharsets.UTF_8)); + } + + @Benchmark + public void stringUSAsciiBytes() { + Unpooled.wrappedBuffer(string.getBytes(StandardCharsets.US_ASCII)); + } +} diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 131f4d9ca5..8954921a5c 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -128,6 +128,19 @@ kotlinx-coroutines-core test + + net.bytebuddy + byte-buddy + 1.17.5 + test + + + + com.puppycrawl.tools + checkstyle + 10.26.1 + test + diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java index d649fde5f2..9b3001e5cc 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java @@ -185,12 +185,15 @@ public static List parse( new IllegalStateException( "Mvc class not found: " + InsnSupport.toString(node))); return parse(ctx, prefix, type); - } else if (signature.matches(MVC_EXTENSION) || signature.matches(Object.class)) { + } else if (signature.matches(MVC_EXTENSION)) { AbstractInsnNode previous = node.getPrevious(); - if (previous instanceof MethodInsnNode) { - MethodInsnNode methodInsnNode = (MethodInsnNode) previous; + if (previous instanceof TypeInsnNode) { + // kt version of mvc(Controller_()) + previous = previous.getPrevious(); + } + if (previous instanceof MethodInsnNode methodInsnNode) { if (methodInsnNode.getOpcode() == Opcodes.INVOKESPECIAL) { - // mvc(new Controller(...)); + // mvc(new Controller_(...)); var type = Type.getObjectType(methodInsnNode.owner); var classNode = ctx.classNode(type); var controllerType = @@ -203,25 +206,42 @@ public static List parse( .findFirst() .orElse(type); return parse(ctx, prefix, controllerType); - } else if (methodInsnNode.getOpcode() == Opcodes.INVOKEINTERFACE) { - AbstractInsnNode methodPrev = methodInsnNode.getPrevious(); - if (methodPrev instanceof VarInsnNode) { - // mvc(daggerApp.myController()); - Type type = Type.getReturnType(methodInsnNode.desc); - return parse(ctx, prefix, type); - } else if (methodPrev instanceof LdcInsnNode ldcInsnNode) { - // mvc(beanScope.get(...)); - Type type = (Type) (ldcInsnNode).cst; - return parse(ctx, prefix, type); - } } else { if (methodInsnNode.getPrevious() instanceof LdcInsnNode ldcInsnNode) { // mvc(require(Controller.class)) Type type = (Type) (ldcInsnNode).cst; return parse(ctx, prefix, type); } else { - // mvc(some.myController()); Type type = Type.getReturnType(methodInsnNode.desc); + if (type.equals(MVC_EXTENSION)) { + // support test code: toMvcExtension() but also any other thing that generate an + // extension from controller class + type = + InsnSupport.prev(methodInsnNode.getPrevious()) + .filter( + e -> + (e instanceof LdcInsnNode ldcInsnNode + && (ldcInsnNode.cst instanceof Type)) + || (e instanceof MethodInsnNode method) + && (!Type.getReturnType(method.desc) + .getClassName() + .equals(Object.class.getName()))) + .findFirst() + .map( + e -> { + if (e instanceof LdcInsnNode ldcInsnNode) { + return ldcInsnNode.cst; + } else if (e instanceof MethodInsnNode method) { + return Type.getReturnType(method.desc); + } else { + return e; + } + }) + .filter(it -> it instanceof Type) + .map(it -> (Type) it) + .orElse(type); + } + // mvc(some.myController()); return parse(ctx, prefix, type); } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RequestParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RequestParser.java index bdd694abb2..23ec167921 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RequestParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RequestParser.java @@ -369,8 +369,7 @@ private static WebArgument argumentValue(String argumentName, MethodInsnNode nod private static Predicate valueOwner() { return e -> { if (e instanceof MethodInsnNode) { - return ((MethodInsnNode) e).owner.equals("io/jooby/Value") - || ((MethodInsnNode) e).owner.equals("io/jooby/ValueNode") + return ((MethodInsnNode) e).owner.equals("io/jooby/value/Value") || ((MethodInsnNode) e).owner.equals("io/jooby/Body") || (((MethodInsnNode) e).owner.equals("io/jooby/Context") && isFileUpload((MethodInsnNode) e)); diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java index 1d506a24c0..4caad4f0ff 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java @@ -20,16 +20,7 @@ import java.io.IOException; import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiPredicate; import java.util.function.Consumer; @@ -56,7 +47,6 @@ import io.jooby.Context; import io.jooby.MediaType; import io.jooby.Route; -import io.jooby.RouteSet; import io.jooby.Router; import io.jooby.SneakyThrows; import io.jooby.annotation.OpenApiRegister; @@ -477,14 +467,14 @@ private List routeHandler(ParserContext ctx, String prefix, Method parseText(it, instructionTo, handlerList.get(handlerList.size() - 1)::setDescription); } else if (signature.matches(Route.class, "tags", String[].class)) { instructionTo = parseTags(it, instructionTo, handlerList.get(handlerList.size() - 1)); - } else if (signature.matches(RouteSet.class, "summary", String.class)) { + } else if (signature.matches(Route.Set.class, "summary", String.class)) { instructionTo = parseText(it, instructionTo, handlerList.get(handlerList.size() - 1)::setPathSummary); - } else if (signature.matches(RouteSet.class, "description", String.class)) { + } else if (signature.matches(Route.Set.class, "description", String.class)) { instructionTo = parseText( it, instructionTo, handlerList.get(handlerList.size() - 1)::setPathDescription); - } else if (signature.matches(RouteSet.class, "tags", String[].class)) { + } else if (signature.matches(Route.Set.class, "tags", String[].class)) { if (routeIndex >= 0) { for (int i = routeIndex; i < handlerList.size(); i++) { instructionTo = parseTags(it, instructionTo, handlerList.get(i)); diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/TypeFactory.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/TypeFactory.java index 66e1dc6b59..7d6d81a2af 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/TypeFactory.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/TypeFactory.java @@ -30,7 +30,7 @@ public class TypeFactory { public static final Type CONTEXT = Type.getType(Context.class); - public static final Type MVC_EXTENSION = Type.getType("Lio/jooby/MvcExtension;"); + public static final Type MVC_EXTENSION = Type.getType("Lio/jooby/Extension;"); public static final Type GENERATED = Type.getType("Lio/jooby/annotation/Generated;"); public static final Type KOOBY = Type.getType("Lio/jooby/kt/Kooby;"); diff --git a/modules/jooby-openapi/src/test/java/examples/ControllerExample.java b/modules/jooby-openapi/src/test/java/examples/ControllerExample.java index e14fe5768f..dad18c2d7d 100644 --- a/modules/jooby-openapi/src/test/java/examples/ControllerExample.java +++ b/modules/jooby-openapi/src/test/java/examples/ControllerExample.java @@ -10,10 +10,7 @@ import io.jooby.Context; import io.jooby.Session; -import io.jooby.annotation.GET; -import io.jooby.annotation.POST; -import io.jooby.annotation.Path; -import io.jooby.annotation.QueryParam; +import io.jooby.annotation.*; @Path("/api") public class ControllerExample { diff --git a/modules/jooby-openapi/src/test/java/examples/FormMvcApp.java b/modules/jooby-openapi/src/test/java/examples/FormMvcApp.java index 2ece128d9b..ac748951d0 100644 --- a/modules/jooby-openapi/src/test/java/examples/FormMvcApp.java +++ b/modules/jooby-openapi/src/test/java/examples/FormMvcApp.java @@ -5,11 +5,13 @@ */ package examples; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class FormMvcApp extends Jooby { { - mvc(new FormController()); + mvc(toMvcExtension(FormController.class)); } } diff --git a/modules/jooby-openapi/src/test/java/examples/MvcApp.java b/modules/jooby-openapi/src/test/java/examples/MvcApp.java index b2c12a75c5..ff4f2b8b82 100644 --- a/modules/jooby-openapi/src/test/java/examples/MvcApp.java +++ b/modules/jooby-openapi/src/test/java/examples/MvcApp.java @@ -5,11 +5,13 @@ */ package examples; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class MvcApp extends Jooby { { - mvc(ControllerExample.class); + mvc(toMvcExtension(ControllerExample.class)); } } diff --git a/modules/jooby-openapi/src/test/java/examples/MvcAppWithRoutes.java b/modules/jooby-openapi/src/test/java/examples/MvcAppWithRoutes.java index f8a41c5ba0..66cbc5f31c 100644 --- a/modules/jooby-openapi/src/test/java/examples/MvcAppWithRoutes.java +++ b/modules/jooby-openapi/src/test/java/examples/MvcAppWithRoutes.java @@ -5,11 +5,13 @@ */ package examples; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class MvcAppWithRoutes extends Jooby { { - routes(() -> mvc(ControllerExample.class)); + routes(() -> mvc(toMvcExtension(ControllerExample.class))); } } diff --git a/modules/jooby-openapi/src/test/java/examples/MvcDaggerApp.java b/modules/jooby-openapi/src/test/java/examples/MvcDaggerApp.java index cdfc10dab7..ca9c4dc9a7 100644 --- a/modules/jooby-openapi/src/test/java/examples/MvcDaggerApp.java +++ b/modules/jooby-openapi/src/test/java/examples/MvcDaggerApp.java @@ -5,6 +5,8 @@ */ package examples; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; import io.jooby.OpenAPIModule; import io.jooby.annotation.GET; @@ -37,6 +39,6 @@ public String sayHi() { install(new OpenAPIModule()); DaggerApp daggerApp = new DaggerAppImpl(); - mvc(daggerApp.controller()); + mvc(toMvcExtension(daggerApp.controller())); } } diff --git a/modules/jooby-openapi/src/test/java/examples/MvcInstanceApp.java b/modules/jooby-openapi/src/test/java/examples/MvcInstanceApp.java index 605464a87d..aed1ceb1ad 100644 --- a/modules/jooby-openapi/src/test/java/examples/MvcInstanceApp.java +++ b/modules/jooby-openapi/src/test/java/examples/MvcInstanceApp.java @@ -5,11 +5,13 @@ */ package examples; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class MvcInstanceApp extends Jooby { { - mvc(new ControllerExample()); + mvc(toMvcExtension(ControllerExample.class)); } } diff --git a/modules/jooby-openapi/src/test/java/examples/MvcRequireApp.java b/modules/jooby-openapi/src/test/java/examples/MvcRequireApp.java index a1adb4429f..1ed654ad80 100644 --- a/modules/jooby-openapi/src/test/java/examples/MvcRequireApp.java +++ b/modules/jooby-openapi/src/test/java/examples/MvcRequireApp.java @@ -5,6 +5,8 @@ */ package examples; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; import io.jooby.OpenAPIModule; import io.jooby.annotation.GET; @@ -24,6 +26,6 @@ public String sayHi() { { install(new OpenAPIModule()); - mvc(require(Controller.class)); + mvc(toMvcExtension(require(Controller.class))); } } diff --git a/modules/jooby-openapi/src/test/java/examples/OpenApiApp.java b/modules/jooby-openapi/src/test/java/examples/OpenApiApp.java index af061516d2..a17f7907fe 100644 --- a/modules/jooby-openapi/src/test/java/examples/OpenApiApp.java +++ b/modules/jooby-openapi/src/test/java/examples/OpenApiApp.java @@ -5,10 +5,12 @@ */ package examples; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class OpenApiApp extends Jooby { { - mvc(new OpenApiController()); + mvc(toMvcExtension(OpenApiController.class)); } } diff --git a/modules/jooby-openapi/src/test/java/io/jooby/openapi/MvcExtensionGenerator.java b/modules/jooby-openapi/src/test/java/io/jooby/openapi/MvcExtensionGenerator.java new file mode 100644 index 0000000000..c8828a53c9 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/io/jooby/openapi/MvcExtensionGenerator.java @@ -0,0 +1,36 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.openapi; + +import io.jooby.Extension; +import io.jooby.annotation.Generated; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; + +public class MvcExtensionGenerator { + + public static Extension toMvcExtension(Object controller) { + return toMvcExtension(controller.getClass()); + } + + public static Extension toMvcExtension(Class controller) { + try { + return new ByteBuddy() + .subclass(Extension.class) + .annotateType( + AnnotationDescription.Builder.ofType(Generated.class) + .define("value", controller) + .build()) + .make() + .load(controller.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) + .getLoaded() + .newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/App1359.java b/modules/jooby-openapi/src/test/java/issues/App1359.java index e010fa7c2e..238ecfda1c 100644 --- a/modules/jooby-openapi/src/test/java/issues/App1359.java +++ b/modules/jooby-openapi/src/test/java/issues/App1359.java @@ -5,6 +5,8 @@ */ package issues; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Context; import io.jooby.Jooby; import io.jooby.MediaType; @@ -15,8 +17,7 @@ public class App1359 extends Jooby { { get("/script/1359", this::defaultResponse).produces(MediaType.text); - - mvc(new Controller1359()); + mvc(toMvcExtension(Controller1359.class)); } @ApiResponses({ diff --git a/modules/jooby-openapi/src/test/java/issues/App1586.java b/modules/jooby-openapi/src/test/java/issues/App1586.java index c249c8c9c9..4203114575 100644 --- a/modules/jooby-openapi/src/test/java/issues/App1586.java +++ b/modules/jooby-openapi/src/test/java/issues/App1586.java @@ -5,11 +5,13 @@ */ package issues; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import examples.SubController; import io.jooby.Jooby; public class App1586 extends Jooby { { - mvc(new SubController()); + mvc(toMvcExtension(SubController.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/App1586b.java b/modules/jooby-openapi/src/test/java/issues/App1586b.java index 34e71f73a7..2ba271d389 100644 --- a/modules/jooby-openapi/src/test/java/issues/App1586b.java +++ b/modules/jooby-openapi/src/test/java/issues/App1586b.java @@ -5,11 +5,13 @@ */ package issues; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import examples.EmptySubClassController; import io.jooby.Jooby; public class App1586b extends Jooby { { - mvc(new EmptySubClassController()); + mvc(toMvcExtension(EmptySubClassController.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/App1586c.java b/modules/jooby-openapi/src/test/java/issues/App1586c.java index 9be630f4de..08bf784c38 100644 --- a/modules/jooby-openapi/src/test/java/issues/App1586c.java +++ b/modules/jooby-openapi/src/test/java/issues/App1586c.java @@ -5,11 +5,13 @@ */ package issues; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import examples.OverrideMethodSubClassController; import io.jooby.Jooby; public class App1586c extends Jooby { { - mvc(new OverrideMethodSubClassController()); + mvc(toMvcExtension(OverrideMethodSubClassController.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i1573/App1573.java b/modules/jooby-openapi/src/test/java/issues/i1573/App1573.java index c53b085316..7dcd23cdcf 100644 --- a/modules/jooby-openapi/src/test/java/issues/i1573/App1573.java +++ b/modules/jooby-openapi/src/test/java/issues/i1573/App1573.java @@ -5,6 +5,8 @@ */ package issues.i1573; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App1573 extends Jooby { @@ -15,6 +17,6 @@ public class App1573 extends Jooby { return ctx.path("id").value("self"); }); - mvc(new Controller1573()); + mvc(toMvcExtension(Controller1573.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i1580/App1580.java b/modules/jooby-openapi/src/test/java/issues/i1580/App1580.java index 022a617f8d..75a0438d78 100644 --- a/modules/jooby-openapi/src/test/java/issues/i1580/App1580.java +++ b/modules/jooby-openapi/src/test/java/issues/i1580/App1580.java @@ -5,10 +5,12 @@ */ package issues.i1580; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App1580 extends Jooby { { - mvc(Controller1580.class); + mvc(toMvcExtension(Controller1580.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i1581/App1581.java b/modules/jooby-openapi/src/test/java/issues/i1581/App1581.java index 86fa096b8b..0f768c2c06 100644 --- a/modules/jooby-openapi/src/test/java/issues/i1581/App1581.java +++ b/modules/jooby-openapi/src/test/java/issues/i1581/App1581.java @@ -5,12 +5,14 @@ */ package issues.i1581; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App1581 extends Jooby { { AppComponent dagger = DaggerAppComponent.builder().build(); - mvc(dagger.myController()); + mvc(toMvcExtension(dagger.myController())); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i1596/ClassLevelTagApp.java b/modules/jooby-openapi/src/test/java/issues/i1596/ClassLevelTagApp.java index 021ddae2a3..0d56ae3893 100644 --- a/modules/jooby-openapi/src/test/java/issues/i1596/ClassLevelTagApp.java +++ b/modules/jooby-openapi/src/test/java/issues/i1596/ClassLevelTagApp.java @@ -5,10 +5,12 @@ */ package issues.i1596; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class ClassLevelTagApp extends Jooby { { - mvc(new ClassLevelController()); + mvc(toMvcExtension(ClassLevelController.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i1601/App1601b.java b/modules/jooby-openapi/src/test/java/issues/i1601/App1601b.java index f575a4973f..a8cab673fb 100644 --- a/modules/jooby-openapi/src/test/java/issues/i1601/App1601b.java +++ b/modules/jooby-openapi/src/test/java/issues/i1601/App1601b.java @@ -5,6 +5,8 @@ */ package issues.i1601; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.info.Contact; @@ -39,6 +41,6 @@ security = @SecurityRequirement(name = "oauth", scopes = "read:write")) public class App1601b extends Jooby { { - mvc(new Controller1601()); + mvc(toMvcExtension(Controller1601.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i1768/App1768.java b/modules/jooby-openapi/src/test/java/issues/i1768/App1768.java index cc95142dcd..0888aacbd9 100644 --- a/modules/jooby-openapi/src/test/java/issues/i1768/App1768.java +++ b/modules/jooby-openapi/src/test/java/issues/i1768/App1768.java @@ -5,10 +5,12 @@ */ package issues.i1768; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App1768 extends Jooby { { - mvc(new Controller1768()); + mvc(toMvcExtension(Controller1768.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i1794/App1794.java b/modules/jooby-openapi/src/test/java/issues/i1794/App1794.java index 68cf6a11d6..8eb1451f07 100644 --- a/modules/jooby-openapi/src/test/java/issues/i1794/App1794.java +++ b/modules/jooby-openapi/src/test/java/issues/i1794/App1794.java @@ -5,11 +5,13 @@ */ package issues.i1794; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App1794 extends Jooby { { - mvc(new Controller1794()); + mvc(toMvcExtension(Controller1794.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i1795/App1795.java b/modules/jooby-openapi/src/test/java/issues/i1795/App1795.java index ade16094ab..f9699e4657 100644 --- a/modules/jooby-openapi/src/test/java/issues/i1795/App1795.java +++ b/modules/jooby-openapi/src/test/java/issues/i1795/App1795.java @@ -5,11 +5,13 @@ */ package issues.i1795; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App1795 extends Jooby { { - mvc(new Controller1795()); + mvc(toMvcExtension(Controller1795.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i1805/App1805.java b/modules/jooby-openapi/src/test/java/issues/i1805/App1805.java index c6607778de..5a57ef3fe1 100644 --- a/modules/jooby-openapi/src/test/java/issues/i1805/App1805.java +++ b/modules/jooby-openapi/src/test/java/issues/i1805/App1805.java @@ -5,10 +5,12 @@ */ package issues.i1805; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App1805 extends Jooby { { - mvc(C1805.class); + mvc(toMvcExtension(C1805.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i1855/App1855.java b/modules/jooby-openapi/src/test/java/issues/i1855/App1855.java index 0c8ae836e3..060062d183 100644 --- a/modules/jooby-openapi/src/test/java/issues/i1855/App1855.java +++ b/modules/jooby-openapi/src/test/java/issues/i1855/App1855.java @@ -5,10 +5,12 @@ */ package issues.i1855; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App1855 extends Jooby { { - mvc(new Controller1855()); + mvc(toMvcExtension(Controller1855.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i1934/App1934.java b/modules/jooby-openapi/src/test/java/issues/i1934/App1934.java index 7a6911dbf2..b622f7f642 100644 --- a/modules/jooby-openapi/src/test/java/issues/i1934/App1934.java +++ b/modules/jooby-openapi/src/test/java/issues/i1934/App1934.java @@ -5,10 +5,12 @@ */ package issues.i1934; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App1934 extends Jooby { { - mvc(new Controller1934()); + mvc(toMvcExtension(Controller1934.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i2403/App2403.java b/modules/jooby-openapi/src/test/java/issues/i2403/App2403.java index 4567c5267e..f3342f4fc3 100644 --- a/modules/jooby-openapi/src/test/java/issues/i2403/App2403.java +++ b/modules/jooby-openapi/src/test/java/issues/i2403/App2403.java @@ -5,10 +5,12 @@ */ package issues.i2403; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App2403 extends Jooby { { - mvc(Controller2403Copy.class); + mvc(toMvcExtension(Controller2403Copy.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i2505/App2505.java b/modules/jooby-openapi/src/test/java/issues/i2505/App2505.java index d62f630941..d26ea97835 100644 --- a/modules/jooby-openapi/src/test/java/issues/i2505/App2505.java +++ b/modules/jooby-openapi/src/test/java/issues/i2505/App2505.java @@ -5,10 +5,12 @@ */ package issues.i2505; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App2505 extends Jooby { { - mvc(Controller2505.class); + mvc(toMvcExtension(Controller2505.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i2542/App2542.java b/modules/jooby-openapi/src/test/java/issues/i2542/App2542.java index 2d1fcd822e..d28e8ec97d 100644 --- a/modules/jooby-openapi/src/test/java/issues/i2542/App2542.java +++ b/modules/jooby-openapi/src/test/java/issues/i2542/App2542.java @@ -5,10 +5,12 @@ */ package issues.i2542; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App2542 extends Jooby { { - mvc(Controller2542.class); + mvc(toMvcExtension(Controller2542.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i2594/App2594.java b/modules/jooby-openapi/src/test/java/issues/i2594/App2594.java index b53cb7c470..b807ef5fd5 100644 --- a/modules/jooby-openapi/src/test/java/issues/i2594/App2594.java +++ b/modules/jooby-openapi/src/test/java/issues/i2594/App2594.java @@ -5,6 +5,8 @@ */ package issues.i2594; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; import io.jooby.OpenAPIModule; @@ -12,7 +14,7 @@ public class App2594 extends Jooby { { install(new OpenAPIModule()); - mvc(HealthController2594.class); + mvc(toMvcExtension(HealthController2594.class)); mount("/api/v1", new ControllersAppV12594()); mount("/api/v2", new ControllersAppV22594()); diff --git a/modules/jooby-openapi/src/test/java/issues/i2594/ControllersAppV12594.java b/modules/jooby-openapi/src/test/java/issues/i2594/ControllersAppV12594.java index 7dbf841668..1e580438ce 100644 --- a/modules/jooby-openapi/src/test/java/issues/i2594/ControllersAppV12594.java +++ b/modules/jooby-openapi/src/test/java/issues/i2594/ControllersAppV12594.java @@ -5,11 +5,13 @@ */ package issues.i2594; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class ControllersAppV12594 extends Jooby { public ControllersAppV12594() { - mvc(ControllerV12594.class); + mvc(toMvcExtension(ControllerV12594.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i2594/ControllersAppV22594.java b/modules/jooby-openapi/src/test/java/issues/i2594/ControllersAppV22594.java index 2601d3b508..ba4b906d54 100644 --- a/modules/jooby-openapi/src/test/java/issues/i2594/ControllersAppV22594.java +++ b/modules/jooby-openapi/src/test/java/issues/i2594/ControllersAppV22594.java @@ -5,11 +5,13 @@ */ package issues.i2594; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class ControllersAppV22594 extends Jooby { public ControllersAppV22594() { - mvc(ControllerV22594.class); + mvc(toMvcExtension(ControllerV22594.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i2968/C2968_.java b/modules/jooby-openapi/src/test/java/issues/i2968/C2968_.java index f84e02f057..bcc3f192ab 100644 --- a/modules/jooby-openapi/src/test/java/issues/i2968/C2968_.java +++ b/modules/jooby-openapi/src/test/java/issues/i2968/C2968_.java @@ -10,12 +10,12 @@ import org.jetbrains.annotations.NotNull; import io.jooby.Context; +import io.jooby.Extension; import io.jooby.Jooby; -import io.jooby.MvcExtension; import io.jooby.annotation.Generated; @Generated(C2968.class) -public class C2968_ implements MvcExtension { +public class C2968_ implements Extension { private Function provider; public C2968_() { diff --git a/modules/jooby-openapi/src/test/java/issues/i3059/IndirectRunner.java b/modules/jooby-openapi/src/test/java/issues/i3059/IndirectRunner.java index 434012d8cd..d59eb7a613 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3059/IndirectRunner.java +++ b/modules/jooby-openapi/src/test/java/issues/i3059/IndirectRunner.java @@ -5,6 +5,8 @@ */ package issues.i3059; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import java.util.ArrayList; import java.util.List; @@ -35,7 +37,7 @@ public void run() { private void bindResources(Jooby jooby) { for (Object resource : resourcesToBind) { - jooby.mvc(resource); + jooby.mvc(toMvcExtension(resource)); } } } diff --git a/modules/jooby-openapi/src/test/java/issues/i3397/App3397.java b/modules/jooby-openapi/src/test/java/issues/i3397/App3397.java index 5843295043..b28c1312ec 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3397/App3397.java +++ b/modules/jooby-openapi/src/test/java/issues/i3397/App3397.java @@ -5,6 +5,8 @@ */ package issues.i3397; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.avaje.inject.BeanScope; import io.jooby.Jooby; import io.jooby.OpenAPIModule; @@ -15,6 +17,6 @@ public class App3397 extends Jooby { BeanScope beanScope = BeanScope.builder().build(); - mvc(beanScope.get(Controller3397.class)); + mvc(toMvcExtension(beanScope.get(Controller3397.class))); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i3412/App3412.java b/modules/jooby-openapi/src/test/java/issues/i3412/App3412.java index f2cc6c11bf..bb13c209a0 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3412/App3412.java +++ b/modules/jooby-openapi/src/test/java/issues/i3412/App3412.java @@ -11,6 +11,7 @@ import io.jooby.annotation.GET; import io.jooby.annotation.Path; import io.jooby.annotation.QueryParam; +import io.jooby.openapi.MvcExtensionGenerator; public class App3412 extends Jooby { @@ -26,6 +27,6 @@ public String sayHi(@QueryParam @NonNull String greeting, @QueryParam String lan { install(new OpenAPIModule()); - mvc(new Controller()); + mvc(MvcExtensionGenerator.toMvcExtension(Controller.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i3461/App3461.java b/modules/jooby-openapi/src/test/java/issues/i3461/App3461.java index 9e8cf3081c..fdfa0d6045 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3461/App3461.java +++ b/modules/jooby-openapi/src/test/java/issues/i3461/App3461.java @@ -5,6 +5,8 @@ */ package issues.i3461; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import java.util.UUID; import io.jooby.Jooby; @@ -26,6 +28,6 @@ public String getBlah( } { - mvc(new Controller()); + mvc(toMvcExtension(Controller.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i3575/App3575.java b/modules/jooby-openapi/src/test/java/issues/i3575/App3575.java index 8d8101fd92..3c5676f5c5 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3575/App3575.java +++ b/modules/jooby-openapi/src/test/java/issues/i3575/App3575.java @@ -5,6 +5,8 @@ */ package issues.i3575; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Context; import io.jooby.Jooby; import io.swagger.v3.oas.annotations.Hidden; @@ -15,7 +17,7 @@ public class App3575 extends Jooby { get("/", this::home); get("/hide-op", this::hideOp); - mvc(Controller3575.class); + mvc(toMvcExtension(Controller3575.class)); } @Operation(hidden = true) diff --git a/modules/jooby-openapi/src/test/java/issues/i3652/App3652.java b/modules/jooby-openapi/src/test/java/issues/i3652/App3652.java index e6c0f42fc7..b6a8be5291 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3652/App3652.java +++ b/modules/jooby-openapi/src/test/java/issues/i3652/App3652.java @@ -5,10 +5,12 @@ */ package issues.i3652; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App3652 extends Jooby { { - mvc(Controller3652.class); + mvc(toMvcExtension(Controller3652.class)); } } diff --git a/modules/jooby-openapi/src/test/java/issues/i3654/App3654.java b/modules/jooby-openapi/src/test/java/issues/i3654/App3654.java index 76abc7da1b..8270a05948 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3654/App3654.java +++ b/modules/jooby-openapi/src/test/java/issues/i3654/App3654.java @@ -5,10 +5,12 @@ */ package issues.i3654; +import static io.jooby.openapi.MvcExtensionGenerator.toMvcExtension; + import io.jooby.Jooby; public class App3654 extends Jooby { { - mvc(Controller3654.class); + mvc(toMvcExtension(Controller3654.class)); } } diff --git a/modules/jooby-openapi/src/test/kotlin/kt/KtMvcApp.kt b/modules/jooby-openapi/src/test/kotlin/kt/KtMvcApp.kt index 8d12866f08..2b22858c45 100644 --- a/modules/jooby-openapi/src/test/kotlin/kt/KtMvcApp.kt +++ b/modules/jooby-openapi/src/test/kotlin/kt/KtMvcApp.kt @@ -6,10 +6,6 @@ package kt import io.jooby.kt.Kooby +import io.jooby.openapi.MvcExtensionGenerator.toMvcExtension -class KtMvcApp : - Kooby({ - val provider = { KtController() } - - mvc(KtController::class, provider) - }) +class KtMvcApp : Kooby({ mvc(toMvcExtension(KtController::class.java)) }) diff --git a/modules/jooby-openapi/src/test/kotlin/kt/KtMvcAppWithRoutes.kt b/modules/jooby-openapi/src/test/kotlin/kt/KtMvcAppWithRoutes.kt index f3433dc8c1..fb19cb637c 100644 --- a/modules/jooby-openapi/src/test/kotlin/kt/KtMvcAppWithRoutes.kt +++ b/modules/jooby-openapi/src/test/kotlin/kt/KtMvcAppWithRoutes.kt @@ -6,10 +6,6 @@ package kt import io.jooby.kt.Kooby +import io.jooby.openapi.MvcExtensionGenerator.toMvcExtension -class KtMvcAppWithRoutes : - Kooby({ - val provider = { KtController() } - - routes { mvc(KtController::class, provider) } - }) +class KtMvcAppWithRoutes : Kooby({ routes { mvc(toMvcExtension(KtController::class.java)) } }) diff --git a/modules/jooby-openapi/src/test/kotlin/kt/KtMvcInstanceApp.kt b/modules/jooby-openapi/src/test/kotlin/kt/KtMvcInstanceApp.kt index 5af48e6fc7..6118a42148 100644 --- a/modules/jooby-openapi/src/test/kotlin/kt/KtMvcInstanceApp.kt +++ b/modules/jooby-openapi/src/test/kotlin/kt/KtMvcInstanceApp.kt @@ -6,5 +6,6 @@ package kt import io.jooby.kt.Kooby +import io.jooby.openapi.MvcExtensionGenerator.toMvcExtension -class KtMvcInstanceApp : Kooby({ mvc(KtController()) }) +class KtMvcInstanceApp : Kooby({ mvc(toMvcExtension(KtController::class.java)) }) diff --git a/modules/jooby-openapi/src/test/kotlin/kt/KtMvcObjectApp.kt b/modules/jooby-openapi/src/test/kotlin/kt/KtMvcObjectApp.kt index e0cda41174..5add07f0d9 100644 --- a/modules/jooby-openapi/src/test/kotlin/kt/KtMvcObjectApp.kt +++ b/modules/jooby-openapi/src/test/kotlin/kt/KtMvcObjectApp.kt @@ -6,5 +6,6 @@ package kt import io.jooby.kt.Kooby +import io.jooby.openapi.MvcExtensionGenerator.toMvcExtension -class KtMvcObjectApp : Kooby({ mvc(KtObjectController) }) +class KtMvcObjectApp : Kooby({ mvc(toMvcExtension(KtObjectController::class.java)) }) diff --git a/modules/jooby-openapi/src/test/kotlin/kt/i2121/App2121.kt b/modules/jooby-openapi/src/test/kotlin/kt/i2121/App2121.kt index da93204934..b1255a2b5b 100644 --- a/modules/jooby-openapi/src/test/kotlin/kt/i2121/App2121.kt +++ b/modules/jooby-openapi/src/test/kotlin/kt/i2121/App2121.kt @@ -6,5 +6,6 @@ package kt.i2121 import io.jooby.kt.Kooby +import io.jooby.openapi.MvcExtensionGenerator.toMvcExtension -class App2121 : Kooby({ coroutine { mvc(Controller2121()) } }) +class App2121 : Kooby({ coroutine { mvc(toMvcExtension(Controller2121::class.java)) } }) diff --git a/modules/jooby-openapi/src/test/kotlin/kt/i3217/App3217.kt b/modules/jooby-openapi/src/test/kotlin/kt/i3217/App3217.kt index ded4107226..26c3a36c53 100644 --- a/modules/jooby-openapi/src/test/kotlin/kt/i3217/App3217.kt +++ b/modules/jooby-openapi/src/test/kotlin/kt/i3217/App3217.kt @@ -7,13 +7,14 @@ package kt.i3217 import io.jooby.annotation.GET import io.jooby.kt.Kooby +import io.jooby.openapi.MvcExtensionGenerator.toMvcExtension class App3217 : Kooby({ // mount the script router mount(ScriptRouter3217()) // mount the mvc router - mvc(MvcRouter3217()) + mvc(toMvcExtension(MvcRouter3217::class.java)) }) // a router using script api diff --git a/modules/jooby-openapi/src/test/kotlin/kt/i3705/C3705.kt b/modules/jooby-openapi/src/test/kotlin/kt/i3705/C3705.kt new file mode 100644 index 0000000000..76f8a98ac8 --- /dev/null +++ b/modules/jooby-openapi/src/test/kotlin/kt/i3705/C3705.kt @@ -0,0 +1,47 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package kt.i3705 + +import io.jooby.Extension +import io.jooby.Jooby +import io.jooby.SneakyThrows +import io.jooby.annotation.GET +import io.jooby.annotation.Path +import io.jooby.kt.Kooby + +@Path("/") +class C3705 { + @GET("/search") + fun search(): String { + return "Hello" + } +} + +class BeanScope { + fun get(type: Class): T { + return type.newInstance() + } +} + +@io.jooby.annotation.Generated(C3705::class) +class C3705_ : Extension { + + constructor() {} + + constructor(provider: SneakyThrows.Function, C3705>) {} + + override fun install(application: Jooby) { + TODO("Not yet implemented") + } +} + +class App3705 : Kooby({ mvc(C3705_()) }) + +class App3705b : + Kooby({ + val beanScope = BeanScope() + mvc(C3705_(beanScope::get)) + }) diff --git a/modules/jooby-openapi/src/test/kotlin/kt/i3705/Issue3705.kt b/modules/jooby-openapi/src/test/kotlin/kt/i3705/Issue3705.kt new file mode 100644 index 0000000000..2629d952fa --- /dev/null +++ b/modules/jooby-openapi/src/test/kotlin/kt/i3705/Issue3705.kt @@ -0,0 +1,59 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package kt.i3705 + +import io.jooby.openapi.OpenAPIResult +import io.jooby.openapi.OpenAPITest +import org.junit.jupiter.api.Assertions.assertEquals + +class Issue3705 { + + @OpenAPITest(value = App3705::class) + fun shouldParseMvcExtension(result: OpenAPIResult) { + assertEquals( + "openapi: 3.0.1\n" + + "info:\n" + + " title: 3705 API\n" + + " description: 3705 API description\n" + + " version: \"1.0\"\n" + + "paths:\n" + + " /search:\n" + + " get:\n" + + " operationId: search\n" + + " responses:\n" + + " \"200\":\n" + + " description: Success\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: string\n", + result.toYaml(), + ) + } + + @OpenAPITest(value = App3705b::class) + fun shouldParseMvcExtensionBeanScope(result: OpenAPIResult) { + assertEquals( + "openapi: 3.0.1\n" + + "info:\n" + + " title: 3705b API\n" + + " description: 3705b API description\n" + + " version: \"1.0\"\n" + + "paths:\n" + + " /search:\n" + + " get:\n" + + " operationId: search\n" + + " responses:\n" + + " \"200\":\n" + + " description: Success\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: string\n", + result.toYaml(), + ) + } +} diff --git a/modules/jooby-openapi/src/test/kotlin/kt/issues/i2004/App2004.kt b/modules/jooby-openapi/src/test/kotlin/kt/issues/i2004/App2004.kt index d612f991d8..df0e3c9b8f 100644 --- a/modules/jooby-openapi/src/test/kotlin/kt/issues/i2004/App2004.kt +++ b/modules/jooby-openapi/src/test/kotlin/kt/issues/i2004/App2004.kt @@ -6,5 +6,6 @@ package kt.issues.i2004 import io.jooby.kt.Kooby +import io.jooby.openapi.MvcExtensionGenerator -class App2004 : Kooby({ mvc(Controller2004()) }) +class App2004 : Kooby({ mvc(MvcExtensionGenerator.toMvcExtension(Controller2004::class.java)) }) diff --git a/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/Pac4jSession.java b/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/Pac4jSession.java index caf14530c6..4ebde0fd54 100644 --- a/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/Pac4jSession.java +++ b/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/Pac4jSession.java @@ -12,6 +12,7 @@ import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.*; import io.jooby.pac4j.Pac4jUntrustedDataFound; +import io.jooby.value.Value; class Pac4jSession implements Session { public static final String PAC4J = "p4j~"; diff --git a/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/SessionStoreImpl.java b/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/SessionStoreImpl.java index eaf078eeca..be97790beb 100644 --- a/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/SessionStoreImpl.java +++ b/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/SessionStoreImpl.java @@ -16,7 +16,6 @@ import static io.jooby.internal.pac4j.Pac4jSession.BIN; import static io.jooby.internal.pac4j.Pac4jSession.PAC4J; -import java.io.*; import java.util.Optional; import org.pac4j.core.context.WebContext; @@ -36,8 +35,8 @@ import io.jooby.Context; import io.jooby.Session; -import io.jooby.Value; import io.jooby.pac4j.Pac4jContext; +import io.jooby.value.Value; public class SessionStoreImpl implements org.pac4j.core.context.session.SessionStore { diff --git a/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/WebContextImpl.java b/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/WebContextImpl.java index e231668092..2597a16014 100644 --- a/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/WebContextImpl.java +++ b/modules/jooby-pac4j/src/main/java/io/jooby/internal/pac4j/WebContextImpl.java @@ -20,9 +20,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SameSite; -import io.jooby.Value; import io.jooby.pac4j.Pac4jContext; import io.jooby.pac4j.Pac4jOptions; +import io.jooby.value.Value; public class WebContextImpl implements Pac4jContext { diff --git a/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleTemplateEngine.java b/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleTemplateEngine.java index cd2121ee9c..d4408dc406 100644 --- a/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleTemplateEngine.java +++ b/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleTemplateEngine.java @@ -15,7 +15,7 @@ import io.jooby.MapModelAndView; import io.jooby.ModelAndView; import io.jooby.TemplateEngine; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; import io.pebbletemplates.pebble.PebbleEngine; class PebbleTemplateEngine implements TemplateEngine { @@ -34,9 +34,9 @@ public List extensions() { } @Override - public DataBuffer render(Context ctx, ModelAndView modelAndView) throws Exception { + public Output render(Context ctx, ModelAndView modelAndView) throws Exception { if (modelAndView instanceof MapModelAndView mapModelAndView) { - var buffer = ctx.getBufferFactory().allocateBuffer(); + var buffer = ctx.getOutputFactory().allocate(); var template = engine.getTemplate(modelAndView.getView()); Map model = new HashMap<>(ctx.getAttributes()); model.putAll(mapModelAndView.getModel()); diff --git a/modules/jooby-pebble/src/test/java/io/jooby/pebble/PebbleModuleTest.java b/modules/jooby-pebble/src/test/java/io/jooby/pebble/PebbleModuleTest.java index ff81fa884b..26e0450826 100644 --- a/modules/jooby-pebble/src/test/java/io/jooby/pebble/PebbleModuleTest.java +++ b/modules/jooby-pebble/src/test/java/io/jooby/pebble/PebbleModuleTest.java @@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -21,6 +22,7 @@ import io.jooby.Environment; import io.jooby.Jooby; import io.jooby.ModelAndView; +import io.jooby.output.Output; import io.jooby.test.MockContext; import io.pebbletemplates.pebble.PebbleEngine; @@ -58,7 +60,8 @@ public void render() throws Exception { engine.render( ctx, ModelAndView.map("index.peb").put("user", new User("foo", "bar")).put("sign", "!")); - assertEquals("Hello foo bar var!", output.toString(StandardCharsets.UTF_8)); + assertEquals( + "Hello foo bar var!", StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString()); } @Test @@ -89,7 +92,8 @@ public void renderFileSystem() throws Exception { engine.render( ctx, ModelAndView.map("index.peb").put("user", new User("foo", "bar")).put("sign", "!")); - assertEquals("Hello foo bar var!", output.toString(StandardCharsets.UTF_8)); + assertEquals( + "Hello foo bar var!", StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString()); } @Test @@ -97,37 +101,31 @@ public void renderWithLocale() throws Exception { PebbleEngine.Builder builder = PebbleModule.create() .build(new Environment(getClass().getClassLoader(), ConfigFactory.empty())); - PebbleTemplateEngine engine = - new PebbleTemplateEngine(builder, Collections.singletonList(".peb")); - MockContext ctx = - new MockContext().setRouter(new Jooby().setLocales(singletonList(Locale.ENGLISH))); + var engine = new PebbleTemplateEngine(builder, List.of(".peb")); + MockContext ctx = new MockContext().setRouter(new Jooby().setLocales(List.of(Locale.ENGLISH))); - assertEquals( - "Greetings!", - engine.render(ctx, ModelAndView.map("locales.peb")).toString(StandardCharsets.UTF_8)); + assertEquals("Greetings!", toString(engine.render(ctx, ModelAndView.map("locales.peb")))); assertEquals( "Hi!", - engine - .render(ctx, ModelAndView.map("locales.peb").setLocale(new Locale("en", "GB"))) - .toString(StandardCharsets.UTF_8)); + toString( + engine.render(ctx, ModelAndView.map("locales.peb").setLocale(new Locale("en", "GB"))))); assertEquals( "Grüß Gott!", - engine - .render(ctx, ModelAndView.map("locales.peb").setLocale(Locale.GERMAN)) - .toString(StandardCharsets.UTF_8)); + toString(engine.render(ctx, ModelAndView.map("locales.peb").setLocale(Locale.GERMAN)))); assertEquals( "Grüß Gott!", - engine - .render(ctx, ModelAndView.map("locales.peb").setLocale(Locale.GERMANY)) - .toString(StandardCharsets.UTF_8)); + toString(engine.render(ctx, ModelAndView.map("locales.peb").setLocale(Locale.GERMANY)))); assertEquals( "Servus!", - engine - .render(ctx, ModelAndView.map("locales.peb").setLocale(new Locale("de", "AT"))) - .toString(StandardCharsets.UTF_8)); + toString( + engine.render(ctx, ModelAndView.map("locales.peb").setLocale(new Locale("de", "AT"))))); + } + + private String toString(Output output) { + return StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString(); } } diff --git a/modules/jooby-reactor/src/main/java/io/jooby/reactor/Reactor.java b/modules/jooby-reactor/src/main/java/io/jooby/reactor/Reactor.java index 81afd1efa2..b4dc6e15d4 100644 --- a/modules/jooby-reactor/src/main/java/io/jooby/reactor/Reactor.java +++ b/modules/jooby-reactor/src/main/java/io/jooby/reactor/Reactor.java @@ -13,7 +13,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.Route; -import io.jooby.annotation.ResultType; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -22,10 +21,6 @@ * * @author edgar */ -@ResultType( - types = {Flux.class, Mono.class}, - handler = "reactor", - nonBlocking = true) public class Reactor { private static final Route.Filter REACTOR = @@ -55,23 +50,24 @@ public Route.Handler apply(@NonNull Route.Handler next) { // Return context to mark as handled return ctx; } else if (result instanceof Mono mono) { - mono.defaultIfEmpty(ctx.getResponseCode()).subscribe( - value -> { - // fire after: - after(ctx, value, null); - // See https://github.com/jooby-project/jooby/issues/3486 - if (!ctx.isResponseStarted() && value != ctx) { + mono.defaultIfEmpty(ctx.getResponseCode()) + .subscribe( + value -> { + // fire after: + after(ctx, value, null); + // See https://github.com/jooby-project/jooby/issues/3486 + if (!ctx.isResponseStarted() && value != ctx) { - // render: - ctx.render(value); - } - }, - failure -> { - // fire after: - after(ctx, null, (Throwable) failure); - // send error: - ctx.sendError((Throwable) failure); - }); + // render: + ctx.render(value); + } + }, + failure -> { + // fire after: + after(ctx, null, (Throwable) failure); + // send error: + ctx.sendError((Throwable) failure); + }); // Return context to mark as handled return ctx; } diff --git a/modules/jooby-redis/src/main/java/io/jooby/redis/RedisModule.java b/modules/jooby-redis/src/main/java/io/jooby/redis/RedisModule.java index ef67cf3fe9..d343d14b92 100644 --- a/modules/jooby-redis/src/main/java/io/jooby/redis/RedisModule.java +++ b/modules/jooby-redis/src/main/java/io/jooby/redis/RedisModule.java @@ -5,6 +5,8 @@ */ package io.jooby.redis; +import static io.lettuce.core.support.ConnectionPoolSupport.createGenericObjectPool; + import java.util.stream.Stream; import org.apache.commons.pool2.impl.GenericObjectPool; @@ -20,7 +22,6 @@ import io.lettuce.core.RedisURI; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.pubsub.StatefulRedisPubSubConnection; -import io.lettuce.core.support.ConnectionPoolSupport; /** * Redis module: https://jooby.io/modules/redis. @@ -110,14 +111,12 @@ public void install(@NonNull Jooby application) throws Exception { StatefulRedisConnection connection = client.connect(); StatefulRedisPubSubConnection connectPubSub = client.connectPubSub(); - GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); - GenericObjectPool> pool = - ConnectionPoolSupport.createGenericObjectPool(() -> client.connect(), poolConfig); + var pool = createGenericObjectPool(client::connect, new GenericObjectPoolConfig<>()); // Close client and connection on shutdown - application.onStop(pool::close); - application.onStop(connection::close); - application.onStop(connectPubSub::close); + application.onStop(pool); + application.onStop(connection); + application.onStop(connectPubSub); application.onStop(client::shutdown); ServiceRegistry registry = application.getServices(); diff --git a/modules/jooby-redis/src/main/java/io/jooby/redis/RedisSessionStore.java b/modules/jooby-redis/src/main/java/io/jooby/redis/RedisSessionStore.java index ef38d7a1a9..227655b25a 100644 --- a/modules/jooby-redis/src/main/java/io/jooby/redis/RedisSessionStore.java +++ b/modules/jooby-redis/src/main/java/io/jooby/redis/RedisSessionStore.java @@ -20,11 +20,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.Context; -import io.jooby.Session; -import io.jooby.SessionStore; -import io.jooby.SessionToken; -import io.jooby.SneakyThrows; +import io.jooby.*; import io.lettuce.core.RedisClient; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.async.RedisAsyncCommands; @@ -42,32 +38,37 @@ public class RedisSessionStore implements SessionStore { private static final String LAST_ACCESSED_AT = "__accessed_at"; private static final String CREATED_AT = "__created_at"; - private Logger log = LoggerFactory.getLogger(getClass()); + private final Logger log = LoggerFactory.getLogger(getClass()); - private SessionToken token = SessionToken.cookieId(SessionToken.SID); + private final SessionToken token; private String namespace = "sessions"; private Duration timeout = Duration.ofMinutes(DEFAULT_TIMEOUT); - private GenericObjectPool> pool; + private final GenericObjectPool> pool; /** * Creates a new session store. * + * @param token Session token. * @param pool Redis connection pool. */ public RedisSessionStore( + @NonNull SessionToken token, @NonNull GenericObjectPool> pool) { + this.token = token; this.pool = pool; } /** * Creates a new session store. * + * @param token Session token. * @param redis Redis connection. */ - public RedisSessionStore(@NonNull RedisClient redis) { + public RedisSessionStore(@NonNull SessionToken token, @NonNull RedisClient redis) { this( + token, ConnectionPoolSupport.createGenericObjectPool( - () -> redis.connect(), new GenericObjectPoolConfig())); + redis::connect, new GenericObjectPoolConfig<>())); } /** @@ -95,7 +96,7 @@ public RedisSessionStore(@NonNull RedisClient redis) { * * @return Session timeout. Default is: 30 minutes. */ - public @NonNull Duration getTimeout() { + public @Nullable Duration getTimeout() { return timeout; } @@ -106,7 +107,7 @@ public RedisSessionStore(@NonNull RedisClient redis) { * @return This store. */ public @NonNull RedisSessionStore setTimeout(@NonNull Duration timeout) { - this.timeout = Optional.ofNullable(timeout).filter(t -> t.getSeconds() > 0).orElse(null); + this.timeout = timeout; return this; } @@ -123,23 +124,12 @@ public RedisSessionStore(@NonNull RedisClient redis) { /** * Session token. * - * @return Session token. Uses a cookie by default: {@link SessionToken#SID}. + * @return Session token. */ public @NonNull SessionToken getToken() { return token; } - /** - * Set custom session token. - * - * @param token Session token. - * @return This store. - */ - public @NonNull RedisSessionStore setToken(@NonNull SessionToken token) { - this.token = token; - return this; - } - @NonNull @Override public Session newSession(@NonNull Context ctx) { String sessionId = token.newToken(); diff --git a/modules/jooby-redoc/build.xml b/modules/jooby-redoc/build.xml index b7d2e2dbd7..d981eb525d 100644 --- a/modules/jooby-redoc/build.xml +++ b/modules/jooby-redoc/build.xml @@ -4,7 +4,8 @@ - + + diff --git a/modules/jooby-redoc/package-lock.json b/modules/jooby-redoc/package-lock.json deleted file mode 100644 index bb48056064..0000000000 --- a/modules/jooby-redoc/package-lock.json +++ /dev/null @@ -1,1099 +0,0 @@ -{ - "name": "jooby-redoc", - "version": "3.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "jooby-redoc", - "version": "3.0.0", - "license": "ASF", - "dependencies": { - "redoc": "^2.5.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", - "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", - "license": "MIT", - "peer": true - }, - "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", - "license": "MIT", - "peer": true - }, - "node_modules/@exodus/schemasafe": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", - "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==" - }, - "node_modules/@redocly/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-9GWx27t7xWhDIR02PA18nzBdLcKQRgc46xNQvjFkrYk4UOmvKhJ/dawwiX0cCOeetN5LcaaiqQbVOWYK62SGHw==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@redocly/openapi-core": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.8.2.tgz", - "integrity": "sha512-VjUz3wrqcDbO1HfEB0AUzh6Y7T1jNJR4Jmgfs0ipuoipLjU5bDsdfKJGSSz2u0WpfmqklPsd11ynkgL5Y+MlCg==", - "dependencies": { - "@redocly/ajv": "^8.11.0", - "colorette": "^1.2.0", - "js-levenshtein": "^1.1.6", - "js-yaml": "^4.1.0", - "lodash.isequal": "^4.5.0", - "minimatch": "^5.0.1", - "node-fetch": "^2.6.1", - "pluralize": "^8.0.0", - "yaml-ast-parser": "0.0.43" - }, - "engines": { - "node": ">=14.19.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "node_modules/@types/stylis": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", - "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", - "license": "MIT", - "peer": true - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" - }, - "node_modules/camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" - }, - "node_modules/core-js": { - "version": "3.41.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.41.0.tgz", - "integrity": "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==", - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/css-to-react-native": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", - "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true - }, - "node_modules/decko": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decko/-/decko-1.2.0.tgz", - "integrity": "sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==" - }, - "node_modules/dompurify": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz", - "integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==" - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" - }, - "node_modules/foreach": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", - "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==" - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/http2-client": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", - "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/js-levenshtein": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-pointer": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", - "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", - "dependencies": { - "foreach": "^2.0.4" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead." - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" - }, - "node_modules/mark.js": { - "version": "8.11.1", - "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", - "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==" - }, - "node_modules/marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mobx": { - "version": "6.13.7", - "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz", - "integrity": "sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mobx" - } - }, - "node_modules/mobx-react": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-9.1.1.tgz", - "integrity": "sha512-gVV7AdSrAAxqXOJ2bAbGa5TkPqvITSzaPiiEkzpW4rRsMhSec7C2NBCJYILADHKp2tzOAIETGRsIY0UaCV5aEw==", - "dependencies": { - "mobx-react-lite": "^4.0.7" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mobx" - }, - "peerDependencies": { - "mobx": "^6.9.0", - "react": "^16.8.0 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/mobx-react-lite": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.0.7.tgz", - "integrity": "sha512-RjwdseshK9Mg8On5tyJZHtGD+J78ZnCnRaxeQDSiciKVQDUbfZcXhmld0VMxAwvcTnPEHZySGGewm467Fcpreg==", - "dependencies": { - "use-sync-external-store": "^1.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mobx" - }, - "peerDependencies": { - "mobx": "^6.9.0", - "react": "^16.8.0 || ^17 || ^18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch-h2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", - "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", - "dependencies": { - "http2-client": "^1.2.5" - }, - "engines": { - "node": "4.x || >=6.0.0" - } - }, - "node_modules/node-readfiles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", - "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", - "dependencies": { - "es6-promise": "^3.2.1" - } - }, - "node_modules/oas-kit-common": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", - "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", - "dependencies": { - "fast-safe-stringify": "^2.0.7" - } - }, - "node_modules/oas-linter": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", - "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", - "dependencies": { - "@exodus/schemasafe": "^1.0.0-rc.2", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-resolver": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", - "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", - "dependencies": { - "node-fetch-h2": "^2.3.0", - "oas-kit-common": "^1.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "resolve": "resolve.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-schema-walker": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", - "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-validator": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", - "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", - "dependencies": { - "call-me-maybe": "^1.0.1", - "oas-kit-common": "^1.0.8", - "oas-linter": "^3.2.2", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "reftools": "^1.1.9", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/openapi-sampler": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.5.1.tgz", - "integrity": "sha512-tIWIrZUKNAsbqf3bd9U1oH6JEXo8LNYuDlXw26By67EygpjT+ArFnsxxyTMjFWRfbqo5ozkvgSQDK69Gd8CddA==", - "dependencies": { - "@types/json-schema": "^7.0.7", - "json-pointer": "0.6.2" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" - }, - "node_modules/perfect-scrollbar": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-1.5.5.tgz", - "integrity": "sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g==" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC", - "peer": true - }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/polished": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", - "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", - "dependencies": { - "@babel/runtime": "^7.17.8" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT", - "peer": true - }, - "node_modules/prismjs": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-tabs": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz", - "integrity": "sha512-aQXTKolnM28k3KguGDBSAbJvcowOQr23A+CUJdzJtOSDOtTwzEaJA+1U4KwhNL9+Obe+jFS7geuvA7ICQPXOnQ==", - "dependencies": { - "clsx": "^2.0.0", - "prop-types": "^15.5.0" - }, - "peerDependencies": { - "react": "^18.0.0" - } - }, - "node_modules/redoc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.5.0.tgz", - "integrity": "sha512-NpYsOZ1PD9qFdjbLVBZJWptqE+4Y6TkUuvEOqPUmoH7AKOmPcE+hYjotLxQNTqVoWL4z0T2uxILmcc8JGDci+Q==", - "license": "MIT", - "dependencies": { - "@redocly/openapi-core": "^1.4.0", - "classnames": "^2.3.2", - "decko": "^1.2.0", - "dompurify": "^3.2.4", - "eventemitter3": "^5.0.1", - "json-pointer": "^0.6.2", - "lunr": "^2.3.9", - "mark.js": "^8.11.1", - "marked": "^4.3.0", - "mobx-react": "^9.1.1", - "openapi-sampler": "^1.5.0", - "path-browserify": "^1.0.1", - "perfect-scrollbar": "^1.5.5", - "polished": "^4.2.2", - "prismjs": "^1.29.0", - "prop-types": "^15.8.1", - "react-tabs": "^6.0.2", - "slugify": "~1.4.7", - "stickyfill": "^1.1.1", - "swagger2openapi": "^7.0.8", - "url-template": "^2.0.8" - }, - "engines": { - "node": ">=6.9", - "npm": ">=3.0.0" - }, - "peerDependencies": { - "core-js": "^3.1.4", - "mobx": "^6.0.4", - "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "styled-components": "^4.1.1 || ^5.1.1 || ^6.0.5" - } - }, - "node_modules/reftools": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", - "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", - "license": "MIT", - "peer": true - }, - "node_modules/should": { - "version": "13.2.3", - "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", - "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", - "dependencies": { - "should-equal": "^2.0.0", - "should-format": "^3.0.3", - "should-type": "^1.4.0", - "should-type-adaptors": "^1.0.1", - "should-util": "^1.0.0" - } - }, - "node_modules/should-equal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", - "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", - "dependencies": { - "should-type": "^1.4.0" - } - }, - "node_modules/should-format": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", - "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", - "dependencies": { - "should-type": "^1.3.0", - "should-type-adaptors": "^1.0.1" - } - }, - "node_modules/should-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", - "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==" - }, - "node_modules/should-type-adaptors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", - "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", - "dependencies": { - "should-type": "^1.3.0", - "should-util": "^1.0.0" - } - }, - "node_modules/should-util": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", - "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==" - }, - "node_modules/slugify": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.4.7.tgz", - "integrity": "sha512-tf+h5W1IrjNm/9rKKj0JU2MDMruiopx0jjVA5zCdBtcGjfp0+c5rHw/zADLC3IeKlGHtVbHtpfzvYA0OYT+HKg==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stickyfill": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stickyfill/-/stickyfill-1.1.1.tgz", - "integrity": "sha512-GCp7vHAfpao+Qh/3Flh9DXEJ/qSi0KJwJw6zYlZOtRYXWUIpMM6mC2rIep/dK8RQqwW0KxGJIllmjPIBOGN8AA==" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/styled-components": { - "version": "6.1.16", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.16.tgz", - "integrity": "sha512-KpWB6ORAWGmbWM10cDJfEV6sXc/uVkkkQV3SLwTNQ/E/PqWgNHIoMSLh1Lnk2FkB9+JHK7uuMq1i+9ArxDD7iQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@emotion/is-prop-valid": "1.2.2", - "@emotion/unitless": "0.8.1", - "@types/stylis": "4.2.5", - "css-to-react-native": "3.2.0", - "csstype": "3.1.3", - "postcss": "8.4.49", - "shallowequal": "1.1.0", - "stylis": "4.3.2", - "tslib": "2.6.2" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0" - } - }, - "node_modules/stylis": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", - "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", - "license": "MIT", - "peer": true - }, - "node_modules/swagger2openapi": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", - "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", - "dependencies": { - "call-me-maybe": "^1.0.1", - "node-fetch": "^2.6.1", - "node-fetch-h2": "^2.3.0", - "node-readfiles": "^0.2.0", - "oas-kit-common": "^1.0.8", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "oas-validator": "^5.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "boast": "boast.js", - "oas-validate": "oas-validate.js", - "swagger2openapi": "swagger2openapi.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "license": "0BSD", - "peer": true - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-template": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" - }, - "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yaml-ast-parser": { - "version": "0.0.43", - "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", - "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } - } - } -} diff --git a/modules/jooby-redoc/package.json b/modules/jooby-redoc/package.json deleted file mode 100644 index 83b72c57a5..0000000000 --- a/modules/jooby-redoc/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "jooby-redoc", - "version": "3.0.0", - "private": true, - "license": "ASF", - "dependencies": { - "redoc": "^2.5.0" - } -} diff --git a/modules/jooby-redoc/pom.xml b/modules/jooby-redoc/pom.xml index 473d9ea04a..85d09b29bf 100644 --- a/modules/jooby-redoc/pom.xml +++ b/modules/jooby-redoc/pom.xml @@ -39,28 +39,6 @@ - - com.github.eirslett - frontend-maven-plugin - - ${node.version} - - - - - install-node-and-npm - - generate-resources - - - npm install - - npm - - generate-resources - - - org.apache.maven.plugins maven-antrun-plugin @@ -81,34 +59,6 @@ - - org.apache.maven.plugins - maven-shade-plugin - - - fat-jar - - shade - - package - - true - - - commons-io:* - - - - - org.apache.commons - io.jooby.internal.commons - - - - - - - org.apache.maven.plugins maven-jar-plugin diff --git a/modules/jooby-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java b/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java similarity index 58% rename from modules/jooby-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java rename to modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java index dccfb4e345..ab31ef13fa 100644 --- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java +++ b/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java @@ -10,15 +10,16 @@ import com.fizzed.rocker.ContentType; import com.fizzed.rocker.RockerOutput; import com.fizzed.rocker.RockerOutputFactory; -import io.jooby.buffer.DataBuffer; -import io.jooby.buffer.DataBufferFactory; +import io.jooby.output.BufferedOutput; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; /** * Rocker output that uses a byte array to render the output. * * @author edgar */ -public class DataBufferOutput implements RockerOutput { +public class BufferedRockerOutput implements RockerOutput { /** Default buffer size: 4k. */ public static final int BUFFER_SIZE = 4096; @@ -27,12 +28,12 @@ public class DataBufferOutput implements RockerOutput { private final ContentType contentType; /** The buffer where data is stored. */ - protected DataBuffer buffer; + protected BufferedOutput output; - DataBufferOutput(Charset charset, ContentType contentType, DataBuffer buffer) { + BufferedRockerOutput(Charset charset, ContentType contentType, BufferedOutput output) { this.charset = charset; this.contentType = contentType; - this.buffer = buffer; + this.output = output; } @Override @@ -46,20 +47,20 @@ public Charset getCharset() { } @Override - public DataBufferOutput w(String string) { - buffer.write(string, getCharset()); + public BufferedRockerOutput w(String string) { + output.write(string, getCharset()); return this; } @Override - public DataBufferOutput w(byte[] bytes) { - buffer.write(bytes); + public BufferedRockerOutput w(byte[] bytes) { + output.write(bytes); return this; } @Override public int getByteLength() { - return buffer.readableByteCount(); + return output.size(); } /** @@ -67,13 +68,12 @@ public int getByteLength() { * * @return Byte buffer. */ - public DataBuffer toBuffer() { - return buffer; + public Output asOutput() { + return output; } - static RockerOutputFactory factory( - Charset charset, DataBufferFactory factory, int bufferSize) { + static RockerOutputFactory factory(Charset charset, OutputFactory factory) { return (contentType, charsetName) -> - new DataBufferOutput(charset, contentType, factory.allocateBuffer(bufferSize)); + new BufferedRockerOutput(charset, contentType, factory.newComposite()); } } diff --git a/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerHandler.java b/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerHandler.java deleted file mode 100644 index 5d9caf922f..0000000000 --- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.rocker; - -import com.fizzed.rocker.RockerModel; -import com.fizzed.rocker.RockerOutputFactory; -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.MediaType; -import io.jooby.Route; - -class RockerHandler implements Route.Filter { - private final RockerOutputFactory factory; - - RockerHandler(RockerOutputFactory factory) { - this.factory = factory; - } - - @NonNull @Override - public Route.Handler apply(@NonNull Route.Handler next) { - return ctx -> { - try { - RockerModel template = (RockerModel) next.apply(ctx); - ctx.setResponseType(MediaType.html); - return ctx.send(template.render(factory).toBuffer()); - } catch (Throwable x) { - ctx.sendError(x); - return x; - } - }; - } -} diff --git a/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerMessageEncoder.java b/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerMessageEncoder.java index cefed53f62..841ebd49a2 100644 --- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerMessageEncoder.java +++ b/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerMessageEncoder.java @@ -11,22 +11,22 @@ import io.jooby.Context; import io.jooby.MediaType; import io.jooby.MessageEncoder; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; class RockerMessageEncoder implements MessageEncoder { - private final RockerOutputFactory factory; + private final RockerOutputFactory factory; - RockerMessageEncoder(RockerOutputFactory factory) { + RockerMessageEncoder(RockerOutputFactory factory) { this.factory = factory; } @Override - public DataBuffer encode(@NonNull Context ctx, @NonNull Object value) { + public Output encode(@NonNull Context ctx, @NonNull Object value) { if (value instanceof RockerModel template) { var output = template.render(factory); ctx.setResponseLength(output.getByteLength()); ctx.setDefaultResponseType(MediaType.html); - return output.toBuffer(); + return output.asOutput(); } return null; } diff --git a/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerModule.java b/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerModule.java index 26798bba23..ed500543e3 100644 --- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerModule.java +++ b/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerModule.java @@ -24,16 +24,10 @@ */ public class RockerModule implements Extension { private Boolean reloading; - private int bufferSize; private final Charset charset; - public RockerModule(@NonNull Charset charset, int bufferSize) { - this.charset = charset; - this.bufferSize = bufferSize; - } - public RockerModule(@NonNull Charset charset) { - this(charset, DataBufferOutput.BUFFER_SIZE); + this.charset = charset; } public RockerModule() { @@ -51,44 +45,6 @@ public RockerModule() { return this; } - /** - * Configure buffer size to use while rendering. The buffer can grow ups when need it, so this - * option works as a hint to allocate initial memory. - * - * @param bufferSize Buffer size. - * @return This module. - * @deprecated Use {@link #bufferSize} - */ - @Deprecated(forRemoval = true) - public @NonNull RockerModule useBuffer(int bufferSize) { - return bufferSize(bufferSize); - } - - /** - * Configure buffer size to use while rendering. The buffer can grow ups when need it, so this - * option works as a hint to allocate initial memory. - * - * @param bufferSize Buffer size. - * @return This module. - */ - public @NonNull RockerModule bufferSize(int bufferSize) { - this.bufferSize = bufferSize; - return this; - } - - /** - * Allow simple reuse of raw byte buffers. It is usually used through ThreadLocal - * variable pointing to instance of {@link DataBufferOutput}. - * - * @param reuseBuffer True for reuse the buffer. Default is: false - * @return This module. - * @deprecated - */ - @Deprecated(forRemoval = true) - public RockerModule reuseBuffer(boolean reuseBuffer) { - return this; - } - @Override public void install(@NonNull Jooby application) { var env = application.getEnvironment(); @@ -97,7 +53,7 @@ public void install(@NonNull Jooby application) { this.reloading == null ? (env.isActive("dev") && runtime.isReloadingPossible()) : this.reloading; - var factory = DataBufferOutput.factory(charset, application.getBufferFactory(), bufferSize); + var factory = BufferedRockerOutput.factory(charset, application.getOutputFactory()); runtime.setReloading(reloading); // renderer application.encoder(new RockerMessageEncoder(factory)); diff --git a/modules/jooby-rxjava3/src/main/java/io/jooby/rxjava3/Reactivex.java b/modules/jooby-rxjava3/src/main/java/io/jooby/rxjava3/Reactivex.java index 99508d0073..767f108f90 100644 --- a/modules/jooby-rxjava3/src/main/java/io/jooby/rxjava3/Reactivex.java +++ b/modules/jooby-rxjava3/src/main/java/io/jooby/rxjava3/Reactivex.java @@ -10,7 +10,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Route; -import io.jooby.annotation.ResultType; import io.jooby.internal.rxjava3.RxObserver; import io.jooby.internal.rxjava3.RxSubscriber; import io.reactivex.rxjava3.core.Flowable; @@ -24,10 +23,6 @@ * * @author edgar */ -@ResultType( - types = {Flowable.class, Single.class, Observable.class, Maybe.class, Disposable.class}, - handler = "rx", - nonBlocking = true) public class Reactivex { private static final Route.Filter RX = diff --git a/modules/jooby-test/src/main/java/io/jooby/test/JoobyExtension.java b/modules/jooby-test/src/main/java/io/jooby/test/JoobyExtension.java index eedbe3a646..d5a175518f 100644 --- a/modules/jooby-test/src/main/java/io/jooby/test/JoobyExtension.java +++ b/modules/jooby-test/src/main/java/io/jooby/test/JoobyExtension.java @@ -31,7 +31,6 @@ import io.jooby.Environment; import io.jooby.Jooby; import io.jooby.Server; -import io.jooby.ServerOptions; import io.jooby.SneakyThrows; /** @@ -70,12 +69,16 @@ public void beforeAll(ExtensionContext context) throws Exception { } private Jooby startApp(ExtensionContext context, JoobyTest metadata) throws Exception { + var server = Server.loadServer(); + var serverOptions = server.getOptions(); + serverOptions.setPort(port(metadata.port(), DEFAULT_PORT)); + server.setOptions(serverOptions); Jooby app; String factoryMethod = metadata.factoryMethod(); if (factoryMethod.isEmpty()) { var defaultEnv = System.getProperty("application.env"); System.setProperty("application.env", metadata.environment()); - app = Jooby.createApp(metadata.executionMode(), reflectionProvider(metadata.value())); + app = Jooby.createApp(server, metadata.executionMode(), reflectionProvider(metadata.value())); if (defaultEnv != null) { System.setProperty("application.env", defaultEnv); } else { @@ -84,13 +87,6 @@ private Jooby startApp(ExtensionContext context, JoobyTest metadata) throws Exce } else { app = fromFactoryMethod(context, metadata, factoryMethod); } - var server = Server.loadServer(); - ServerOptions serverOptions = app.getServerOptions(); - if (serverOptions == null) { - serverOptions = server.getOptions(); - } - serverOptions.setPort(port(metadata.port(), DEFAULT_PORT)); - server.setOptions(serverOptions); server.start(app); ExtensionContext.Store store = getStore(context); store.put("server", server); @@ -236,8 +232,7 @@ private int port(int port, int fallback) { public void postProcessTestInstance(Object instance, ExtensionContext context) throws Exception { for (Field field : instance.getClass().getDeclaredFields()) { if (!Modifier.isStatic(field.getModifiers())) { - Supplier injectionPoint = - injectionPoint(context, field.getType(), () -> field.getName()); + Supplier injectionPoint = injectionPoint(context, field.getType(), field::getName); if (injectionPoint != null) { field.setAccessible(true); field.set(instance, injectionPoint.get()); diff --git a/modules/jooby-test/src/main/java/io/jooby/test/MockContext.java b/modules/jooby-test/src/main/java/io/jooby/test/MockContext.java index 0a96be696b..a305f2e388 100644 --- a/modules/jooby-test/src/main/java/io/jooby/test/MockContext.java +++ b/modules/jooby-test/src/main/java/io/jooby/test/MockContext.java @@ -52,13 +52,13 @@ import io.jooby.ServerSentEmitter; import io.jooby.Session; import io.jooby.StatusCode; -import io.jooby.Value; -import io.jooby.ValueNode; import io.jooby.WebSocket; -import io.jooby.buffer.DataBuffer; -import io.jooby.buffer.DataBufferFactory; -import io.jooby.buffer.DefaultDataBufferFactory; import io.jooby.exception.TypeMismatchException; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; /** Unit test friendly context implementation. Allows to set context properties. */ public class MockContext implements DefaultContext { @@ -75,7 +75,9 @@ public class MockContext implements DefaultContext { private Map> headers = new HashMap<>(); - private Formdata formdata = Formdata.create(this); + private ValueFactory valueFactory = new ValueFactory(); + + private Formdata formdata = Formdata.create(valueFactory); private Body body; @@ -117,14 +119,14 @@ public class MockContext implements DefaultContext { private int port = -1; - private DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); + private OutputFactory outputFactory = OutputFactory.create(OutputOptions.small()); - @NonNull @Override + @Override public String getMethod() { return method; } - @NonNull @Override + @Override public Context setPort(int port) { this.port = port; return this; @@ -135,9 +137,9 @@ public int getPort() { return port; } - @NonNull @Override - public DataBufferFactory getBufferFactory() { - return bufferFactory; + @Override + public OutputFactory getOutputFactory() { + return outputFactory; } /** @@ -147,13 +149,13 @@ public DataBufferFactory getBufferFactory() { * @return This context. */ @Override - public @NonNull MockContext setMethod(@NonNull String method) { + public MockContext setMethod(@NonNull String method) { this.method = method.toUpperCase(); return this; } @Override - public @NonNull Session session() { + public Session session() { if (session == null) { session = new MockSession(this); } @@ -166,7 +168,7 @@ public DataBufferFactory getBufferFactory() { * @param session Mock session. * @return This context. */ - public @NonNull MockContext setSession(@NonNull MockSession session) { + public MockContext setSession(@NonNull MockSession session) { this.session = session; return this; } @@ -176,12 +178,12 @@ public Session sessionOrNull() { return session; } - @NonNull @Override + @Override public Map cookieMap() { return cookies; } - @NonNull @Override + @Override public Object forward(@NonNull String path) { setRequestPath(path); if (mockRouter != null) { @@ -196,12 +198,12 @@ public Object forward(@NonNull String path) { * @param cookies Cookie map. * @return This context. */ - @NonNull public MockContext setCookieMap(@NonNull Map cookies) { + public MockContext setCookieMap(@NonNull Map cookies) { this.cookies = cookies; return this; } - @NonNull @Override + @Override public FlashMap flash() { return flashMap; } @@ -224,23 +226,23 @@ public MockContext setFlashMap(@NonNull FlashMap flashMap) { * @param value Flash value. * @return This context. */ - @NonNull public MockContext setFlashAttribute(@NonNull String name, @NonNull String value) { + public MockContext setFlashAttribute(@NonNull String name, @NonNull String value) { flashMap.put(name, value); return this; } - @NonNull @Override + @Override public Route getRoute() { return route; } - @NonNull @Override + @Override public MockContext setRoute(@NonNull Route route) { this.route = route; return this; } - @NonNull @Override + @Override public String getRequestPath() { return requestPath; } @@ -252,7 +254,7 @@ public String getRequestPath() { * @return This context. */ @Override - public @NonNull MockContext setRequestPath(@NonNull String pathString) { + public MockContext setRequestPath(@NonNull String pathString) { int q = pathString.indexOf("?"); if (q > 0) { this.requestPath = pathString.substring(0, q); @@ -262,23 +264,23 @@ public String getRequestPath() { return this; } - @NonNull @Override + @Override public Map pathMap() { return pathMap; } - @NonNull @Override + @Override public MockContext setPathMap(@NonNull Map pathMap) { this.pathMap = pathMap; return this; } - @NonNull @Override + @Override public QueryString query() { - return QueryString.create(this, queryString); + return QueryString.create(valueFactory, queryString); } - @NonNull @Override + @Override public String queryString() { return queryString; } @@ -289,14 +291,14 @@ public String queryString() { * @param queryString Query string (starting with ?). * @return This context. */ - public @NonNull MockContext setQueryString(@NonNull String queryString) { + public MockContext setQueryString(@NonNull String queryString) { this.queryString = queryString; return this; } - @NonNull @Override - public ValueNode header() { - return Value.headers(this, headers); + @Override + public Value header() { + return Value.headers(valueFactory, headers); } /** @@ -305,7 +307,7 @@ public ValueNode header() { * @param headers Request headers. * @return This context. */ - @NonNull public MockContext setHeaders(@NonNull Map> headers) { + public MockContext setHeaders(@NonNull Map> headers) { this.headers = headers; return this; } @@ -317,18 +319,18 @@ public ValueNode header() { * @param value Request value. * @return This context. */ - @NonNull public MockContext setRequestHeader(@NonNull String name, @NonNull String value) { + public MockContext setRequestHeader(@NonNull String name, @NonNull String value) { Collection values = this.headers.computeIfAbsent(name, k -> new ArrayList<>()); values.add(value); return this; } - @NonNull @Override + @Override public Formdata form() { return formdata; } - @NonNull @Override + @Override public List files() { return files.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); } @@ -345,7 +347,7 @@ public MockContext setFile(@NonNull String name, @NonNull FileUpload file) { return this; } - @NonNull @Override + @Override public List files(@NonNull String name) { return files.entrySet().stream() .filter(it -> it.getKey().equals(name)) @@ -353,7 +355,7 @@ public List files(@NonNull String name) { .collect(Collectors.toList()); } - @NonNull @Override + @Override public FileUpload file(@NonNull String name) { return files.entrySet().stream() .filter(it -> it.getKey().equals(name)) @@ -368,12 +370,12 @@ public FileUpload file(@NonNull String name) { * @param formdata Form. * @return This context. */ - @NonNull public MockContext setForm(@NonNull Formdata formdata) { + public MockContext setForm(@NonNull Formdata formdata) { this.formdata = formdata; return this; } - @NonNull @Override + @Override public Body body() { if (body == null) { throw new IllegalStateException("No body was set, use setBody() to set one."); @@ -381,17 +383,17 @@ public Body body() { return body; } - @NonNull @Override + @Override public T body(@NonNull Class type) { return decode(type, MediaType.text); } - @NonNull @Override + @Override public T body(@NonNull Type type) { return decode(type, MediaType.text); } - @NonNull @Override + @Override public T decode(@NonNull Type type, @NonNull MediaType contentType) { if (bodyObject == null) { throw new IllegalStateException("No body was set, use setBodyObject() to set one."); @@ -409,7 +411,7 @@ public T decode(@NonNull Type type, @NonNull MediaType contentType) { * @param body Request body. * @return This context. */ - @NonNull public MockContext setBody(@NonNull Body body) { + public MockContext setBody(@NonNull Body body) { this.body = body; return this; } @@ -420,7 +422,7 @@ public T decode(@NonNull Type type, @NonNull MediaType contentType) { * @param body Request body. * @return This context. */ - @NonNull public MockContext setBodyObject(@NonNull Object body) { + public MockContext setBodyObject(@NonNull Object body) { this.bodyObject = body; return this; } @@ -431,7 +433,7 @@ public T decode(@NonNull Type type, @NonNull MediaType contentType) { * @param body Request body. * @return This context. */ - @NonNull public MockContext setBody(@NonNull String body) { + public MockContext setBody(@NonNull String body) { byte[] bytes = body.getBytes(StandardCharsets.UTF_8); return setBody(bytes); } @@ -442,12 +444,12 @@ public T decode(@NonNull Type type, @NonNull MediaType contentType) { * @param body Request body. * @return This context. */ - @NonNull public MockContext setBody(@NonNull byte[] body) { + public MockContext setBody(@NonNull byte[] body) { setBody(Body.of(this, new ByteArrayInputStream(body), body.length)); return this; } - @NonNull @Override + @Override public MessageDecoder decoder(@NonNull MediaType contentType) { return decoders.getOrDefault(contentType, MessageDecoder.UNSUPPORTED_MEDIA_TYPE); } @@ -457,30 +459,30 @@ public boolean isInIoThread() { return false; } - @NonNull @Override + @Override public MockContext dispatch(@NonNull Runnable action) { action.run(); return this; } - @NonNull @Override + @Override public MockContext dispatch(@NonNull Executor executor, @NonNull Runnable action) { action.run(); return this; } - @NonNull @Override + @Override public MockContext detach(@NonNull Route.Handler next) throws Exception { next.apply(this); return this; } - @NonNull @Override + @Override public Map getAttributes() { return attributes; } - @NonNull @Override + @Override public MockContext removeResponseHeader(@NonNull String name) { responseHeaders.remove(name); return this; @@ -492,13 +494,13 @@ public String getResponseHeader(@NonNull String name) { return value == null ? null : value.toString(); } - @NonNull @Override + @Override public MockContext setResponseHeader(@NonNull String name, @NonNull String value) { responseHeaders.put(name, value); return this; } - @NonNull @Override + @Override public MockContext setResponseLength(long length) { response.setContentLength(length); return this; @@ -509,30 +511,30 @@ public long getResponseLength() { return response.getContentLength(); } - @NonNull @Override + @Override public MockContext setResponseType(@NonNull String contentType) { response.setContentType(MediaType.valueOf(contentType)); return this; } - @NonNull @Override + @Override public MockContext setResponseType(@NonNull MediaType contentType, @Nullable Charset charset) { response.setContentType(contentType); return this; } - @NonNull @Override + @Override public MockContext setResponseCode(int statusCode) { response.setStatusCode(StatusCode.valueOf(statusCode)); return this; } - @NonNull @Override + @Override public StatusCode getResponseCode() { return response.getStatusCode(); } - @NonNull @Override + @Override public MockContext render(@NonNull Object result) { responseStarted = true; this.response.setResult(result); @@ -544,12 +546,12 @@ public MockContext render(@NonNull Object result) { * * @return Mock response. */ - @NonNull public MockResponse getResponse() { + public MockResponse getResponse() { response.setHeaders(responseHeaders); return response; } - @NonNull @Override + @Override public OutputStream responseStream() { responseStarted = true; ByteArrayOutputStream out = new ByteArrayOutputStream(ServerOptions._16KB); @@ -557,7 +559,7 @@ public OutputStream responseStream() { return out; } - @NonNull @Override + @Override public Sender responseSender() { responseStarted = true; return new Sender() { @@ -569,8 +571,8 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { - response.setResult(data); + public Sender write(@NonNull Output output, @NonNull Callback callback) { + response.setResult(output); callback.onComplete(MockContext.this, null); return this; } @@ -582,50 +584,50 @@ public void close() { }; } - @NonNull @Override + @Override public String getHost() { return host; } - @NonNull @Override + @Override public Context setHost(@NonNull String host) { this.host = host; return this; } - @NonNull @Override + @Override public String getRemoteAddress() { return remoteAddress; } - @NonNull @Override + @Override public Context setRemoteAddress(@NonNull String remoteAddress) { this.remoteAddress = remoteAddress; return this; } - @NonNull @Override + @Override public String getProtocol() { return "HTTP/1.1"; } - @NonNull @Override + @Override public List getClientCertificates() { return new ArrayList(); } - @NonNull @Override + @Override public String getScheme() { return scheme; } - @NonNull @Override + @Override public Context setScheme(@NonNull String scheme) { this.scheme = scheme; return this; } - @NonNull @Override + @Override public PrintWriter responseWriter(MediaType type, Charset charset) { responseStarted = true; PrintWriter writer = new PrintWriter(new StringWriter()); @@ -633,7 +635,7 @@ public PrintWriter responseWriter(MediaType type, Charset charset) { return writer; } - @NonNull @Override + @Override public MockContext send(@NonNull String data, @NonNull Charset charset) { responseStarted = true; this.response.setResult(data).setContentLength(data.length()); @@ -641,7 +643,7 @@ public MockContext send(@NonNull String data, @NonNull Charset charset) { return this; } - @NonNull @Override + @Override public MockContext send(@NonNull byte[] data) { responseStarted = true; this.response.setResult(data).setContentLength(data.length); @@ -649,7 +651,7 @@ public MockContext send(@NonNull byte[] data) { return this; } - @NonNull @Override + @Override public MockContext send(@NonNull byte[]... data) { responseStarted = true; this.response @@ -659,7 +661,7 @@ public MockContext send(@NonNull byte[]... data) { return this; } - @NonNull @Override + @Override public MockContext send(@NonNull ByteBuffer data) { responseStarted = true; this.response.setResult(data).setContentLength(data.remaining()); @@ -667,15 +669,15 @@ public MockContext send(@NonNull ByteBuffer data) { return this; } - @NonNull @Override - public Context send(@NonNull DataBuffer data) { + @Override + public Context send(@NonNull Output output) { responseStarted = true; - this.response.setResult(data).setContentLength(data.readableByteCount()); + this.response.setResult(output).setContentLength(output.size()); listeners.run(this); return this; } - @NonNull @Override + @Override public Context send(@NonNull ByteBuffer[] data) { responseStarted = true; this.response @@ -685,7 +687,7 @@ public Context send(@NonNull ByteBuffer[] data) { return this; } - @NonNull @Override + @Override public MockContext send(InputStream input) { responseStarted = true; this.response.setResult(input); @@ -693,7 +695,7 @@ public MockContext send(InputStream input) { return this; } - @NonNull @Override + @Override public Context send(@NonNull FileDownload file) { responseStarted = true; this.response.setResult(file); @@ -701,7 +703,7 @@ public Context send(@NonNull FileDownload file) { return this; } - @NonNull @Override + @Override public Context send(@NonNull Path file) { responseStarted = true; this.response.setResult(file); @@ -709,7 +711,7 @@ public Context send(@NonNull Path file) { return this; } - @NonNull @Override + @Override public MockContext send(@NonNull ReadableByteChannel channel) { responseStarted = true; this.response.setResult(channel); @@ -717,7 +719,7 @@ public MockContext send(@NonNull ReadableByteChannel channel) { return this; } - @NonNull @Override + @Override public MockContext send(@NonNull FileChannel file) { responseStarted = true; this.response.setResult(file); @@ -725,19 +727,19 @@ public MockContext send(@NonNull FileChannel file) { return this; } - @NonNull @Override + @Override public MockContext send(StatusCode statusCode) { responseStarted = true; this.response.setContentLength(0).setStatusCode(statusCode); return this; } - @NonNull @Override + @Override public MockContext sendError(@NonNull Throwable cause) { return sendError(cause, router.errorCode(cause)); } - @NonNull @Override + @Override public MockContext sendError(@NonNull Throwable cause, @NonNull StatusCode code) { responseStarted = true; this.response.setResult(cause).setStatusCode(router.errorCode(cause)); @@ -745,13 +747,13 @@ public MockContext sendError(@NonNull Throwable cause, @NonNull StatusCode code) return this; } - @NonNull @Override + @Override public MockContext setDefaultResponseType(@NonNull MediaType contentType) { response.setContentType(contentType); return this; } - @NonNull @Override + @Override public MockContext setResponseCookie(@NonNull Cookie cookie) { String setCookie = (String) response.getHeaders().get("Set-Cookie"); if (setCookie == null) { @@ -763,12 +765,12 @@ public MockContext setResponseCookie(@NonNull Cookie cookie) { return this; } - @NonNull @Override + @Override public MediaType getResponseType() { return response.getContentType(); } - @NonNull @Override + @Override public MockContext setResponseCode(@NonNull StatusCode statusCode) { response.setStatusCode(statusCode); return this; @@ -790,13 +792,13 @@ public MockContext setResetHeadersOnError(boolean resetHeadersOnError) { return this; } - @NonNull @Override + @Override public Context removeResponseHeaders() { responseHeaders.clear(); return this; } - @NonNull @Override + @Override public Router getRouter() { return router; } @@ -807,27 +809,22 @@ public Router getRouter() { * @param router Mock router. * @return This context. */ - @NonNull public MockContext setRouter(@NonNull Router router) { + public MockContext setRouter(@NonNull Router router) { this.router = router; return this; } - @NonNull @Override - public T convert(@NonNull ValueNode value, @NonNull Class type) { - return DefaultContext.super.convert(value, type); - } - - @NonNull @Override + @Override public MockContext upgrade(@NonNull WebSocket.Initializer handler) { return this; } - @NonNull @Override + @Override public Context upgrade(@NonNull ServerSentEmitter.Handler handler) { return this; } - @NonNull @Override + @Override public Context onComplete(@NonNull Route.Complete task) { listeners.addListener(task); return this; @@ -845,4 +842,12 @@ void setConsumer(Consumer consumer) { void setMockRouter(MockRouter mockRouter) { this.mockRouter = mockRouter; } + + public ValueFactory getValueFactory() { + return valueFactory; + } + + public void setValueFactory(ValueFactory valueFactory) { + this.valueFactory = valueFactory; + } } diff --git a/modules/jooby-test/src/main/java/io/jooby/test/MockRouter.java b/modules/jooby-test/src/main/java/io/jooby/test/MockRouter.java index 6d7352d3a9..6c9564bb9a 100644 --- a/modules/jooby-test/src/main/java/io/jooby/test/MockRouter.java +++ b/modules/jooby-test/src/main/java/io/jooby/test/MockRouter.java @@ -487,7 +487,7 @@ private MockValue call( Router.Match match = router.match(findContext); Route route = match.route(); - boolean isCoroutine = route.attribute("coroutine") == Boolean.TRUE; + boolean isCoroutine = route.getAttribute("coroutine") == Boolean.TRUE; if (isCoroutine) { router.setWorker(Optional.ofNullable(getWorker()).orElseGet(MockRouter::singleThreadWorker)); } @@ -495,12 +495,12 @@ private MockValue call( findContext.setRoute(route); Object value; try { - Route.Handler handler = fullExecution ? route.getPipeline() : route.getHandler(); if (route.getMethod().equals(Router.WS)) { - WebSocket.Initializer initializer = (WebSocket.Initializer) route.getHandle(); - MockWebSocketConfigurer configurer = new MockWebSocketConfigurer(ctx, initializer); + var initializer = ((WebSocket.Handler) route.getHandler()).getInitializer(); + var configurer = new MockWebSocketConfigurer(ctx, initializer); return new SingleMockValue(configurer); } else { + var handler = fullExecution ? route.getPipeline() : route.getHandler(); value = handler.apply(ctx); if (ctx instanceof MockContext) { MockResponse response = ((MockContext) ctx).getResponse(); diff --git a/modules/jooby-test/src/main/java/io/jooby/test/MockSession.java b/modules/jooby-test/src/main/java/io/jooby/test/MockSession.java index eefaa32b59..edb56d1694 100644 --- a/modules/jooby-test/src/main/java/io/jooby/test/MockSession.java +++ b/modules/jooby-test/src/main/java/io/jooby/test/MockSession.java @@ -14,12 +14,14 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.Session; -import io.jooby.Value; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; /** Mock session. */ public class MockSession implements Session { private MockContext ctx; private String sessionId; + private ValueFactory valueFactory = new ValueFactory(); private Map data = new HashMap<>(); private Instant creationTime; @@ -89,7 +91,7 @@ public MockSession setId(@Nullable String id) { @NonNull @Override public Value get(@NonNull String name) { return Optional.ofNullable(data.get(name)) - .map(value -> Value.create(ctx, name, value)) + .map(value -> Value.create(valueFactory, name, value)) .orElse(Value.missing(name)); } diff --git a/modules/jooby-test/src/main/java/io/jooby/test/MockWebSocket.java b/modules/jooby-test/src/main/java/io/jooby/test/MockWebSocket.java index 94ed4a6764..1445614eac 100644 --- a/modules/jooby-test/src/main/java/io/jooby/test/MockWebSocket.java +++ b/modules/jooby-test/src/main/java/io/jooby/test/MockWebSocket.java @@ -14,7 +14,7 @@ import io.jooby.SneakyThrows; import io.jooby.WebSocket; import io.jooby.WebSocketCloseStatus; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; /** * Mock implementation of {@link WebSocket} for unit testing purpose. @@ -92,7 +92,7 @@ public WebSocket send(@NonNull ByteBuffer message, @NonNull WriteCallback callba } @NonNull @Override - public WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callback) { + public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendObject(message, callback); } @@ -112,7 +112,7 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback } @NonNull @Override - public WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback) { + public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { return sendObject(message, callback); } diff --git a/modules/jooby-test/src/test/java/io/jooby/test/LookupTest.java b/modules/jooby-test/src/test/java/io/jooby/test/LookupTest.java index 8a109187d7..39e13c2d39 100644 --- a/modules/jooby-test/src/test/java/io/jooby/test/LookupTest.java +++ b/modules/jooby-test/src/test/java/io/jooby/test/LookupTest.java @@ -14,6 +14,8 @@ import static io.jooby.ParamSource.SESSION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.HashMap; import java.util.Map; @@ -23,11 +25,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import io.jooby.Formdata; -import io.jooby.ParamLookup; -import io.jooby.ParamSource; -import io.jooby.Value; +import io.jooby.*; import io.jooby.exception.MissingValueException; +import io.jooby.value.Value; +import io.jooby.value.ValueFactory; public class LookupTest { @@ -35,7 +36,7 @@ public class LookupTest { @BeforeEach public void makeContext() { - context = new MockContext(); + context = mockContext(); Map pathMap = new HashMap<>(); pathMap.put("foo", "path-bar"); @@ -55,7 +56,7 @@ public void makeContext() { context.setQueryString("?foo=query-bar"); - Formdata formdata = Value.formdata(context); + Formdata formdata = Value.formdata(context.getValueFactory()); formdata.put("foo", "form-bar"); context.setForm(formdata); } @@ -91,14 +92,12 @@ public void testMissingValue() { assertThrows( MissingValueException.class, () -> - new MockContext() - .lookup("foo", PATH, HEADER, COOKIE, FLASH, SESSION, QUERY, FORM) - .value()); + mockContext().lookup("foo", PATH, HEADER, COOKIE, FLASH, SESSION, QUERY, FORM).value()); assertThrows( MissingValueException.class, () -> - new MockContext() + mockContext() .lookup() .inPath() .inHeader() @@ -111,6 +110,14 @@ public void testMissingValue() { .value()); } + private static MockContext mockContext() { + var router = mock(Router.class); + when(router.getValueFactory()).thenReturn(new ValueFactory()); + var ctx = new MockContext(); + ctx.setRouter(router); + return ctx; + } + private void test(ParamSource... sources) { String value = context.lookup("foo", sources).value(); assertEquals(sources[0].name().toLowerCase() + "-bar", value); diff --git a/modules/jooby-test/src/test/java/io/jooby/test/TestApp.java b/modules/jooby-test/src/test/java/io/jooby/test/TestApp.java index 7d943eb52e..365ba1f4ff 100644 --- a/modules/jooby-test/src/test/java/io/jooby/test/TestApp.java +++ b/modules/jooby-test/src/test/java/io/jooby/test/TestApp.java @@ -8,13 +8,13 @@ import java.util.List; import io.jooby.Jooby; -import io.jooby.RouterOption; +import io.jooby.RouterOptions; import io.jooby.StartupSummary; public class TestApp extends Jooby { { setStartupSummary(List.of(StartupSummary.NONE)); - setRouterOptions(RouterOption.IGNORE_CASE, RouterOption.IGNORE_TRAILING_SLASH); + setRouterOptions(new RouterOptions().ignoreCase(true).ignoreTrailingSlash(true)); setContextPath("/test"); get("/", ctx -> "OK"); } diff --git a/modules/jooby-test/src/test/java/io/jooby/test/UnitTest.java b/modules/jooby-test/src/test/java/io/jooby/test/UnitTest.java index f9d277b3fb..719d975548 100644 --- a/modules/jooby-test/src/test/java/io/jooby/test/UnitTest.java +++ b/modules/jooby-test/src/test/java/io/jooby/test/UnitTest.java @@ -20,8 +20,8 @@ import io.jooby.Formdata; import io.jooby.Jooby; import io.jooby.StatusCode; -import io.jooby.ValueNode; import io.jooby.WebSocketMessage; +import io.jooby.value.Value; import io.reactivex.rxjava3.core.Single; public class UnitTest { @@ -125,7 +125,7 @@ public void formdata() { MockRouter router = new MockRouter(app); MockContext context = new MockContext(); - Formdata formdata = Formdata.create(context); + Formdata formdata = Formdata.create(context.getValueFactory()); formdata.put("name", "Easy Unit"); context.setForm(formdata); @@ -138,7 +138,7 @@ public void formdataMock() { app.post("/", ctx -> ctx.form("name").value()); - ValueNode name = mock(ValueNode.class); + Value name = mock(Value.class); when(name.value()).thenReturn("Easy Unit"); Context context = mock(Context.class); diff --git a/modules/jooby-thymeleaf/src/main/java/io/jooby/internal/thymeleaf/ThymeleafTemplateEngine.java b/modules/jooby-thymeleaf/src/main/java/io/jooby/internal/thymeleaf/ThymeleafTemplateEngine.java index 85bd9e658e..ae16dffd35 100644 --- a/modules/jooby-thymeleaf/src/main/java/io/jooby/internal/thymeleaf/ThymeleafTemplateEngine.java +++ b/modules/jooby-thymeleaf/src/main/java/io/jooby/internal/thymeleaf/ThymeleafTemplateEngine.java @@ -16,12 +16,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.MapModelAndView; import io.jooby.ModelAndView; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class ThymeleafTemplateEngine implements io.jooby.TemplateEngine { - - private TemplateEngine templateEngine; - private List extensions; + private final TemplateEngine templateEngine; + private final List extensions; public ThymeleafTemplateEngine(TemplateEngine templateEngine, List extensions) { this.templateEngine = templateEngine; @@ -34,7 +33,7 @@ public List extensions() { } @Override - public @NonNull DataBuffer render(io.jooby.Context ctx, ModelAndView modelAndView) { + public @NonNull Output render(io.jooby.Context ctx, ModelAndView modelAndView) { if (modelAndView instanceof MapModelAndView mapModelAndView) { Map model = new HashMap<>(ctx.getAttributes()); model.putAll(mapModelAndView.getModel()); @@ -44,7 +43,7 @@ public List extensions() { if (locale == null) { locale = ctx.locale(); } - var buffer = ctx.getBufferFactory().allocateBuffer(); + var buffer = ctx.getOutputFactory().allocate(); var context = new Context(locale, model); var templateName = modelAndView.getView(); if (!templateName.startsWith("/")) { diff --git a/modules/jooby-undertow/pom.xml b/modules/jooby-undertow/pom.xml index 4e62f9126d..9e9422b3ab 100644 --- a/modules/jooby-undertow/pom.xml +++ b/modules/jooby-undertow/pom.xml @@ -24,12 +24,17 @@ - org.jboss.xnio xnio-api 3.8.16.Final + + org.jboss.logging + jboss-logging + 3.6.1.Final + + io.undertow undertow-core diff --git a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java index d3d75f0647..17441c9d7a 100644 --- a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java +++ b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java @@ -49,17 +49,15 @@ import io.jooby.QueryString; import io.jooby.Route; import io.jooby.Router; -import io.jooby.RouterOption; import io.jooby.Server; import io.jooby.ServerSentEmitter; import io.jooby.Session; import io.jooby.SessionStore; import io.jooby.SneakyThrows; import io.jooby.StatusCode; -import io.jooby.Value; -import io.jooby.ValueNode; import io.jooby.WebSocket; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; +import io.jooby.value.Value; import io.undertow.Handlers; import io.undertow.io.IoCallback; import io.undertow.io.Sender; @@ -76,7 +74,7 @@ public class UndertowContext implements DefaultContext, IoCallback { private Router router; private QueryString query; private Formdata formdata; - private ValueNode headers; + private Value headers; private Map pathMap = Collections.EMPTY_MAP; private Map attributes; Body body; @@ -257,11 +255,11 @@ public Context setPort(int port) { @NonNull @Override public Value header(@NonNull String name) { - return Value.create(this, name, exchange.getRequestHeaders().get(name)); + return Value.create(getValueFactory(), name, exchange.getRequestHeaders().get(name)); } @NonNull @Override - public ValueNode header() { + public Value header() { HeaderMap map = exchange.getRequestHeaders(); if (headers == null) { Map> headerMap = new LinkedHashMap<>(); @@ -270,7 +268,7 @@ public ValueNode header() { HeaderValues values = map.get(name); headerMap.put(name.toString(), values); } - headers = Value.headers(this, headerMap); + headers = Value.headers(getValueFactory(), headerMap); } return headers; } @@ -278,7 +276,7 @@ public ValueNode header() { @NonNull @Override public QueryString query() { if (query == null) { - query = QueryString.create(this, exchange.getQueryString()); + query = QueryString.create(getValueFactory(), exchange.getQueryString()); } return query; } @@ -286,7 +284,7 @@ public QueryString query() { @NonNull @Override public Formdata form() { if (formdata == null) { - formdata = Formdata.create(this); + formdata = Formdata.create(getValueFactory()); formData(formdata, exchange.getAttachment(FORM_DATA)); } return formdata; @@ -493,14 +491,15 @@ public Context send(@NonNull ByteBuffer[] data) { @NonNull @Override public Context send(@NonNull ByteBuffer data) { + exchange.setResponseContentLength(data.remaining()); exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, Long.toString(data.remaining())); exchange.getResponseSender().send(data, this); return this; } @NonNull @Override - public Context send(@NonNull DataBuffer data) { - data.send(this); + public Context send(@NonNull Output output) { + output.send(this); return this; } @@ -552,7 +551,7 @@ public boolean isResponseStarted() { @Override public boolean getResetHeadersOnError() { return resetHeadersOnError == null - ? getRouter().getRouterOptions().contains(RouterOption.RESET_HEADERS_ON_ERROR) + ? getRouter().getRouterOptions().isResetHeadersOnError() : resetHeadersOnError.booleanValue(); } diff --git a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowDataBufferCallback.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowDataBufferCallback.java deleted file mode 100644 index f7c131f458..0000000000 --- a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowDataBufferCallback.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.undertow; - -import java.io.IOException; - -import io.jooby.SneakyThrows; -import io.jooby.buffer.DataBuffer; -import io.undertow.io.IoCallback; -import io.undertow.io.Sender; -import io.undertow.server.HttpServerExchange; - -public class UndertowDataBufferCallback implements IoCallback { - - private DataBuffer.ByteBufferIterator iterator; - private IoCallback callback; - - public UndertowDataBufferCallback(DataBuffer buffer, IoCallback callback) { - this.iterator = buffer.readableByteBuffers(); - this.callback = callback; - } - - public void send(HttpServerExchange exchange) { - try { - exchange.getResponseSender().send(iterator.next(), this); - } catch (Throwable cause) { - try { - iterator.close(); - } finally { - throw SneakyThrows.propagate(cause); - } - } - } - - @Override - public void onComplete(HttpServerExchange exchange, Sender sender) { - if (iterator.hasNext()) { - sender.send(iterator.next(), this); - } else { - try { - callback.onComplete(exchange, sender); - } finally { - iterator.close(); - } - } - } - - @Override - public void onException(HttpServerExchange exchange, Sender sender, IOException exception) { - try { - callback.onException(exchange, sender, exception); - } finally { - iterator.close(); - } - } -} diff --git a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowOutputCallback.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowOutputCallback.java new file mode 100644 index 0000000000..0a2222f6cc --- /dev/null +++ b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowOutputCallback.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.internal.undertow; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Iterator; + +import io.jooby.output.Output; +import io.undertow.io.IoCallback; +import io.undertow.io.Sender; +import io.undertow.server.HttpServerExchange; + +public class UndertowOutputCallback implements IoCallback { + + private Iterator iterator; + private IoCallback callback; + + public UndertowOutputCallback(Output buffer, IoCallback callback) { + this.iterator = buffer.iterator(); + this.callback = callback; + } + + public void send(HttpServerExchange exchange) { + exchange.getResponseSender().send(iterator.next(), this); + } + + @Override + public void onComplete(HttpServerExchange exchange, Sender sender) { + if (iterator.hasNext()) { + sender.send(iterator.next(), this); + } else { + callback.onComplete(exchange, sender); + } + } + + @Override + public void onException(HttpServerExchange exchange, Sender sender, IOException exception) { + callback.onException(exchange, sender, exception); + } +} diff --git a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowSender.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowSender.java index 42f86eec1c..a3844cfc6f 100644 --- a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowSender.java +++ b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowSender.java @@ -10,7 +10,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Sender; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; import io.undertow.io.IoCallback; import io.undertow.server.HttpServerExchange; @@ -30,8 +30,8 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { - new UndertowDataBufferCallback(data, newIoCallback(ctx, callback)).send(exchange); + public Sender write(@NonNull Output output, @NonNull Callback callback) { + new UndertowOutputCallback(output, newIoCallback(ctx, callback)).send(exchange); return this; } diff --git a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowServerSentConnection.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowServerSentConnection.java index 3266cc147c..0ba3230b7b 100644 --- a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowServerSentConnection.java +++ b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowServerSentConnection.java @@ -22,9 +22,10 @@ import org.xnio.IoUtils; import org.xnio.channels.StreamSinkChannel; +import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.ServerSentMessage; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; @@ -122,37 +123,37 @@ private void fillBuffer() { } else { pooled.getBuffer().clear(); } - ByteBuffer buffer = pooled.getBuffer(); + var buffer = pooled.getBuffer(); while (!queue.isEmpty() && buffer.hasRemaining()) { - UndertowServerSentConnection.SSEData data = queue.poll(); + var data = queue.poll(); buffered.add(data); if (data.leftOverData == null) { var message = data.message.encode(context); - if (message.readableByteCount() < buffer.remaining()) { - message.toByteBuffer(buffer); - buffer.position(buffer.position() + message.readableByteCount()); + if (message.size() < buffer.remaining()) { + transferTo(message, buffer); + buffer.position(buffer.position() + message.size()); data.endBufferPosition = buffer.position(); } else { queue.addFirst(data); int rem = buffer.remaining(); - message.toByteBuffer(0, buffer, buffer.position(), rem); + transferTo(message, 0, buffer, buffer.position(), rem); buffer.position(buffer.position() + rem); data.leftOverData = message; data.leftOverDataOffset = rem; } } else { - int remainingData = data.leftOverData.readableByteCount() - data.leftOverDataOffset; + int remainingData = data.leftOverData.size() - data.leftOverDataOffset; if (remainingData > buffer.remaining()) { queue.addFirst(data); int toWrite = buffer.remaining(); - data.leftOverData.toByteBuffer( - data.leftOverDataOffset, buffer, buffer.position(), toWrite); + transferTo( + data.leftOverData, data.leftOverDataOffset, buffer, buffer.position(), toWrite); buffer.position(buffer.position() + toWrite); data.leftOverDataOffset += toWrite; } else { - data.leftOverData.toByteBuffer( - data.leftOverDataOffset, buffer, buffer.position(), remainingData); + transferTo( + data.leftOverData, data.leftOverDataOffset, buffer, buffer.position(), remainingData); buffer.position(buffer.position() + remainingData); data.endBufferPosition = buffer.position(); data.leftOverData = null; @@ -163,6 +164,31 @@ private void fillBuffer() { sink.resumeWrites(); } + /** + * Copy bytes into the given buffer. + * + * @param dest Destination buffer. + */ + private void transferTo(@NonNull Output source, @NonNull ByteBuffer dest) { + transferTo(source, 0, dest, dest.position(), source.size()); + } + + /** + * Copies the given length from this data buffer into the given destination {@code ByteBuffer}, + * beginning at the given source position, and the given destination position in the destination + * byte buffer. + * + * @param srcPos the position of this data buffer from where copying should start + * @param dest the destination byte buffer + * @param destPos the position in {@code dest} to where copying should start + * @param length the amount of data to copy + */ + private void transferTo( + @NonNull Output source, int srcPos, @NonNull ByteBuffer dest, int destPos, int length) { + dest = dest.duplicate().clear(); + dest.put(destPos, source.asByteBuffer(), srcPos, length); + } + /** execute a graceful shutdown once all data has been sent */ public void shutdown() { if (open == 0 || shutdown) { @@ -247,7 +273,7 @@ private static class SSEData { final ServerSentMessage message; final UndertowServerSentConnection.EventCallback callback; private int endBufferPosition = -1; - private DataBuffer leftOverData; + private Output leftOverData; private int leftOverDataOffset; private SSEData( diff --git a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowWebSocket.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowWebSocket.java index 22b06de829..5400c68a72 100644 --- a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowWebSocket.java +++ b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowWebSocket.java @@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -32,7 +33,7 @@ import io.jooby.WebSocketCloseStatus; import io.jooby.WebSocketConfigurer; import io.jooby.WebSocketMessage; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; import io.undertow.websockets.core.AbstractReceiveListener; import io.undertow.websockets.core.BufferedBinaryMessage; import io.undertow.websockets.core.BufferedTextMessage; @@ -86,24 +87,24 @@ public void onError(WebSocketChannel channel, Void context, Throwable cause) { } } - private static class WebSocketDataBufferCallback implements WebSocketCallback { - private final DataBuffer.ByteBufferIterator it; + private static class WebSocketOutputCallback implements WebSocketCallback { + private final Iterator it; private final boolean binary; private final WebSocketChannel channel; private final UndertowWebSocket ws; private final WriteCallback cb; - public WebSocketDataBufferCallback( + public WebSocketOutputCallback( UndertowWebSocket ws, WebSocketChannel channel, WriteCallback callback, boolean binary, - DataBuffer buffer) { + Output buffer) { this.ws = ws; this.channel = channel; this.binary = binary; this.cb = callback; - this.it = buffer.readableByteBuffers(); + this.it = buffer.iterator(); } public void send() { @@ -222,12 +223,12 @@ public WebSocket sendBinary(@NonNull String message, @NonNull WriteCallback call } @NonNull @Override - public WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback) { + public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage(message, true, callback); } @NonNull @Override - public WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callback) { + public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage(message, false, callback); } @@ -236,10 +237,10 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback return sendMessage(message, true, callback); } - private WebSocket sendMessage(DataBuffer buffer, boolean binary, WriteCallback callback) { + private WebSocket sendMessage(Output buffer, boolean binary, WriteCallback callback) { if (isOpen()) { try { - new WebSocketDataBufferCallback(this, channel, callback, binary, buffer).send(); + new WebSocketOutputCallback(this, channel, callback, binary, buffer).send(); } catch (Throwable x) { onError(channel, x); } diff --git a/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java b/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java index db3b2bfe5e..9c665e4976 100644 --- a/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java +++ b/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java @@ -24,6 +24,7 @@ import io.jooby.exception.StartupException; import io.jooby.internal.undertow.UndertowHandler; import io.jooby.internal.undertow.UndertowWebSocket; +import io.jooby.output.OutputFactory; import io.undertow.Undertow; import io.undertow.UndertowOptions; import io.undertow.server.HttpHandler; @@ -41,6 +42,13 @@ */ public class UndertowServer extends Server.Base { + private static final String NAME = "undertow"; + + static { + // Default values + System.setProperty("server.name", NAME); + } + private static final int BACKLOG = 8192; private static final int _100 = 100; @@ -48,10 +56,19 @@ public class UndertowServer extends Server.Base { private static final int _10 = 10; private Undertow server; + private List applications; private XnioWorker worker; + private OutputFactory outputFactory; + + public UndertowServer(@NonNull ServerOptions options) { + setOptions(options); + } + + public UndertowServer() {} + @NonNull @Override public UndertowServer setOptions(@NonNull ServerOptions options) { // default io threads @@ -59,14 +76,22 @@ public UndertowServer setOptions(@NonNull ServerOptions options) { return this; } - @Override - protected ServerOptions defaultOptions() { - return new ServerOptions().setIoThreads(ServerOptions.IO_THREADS).setServer("utow"); + @NonNull @Override + public OutputFactory getOutputFactory() { + if (outputFactory == null) { + outputFactory = OutputFactory.create(getOptions().getOutput()); + } + return outputFactory; + } + + public UndertowServer setOutputFactory(OutputFactory outputFactory) { + this.outputFactory = outputFactory; + return this; } @NonNull @Override public String getName() { - return "undertow"; + return NAME; } @Override @@ -77,12 +102,17 @@ public String getName() { try { this.applications = List.of(application); + for (var app : applications) { + app.getServices().put(ServerOptions.class, options); + app.getServices().put(Server.class, this); + } + addShutdownHook(); HttpHandler handler = new UndertowHandler( this.applications, - options.getBufferSize(), + getOptions().getOutput().getSize(), options.getMaxRequestSize(), options.getDefaultHeaders()); @@ -116,10 +146,11 @@ public String getName() { Undertow.Builder builder = Undertow.builder() - .setBufferSize(options.getBufferSize()) - /** Socket : */ + .setBufferSize(options.getOutput().getSize()) + .setDirectBuffers(options.getOutput().isDirectBuffers()) + /* Socket : */ .setSocketOption(Options.BACKLOG, BACKLOG) - /** Server: */ + /* Server: */ // HTTP/1.1 is keep-alive by default, turn this option off .setServerOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, false) .setServerOption(UndertowOptions.MAX_HEADER_SIZE, options.getMaxHeaderSize()) @@ -127,7 +158,7 @@ public String getName() { .setServerOption(UndertowOptions.ALWAYS_SET_DATE, options.getDefaultHeaders()) .setServerOption(UndertowOptions.RECORD_REQUEST_START_TIME, false) .setServerOption(UndertowOptions.DECODE_URL, false) - /** Worker: */ + /* Worker: */ .setWorker(worker) .setHandler(handler); diff --git a/modules/jooby-yasson/src/main/java/io/jooby/yasson/YassonModule.java b/modules/jooby-yasson/src/main/java/io/jooby/yasson/YassonModule.java index 24ccc7ecc7..b136a16a9f 100644 --- a/modules/jooby-yasson/src/main/java/io/jooby/yasson/YassonModule.java +++ b/modules/jooby-yasson/src/main/java/io/jooby/yasson/YassonModule.java @@ -5,8 +5,6 @@ */ package io.jooby.yasson; -import static java.nio.charset.StandardCharsets.UTF_8; - import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -22,7 +20,7 @@ import io.jooby.MessageDecoder; import io.jooby.MessageEncoder; import io.jooby.ServiceRegistry; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; @@ -103,8 +101,11 @@ public Object decode(@NonNull final Context ctx, @NonNull final Type type) throw } @Nullable @Override - public DataBuffer encode(@NonNull final Context ctx, @NonNull final Object value) { + public Output encode(@NonNull final Context ctx, @NonNull final Object value) { ctx.setDefaultResponseType(MediaType.json); - return ctx.getBufferFactory().wrap(jsonb.toJson(value).getBytes(UTF_8)); + var factory = ctx.getOutputFactory(); + var output = factory.allocate(); + jsonb.toJson(value, output.asOutputStream()); + return output; } } diff --git a/modules/jooby-yasson/src/test/java/io/jooby/yasson/YassonModuleTest.java b/modules/jooby-yasson/src/test/java/io/jooby/yasson/YassonModuleTest.java index 00d22646ee..d131d9f748 100644 --- a/modules/jooby-yasson/src/test/java/io/jooby/yasson/YassonModuleTest.java +++ b/modules/jooby-yasson/src/test/java/io/jooby/yasson/YassonModuleTest.java @@ -19,7 +19,8 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; public class YassonModuleTest { @@ -31,19 +32,18 @@ public static class User { @Test public void render() { - - YassonModule YassonModule = new YassonModule(); + var yasson = new YassonModule(); User user = new User(); user.id = -1; user.name = "Lorem €@!?"; user.age = Integer.MAX_VALUE; Context ctx = mock(Context.class); - when(ctx.getBufferFactory()).thenReturn(new DefaultDataBufferFactory()); - var buffer = YassonModule.encode(ctx, user); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small())); + var buffer = yasson.encode(ctx, user); assertEquals( "{\"age\":2147483647,\"id\":-1,\"name\":\"Lorem €@!?\"}", - buffer.toString(StandardCharsets.UTF_8)); + StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); verify(ctx).setDefaultResponseType(MediaType.json); } diff --git a/pom.xml b/pom.xml index f79b83b8fd..bc90e4e548 100644 --- a/pom.xml +++ b/pom.xml @@ -72,7 +72,7 @@ 6.3.0 1.2 - 6.6.22.Final + 7.0.4.Final 15.11.0 3.49.5 11.8.2 diff --git a/tests/src/test/java/examples/InstanceRouter.java b/tests/src/test/java/examples/InstanceRouter.java index 8e05d14c42..7b226f4c5c 100644 --- a/tests/src/test/java/examples/InstanceRouter.java +++ b/tests/src/test/java/examples/InstanceRouter.java @@ -19,7 +19,7 @@ public class InstanceRouter { @POST @Role("some") public String getIt(Route route) { - return route.attribute("role"); + return route.getAttribute("role"); } @GET diff --git a/tests/src/test/java/examples/Performance.java b/tests/src/test/java/examples/Performance.java new file mode 100644 index 0000000000..95206a21f9 --- /dev/null +++ b/tests/src/test/java/examples/Performance.java @@ -0,0 +1,46 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package examples; + +import static io.jooby.ExecutionMode.EVENT_LOOP; +import static io.jooby.MediaType.JSON; + +import io.jooby.Jooby; +import io.jooby.StatusCode; +import io.jooby.netty.NettyServer; + +public class Performance extends Jooby { + + private static final String MESSAGE = "Hello, World!"; + + { + var outputFactory = getOutputFactory(); + var message = outputFactory.wrap(MESSAGE); + get( + "/plaintext", + ctx -> { + return ctx.send(message); + }); + + get("/json", ctx -> ctx.setResponseType(JSON).render(new Message(MESSAGE))); + + get("/db", ctx -> ctx.send(StatusCode.OK)); + + get("/queries", ctx -> ctx.send(StatusCode.OK)); + + get("/fortunes", ctx -> ctx.send(StatusCode.OK)); + + get("/updates", ctx -> ctx.send(StatusCode.OK)); + } + + public static void main(final String[] args) { + System.setProperty("io.netty.disableHttpHeadersValidation", "true"); + // runApp(args, new + // UndertowServer().setOutputFactory(OutputFactory.threadLocal(OutputFactory.create())), + // EVENT_LOOP, Performance::new); + runApp(args, new NettyServer(), EVENT_LOOP, Performance::new); + } +} diff --git a/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java b/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java deleted file mode 100644 index 24c77e164e..0000000000 --- a/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.avaje.jsonb; - -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.openjdk.jmh.annotations.*; - -import io.avaje.jsonb.Jsonb; -import io.jooby.buffer.DataBufferFactory; -import io.jooby.buffer.DefaultDataBuffer; -import io.jooby.buffer.DefaultDataBufferFactory; - -@Fork(5) -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 10, time = 1) -@BenchmarkMode(Mode.Throughput) -@OutputTimeUnit(TimeUnit.SECONDS) -@State(Scope.Benchmark) -public class AvajeJsonbEncoderBench { - - private Jsonb jsonb; - private Map message; - - private DataBufferFactory factory; - private ThreadLocal cache = - ThreadLocal.withInitial( - () -> { - return (DefaultDataBuffer) factory.allocateBuffer(1024); - }); - - @Setup - public void setup() { - message = Map.of("id", 98, "value", "Hello World"); - jsonb = Jsonb.builder().build(); - factory = new DefaultDataBufferFactory(); - } - - @Benchmark - public void withJsonBuffer() { - jsonb.toJsonBytes(message); - } - - @Benchmark - public void withDataBufferOutputStream() { - var buffer = cache.get(); - jsonb.toJson(message, jsonb.writer(new DataBufferJsonOutputBench(buffer))); - buffer.getNativeBuffer().clear(); - } -} diff --git a/tests/src/test/java/io/jooby/avaje/jsonb/DataBufferJsonOutputBench.java b/tests/src/test/java/io/jooby/avaje/jsonb/DataBufferJsonOutputBench.java deleted file mode 100644 index 7c134b5084..0000000000 --- a/tests/src/test/java/io/jooby/avaje/jsonb/DataBufferJsonOutputBench.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.avaje.jsonb; - -import java.io.IOException; -import java.io.OutputStream; - -import io.avaje.json.stream.JsonOutput; -import io.jooby.buffer.DataBuffer; - -public class DataBufferJsonOutputBench implements JsonOutput { - - private DataBuffer buffer; - - public DataBufferJsonOutputBench(DataBuffer buffer) { - this.buffer = buffer; - } - - @Override - public void write(byte[] content, int offset, int length) throws IOException { - buffer.write(content, offset, length); - } - - @Override - public void flush() throws IOException {} - - @Override - public OutputStream unwrapOutputStream() { - return this.buffer.asOutputStream(); - } - - @Override - public void close() throws IOException {} -} diff --git a/tests/src/test/java/io/jooby/buffer/AbstractDataBufferAllocatingTests.java b/tests/src/test/java/io/jooby/buffer/AbstractDataBufferAllocatingTests.java deleted file mode 100644 index 67d3dcb752..0000000000 --- a/tests/src/test/java/io/jooby/buffer/AbstractDataBufferAllocatingTests.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Named.named; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.time.Instant; -import java.util.Arrays; -import java.util.List; -import java.util.function.Consumer; -import java.util.stream.Stream; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import io.jooby.netty.buffer.NettyDataBufferFactory; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.PoolArenaMetric; -import io.netty.buffer.PooledByteBufAllocator; -import io.netty.buffer.PooledByteBufAllocatorMetric; -import io.netty.buffer.UnpooledByteBufAllocator; -import reactor.core.publisher.Mono; - -/** - * Base class for tests that read or write data buffers with an extension to check that allocated - * buffers have been released. - * - * @author Arjen Poutsma - * @author Rossen Stoyanchev - * @author Sam Brannen - */ -public abstract class AbstractDataBufferAllocatingTests { - - private static UnpooledByteBufAllocator netty4OffHeapUnpooled; - - private static UnpooledByteBufAllocator netty4OnHeapUnpooled; - - private static PooledByteBufAllocator netty4OffHeapPooled; - - private static PooledByteBufAllocator netty4OnHeapPooled; - - @RegisterExtension - AfterEachCallback leakDetector = context -> waitForDataBufferRelease(Duration.ofSeconds(2)); - - protected DataBufferFactory bufferFactory; - - protected DataBuffer createDataBuffer(int capacity) { - return this.bufferFactory.allocateBuffer(capacity); - } - - protected DataBuffer stringBuffer(String value) { - return byteBuffer(value.getBytes(StandardCharsets.UTF_8)); - } - - protected Mono deferStringBuffer(String value) { - return Mono.defer(() -> Mono.just(stringBuffer(value))); - } - - protected DataBuffer byteBuffer(byte[] value) { - DataBuffer buffer = this.bufferFactory.allocateBuffer(value.length); - buffer.write(value); - return buffer; - } - - protected void release(DataBuffer... buffers) { - Arrays.stream(buffers).forEach(DataBufferUtils::release); - } - - protected Consumer stringConsumer(String expected) { - return stringConsumer(expected, UTF_8); - } - - protected Consumer stringConsumer(String expected, Charset charset) { - return dataBuffer -> { - String value = dataBuffer.toString(charset); - DataBufferUtils.release(dataBuffer); - assertThat(value).isEqualTo(expected); - }; - } - - /** Wait until allocations are at 0, or the given duration elapses. */ - private void waitForDataBufferRelease(Duration duration) throws InterruptedException { - Instant start = Instant.now(); - while (true) { - try { - verifyAllocations(); - break; - } catch (AssertionError ex) { - if (Instant.now().isAfter(start.plus(duration))) { - throw ex; - } - } - Thread.sleep(50); - } - } - - private void verifyAllocations() { - if (this.bufferFactory instanceof NettyDataBufferFactory) { - ByteBufAllocator allocator = - ((NettyDataBufferFactory) this.bufferFactory).getByteBufAllocator(); - if (allocator instanceof PooledByteBufAllocator) { - Instant start = Instant.now(); - while (true) { - PooledByteBufAllocatorMetric metric = ((PooledByteBufAllocator) allocator).metric(); - long total = getAllocations(metric.directArenas()) + getAllocations(metric.heapArenas()); - if (total == 0) { - return; - } - if (Instant.now().isBefore(start.plus(Duration.ofSeconds(5)))) { - try { - Thread.sleep(50); - } catch (InterruptedException ex) { - // ignore - } - continue; - } - assertThat(total).as("ByteBuf Leak: " + total + " unreleased allocations").isEqualTo(0); - } - } - } - } - - private static long getAllocations(List metrics) { - return metrics.stream().mapToLong(PoolArenaMetric::numActiveAllocations).sum(); - } - - @BeforeAll - @SuppressWarnings("deprecation") // PooledByteBufAllocator no longer supports tinyCacheSize. - public static void createAllocators() { - netty4OnHeapUnpooled = new UnpooledByteBufAllocator(false); - netty4OffHeapUnpooled = new UnpooledByteBufAllocator(true); - netty4OnHeapPooled = new PooledByteBufAllocator(false, 1, 1, 4096, 4, 0, 0, 0, true); - netty4OffHeapPooled = new PooledByteBufAllocator(true, 1, 1, 4096, 4, 0, 0, 0, true); - } - - @AfterAll - static void closeAllocators() {} - - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.METHOD) - @ParameterizedTest(name = "[{index}] {0}") - @MethodSource("io.jooby.buffer.AbstractDataBufferAllocatingTests#dataBufferFactories()") - public @interface ParameterizedDataBufferAllocatingTest {} - - public static Stream dataBufferFactories() { - return Stream.of( - // Netty 4 - arguments( - named( - "NettyDataBufferFactory - UnpooledByteBufAllocator - preferDirect = true", - new NettyDataBufferFactory(netty4OffHeapUnpooled))), - arguments( - named( - "NettyDataBufferFactory - UnpooledByteBufAllocator - preferDirect = false", - new NettyDataBufferFactory(netty4OnHeapUnpooled))), - arguments( - named( - "NettyDataBufferFactory - PooledByteBufAllocator - preferDirect = true", - new NettyDataBufferFactory(netty4OffHeapPooled))), - arguments( - named( - "NettyDataBufferFactory - PooledByteBufAllocator - preferDirect = false", - new NettyDataBufferFactory(netty4OnHeapPooled))), - // Default - arguments( - named( - "DefaultDataBufferFactory - preferDirect = true", - new DefaultDataBufferFactory(true))), - arguments( - named( - "DefaultDataBufferFactory - preferDirect = false", - new DefaultDataBufferFactory(false)))); - } -} diff --git a/tests/src/test/java/io/jooby/buffer/DataBufferTests.java b/tests/src/test/java/io/jooby/buffer/DataBufferTests.java deleted file mode 100644 index d49a01c79c..0000000000 --- a/tests/src/test/java/io/jooby/buffer/DataBufferTests.java +++ /dev/null @@ -1,698 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import static org.assertj.core.api.Assertions.*; - -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; - -/** - * @author Arjen Poutsma - * @author Sam Brannen - */ -class DataBufferTests extends AbstractDataBufferAllocatingTests { - - @ParameterizedDataBufferAllocatingTest - void byteCountsAndPositions(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(2); - - assertThat(buffer.readPosition()).isEqualTo(0); - assertThat(buffer.writePosition()).isEqualTo(0); - assertThat(buffer.readableByteCount()).isEqualTo(0); - assertThat(buffer.writableByteCount()).isEqualTo(2); - assertThat(buffer.capacity()).isEqualTo(2); - - buffer.write((byte) 'a'); - assertThat(buffer.readPosition()).isEqualTo(0); - assertThat(buffer.writePosition()).isEqualTo(1); - assertThat(buffer.readableByteCount()).isEqualTo(1); - assertThat(buffer.writableByteCount()).isEqualTo(1); - assertThat(buffer.capacity()).isEqualTo(2); - - buffer.write((byte) 'b'); - assertThat(buffer.readPosition()).isEqualTo(0); - assertThat(buffer.writePosition()).isEqualTo(2); - assertThat(buffer.readableByteCount()).isEqualTo(2); - assertThat(buffer.writableByteCount()).isEqualTo(0); - assertThat(buffer.capacity()).isEqualTo(2); - - buffer.read(); - assertThat(buffer.readPosition()).isEqualTo(1); - assertThat(buffer.writePosition()).isEqualTo(2); - assertThat(buffer.readableByteCount()).isEqualTo(1); - assertThat(buffer.writableByteCount()).isEqualTo(0); - assertThat(buffer.capacity()).isEqualTo(2); - - buffer.read(); - assertThat(buffer.readPosition()).isEqualTo(2); - assertThat(buffer.writePosition()).isEqualTo(2); - assertThat(buffer.readableByteCount()).isEqualTo(0); - assertThat(buffer.writableByteCount()).isEqualTo(0); - assertThat(buffer.capacity()).isEqualTo(2); - - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void readPositionSmallerThanZero(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(1); - try { - assertThatExceptionOfType(IndexOutOfBoundsException.class) - .isThrownBy(() -> buffer.readPosition(-1)); - } finally { - release(buffer); - } - } - - @ParameterizedDataBufferAllocatingTest - void readPositionGreaterThanWritePosition(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(1); - try { - assertThatExceptionOfType(IndexOutOfBoundsException.class) - .isThrownBy(() -> buffer.readPosition(1)); - } finally { - release(buffer); - } - } - - @ParameterizedDataBufferAllocatingTest - void writePositionSmallerThanReadPosition(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(2); - try { - buffer.write((byte) 'a'); - buffer.read(); - assertThatExceptionOfType(IndexOutOfBoundsException.class) - .isThrownBy(() -> buffer.writePosition(0)); - } finally { - release(buffer); - } - } - - @ParameterizedDataBufferAllocatingTest - void writePositionGreaterThanCapacity(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(1); - try { - assertThatExceptionOfType(IndexOutOfBoundsException.class) - .isThrownBy(() -> buffer.writePosition(2)); - } finally { - release(buffer); - } - } - - @ParameterizedDataBufferAllocatingTest - void writeAndRead(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(5); - buffer.write(new byte[] {'a', 'b', 'c'}); - - int ch = buffer.read(); - assertThat(ch).isEqualTo((byte) 'a'); - - buffer.write((byte) 'd'); - buffer.write((byte) 'e'); - - byte[] result = new byte[4]; - buffer.read(result); - - assertThat(result).isEqualTo(new byte[] {'b', 'c', 'd', 'e'}); - - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void writeNullString(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(1); - try { - assertThatNullPointerException().isThrownBy(() -> buffer.write(null, StandardCharsets.UTF_8)); - } finally { - release(buffer); - } - } - - @ParameterizedDataBufferAllocatingTest - void writeNullCharset(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(1); - try { - assertThatNullPointerException().isThrownBy(() -> buffer.write("test", null)); - } finally { - release(buffer); - } - } - - @ParameterizedDataBufferAllocatingTest - void writeEmptyString(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(1); - buffer.write("", StandardCharsets.UTF_8); - - assertThat(buffer.readableByteCount()).isEqualTo(0); - - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void writeUtf8String(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(6); - buffer.write("Spring", StandardCharsets.UTF_8); - - byte[] result = new byte[6]; - buffer.read(result); - - assertThat(result).isEqualTo("Spring".getBytes(StandardCharsets.UTF_8)); - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void writeUtf8StringOutGrowsCapacity(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(5); - buffer.write("Spring €", StandardCharsets.UTF_8); - - byte[] result = new byte[10]; - buffer.read(result); - - assertThat(result).isEqualTo("Spring €".getBytes(StandardCharsets.UTF_8)); - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void writeIsoString(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(3); - buffer.write("\u00A3", StandardCharsets.ISO_8859_1); - - byte[] result = new byte[1]; - buffer.read(result); - - assertThat(result).isEqualTo("\u00A3".getBytes(StandardCharsets.ISO_8859_1)); - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void writeMultipleUtf8String(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(1); - buffer.write("abc", StandardCharsets.UTF_8); - assertThat(buffer.readableByteCount()).isEqualTo(3); - - buffer.write("def", StandardCharsets.UTF_8); - assertThat(buffer.readableByteCount()).isEqualTo(6); - - buffer.write("ghi", StandardCharsets.UTF_8); - assertThat(buffer.readableByteCount()).isEqualTo(9); - - byte[] result = new byte[9]; - buffer.read(result); - - assertThat(result).isEqualTo("abcdefghi".getBytes()); - - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void toStringNullCharset(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(1); - try { - assertThatNullPointerException().isThrownBy(() -> buffer.toString(null)); - } finally { - release(buffer); - } - } - - @ParameterizedDataBufferAllocatingTest - void toStringUtf8(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - String spring = "Spring"; - byte[] bytes = spring.getBytes(StandardCharsets.UTF_8); - DataBuffer buffer = createDataBuffer(bytes.length); - buffer.write(bytes); - - String result = buffer.toString(StandardCharsets.UTF_8); - - assertThat(result).isEqualTo(spring); - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void toStringSection(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - String spring = "Spring"; - byte[] bytes = spring.getBytes(StandardCharsets.UTF_8); - DataBuffer buffer = createDataBuffer(bytes.length); - buffer.write(bytes); - - String result = buffer.toString(1, 3, StandardCharsets.UTF_8); - - assertThat(result).isEqualTo("pri"); - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void inputStream(DataBufferFactory bufferFactory) throws Exception { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(4); - buffer.write(new byte[] {'a', 'b', 'c', 'd', 'e'}); - buffer.readPosition(1); - - InputStream inputStream = buffer.asInputStream(); - - assertThat(inputStream.available()).isEqualTo(4); - - int result = inputStream.read(); - assertThat(result).isEqualTo((byte) 'b'); - assertThat(inputStream.available()).isEqualTo(3); - - assertThat(inputStream.markSupported()).isTrue(); - inputStream.mark(2); - - byte[] bytes = new byte[2]; - int len = inputStream.read(bytes); - assertThat(len).isEqualTo(2); - assertThat(bytes).isEqualTo(new byte[] {'c', 'd'}); - assertThat(inputStream.available()).isEqualTo(1); - - Arrays.fill(bytes, (byte) 0); - len = inputStream.read(bytes); - assertThat(len).isEqualTo(1); - assertThat(bytes).isEqualTo(new byte[] {'e', (byte) 0}); - assertThat(inputStream.available()).isEqualTo(0); - - assertThat(inputStream.read()).isEqualTo(-1); - assertThat(inputStream.read(bytes)).isEqualTo(-1); - - inputStream.reset(); - bytes = new byte[3]; - len = inputStream.read(bytes); - assertThat(len).isEqualTo(3); - assertThat(bytes).containsExactly('c', 'd', 'e'); - - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void inputStreamReleaseOnClose(DataBufferFactory bufferFactory) throws Exception { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(3); - byte[] bytes = {'a', 'b', 'c'}; - buffer.write(bytes); - - try (InputStream inputStream = buffer.asInputStream(true)) { - byte[] result = new byte[3]; - int len = inputStream.read(result); - assertThat(len).isEqualTo(3); - assertThat(result).isEqualTo(bytes); - } - - // AbstractDataBufferAllocatingTests.leakDetector will verify the buffer's release - } - - @ParameterizedDataBufferAllocatingTest - void outputStream(DataBufferFactory bufferFactory) throws Exception { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(4); - buffer.write((byte) 'a'); - - OutputStream outputStream = buffer.asOutputStream(); - outputStream.write('b'); - outputStream.write(new byte[] {'c', 'd'}); - - buffer.write((byte) 'e'); - - byte[] bytes = new byte[5]; - buffer.read(bytes); - assertThat(bytes).isEqualTo(new byte[] {'a', 'b', 'c', 'd', 'e'}); - - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void expand(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(1); - buffer.write((byte) 'a'); - assertThat(buffer.capacity()).isEqualTo(1); - buffer.write((byte) 'b'); - - assertThat(buffer.capacity()).isGreaterThan(1); - - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void writeByteBuffer(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer1 = createDataBuffer(1); - buffer1.write((byte) 'a'); - ByteBuffer buffer2 = createByteBuffer(2); - buffer2.put((byte) 'b'); - buffer2.flip(); - ByteBuffer buffer3 = createByteBuffer(3); - buffer3.put((byte) 'c'); - buffer3.flip(); - - buffer1.write(buffer2, buffer3); - buffer1.write((byte) 'd'); // make sure the write index is correctly set - - assertThat(buffer1.readableByteCount()).isEqualTo(4); - byte[] result = new byte[4]; - buffer1.read(result); - - assertThat(result).isEqualTo(new byte[] {'a', 'b', 'c', 'd'}); - - release(buffer1); - } - - private ByteBuffer createByteBuffer(int capacity) { - return ByteBuffer.allocate(capacity); - } - - @ParameterizedDataBufferAllocatingTest - void writeDataBuffer(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer1 = createDataBuffer(1); - buffer1.write((byte) 'a'); - DataBuffer buffer2 = createDataBuffer(2); - buffer2.write((byte) 'b'); - DataBuffer buffer3 = createDataBuffer(3); - buffer3.write((byte) 'c'); - - buffer1.write(buffer2, buffer3); - buffer1.write((byte) 'd'); // make sure the write index is correctly set - - assertThat(buffer1.readableByteCount()).isEqualTo(4); - byte[] result = new byte[4]; - buffer1.read(result); - - assertThat(result).isEqualTo(new byte[] {'a', 'b', 'c', 'd'}); - - release(buffer1, buffer2, buffer3); - } - - @ParameterizedDataBufferAllocatingTest - void toByteBufferDestination(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(4); - buffer.write(new byte[] {'a', 'b', 'c'}); - - ByteBuffer byteBuffer = createByteBuffer(2); - buffer.toByteBuffer(1, byteBuffer, 0, 2); - assertThat(byteBuffer.capacity()).isEqualTo(2); - assertThat(byteBuffer.remaining()).isEqualTo(2); - - byte[] resultBytes = new byte[2]; - byteBuffer.get(resultBytes); - assertThat(resultBytes).isEqualTo(new byte[] {'b', 'c'}); - - assertThatExceptionOfType(IndexOutOfBoundsException.class) - .isThrownBy(() -> buffer.toByteBuffer(0, byteBuffer, 0, 3)); - - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void readableByteBuffers(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(3); - dataBuffer.write("abc".getBytes(StandardCharsets.UTF_8)); - dataBuffer.readPosition(1); - dataBuffer.writePosition(2); - - byte[] result = new byte[1]; - try (var iterator = dataBuffer.readableByteBuffers()) { - assertThat(iterator).hasNext(); - int i = 0; - while (iterator.hasNext()) { - ByteBuffer byteBuffer = iterator.next(); - assertThat(byteBuffer.position()).isEqualTo(0); - assertThat(byteBuffer.limit()).isEqualTo(1); - assertThat(byteBuffer.capacity()).isEqualTo(1); - assertThat(byteBuffer.remaining()).isEqualTo(1); - - byteBuffer.get(result, i, 1); - - assertThat(iterator).isExhausted(); - } - } - - assertThat(result).containsExactly('b'); - - release(dataBuffer); - } - - @ParameterizedDataBufferAllocatingTest - void readableByteBuffersJoined(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer dataBuffer = - this.bufferFactory.join( - Arrays.asList(stringBuffer("a"), stringBuffer("b"), stringBuffer("c"))); - - byte[] result = new byte[3]; - try (var iterator = dataBuffer.readableByteBuffers()) { - assertThat(iterator).hasNext(); - int i = 0; - while (iterator.hasNext()) { - ByteBuffer byteBuffer = iterator.next(); - int len = byteBuffer.remaining(); - byteBuffer.get(result, i, len); - i += len; - assertThatException().isThrownBy(() -> byteBuffer.put((byte) 'd')); - } - } - - assertThat(result).containsExactly('a', 'b', 'c'); - - release(dataBuffer); - } - - @ParameterizedDataBufferAllocatingTest - void writableByteBuffers(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(3); - dataBuffer.write("ab".getBytes(StandardCharsets.UTF_8)); - dataBuffer.readPosition(1); - - try (DataBuffer.ByteBufferIterator iterator = dataBuffer.writableByteBuffers()) { - assertThat(iterator).hasNext(); - ByteBuffer byteBuffer = iterator.next(); - assertThat(byteBuffer.position()).isEqualTo(0); - assertThat(byteBuffer.limit()).isEqualTo(1); - assertThat(byteBuffer.capacity()).isEqualTo(1); - assertThat(byteBuffer.remaining()).isEqualTo(1); - - byteBuffer.put((byte) 'c'); - dataBuffer.writePosition(3); - - assertThat(iterator).isExhausted(); - } - byte[] result = new byte[2]; - dataBuffer.read(result); - assertThat(result).containsExactly('b', 'c'); - - release(dataBuffer); - } - - @ParameterizedDataBufferAllocatingTest - void indexOf(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(3); - buffer.write(new byte[] {'a', 'b', 'c'}); - - int result = buffer.indexOf(b -> b == 'c', 0); - assertThat(result).isEqualTo(2); - - result = buffer.indexOf(b -> b == 'c', Integer.MIN_VALUE); - assertThat(result).isEqualTo(2); - - result = buffer.indexOf(b -> b == 'c', Integer.MAX_VALUE); - assertThat(result).isEqualTo(-1); - - result = buffer.indexOf(b -> b == 'z', 0); - assertThat(result).isEqualTo(-1); - - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void lastIndexOf(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(3); - buffer.write(new byte[] {'a', 'b', 'c'}); - - int result = buffer.lastIndexOf(b -> b == 'b', 2); - assertThat(result).isEqualTo(1); - - result = buffer.lastIndexOf(b -> b == 'c', 2); - assertThat(result).isEqualTo(2); - - result = buffer.lastIndexOf(b -> b == 'b', Integer.MAX_VALUE); - assertThat(result).isEqualTo(1); - - result = buffer.lastIndexOf(b -> b == 'c', Integer.MAX_VALUE); - assertThat(result).isEqualTo(2); - - result = buffer.lastIndexOf(b -> b == 'b', Integer.MIN_VALUE); - assertThat(result).isEqualTo(-1); - - result = buffer.lastIndexOf(b -> b == 'c', Integer.MIN_VALUE); - assertThat(result).isEqualTo(-1); - - result = buffer.lastIndexOf(b -> b == 'z', 0); - assertThat(result).isEqualTo(-1); - - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest - void split(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = createDataBuffer(3); - buffer.write(new byte[] {'a', 'b'}); - - assertThatException().isThrownBy(() -> buffer.split(-1)); - assertThatException().isThrownBy(() -> buffer.split(4)); - - DataBuffer split = buffer.split(1); - - assertThat(split.readPosition()).isEqualTo(0); - assertThat(split.writePosition()).isEqualTo(1); - assertThat(split.capacity()).isEqualTo(1); - assertThat(split.readableByteCount()).isEqualTo(1); - byte[] bytes = new byte[1]; - split.read(bytes); - assertThat(bytes).containsExactly('a'); - - assertThat(buffer.readPosition()).isEqualTo(0); - assertThat(buffer.writePosition()).isEqualTo(1); - assertThat(buffer.capacity()).isEqualTo(2); - - buffer.write((byte) 'c'); - assertThat(buffer.readableByteCount()).isEqualTo(2); - bytes = new byte[2]; - buffer.read(bytes); - - assertThat(bytes).isEqualTo(new byte[] {'b', 'c'}); - - DataBuffer buffer2 = createDataBuffer(1); - buffer2.write(new byte[] {'a'}); - DataBuffer split2 = buffer2.split(1); - - assertThat(split2.readPosition()).isEqualTo(0); - assertThat(split2.writePosition()).isEqualTo(1); - assertThat(split2.capacity()).isEqualTo(1); - assertThat(split2.readableByteCount()).isEqualTo(1); - bytes = new byte[1]; - split2.read(bytes); - assertThat(bytes).containsExactly('a'); - - assertThat(buffer2.readPosition()).isEqualTo(0); - assertThat(buffer2.writePosition()).isEqualTo(0); - assertThat(buffer2.capacity()).isEqualTo(0); - assertThat(buffer.readableByteCount()).isEqualTo(0); - - release(buffer, buffer2, split, split2); - } - - @ParameterizedDataBufferAllocatingTest - void join(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer composite = - this.bufferFactory.join( - Arrays.asList(stringBuffer("a"), stringBuffer("b"), stringBuffer("c"))); - assertThat(composite.readableByteCount()).isEqualTo(3); - byte[] bytes = new byte[3]; - composite.read(bytes); - - assertThat(bytes).isEqualTo(new byte[] {'a', 'b', 'c'}); - - release(composite); - } - - @ParameterizedDataBufferAllocatingTest - void getByte(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = stringBuffer("abc"); - - assertThat(buffer.getByte(0)).isEqualTo((byte) 'a'); - assertThat(buffer.getByte(1)).isEqualTo((byte) 'b'); - assertThat(buffer.getByte(2)).isEqualTo((byte) 'c'); - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> buffer.getByte(-1)); - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> buffer.getByte(3)); - - release(buffer); - } - - @ParameterizedDataBufferAllocatingTest // gh-31605 - void shouldHonorSourceBuffersReadPosition(DataBufferFactory bufferFactory) { - DataBuffer dataBuffer = bufferFactory.wrap("ab".getBytes(StandardCharsets.UTF_8)); - dataBuffer.readPosition(1); - - ByteBuffer byteBuffer = ByteBuffer.allocate(dataBuffer.readableByteCount()); - dataBuffer.toByteBuffer(byteBuffer); - - assertThat(StandardCharsets.UTF_8.decode(byteBuffer).toString()).isEqualTo("b"); - } - - @ParameterizedDataBufferAllocatingTest // gh-31873 - void repeatedWrites(DataBufferFactory bufferFactory) { - super.bufferFactory = bufferFactory; - - DataBuffer buffer = bufferFactory.allocateBuffer(256); - String name = "Müller"; - int repeatCount = 19; - for (int i = 0; i < repeatCount; i++) { - buffer.write(name, StandardCharsets.UTF_8); - } - String result = buffer.toString(StandardCharsets.UTF_8); - String expected = name.repeat(repeatCount); - assertThat(result).isEqualTo(expected); - - release(buffer); - } -} diff --git a/tests/src/test/java/io/jooby/buffer/DefaultDataBufferTests.java b/tests/src/test/java/io/jooby/buffer/DefaultDataBufferTests.java deleted file mode 100644 index 7ec6226f21..0000000000 --- a/tests/src/test/java/io/jooby/buffer/DefaultDataBufferTests.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import static io.jooby.buffer.DataBufferUtils.release; -import static org.assertj.core.api.Assertions.assertThat; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link DefaultDataBuffer}. - * - * @author Injae Kim - * @since 6.2 - */ -class DefaultDataBufferTests { - - private final DefaultDataBufferFactory bufferFactory = new DefaultDataBufferFactory(); - - @Test // gh-30967 - void getNativeBuffer() { - DefaultDataBuffer dataBuffer = this.bufferFactory.allocateBuffer(256); - dataBuffer.write("0123456789", StandardCharsets.UTF_8); - - byte[] result = new byte[7]; - dataBuffer.read(result); - assertThat(result).isEqualTo("0123456".getBytes(StandardCharsets.UTF_8)); - - ByteBuffer nativeBuffer = dataBuffer.getNativeBuffer(); - assertThat(nativeBuffer.position()).isEqualTo(7); - assertThat(dataBuffer.readPosition()).isEqualTo(7); - assertThat(nativeBuffer.limit()).isEqualTo(10); - assertThat(dataBuffer.writePosition()).isEqualTo(10); - assertThat(nativeBuffer.capacity()).isEqualTo(256); - assertThat(dataBuffer.capacity()).isEqualTo(256); - - release(dataBuffer); - } -} diff --git a/tests/src/test/java/io/jooby/buffer/LeakAwareDataBuffer.java b/tests/src/test/java/io/jooby/buffer/LeakAwareDataBuffer.java deleted file mode 100644 index 3876ad8aa8..0000000000 --- a/tests/src/test/java/io/jooby/buffer/LeakAwareDataBuffer.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -/** - * DataBuffer implementation created by {@link LeakAwareDataBufferFactory}. - * - * @author Arjen Poutsma - */ -class LeakAwareDataBuffer extends DataBufferWrapper implements PooledDataBuffer { - - private final AssertionError leakError; - - private final LeakAwareDataBufferFactory dataBufferFactory; - - LeakAwareDataBuffer(DataBuffer delegate, LeakAwareDataBufferFactory dataBufferFactory) { - super(delegate); - Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null"); - this.dataBufferFactory = dataBufferFactory; - this.leakError = createLeakError(delegate); - } - - private static AssertionError createLeakError(DataBuffer delegate) { - String message = - String.format( - "DataBuffer leak detected: {%s} has not been released.%n" - + "Stack trace of buffer allocation statement follows:", - delegate); - AssertionError result = new AssertionError(message); - // remove first four irrelevant stack trace elements - StackTraceElement[] oldTrace = result.getStackTrace(); - StackTraceElement[] newTrace = new StackTraceElement[oldTrace.length - 4]; - System.arraycopy(oldTrace, 4, newTrace, 0, oldTrace.length - 4); - result.setStackTrace(newTrace); - return result; - } - - AssertionError leakError() { - return this.leakError; - } - - @Override - public boolean isAllocated() { - DataBuffer delegate = dataBuffer(); - return delegate instanceof PooledDataBuffer && ((PooledDataBuffer) delegate).isAllocated(); - } - - @Override - public PooledDataBuffer retain() { - DataBufferUtils.retain(dataBuffer()); - return this; - } - - @Override - public PooledDataBuffer touch(Object hint) { - DataBufferUtils.touch(dataBuffer(), hint); - return this; - } - - @Override - public boolean release() { - DataBufferUtils.release(dataBuffer()); - return isAllocated(); - } - - @Override - public LeakAwareDataBufferFactory factory() { - return this.dataBufferFactory; - } - - @Override - public String toString() { - return String.format("LeakAwareDataBuffer (%s)", dataBuffer()); - } -} diff --git a/tests/src/test/java/io/jooby/buffer/LeakAwareDataBufferFactory.java b/tests/src/test/java/io/jooby/buffer/LeakAwareDataBufferFactory.java deleted file mode 100644 index 5872e92dd4..0000000000 --- a/tests/src/test/java/io/jooby/buffer/LeakAwareDataBufferFactory.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import java.nio.ByteBuffer; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import io.jooby.netty.buffer.NettyDataBufferFactory; -import io.netty.buffer.PooledByteBufAllocator; - -/** - * Implementation of the {@code DataBufferFactory} interface that keeps track of memory leaks. - * - *

Useful for unit tests that handle data buffers. Simply inherit from {@link - * AbstractLeakCheckingTests} or call {@link #checkForLeaks()} in a JUnit after method - * yourself, and any buffers that have not been released will result in an {@link AssertionError}. - * - * @author Arjen Poutsma - * @see LeakAwareDataBufferFactory - */ -public class LeakAwareDataBufferFactory implements DataBufferFactory { - - private static final Log logger = LogFactory.getLog(LeakAwareDataBufferFactory.class); - - private final DataBufferFactory delegate; - - private final List created = new ArrayList<>(); - - private final AtomicBoolean trackCreated = new AtomicBoolean(true); - - /** - * Creates a new {@code LeakAwareDataBufferFactory} by wrapping a {@link - * DefaultDataBufferFactory}. - */ - public LeakAwareDataBufferFactory() { - this(new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT)); - } - - /** - * Creates a new {@code LeakAwareDataBufferFactory} by wrapping the given delegate. - * - * @param delegate the delegate buffer factory to wrap. - */ - public LeakAwareDataBufferFactory(DataBufferFactory delegate) { - Assert.notNull(delegate, "Delegate must not be null"); - this.delegate = delegate; - } - - @Override - public int getDefaultInitialCapacity() { - return delegate.getDefaultInitialCapacity(); - } - - @Override - public DataBufferFactory setDefaultInitialCapacity(int defaultInitialCapacity) { - delegate.setDefaultInitialCapacity(defaultInitialCapacity); - return this; - } - - /** - * Checks whether all the data buffers allocated by this factory have also been released. If not, - * then an {@link AssertionError} is thrown. Typically used from a JUnit after method. - */ - public void checkForLeaks() { - checkForLeaks(Duration.ofSeconds(0)); - } - - /** - * Variant of {@link #checkForLeaks()} with the option to wait for buffer release. - * - * @param timeout how long to wait for buffers to be released; 0 for no waiting - */ - public void checkForLeaks(Duration timeout) { - this.trackCreated.set(false); - Instant start = Instant.now(); - while (true) { - if (this.created.stream().noneMatch(LeakAwareDataBuffer::isAllocated)) { - return; - } - if (Instant.now().isBefore(start.plus(timeout))) { - try { - Thread.sleep(50); - } catch (InterruptedException ex) { - // ignore - } - continue; - } - List errors = - this.created.stream() - .filter(LeakAwareDataBuffer::isAllocated) - .map(LeakAwareDataBuffer::leakError) - .toList(); - - throw new AssertionError(errors.size() + " buffer leaks detected (see logs above)"); - } - } - - @Override - @Deprecated - public DataBuffer allocateBuffer() { - return createLeakAwareDataBuffer(this.delegate.allocateBuffer()); - } - - @Override - public DataBuffer allocateBuffer(int initialCapacity) { - return createLeakAwareDataBuffer(this.delegate.allocateBuffer(initialCapacity)); - } - - private DataBuffer createLeakAwareDataBuffer(DataBuffer delegateBuffer) { - LeakAwareDataBuffer dataBuffer = new LeakAwareDataBuffer(delegateBuffer, this); - if (this.trackCreated.get()) { - this.created.add(dataBuffer); - } - return dataBuffer; - } - - @Override - public DataBuffer wrap(ByteBuffer byteBuffer) { - return this.delegate.wrap(byteBuffer); - } - - @Override - public DataBuffer wrap(byte[] bytes) { - return this.delegate.wrap(bytes); - } - - @Override - public DataBuffer wrap(byte[] bytes, int offset, int length) { - return this.delegate.wrap(bytes, offset, length); - } - - @Override - public DataBuffer join(List dataBuffers) { - // Remove LeakAwareDataBuffer wrapper so delegate can find native buffers - dataBuffers = - dataBuffers.stream() - .map(o -> o instanceof LeakAwareDataBuffer ? ((LeakAwareDataBuffer) o).dataBuffer() : o) - .toList(); - return new LeakAwareDataBuffer(this.delegate.join(dataBuffers), this); - } - - @Override - public boolean isDirect() { - return this.delegate.isDirect(); - } -} diff --git a/tests/src/test/java/io/jooby/buffer/LeakAwareDataBufferFactoryTests.java b/tests/src/test/java/io/jooby/buffer/LeakAwareDataBufferFactoryTests.java deleted file mode 100644 index 0b1e6eaa77..0000000000 --- a/tests/src/test/java/io/jooby/buffer/LeakAwareDataBufferFactoryTests.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import static io.jooby.buffer.DataBufferUtils.release; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -/** - * @author Arjen Poutsma - */ -class LeakAwareDataBufferFactoryTests { - - private final LeakAwareDataBufferFactory bufferFactory = new LeakAwareDataBufferFactory(); - - @Test() - @SuppressWarnings("deprecation") - void leak() { - DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(); - try { - Assertions.assertThrows(AssertionError.class, this.bufferFactory::checkForLeaks); - } finally { - release(dataBuffer); - } - } - - @Test - void noLeak() { - DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(256); - release(dataBuffer); - this.bufferFactory.checkForLeaks(); - } -} diff --git a/tests/src/test/java/io/jooby/buffer/LimitedDataBufferListTests.java b/tests/src/test/java/io/jooby/buffer/LimitedDataBufferListTests.java deleted file mode 100644 index d1186ac5fc..0000000000 --- a/tests/src/test/java/io/jooby/buffer/LimitedDataBufferListTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link LimitedDataBufferList}. - * - * @author Rossen Stoyanchev - * @since 5.1.11 - */ -class LimitedDataBufferListTests { - - @Test - void limitEnforced() { - LimitedDataBufferList list = new LimitedDataBufferList(5); - - assertThatThrownBy(() -> list.add(toDataBuffer("123456"))) - .isInstanceOf(DataBufferLimitException.class); - assertThat(list).isEmpty(); - } - - @Test - void limitIgnored() { - new LimitedDataBufferList(-1).add(toDataBuffer("123456")); - } - - @Test - void clearResetsCount() { - LimitedDataBufferList list = new LimitedDataBufferList(5); - list.add(toDataBuffer("12345")); - list.clear(); - list.add(toDataBuffer("12345")); - } - - private static DataBuffer toDataBuffer(String value) { - byte[] bytes = value.getBytes(StandardCharsets.UTF_8); - return DefaultDataBufferFactory.sharedInstance.wrap(bytes); - } -} diff --git a/tests/src/test/java/io/jooby/buffer/PooledDataBufferTests.java b/tests/src/test/java/io/jooby/buffer/PooledDataBufferTests.java deleted file mode 100644 index cd0d4d42ef..0000000000 --- a/tests/src/test/java/io/jooby/buffer/PooledDataBufferTests.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.buffer; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import io.jooby.netty.buffer.NettyDataBufferFactory; -import io.netty.buffer.PooledByteBufAllocator; -import io.netty.buffer.UnpooledByteBufAllocator; - -/** - * @author Arjen Poutsma - * @author Sam Brannen - */ -class PooledDataBufferTests { - - @Nested - class UnpooledByteBufAllocatorWithPreferDirectTrueTests implements PooledDataBufferTestingTrait { - - @Override - public DataBufferFactory createDataBufferFactory() { - return new NettyDataBufferFactory(new UnpooledByteBufAllocator(true)); - } - } - - @Nested - class UnpooledByteBufAllocatorWithPreferDirectFalseTests implements PooledDataBufferTestingTrait { - - @Override - public DataBufferFactory createDataBufferFactory() { - return new NettyDataBufferFactory(new UnpooledByteBufAllocator(true)); - } - } - - @Nested - class PooledByteBufAllocatorWithPreferDirectTrueTests implements PooledDataBufferTestingTrait { - - @Override - public DataBufferFactory createDataBufferFactory() { - return new NettyDataBufferFactory(new PooledByteBufAllocator(true)); - } - } - - @Nested - class PooledByteBufAllocatorWithPreferDirectFalseTests implements PooledDataBufferTestingTrait { - - @Override - public DataBufferFactory createDataBufferFactory() { - return new NettyDataBufferFactory(new PooledByteBufAllocator(true)); - } - } - - interface PooledDataBufferTestingTrait { - - DataBufferFactory createDataBufferFactory(); - - default PooledDataBuffer createDataBuffer(int capacity) { - return (PooledDataBuffer) createDataBufferFactory().allocateBuffer(capacity); - } - - @Test - default void retainAndRelease() { - PooledDataBuffer buffer = createDataBuffer(1); - buffer.write((byte) 'a'); - - buffer.retain(); - assertThat(buffer.release()).isFalse(); - assertThat(buffer.release()).isTrue(); - } - - @Test - default void tooManyReleases() { - PooledDataBuffer buffer = createDataBuffer(1); - buffer.write((byte) 'a'); - - buffer.release(); - assertThatIllegalStateException().isThrownBy(buffer::release); - } - } -} diff --git a/tests/src/test/java/io/jooby/i1570/App1570.java b/tests/src/test/java/io/jooby/i1570/App1570.java index 9202d2c3cd..db84107300 100644 --- a/tests/src/test/java/io/jooby/i1570/App1570.java +++ b/tests/src/test/java/io/jooby/i1570/App1570.java @@ -8,12 +8,13 @@ import io.jooby.Jooby; import io.jooby.MediaType; import io.jooby.Session; +import io.jooby.SessionToken; import io.jooby.jwt.JwtSessionStore; public class App1570 extends Jooby { { String secret = "9968518B15AD9DCD1B33B54316416341CA518B15AD"; - setSessionStore(new JwtSessionStore(secret)); + setSessionStore(new JwtSessionStore(SessionToken.header("sid"), secret)); get( "/registerClient/{name}", diff --git a/tests/src/test/java/io/jooby/i1570/Issue1570.java b/tests/src/test/java/io/jooby/i1570/Issue1570.java index e9d199bb85..d9b06eea50 100644 --- a/tests/src/test/java/io/jooby/i1570/Issue1570.java +++ b/tests/src/test/java/io/jooby/i1570/Issue1570.java @@ -13,10 +13,10 @@ import io.jooby.Context; import io.jooby.Session; -import io.jooby.Value; import io.jooby.test.MockContext; import io.jooby.test.MockRouter; import io.jooby.test.MockSession; +import io.jooby.value.Value; public class Issue1570 { diff --git a/tests/src/test/java/io/jooby/i1937/Issue1937.java b/tests/src/test/java/io/jooby/i1937/Issue1937.java index c01b894de7..7084786497 100644 --- a/tests/src/test/java/io/jooby/i1937/Issue1937.java +++ b/tests/src/test/java/io/jooby/i1937/Issue1937.java @@ -10,26 +10,11 @@ import io.jooby.Context; import io.jooby.exception.RegistryException; -import io.jooby.guice.GuiceModule; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; public class Issue1937 { - @ServerTest - public void shouldFailIfContextAsServiceWasNotCalled(ServerTestRunner runner) { - runner - .define( - app -> - app.get( - "/i1937", - ctx -> { - app.require(Context.class); - return "OK"; - })) - .ready(http -> http.get("/i1937", rsp -> assertEquals(500, rsp.code()))); - } - @ServerTest public void shouldWorkIfContextAsServiceWasCalled(ServerTestRunner runner) { runner @@ -41,8 +26,6 @@ public void shouldWorkIfContextAsServiceWasCalled(ServerTestRunner runner) { app.require(Context.class); return "OK"; }); - - app.setContextAsService(true); }) .ready(http -> http.get("/i1937", rsp -> assertEquals(200, rsp.code()))); } @@ -52,26 +35,6 @@ public void shouldThrowIfOutOfScope(ServerTestRunner runner) { runner .define( app -> { - app.setContextAsService(true); - app.onStarted( - () -> { - Throwable t = - assertThrows(RegistryException.class, () -> app.require(Context.class)); - assertEquals( - t.getMessage(), - "Context is not available. Are you getting it from request scope?"); - }); - }) - .ready(http -> {}); - } - - @ServerTest - public void shouldThrowIfOutOfScopeWithDI(ServerTestRunner runner) { - runner - .define( - app -> { - app.install(new GuiceModule()); - app.setContextAsService(true); app.onStarted( () -> { Throwable t = diff --git a/tests/src/test/java/io/jooby/i2325/Issue2325.java b/tests/src/test/java/io/jooby/i2325/Issue2325.java index ea72634153..6fc67fc421 100644 --- a/tests/src/test/java/io/jooby/i2325/Issue2325.java +++ b/tests/src/test/java/io/jooby/i2325/Issue2325.java @@ -18,7 +18,8 @@ public void shouldNamedParamWorkWithCustomValueConverter(ServerTestRunner runner runner .define( app -> { - app.converter(new VC2325()); + var factory = app.getValueFactory(); + factory.put(MyID2325.class, new VC2325()); app.mvc(new C2325_()); }) diff --git a/tests/src/test/java/io/jooby/i2325/VC2325.java b/tests/src/test/java/io/jooby/i2325/VC2325.java index 2136fb4312..a950fdf505 100644 --- a/tests/src/test/java/io/jooby/i2325/VC2325.java +++ b/tests/src/test/java/io/jooby/i2325/VC2325.java @@ -5,19 +5,19 @@ */ package io.jooby.i2325; -import org.jetbrains.annotations.NotNull; +import java.lang.reflect.Type; -import io.jooby.Value; -import io.jooby.ValueConverter; +import org.jetbrains.annotations.NotNull; -public class VC2325 implements ValueConverter { - @Override - public boolean supports(@NotNull Class type) { - return type == MyID2325.class; - } +import io.jooby.QueryString; +import io.jooby.value.ConversionHint; +import io.jooby.value.Converter; +import io.jooby.value.Value; +public class VC2325 implements Converter { @Override - public Object convert(@NotNull Value value, @NotNull Class type) { - return new MyID2325(value.value()); + public Object convert(@NotNull Type type, @NotNull Value value, @NotNull ConversionHint hint) { + var v = value instanceof QueryString query ? query.get("value").value() : value.value(); + return new MyID2325(v); } } diff --git a/tests/src/test/java/io/jooby/i2363/Issue2363.java b/tests/src/test/java/io/jooby/i2363/Issue2363.java index c09b47c37b..949dd0e668 100644 --- a/tests/src/test/java/io/jooby/i2363/Issue2363.java +++ b/tests/src/test/java/io/jooby/i2363/Issue2363.java @@ -34,10 +34,9 @@ public void shouldAllowExpectAndContinue(ServerTestRunner runner) { runner .define( app -> { - app.setServerOptions(new ServerOptions().setExpectContinue(true)); - app.post("/2363", ctx -> new String(ctx.file("f").bytes(), StandardCharsets.UTF_8)); }) + .options(new ServerOptions().setExpectContinue(true)) .ready( http -> { http.header("Expect", "100-continue") diff --git a/tests/src/test/java/io/jooby/i2399/Issue2399.java b/tests/src/test/java/io/jooby/i2399/Issue2399.java index c53a6d073b..7438446563 100644 --- a/tests/src/test/java/io/jooby/i2399/Issue2399.java +++ b/tests/src/test/java/io/jooby/i2399/Issue2399.java @@ -18,11 +18,6 @@ public void shouldHttp2NotLostStreamIdOnException(ServerTestRunner runner) { runner .define( app -> { - ServerOptions options = new ServerOptions(); - options.setHttp2(true); - options.setSecurePort(8443); - app.setServerOptions(options); - app.error( (ctx, cause, code) -> { ctx.send(cause.getMessage()); @@ -30,6 +25,7 @@ public void shouldHttp2NotLostStreamIdOnException(ServerTestRunner runner) { app.get("/2399", ctx -> ctx.query("q").value()); }) + .options(new ServerOptions().setHttp2(true).setSecurePort(8443)) .ready( (http, https) -> { https.get( diff --git a/tests/src/test/java/io/jooby/i2557/Issue2557.java b/tests/src/test/java/io/jooby/i2557/Issue2557.java index 26ab79227d..703c1b33bf 100644 --- a/tests/src/test/java/io/jooby/i2557/Issue2557.java +++ b/tests/src/test/java/io/jooby/i2557/Issue2557.java @@ -10,16 +10,15 @@ import java.util.UUID; -import io.jooby.annotation.EmptyBean; import io.jooby.jackson.JacksonModule; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; +import io.jooby.value.ConversionHint; import okhttp3.FormBody; public class Issue2557 { public record MyStrict(UUID uuid) {} - @EmptyBean public record MyFlexible(UUID uuid) {} public record AllGoodWithPartialMatching(UUID uuid, String name) {} @@ -39,7 +38,8 @@ public void shouldWorkWithEmptyUUID(ServerTestRunner runner) { app.post( "/2557/flexible", ctx -> { - return ctx.form(MyFlexible.class); + return ctx.getValueFactory() + .convert(MyFlexible.class, ctx.form(), ConversionHint.Empty); }); app.post( "/2557/partial", @@ -53,12 +53,12 @@ public void shouldWorkWithEmptyUUID(ServerTestRunner runner) { "/2557/strict", new FormBody.Builder().add("uuid", "").build(), rsp -> { + var body = rsp.body().string(); assertTrue( - rsp.body() - .string() - .contains( - "Cannot convert value: 'null', to:" - + " 'io.jooby.i2557.Issue2557$MyStrict'")); + body.contains( + "Cannot convert value: 'null', to:" + + " 'io.jooby.i2557.Issue2557$MyStrict'"), + body); }); http.post( "/2557/flexible", diff --git a/tests/src/test/java/io/jooby/i2613/Issue2613.java b/tests/src/test/java/io/jooby/i2613/Issue2613.java index d208f9481f..ddd458e005 100644 --- a/tests/src/test/java/io/jooby/i2613/Issue2613.java +++ b/tests/src/test/java/io/jooby/i2613/Issue2613.java @@ -10,13 +10,14 @@ import java.nio.charset.StandardCharsets; import com.google.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.MediaType; import io.jooby.MessageEncoder; -import io.jooby.buffer.DataBuffer; import io.jooby.jackson.JacksonModule; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; +import io.jooby.output.Output; public class Issue2613 { @@ -29,10 +30,10 @@ public String html() { public static class ThemeResultEncoder implements MessageEncoder { @Override - public DataBuffer encode(Context ctx, Object value) throws Exception { + public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { if (value instanceof ThemeResult) { ctx.setDefaultResponseType(MediaType.html); - return ctx.getBufferFactory() + return ctx.getOutputFactory() .wrap(((ThemeResult) value).html().getBytes(StandardCharsets.UTF_8)); } return null; diff --git a/tests/src/test/java/io/jooby/i2806/Issue2806.java b/tests/src/test/java/io/jooby/i2806/Issue2806.java index 3b5ff321e5..c305b18e01 100644 --- a/tests/src/test/java/io/jooby/i2806/Issue2806.java +++ b/tests/src/test/java/io/jooby/i2806/Issue2806.java @@ -15,6 +15,7 @@ import io.jooby.jackson.JacksonModule; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; +import io.jooby.output.OutputOptions; import okhttp3.MediaType; import okhttp3.RequestBody; @@ -26,13 +27,12 @@ public void renderShouldWorkFromErrorHandlerWhenLargeRequestAreSent(ServerTestRu Arrays.fill(chars, 'S'); String _19kb = new String(chars); runner + .options( + new ServerOptions() + .setOutput(new OutputOptions().setSize(ServerOptions._16KB / 2)) + .setMaxRequestSize(ServerOptions._16KB)) .define( app -> { - app.setServerOptions( - new ServerOptions() - .setBufferSize(ServerOptions._16KB / 2) - .setMaxRequestSize(ServerOptions._16KB)); - app.install(new JacksonModule()); app.error( diff --git a/tests/src/test/java/io/jooby/i3500/WidgetService.java b/tests/src/test/java/io/jooby/i3500/WidgetService.java index 59330c3ce9..abc9f6c164 100644 --- a/tests/src/test/java/io/jooby/i3500/WidgetService.java +++ b/tests/src/test/java/io/jooby/i3500/WidgetService.java @@ -24,10 +24,6 @@ public WidgetService() { mount(new WidgetRouter()); } - - public static void main(String[] args) { - new WidgetService().start(); - } } class WidgetRouter extends Jooby { diff --git a/tests/src/test/java/io/jooby/i3554/Issue3554.java b/tests/src/test/java/io/jooby/i3554/Issue3554.java index 7ad0f5595a..78657decac 100644 --- a/tests/src/test/java/io/jooby/i3554/Issue3554.java +++ b/tests/src/test/java/io/jooby/i3554/Issue3554.java @@ -23,13 +23,11 @@ public class Issue3554 { @ServerTest(executionMode = ExecutionMode.EVENT_LOOP) public void shouldNotThrowErrorOnCompletableWithSideEffect(ServerTestRunner runner) { runner + .options(new ServerOptions().setPort(9000).setDefaultHeaders(false)) .define( app -> { ExecutorService threadPool = Executors.newSingleThreadExecutor(); - var serverOptions = new ServerOptions().setPort(9000).setDefaultHeaders(false); - app.setServerOptions(serverOptions); - app.use(ReactiveSupport.concurrent()); app.onStop(threadPool::shutdown); diff --git a/tests/src/test/java/io/jooby/i3721/Issue3721.java b/tests/src/test/java/io/jooby/i3721/Issue3721.java index 08ef6cc2d2..6d45f5382c 100644 --- a/tests/src/test/java/io/jooby/i3721/Issue3721.java +++ b/tests/src/test/java/io/jooby/i3721/Issue3721.java @@ -19,9 +19,9 @@ public class Issue3721 { @ServerTest public void shouldAllowToSetMaxHeaderSize(ServerTestRunner runner) { runner + .options(new ServerOptions().setMaxHeaderSize(ServerOptions._16KB)) .define( app -> { - app.setServerOptions(new ServerOptions().setMaxHeaderSize(ServerOptions._16KB)); app.get( "/3721", ctx -> { @@ -45,9 +45,9 @@ public void shouldAllowToSetMaxHeaderSize(ServerTestRunner runner) { @ServerTest public void shouldCheckErrorOnLargeHeaderSize(ServerTestRunner runner) { runner + .options(new ServerOptions().setMaxHeaderSize(ServerOptions._4KB)) .define( app -> { - app.setServerOptions(new ServerOptions().setMaxHeaderSize(ServerOptions._4KB)); app.get( "/3721", ctx -> { diff --git a/tests/src/test/java/io/jooby/junit/ServerExtensionImpl.java b/tests/src/test/java/io/jooby/junit/ServerExtensionImpl.java index c33252401b..121734320a 100644 --- a/tests/src/test/java/io/jooby/junit/ServerExtensionImpl.java +++ b/tests/src/test/java/io/jooby/junit/ServerExtensionImpl.java @@ -67,7 +67,7 @@ public Stream provideTestTemplateInvocationContex ServerTest serverTest = context.getRequiredTestMethod().getAnnotation(ServerTest.class); Class[] servers = serverTest.server(); if (servers.length == 0) { - servers = defaultServers(System.getProperty("jooby.server", "*")); + servers = SERVERS; } Set executionModes = EnumSet.copyOf(Arrays.asList(serverTest.executionMode())); if (executionModes.contains(ExecutionMode.DEFAULT) && executionModes.size() == 1) { @@ -107,15 +107,6 @@ public Stream provideTestTemplateInvocationContex .map(this::invocationContext); } - private Class[] defaultServers(String serverName) { - return switch (serverName) { - case "jetty" -> new Class[] {JettyServer.class}; - case "netty" -> new Class[] {NettyServer.class}; - case "undertow" -> new Class[] {UndertowServer.class}; - default -> SERVERS; - }; - } - private TestTemplateInvocationContext invocationContext(ServerInfo serverInfo) { return new TestTemplateInvocationContext() { @Override diff --git a/tests/src/test/java/io/jooby/junit/ServerProvider.java b/tests/src/test/java/io/jooby/junit/ServerProvider.java index 3bb60055a8..09ae8c25b8 100644 --- a/tests/src/test/java/io/jooby/junit/ServerProvider.java +++ b/tests/src/test/java/io/jooby/junit/ServerProvider.java @@ -8,14 +8,14 @@ import static java.util.stream.StreamSupport.stream; import java.util.ServiceLoader; -import java.util.function.Supplier; import io.jooby.Server; +import io.jooby.ServerOptions; import io.jooby.jetty.JettyServer; import io.jooby.netty.NettyServer; import io.jooby.undertow.UndertowServer; -public class ServerProvider implements Supplier { +public class ServerProvider { private Class serverClass; public ServerProvider(Class serverClass) { @@ -39,11 +39,15 @@ public String getName() { return serverClass.getSimpleName().replace("Server", ""); } - @Override - public Server get() { - return stream(ServiceLoader.load(Server.class).spliterator(), false) - .filter(s -> serverClass.isInstance(s)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Server not found: " + serverClass)); + public Server get(ServerOptions options) { + var server = + stream(ServiceLoader.load(Server.class).spliterator(), false) + .filter(s -> serverClass.isInstance(s)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Server not found: " + serverClass)); + if (options != null) { + server.setOptions(options); + } + return server; } } diff --git a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index 18db7540cd..61e4946cd6 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -17,20 +17,9 @@ import org.junit.jupiter.api.condition.DisabledOnOs; -import io.jooby.Context; -import io.jooby.DefaultErrorHandler; -import io.jooby.ErrorHandler; -import io.jooby.ExecutionMode; -import io.jooby.Jooby; -import io.jooby.LoggingService; -import io.jooby.Server; -import io.jooby.ServerOptions; -import io.jooby.SneakyThrows; -import io.jooby.StartupSummary; -import io.jooby.StatusCode; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.*; import io.jooby.internal.MutedServer; -import io.jooby.netty.NettyServer; +import io.jooby.output.OutputFactory; import io.jooby.test.WebClient; public class ServerTestRunner { @@ -46,6 +35,7 @@ public class ServerTestRunner { private final ExecutionMode executionMode; private final Method testMethod; + private ServerOptions serverOptions; private Supplier provider; @@ -64,14 +54,22 @@ public ServerTestRunner( public ServerTestRunner define(Consumer consumer) { use( () -> { - Jooby app = new Jooby(); - app.setExecutionMode(executionMode); + var app = new Jooby(); + if (app.getSessionStore() == SessionStore.UNSUPPORTED) { + // set default session + app.setSessionStore(SessionStore.memory(Cookie.session("jooby.sid"))); + } consumer.accept(app); return app; }); return this; } + public ServerTestRunner options(ServerOptions options) { + this.serverOptions = options; + return this; + } + public ServerTestRunner use(Supplier provider) { this.provider = provider; return this; @@ -85,16 +83,15 @@ public void ready(SneakyThrows.Consumer2 onReady) { if (disabled()) { return; } - Server server = this.server.get(); + Server server = this.server.get(serverOptions); String applogger = null; try { + setServerOptions(Optional.ofNullable(serverOptions).orElse(server.getOptions())); + setOutputFactory(server.getOutputFactory()); + setExecutionMode(Optional.ofNullable(executionMode).orElse(ExecutionMode.DEFAULT)); System.setProperty("___app_name__", testName); System.setProperty("___server_name__", server.getName()); var app = provider.get(); - if (!(server instanceof NettyServer)) { - app.setBufferFactory(new DefaultDataBufferFactory()); - } - Optional.ofNullable(executionMode).ifPresent(app::setExecutionMode); // Reduce log from maven build: var mavenBuild = System.getProperty("surefire.real.class.path", "").length() > 0; if (mavenBuild) { @@ -105,10 +102,6 @@ public void ready(SneakyThrows.Consumer2 onReady) { .orElseGet(ServerTestRunner::buildErrorHandler)); } - ServerOptions serverOptions = app.getServerOptions(); - if (serverOptions != null) { - server.setOptions(serverOptions); - } // HTTP/2 is off while testing unless explicit set ServerOptions options = server.getOptions(); options.setHttp2(Optional.ofNullable(options.isHttp2()).orElse(Boolean.FALSE)); @@ -138,12 +131,47 @@ public void ready(SneakyThrows.Consumer2 onReady) { } } catch (AssertionError x) { throw SneakyThrows.propagate(serverInfo(x)); + } catch (Exception e) { + throw SneakyThrows.propagate(e); } finally { if (applogger != null) { MutedServer.mute(server, applogger).stop(); } else { MutedServer.mute(server).stop(); } + setOutputFactory(null); + setExecutionMode(null); + setServerOptions(null); + } + } + + private void setServerOptions(ServerOptions options) { + try { + var field = Jooby.class.getDeclaredField("SERVER_OPTIONS"); + field.setAccessible(true); + field.set(null, options); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw SneakyThrows.propagate(e); + } + } + + private void setExecutionMode(ExecutionMode executionMode) { + try { + var field = Jooby.class.getDeclaredField("BOOT_EXECUTION_MODE"); + field.setAccessible(true); + field.set(null, executionMode); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw SneakyThrows.propagate(e); + } + } + + private static void setOutputFactory(OutputFactory bufferedOutputFactory) { + try { + var field = Jooby.class.getDeclaredField("OUTPUT_FACTORY"); + field.setAccessible(true); + field.set(null, bufferedOutputFactory); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw SneakyThrows.propagate(e); } } diff --git a/tests/src/test/java/io/jooby/problem/data/App.java b/tests/src/test/java/io/jooby/problem/data/App.java index 2f40bf6d73..44833ad3ae 100644 --- a/tests/src/test/java/io/jooby/problem/data/App.java +++ b/tests/src/test/java/io/jooby/problem/data/App.java @@ -132,7 +132,7 @@ public class App extends Jooby { get( "/throw-missing-value-exception", ctx -> { - ctx.getRouter().attribute("non-existed"); // throws MissingValueException + ctx.getRouter().getAttribute("non-existed"); // throws MissingValueException return ctx.send("Hello"); }); diff --git a/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index a824fb43ba..7ca636fc30 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -22,7 +22,6 @@ import java.io.Reader; import java.io.StringReader; import java.io.Writer; -import java.lang.reflect.Type; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -65,10 +64,9 @@ import io.jooby.Formdata; import io.jooby.InlineFile; import io.jooby.Jooby; -import io.jooby.MessageDecoder; import io.jooby.ModelAndView; import io.jooby.Router; -import io.jooby.RouterOption; +import io.jooby.RouterOptions; import io.jooby.SameSite; import io.jooby.ServerOptions; import io.jooby.ServiceKey; @@ -88,6 +86,7 @@ import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; import io.jooby.netty.NettyServer; +import io.jooby.output.OutputOptions; import io.jooby.rxjava3.Reactivex; import io.jooby.undertow.UndertowServer; import io.reactivex.rxjava3.core.Single; @@ -391,10 +390,9 @@ public void gzip(ServerTestRunner runner) throws IOException { + " ipsum a, scelerisque hendrerit lorem. Sed interdum nibh at ante consequat, vitae" + " fermentum augue luctus."; runner + .options(new ServerOptions().setCompressionLevel(ServerOptions.DEFAULT_COMPRESSION_LEVEL)) .define( app -> { - app.setServerOptions( - new ServerOptions().setCompressionLevel(ServerOptions.DEFAULT_COMPRESSION_LEVEL)); app.get("/gzip", ctx -> text); }) .ready( @@ -1008,11 +1006,7 @@ public void jsonVsRawOutput(ServerTestRunner runner) { return "{\"message\": \"raw\"}".getBytes(StandardCharsets.UTF_8); }); - app.get( - "/", - ctx -> { - return Arrays.asList(mapOf("message", "fooo")); - }); + app.get("/", ctx -> List.of(mapOf("message", "fooo"))); }); }) .ready( @@ -1060,14 +1054,12 @@ public String toString() { app.encoder( io.jooby.MediaType.json, (@NonNull Context ctx, @NonNull Object value) -> - ctx.getBufferFactory() - .wrap(("{" + value + "}").getBytes(StandardCharsets.UTF_8))); + ctx.getOutputFactory().wrap("{" + value + "}")); app.encoder( io.jooby.MediaType.xml, (@NonNull Context ctx, @NonNull Object value) -> - ctx.getBufferFactory() - .wrap(("<" + value + ">").getBytes(StandardCharsets.UTF_8))); + ctx.getOutputFactory().wrap("<" + value + ">")); app.get( "/defaults", @@ -1182,30 +1174,14 @@ public void consumes(ServerTestRunner runner) { runner .define( app -> { - app.decoder( - io.jooby.MediaType.json, - new MessageDecoder() { - @NonNull @Override - public String decode(@NonNull Context ctx, @NonNull Type type) - throws Exception { - return "{" + ctx.body().value("") + "}"; - } - }); + app.decoder(io.jooby.MediaType.json, (ctx, type) -> "{" + ctx.body().value("") + "}"); - app.decoder( - xml, - new MessageDecoder() { - @NonNull @Override - public String decode(@NonNull Context ctx, @NonNull Type type) - throws Exception { - return "<" + ctx.body().value("") + ">"; - } - }); + app.decoder(xml, (ctx, type) -> "<" + ctx.body().value("") + ">"); app.get( "/defaults", ctx -> { - return Optional.ofNullable(ctx.body(String.class)).orElse(""); + return ctx.body(String.class); }); app.get("/consumes", ctx -> ctx.body(String.class)) @@ -1225,13 +1201,6 @@ public String decode(@NonNull Context ctx, @NonNull Type type) rsp -> { assertEquals("<>", rsp.body().string()); }); - client.header("Content-Type", "text/plain"); - client.get( - "/defaults", - rsp -> { - assertEquals("", rsp.body().string()); - }); - client.header("Content-Type", "application/json"); client.get( "/consumes", @@ -1488,7 +1457,7 @@ public void routerCaseInsensitive(ServerTestRunner runner) { runner .define( app -> { - app.setRouterOptions(RouterOption.IGNORE_CASE, RouterOption.IGNORE_TRAILING_SLASH); + app.setRouterOptions(new RouterOptions().ignoreCase(true).ignoreTrailingSlash(true)); app.get("/foo", Context::getRequestPath); app.get("/bar", Context::getRequestPath); @@ -1563,12 +1532,12 @@ public void routerCaseSensitive(ServerTestRunner runner) { @ServerTest public void maxRequestSize(ServerTestRunner runner) { runner + .options( + new ServerOptions() + .setOutput(new OutputOptions().setSize(ServerOptions._16KB / 2)) + .setMaxRequestSize(ServerOptions._16KB)) .define( app -> { - app.setServerOptions( - new ServerOptions() - .setBufferSize(ServerOptions._16KB / 2) - .setMaxRequestSize(ServerOptions._16KB)); app.post("/request-size", ctx -> ctx.body().value("")); app.get("/request-size", ctx -> ctx.body().value("")); @@ -1621,7 +1590,7 @@ public void trailinSlashIsANewRoute(ServerTestRunner runner) { runner .define( app -> { - app.setRouterOptions(RouterOption.IGNORE_TRAILING_SLASH); + app.setRouterOptions(new RouterOptions().ignoreTrailingSlash(true)); app.get("/foo/", ctx -> "foo/"); app.get("/foo", ctx -> "new foo"); @@ -3790,7 +3759,8 @@ public void beanConverter(ServerTestRunner runner) { runner .define( app -> { - app.converter(new MyValueBeanConverter()); + var factory = app.getValueFactory(); + factory.put(MyValue.class, new MyValueBeanConverter()); app.get("/", ctx -> ctx.query(MyValue.class)); @@ -3805,25 +3775,22 @@ public void beanConverter(ServerTestRunner runner) { "/error?string=value", rsp -> { assertEquals( - "GET /error 400 Bad Request\n" - + "Cannot convert value: 'string', to: '" - + myValueClassName - + "'", + "GET /error 400 Bad Request\n" + "Missing value: 'string.string'", rsp.body().string()); }); - client.get( - "/?string=value", - rsp -> { - assertEquals("value", rsp.body().string()); - }); - - client.post( - "/", - new FormBody.Builder().add("string", "form").build(), - rsp -> { - assertEquals("form", rsp.body().string()); - }); + // client.get( + // "/?string=value", + // rsp -> { + // assertEquals("value", rsp.body().string()); + // }); + // + // client.post( + // "/", + // new FormBody.Builder().add("string", "form").build(), + // rsp -> { + // assertEquals("form", rsp.body().string()); + // }); }); } @@ -3853,7 +3820,7 @@ public void requestUrl(ServerTestRunner runner) { runner .define( app -> { - app.setTrustProxy(true); + app.setRouterOptions(new RouterOptions().setTrustProxy(true)); app.get("/{path}", ctx -> ctx.getRequestURL()); }) @@ -3904,7 +3871,7 @@ public void requestUrlWithContextPath(ServerTestRunner runner) { runner .define( app -> { - app.setTrustProxy(true); + app.setRouterOptions(new RouterOptions().setTrustProxy(true)); app.setContextPath("/x"); app.get("/{path}", ctx -> ctx.getRequestURL()); }) diff --git a/tests/src/test/java/io/jooby/test/Http2Test.java b/tests/src/test/java/io/jooby/test/Http2Test.java index 3e7293aac1..649d0ea056 100644 --- a/tests/src/test/java/io/jooby/test/Http2Test.java +++ b/tests/src/test/java/io/jooby/test/Http2Test.java @@ -42,10 +42,9 @@ public class Http2Test { @ServerTest public void http2(ServerTestRunner runner) { runner + .options(new ServerOptions().setHttp2(true).setSecurePort(8443)) .define( app -> { - app.setServerOptions(new ServerOptions().setHttp2(true).setSecurePort(8443)); - app.get( "/", ctx -> @@ -77,10 +76,9 @@ public void http2(ServerTestRunner runner) { @ServerTest public void http2c(ServerTestRunner runner) { runner + .options(new ServerOptions().setHttp2(true)) .define( app -> { - app.setServerOptions(new ServerOptions().setHttp2(true)); - app.get( "/", ctx -> diff --git a/tests/src/test/java/io/jooby/test/HttpsTest.java b/tests/src/test/java/io/jooby/test/HttpsTest.java index 6332b64ec6..fa204c6965 100644 --- a/tests/src/test/java/io/jooby/test/HttpsTest.java +++ b/tests/src/test/java/io/jooby/test/HttpsTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.*; +import io.jooby.RouterOptions; import io.jooby.ServerOptions; import io.jooby.SslOptions; import io.jooby.handler.SSLHandler; @@ -18,9 +19,9 @@ public class HttpsTest { @ServerTest public void httpsPkcs12(ServerTestRunner runner) { runner + .options(new ServerOptions().setSecurePort(8443)) .define( app -> { - app.setServerOptions(new ServerOptions().setSecurePort(8443)); app.get( "/", ctx -> @@ -31,7 +32,7 @@ public void httpsPkcs12(ServerTestRunner runner) { + "; secure: " + ctx.isSecure() + "; ssl: " - + app.getServerOptions().getSsl().getType()); + + ctx.require(ServerOptions.class).getSsl().getType()); }) .ready( (http, https) -> { @@ -54,11 +55,11 @@ public void httpsPkcs12(ServerTestRunner runner) { @ServerTest public void httpsX509(ServerTestRunner runner) { + SslOptions options = SslOptions.selfSigned(SslOptions.X509); runner + .options(new ServerOptions().setSsl(options)) .define( app -> { - SslOptions options = SslOptions.selfSigned(SslOptions.X509); - app.setServerOptions(new ServerOptions().setSsl(options)); app.get( "/", ctx -> @@ -69,7 +70,7 @@ public void httpsX509(ServerTestRunner runner) { + "; secure: " + ctx.isSecure() + "; ssl: " - + app.getServerOptions().getSsl().getType()); + + ctx.require(ServerOptions.class).getSsl().getType()); }) .ready( (http, https) -> { @@ -93,13 +94,12 @@ public void httpsX509(ServerTestRunner runner) { @ServerTest public void forceSSL(ServerTestRunner runner) { runner + .options(new ServerOptions().setSecurePort(8443)) .define( app -> { - app.setTrustProxy(true); + app.setRouterOptions(new RouterOptions().setTrustProxy(true)); app.setContextPath("/secure"); - app.setServerOptions(new ServerOptions().setSecurePort(8443)); - app.before(new SSLHandler()); app.get("/{path}", ctx -> ctx.getRequestPath()); @@ -129,11 +129,10 @@ public void forceSSL(ServerTestRunner runner) { @ServerTest public void forceSSL2(ServerTestRunner runner) { runner + .options(new ServerOptions().setSecurePort(8443)) .define( app -> { - app.setTrustProxy(true); - - app.setServerOptions(new ServerOptions().setSecurePort(8443)); + app.setRouterOptions(new RouterOptions().setTrustProxy(true)); app.before(new SSLHandler()); @@ -163,10 +162,9 @@ public void forceSSL2(ServerTestRunner runner) { @ServerTest public void forceSSLStatic(ServerTestRunner runner) { runner + .options(new ServerOptions().setSecurePort(8443)) .define( app -> { - app.setServerOptions(new ServerOptions().setSecurePort(8443)); - app.before(new SSLHandler("static.org")); app.get("/{path}", ctx -> ctx.getRequestPath()); @@ -194,10 +192,10 @@ public void forceSSLStatic(ServerTestRunner runner) { @ServerTest public void forceSSLStatic2(ServerTestRunner runner) { runner + .options(new ServerOptions().setSecurePort(8443)) .define( app -> { app.setContextPath("/ppp"); - app.setServerOptions(new ServerOptions().setSecurePort(8443)); app.before(new SSLHandler("static.org")); @@ -226,10 +224,9 @@ public void forceSSLStatic2(ServerTestRunner runner) { @ServerTest public void httpsOnly(ServerTestRunner runner) { runner + .options(new ServerOptions().setSecurePort(8443).setHttpsOnly(true)) .define( app -> { - app.setServerOptions(new ServerOptions().setSecurePort(8443).setHttpsOnly(true)); - app.get("/test", ctx -> "test"); }) .ready( @@ -241,9 +238,14 @@ public void httpsOnly(ServerTestRunner runner) { @ServerTest public void customSslContext(ServerTestRunner runner) { runner + .options( + new ServerOptions() + .setSecurePort(8443) + .setHttpsOnly(true) + .setSsl(SslOptions.selfSigned())) .define( app -> { - var options = new ServerOptions().setSecurePort(8443).setHttpsOnly(true); + var options = app.require(ServerOptions.class); options.setSsl(SslOptions.selfSigned()); // a fresh context is created every time based on config var ctx1 = options.getSSLContext(this.getClass().getClassLoader()); @@ -255,7 +257,6 @@ public void customSslContext(ServerTestRunner runner) { assertSame(ctx1, options.getSSLContext(this.getClass().getClassLoader())); assertSame(ctx1, options.getSSLContext(this.getClass().getClassLoader())); - app.setServerOptions(options); app.get("/test", ctx -> "test"); }) .ready( diff --git a/tests/src/test/java/io/jooby/test/Issue1409.java b/tests/src/test/java/io/jooby/test/Issue1409.java index be715a0e06..4c253dd0f4 100644 --- a/tests/src/test/java/io/jooby/test/Issue1409.java +++ b/tests/src/test/java/io/jooby/test/Issue1409.java @@ -8,7 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import io.jooby.Context; -import io.jooby.RouterOption; +import io.jooby.RouterOptions; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; @@ -91,7 +91,7 @@ public void shouldNormRequestPath(ServerTestRunner runner) { .define( app -> { app.setRouterOptions( - RouterOption.NORMALIZE_SLASH, RouterOption.IGNORE_TRAILING_SLASH); + new RouterOptions().normalizeSlash(true).ignoreTrailingSlash(true)); app.get("/doubleslash", Context::getRequestPath); app.path( diff --git a/tests/src/test/java/io/jooby/test/Issue1599.java b/tests/src/test/java/io/jooby/test/Issue1599.java index b4cf2d9ecb..308aa20c8c 100644 --- a/tests/src/test/java/io/jooby/test/Issue1599.java +++ b/tests/src/test/java/io/jooby/test/Issue1599.java @@ -37,10 +37,10 @@ public void issue1599(ServerTestRunner runner) { ctx -> { return toMap(ctx.getRoute()); }) - .attribute("foo", "foo"); + .setAttribute("foo", "foo"); }) .produces(MediaType.html) - .attribute("foo", "bar") + .setAttribute("foo", "bar") .tags("top") .summary("1599 API"); }) diff --git a/tests/src/test/java/io/jooby/test/Issue1656.java b/tests/src/test/java/io/jooby/test/Issue1656.java index 1e0916f48d..555c2e2924 100644 --- a/tests/src/test/java/io/jooby/test/Issue1656.java +++ b/tests/src/test/java/io/jooby/test/Issue1656.java @@ -24,10 +24,9 @@ public class Issue1656 { @ServerTest public void gzip(ServerTestRunner runner) { runner + .options(new ServerOptions().setCompressionLevel(ServerOptions.DEFAULT_COMPRESSION_LEVEL)) .define( app -> { - app.setServerOptions( - new ServerOptions().setCompressionLevel(ServerOptions.DEFAULT_COMPRESSION_LEVEL)); app.assets("/static/*", "/files"); }) .ready( diff --git a/tests/src/test/java/io/jooby/test/Issue1737.java b/tests/src/test/java/io/jooby/test/Issue1737.java index d59d3e7b0d..6665352853 100644 --- a/tests/src/test/java/io/jooby/test/Issue1737.java +++ b/tests/src/test/java/io/jooby/test/Issue1737.java @@ -44,7 +44,7 @@ public void pac4jShouldWorkWithSignedSession(ServerTestRunner runner) { app -> { app.setSessionStore( SessionStore.signed( - "123456789", new Cookie("Test").setMaxAge(Duration.ofDays(7)))); + new Cookie("Test").setMaxAge(Duration.ofDays(7)), "123456789")); app.install( new Pac4jModule() diff --git a/tests/src/test/java/io/jooby/test/Issue2107.java b/tests/src/test/java/io/jooby/test/Issue2107.java index e5173ad242..9d880a1595 100644 --- a/tests/src/test/java/io/jooby/test/Issue2107.java +++ b/tests/src/test/java/io/jooby/test/Issue2107.java @@ -5,21 +5,10 @@ */ package io.jooby.test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.function.Consumer; +import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; -import org.slf4j.LoggerFactory; -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.read.ListAppender; import io.jooby.Jooby; import io.jooby.exception.StartupException; @@ -27,42 +16,24 @@ public class Issue2107 { @Test public void appInitExceptionLogging() { - withLog( - appender -> { - Throwable t = - assertThrows( - StartupException.class, - () -> Jooby.runApp(new String[0], AppWithRuntimeException::new)); - assertEquals("Application initialization resulted in exception", t.getMessage()); - assertNotNull(t.getCause()); - assertTrue(t.getCause() instanceof RuntimeException); - assertEquals("meh", t.getCause().getMessage()); - assertEquals(1, appender.list.size()); - - final ILoggingEvent ev = appender.list.get(0); - assertEquals(Level.ERROR, ev.getLevel()); - assertEquals("Application initialization resulted in exception", ev.getMessage()); - assertEquals("meh", ev.getThrowableProxy().getMessage()); - }); + Throwable t = + assertThrows( + StartupException.class, + () -> Jooby.runApp(new String[0], AppWithRuntimeException::new)); + assertEquals("Application initialization resulted in exception", t.getMessage()); + assertNotNull(t.getCause()); + assertInstanceOf(RuntimeException.class, t.getCause()); + assertEquals("meh", t.getCause().getMessage()); } @Test public void noMatryoshkaExceptionsPlease() { - withLog( - appender -> { - Throwable t = - assertThrows( - StartupException.class, - () -> Jooby.runApp(new String[0], AppWithStartupException::new)); - assertEquals("meh", t.getMessage()); - assertNull(t.getCause()); - assertEquals(1, appender.list.size()); - - final ILoggingEvent ev = appender.list.get(0); - assertEquals(Level.ERROR, ev.getLevel()); - assertEquals("Application initialization resulted in exception", ev.getMessage()); - assertEquals("meh", ev.getThrowableProxy().getMessage()); - }); + Throwable t = + assertThrows( + StartupException.class, + () -> Jooby.runApp(new String[0], AppWithStartupException::new)); + assertEquals("meh", t.getMessage()); + assertNull(t.getCause()); } static class AppWithRuntimeException extends Jooby { @@ -78,18 +49,4 @@ static class AppWithStartupException extends Jooby { if (true) throw new StartupException("meh"); } } - - private void withLog(Consumer> consumer) { - Logger log = (Logger) LoggerFactory.getLogger(Jooby.class); - var appender = new ListAppender(); - try { - appender.start(); - log.addAppender(appender); - log.setAdditive(false); - consumer.accept(appender); - } finally { - log.detachAppender(appender); - log.setAdditive(true); - } - } } diff --git a/tests/src/test/java/io/jooby/test/Issue2372.java b/tests/src/test/java/io/jooby/test/Issue2372.java index f2aab3c631..6a188fe21f 100644 --- a/tests/src/test/java/io/jooby/test/Issue2372.java +++ b/tests/src/test/java/io/jooby/test/Issue2372.java @@ -22,10 +22,9 @@ public class Issue2372 { @ServerTest public void http2(ServerTestRunner runner) { runner + .options(new ServerOptions().setHttp2(true).setSecurePort(8443)) .define( app -> { - app.setServerOptions(new ServerOptions().setHttp2(true).setSecurePort(8443)); - app.before(new SSLHandler()); app.use(Reactor.reactor()); diff --git a/tests/src/test/java/io/jooby/test/MvcTest.java b/tests/src/test/java/io/jooby/test/MvcTest.java index eaac172aa3..8b4d418a10 100644 --- a/tests/src/test/java/io/jooby/test/MvcTest.java +++ b/tests/src/test/java/io/jooby/test/MvcTest.java @@ -124,13 +124,13 @@ public void producesAndConsumes(ServerTestRunner runner) { app.encoder( io.jooby.MediaType.json, (@NonNull Context ctx, @NonNull Object value) -> - ctx.getBufferFactory() + ctx.getOutputFactory() .wrap(("{" + value + "}").getBytes(StandardCharsets.UTF_8))); app.encoder( io.jooby.MediaType.xml, (@NonNull Context ctx, @NonNull Object value) -> - ctx.getBufferFactory() + ctx.getOutputFactory() .wrap(("<" + value + ">").getBytes(StandardCharsets.UTF_8))); app.decoder( @@ -536,7 +536,8 @@ public void beanConverter(ServerTestRunner runner) { runner .define( app -> { - app.converter(new MyValueBeanConverter()); + var factory = app.getValueFactory(); + factory.put(MyValue.class, new MyValueBeanConverter()); app.mvc(new MyValueRouter_()); }) .ready( diff --git a/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java b/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java index 85299555cf..50a946689d 100644 --- a/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java +++ b/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java @@ -5,18 +5,19 @@ */ package io.jooby.test; +import java.lang.reflect.Type; + +import org.jetbrains.annotations.NotNull; + import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.BeanConverter; -import io.jooby.ValueNode; +import io.jooby.value.ConversionHint; +import io.jooby.value.Converter; +import io.jooby.value.Value; -public class MyValueBeanConverter implements BeanConverter { - @Override - public boolean supports(@NonNull Class type) { - return MyValue.class == type; - } +public class MyValueBeanConverter implements Converter { @Override - public Object convert(@NonNull ValueNode value, @NonNull Class type) { + public Object convert(@NotNull Type type, @NotNull Value value, @NonNull ConversionHint hint) { MyValue result = new MyValue(); result.setString(value.get("string").value()); return result; diff --git a/tests/src/test/java/io/jooby/test/RouteAttributeTest.java b/tests/src/test/java/io/jooby/test/RouteAttributeTest.java index 3eb74cabfa..afa63eaf80 100644 --- a/tests/src/test/java/io/jooby/test/RouteAttributeTest.java +++ b/tests/src/test/java/io/jooby/test/RouteAttributeTest.java @@ -21,7 +21,7 @@ public void canRetrieveRouteAttributesForMvcAPI(ServerTestRunner runner) { app.use( next -> ctx -> { - String role = (String) ctx.getRoute().attribute("Role"); + String role = (String) ctx.getRoute().getAttribute("Role"); String level = (String) ctx.getRoute().getAttributes().getOrDefault("Role.level", "one"); @@ -59,13 +59,13 @@ public void canRetrieveRouteAttributesForScriptAPI(ServerTestRunner runner) { app.use( next -> ctx -> { - String foo = (String) ctx.getRoute().attribute("foo"); + String foo = (String) ctx.getRoute().getAttribute("foo"); assertEquals("bar", foo); return next.apply(ctx); }); - app.get("/fb", ctx -> "Hello World!").attribute("foo", "bar"); + app.get("/fb", ctx -> "Hello World!").setAttribute("foo", "bar"); }) .ready( client -> { diff --git a/tests/src/test/java/io/jooby/test/SessionTest.java b/tests/src/test/java/io/jooby/test/SessionTest.java index c373653449..9275fea1e1 100644 --- a/tests/src/test/java/io/jooby/test/SessionTest.java +++ b/tests/src/test/java/io/jooby/test/SessionTest.java @@ -24,6 +24,8 @@ import okhttp3.Response; public class SessionTest { + private static final Cookie SID = Cookie.session("jooby.sid"); + @ServerTest public void sessionIdAsCookie(ServerTestRunner runner) { runner @@ -142,7 +144,7 @@ public void cookieDataSession(ServerTestRunner runner) { runner .define( app -> { - app.setSessionStore(SessionStore.signed("ABC123")); + app.setSessionStore(SessionStore.signed(SID, "ABC123")); app.get( "/session", @@ -300,8 +302,7 @@ public void sessionIdMultiple(ServerTestRunner runner) { SessionToken token = SessionToken.combine( SessionToken.header("TOKEN"), - SessionToken.cookieId( - SessionToken.SID.clone().setMaxAge(Duration.ofMinutes(30)))); + SessionToken.cookieId(SID.clone().setMaxAge(Duration.ofMinutes(30)))); app.setSessionStore((SessionStore.memory(token))); @@ -375,7 +376,9 @@ public void jsonwebtokenSession(ServerTestRunner runner) { runner .define( app -> { - app.setSessionStore(new JwtSessionStore("7a85c3b6-3ef0-4625-82d3-a1da36094804")); + app.setSessionStore( + new JwtSessionStore( + SessionToken.cookieId(SID), "7a85c3b6-3ef0-4625-82d3-a1da36094804")); app.get( "/session", ctx -> { diff --git a/tests/src/test/kotlin/NoPckg.kt b/tests/src/test/kotlin/NoPckg.kt index f162fd0d3f..186cb00b60 100644 --- a/tests/src/test/kotlin/NoPckg.kt +++ b/tests/src/test/kotlin/NoPckg.kt @@ -9,8 +9,5 @@ import io.jooby.kt.runApp data class SearchQuery(val q: String) fun main(args: Array) { - runApp(args, ExecutionMode.EVENT_LOOP) { - serverOptions { ioThreads = 5 } - get("/") { ":+1" } - } + runApp(args, ExecutionMode.EVENT_LOOP) { get("/") { ":+1" } } } diff --git a/tests/src/test/kotlin/io/jooby/FeaturedKotlinTest.kt b/tests/src/test/kotlin/io/jooby/FeaturedKotlinTest.kt index a657c6aa29..a9faf595f3 100644 --- a/tests/src/test/kotlin/io/jooby/FeaturedKotlinTest.kt +++ b/tests/src/test/kotlin/io/jooby/FeaturedKotlinTest.kt @@ -101,12 +101,11 @@ class FeaturedKotlinTest { } client.get("/kotlin/point") { rsp -> + val body = rsp.body!!.string() assertTrue( - rsp.body!! - .string() - .contains( - "Cannot convert value: 'null', to: 'io.jooby.internal.mvc.QueryPoint'" - ) + body.contains( + "Cannot convert value: 'null', to: 'io.jooby.internal.mvc.QueryPoint'" + ) ) }