From 8c8d9f3636af09e2c71b331051e56c5ed9fe9839 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 16 Jun 2025 12:25:54 -0300 Subject: [PATCH 01/60] value api: remove context from api and internal classes - make compile works --- .../main/java/io/jooby/DefaultContext.java | 14 +- jooby/src/main/java/io/jooby/Formdata.java | 7 +- jooby/src/main/java/io/jooby/Jooby.java | 16 +- jooby/src/main/java/io/jooby/QueryString.java | 8 +- jooby/src/main/java/io/jooby/Router.java | 23 +-- jooby/src/main/java/io/jooby/Value.java | 48 +++--- .../java/io/jooby/internal/ArrayValue.java | 14 +- .../java/io/jooby/internal/HashValue.java | 37 ++--- .../java/io/jooby/internal/HeadersValue.java | 6 +- .../java/io/jooby/internal/MultipartNode.java | 6 +- .../io/jooby/internal/QueryStringValue.java | 11 +- .../java/io/jooby/internal/RouterImpl.java | 22 +-- .../java/io/jooby/internal/SessionImpl.java | 6 +- .../java/io/jooby/internal/SingleValue.java | 12 +- .../java/io/jooby/internal/UrlParser.java | 8 +- .../io/jooby/internal/ValueConverters.java | 38 +++-- .../internal/converter/BuiltinConverter.java | 146 +++++++++++++++++- .../main/java/io/jooby/value/Converter.java | 27 ++++ .../java/io/jooby/value/ValueFactory.java | 51 ++++++ jooby/src/main/java/module-info.java | 1 + .../jooby/internal/ValueConverterHelper.java | 86 +++++------ .../io/jooby/internal/jetty/JettyContext.java | 9 +- .../io/jooby/internal/netty/NettyContext.java | 9 +- .../main/java/io/jooby/test/MockContext.java | 9 +- .../main/java/io/jooby/test/MockSession.java | 4 +- .../internal/undertow/UndertowContext.java | 9 +- 26 files changed, 418 insertions(+), 209 deletions(-) create mode 100644 jooby/src/main/java/io/jooby/value/Converter.java create mode 100644 jooby/src/main/java/io/jooby/value/ValueFactory.java diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 7cea85c99a..41d446849c 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -134,7 +134,7 @@ default FlashMap flashOrNull() { */ @Override default @NonNull Value flash(@NonNull String name) { - return Value.create(this, name, flash().get(name)); + return Value.create(getRouter().getValueFactory(), name, flash().get(name)); } @Override @@ -185,7 +185,9 @@ default FlashMap flashOrNull() { @Override default @NonNull 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(getRouter().getValueFactory(), name, value); } @Override @@ -193,7 +195,7 @@ default FlashMap flashOrNull() { String value = pathMap().get(name); return value == null ? new MissingValue(name) - : new SingleValue(this, name, UrlParser.decodePathSegment(value)); + : new SingleValue(getRouter().getValueFactory(), name, UrlParser.decodePathSegment(value)); } @Override @@ -203,7 +205,7 @@ default FlashMap flashOrNull() { @Override @NonNull default ValueNode path() { - HashValue path = new HashValue(this, null); + var path = new HashValue(getRouter().getValueFactory(), null); for (Map.Entry entry : pathMap().entrySet()) { path.put(entry.getKey(), entry.getValue()); } @@ -423,14 +425,14 @@ default boolean isSecure() { @Override default @NonNull T convertOrNull(@NonNull ValueNode value, @NonNull Class type) { - return ValueConverters.convert(value, type, getRouter()); + return ValueConverters.convert(value, type, getRouter().getValueFactory()); } @Override default @NonNull T decode(@NonNull Type type, @NonNull MediaType contentType) { try { if (MediaType.text.equals(contentType)) { - T result = ValueConverters.convert(body(), type, getRouter()); + T result = ValueConverters.convert(body(), type, getRouter().getValueFactory()); return result; } return (T) decoder(contentType).decode(this, type); diff --git a/jooby/src/main/java/io/jooby/Formdata.java b/jooby/src/main/java/io/jooby/Formdata.java index 8c8dfdb9eb..87c6df2aae 100644 --- a/jooby/src/main/java/io/jooby/Formdata.java +++ b/jooby/src/main/java/io/jooby/Formdata.java @@ -10,6 +10,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.internal.MultipartNode; +import io.jooby.value.ValueFactory; /** * Form class for direct MVC parameter provisioning. @@ -87,10 +88,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/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 13b8a0b897..28422b99dc 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -53,6 +53,7 @@ import io.jooby.internal.RegistryRef; import io.jooby.internal.RouterImpl; import io.jooby.problem.ProblemDetailsHandler; +import io.jooby.value.ValueFactory; import jakarta.inject.Provider; /** @@ -919,19 +920,14 @@ 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(); - } - - @NonNull @Override - public List getBeanConverters() { - return router.getBeanConverters(); + public Jooby setValueFactory(@NonNull ValueFactory valueFactory) { + router.setValueFactory(valueFactory); + return this; } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/QueryString.java b/jooby/src/main/java/io/jooby/QueryString.java index 874bd9ef1d..fe716cb8c7 100644 --- a/jooby/src/main/java/io/jooby/QueryString.java +++ b/jooby/src/main/java/io/jooby/QueryString.java @@ -8,6 +8,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.internal.UrlParser; +import io.jooby.value.ValueFactory; /** * Query string class for direct MVC parameter provisioning. @@ -33,11 +34,12 @@ 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 @NonNull QueryString create( + @NonNull ValueFactory valueFactory, @Nullable String queryString) { + return UrlParser.queryString(valueFactory, queryString); } } diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 544ab357a0..12d6de0730 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -37,6 +37,7 @@ import io.jooby.exception.MissingValueException; import io.jooby.handler.AssetHandler; import io.jooby.handler.AssetSource; +import io.jooby.value.ValueFactory; import jakarta.inject.Provider; /** @@ -951,27 +952,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); + @NonNull ValueFactory getValueFactory(); - /** - * Get all simple/string value converters. - * - * @return All simple/string value converters. - */ - @NonNull List getConverters(); - - /** - * Get all complex/bean value converters. - * - * @return All complex/bean value converters. - */ - @NonNull List getBeanConverters(); + @NonNull Router setValueFactory(@NonNull ValueFactory valueFactory); /** * Available server options. diff --git a/jooby/src/main/java/io/jooby/Value.java b/jooby/src/main/java/io/jooby/Value.java index 4ce8fc3406..7aa8c76b61 100644 --- a/jooby/src/main/java/io/jooby/Value.java +++ b/jooby/src/main/java/io/jooby/Value.java @@ -28,6 +28,7 @@ import io.jooby.internal.MissingValue; import io.jooby.internal.MultipartNode; import io.jooby.internal.SingleValue; +import io.jooby.value.ValueFactory; /** * Unified API for HTTP value. This API plays two role: @@ -451,27 +452,27 @@ default boolean isObject() { /** * 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); + @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); + @NonNull ValueFactory valueFactory, @NonNull String name, @NonNull List values) { + return new ArrayValue(valueFactory, name).add(values); } /** @@ -480,20 +481,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) { + ValueFactory valueFactory, @NonNull String name, @Nullable List values) { if (values == null || values.size() == 0) { 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 +503,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 @NonNull ValueNode create( + 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 @NonNull ValueNode hash( + ValueFactory valueFactory, @NonNull Map> values) { + var node = new HashValue(valueFactory, null); node.put(values); return node; } @@ -530,22 +533,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 @NonNull Formdata formdata(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 @NonNull ValueNode headers( + ValueFactory valueFactory, @NonNull Map> values) { + HeadersValue node = new HeadersValue(valueFactory); node.put(values); return node; } diff --git a/jooby/src/main/java/io/jooby/internal/ArrayValue.java b/jooby/src/main/java/io/jooby/internal/ArrayValue.java index 60e2090bdc..c4c88c6acf 100644 --- a/jooby/src/main/java/io/jooby/internal/ArrayValue.java +++ b/jooby/src/main/java/io/jooby/internal/ArrayValue.java @@ -16,20 +16,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.exception.MissingValueException; import io.jooby.exception.TypeMismatchException; +import io.jooby.value.ValueFactory; public class ArrayValue implements ValueNode { - private final Context ctx; + private final ValueFactory factory; private final String name; 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; } @@ -51,7 +51,7 @@ 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 @@ -91,12 +91,12 @@ public String toString() { @NonNull @Override public T to(@NonNull Class type) { - return ctx.convert(list.get(0), type); + return (T) factory.convert(type, list.get(0)); } @Nullable @Override public T toNullable(@NonNull Class type) { - return list.isEmpty() ? null : ctx.convertOrNull(list.get(0), type); + return list.isEmpty() ? null : (T) factory.convert(type, list.get(0)); } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/internal/HashValue.java b/jooby/src/main/java/io/jooby/internal/HashValue.java index 6297c94aee..268eeaa3f3 100644 --- a/jooby/src/main/java/io/jooby/internal/HashValue.java +++ b/jooby/src/main/java/io/jooby/internal/HashValue.java @@ -23,24 +23,24 @@ 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; +import io.jooby.value.ValueFactory; public class HashValue implements ValueNode { protected static final Map EMPTY = Collections.emptyMap(); - private Context ctx; + private 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; } @@ -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); @@ -80,13 +80,13 @@ public void put(String path, Collection values) { for (String value : values) { ValueNode existing = scope.get(name); if (existing == null) { - scope.put(name, new SingleValue(ctx, name, decode(value))); + scope.put(name, new SingleValue(factory, name, decode(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)); @@ -165,7 +165,7 @@ 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) { @@ -240,16 +240,17 @@ public Optional toOptional(@NonNull Class type) { @NonNull @Override public T to(@NonNull Class type) { - return ctx.convert(this, type); + return (T) factory.convert(type, this); } @Nullable @Override public final T toNullable(@NonNull Class type) { - return toNullable(ctx, type, allowEmptyBean()); + return toNullable(factory, type, allowEmptyBean()); } - protected T toNullable(@NonNull Context ctx, @NonNull Class type, boolean allowEmpty) { - return ValueConverters.convert(this, type, ctx.getRouter(), allowEmpty); + protected T toNullable( + @NonNull ValueFactory factory, @NonNull Class type, boolean allowEmpty) { + return ValueConverters.convert(this, type, factory, allowEmpty); } @Override @@ -288,22 +289,22 @@ private > C toCollection(@NonNull Class type, C co 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, false); if (item != null) { container.add(item); } diff --git a/jooby/src/main/java/io/jooby/internal/HeadersValue.java b/jooby/src/main/java/io/jooby/internal/HeadersValue.java index 6d870be9ba..41132ccd79 100644 --- a/jooby/src/main/java/io/jooby/internal/HeadersValue.java +++ b/jooby/src/main/java/io/jooby/internal/HeadersValue.java @@ -11,13 +11,13 @@ import java.util.TreeMap; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Context; import io.jooby.ValueNode; +import io.jooby.value.ValueFactory; public class HeadersValue extends HashValue implements ValueNode { - public HeadersValue(final Context ctx) { - super(ctx); + public HeadersValue(ValueFactory valueFactory) { + super(valueFactory); } @Override diff --git a/jooby/src/main/java/io/jooby/internal/MultipartNode.java b/jooby/src/main/java/io/jooby/internal/MultipartNode.java index d16f017b39..2987438297 100644 --- a/jooby/src/main/java/io/jooby/internal/MultipartNode.java +++ b/jooby/src/main/java/io/jooby/internal/MultipartNode.java @@ -9,16 +9,16 @@ 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<>(); - public MultipartNode(Context ctx) { - super(ctx); + public MultipartNode(ValueFactory valueFactory) { + super(valueFactory); } @Override diff --git a/jooby/src/main/java/io/jooby/internal/QueryStringValue.java b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java index 366262c16b..79f4999592 100644 --- a/jooby/src/main/java/io/jooby/internal/QueryStringValue.java +++ b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java @@ -6,14 +6,14 @@ package io.jooby.internal; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Context; import io.jooby.QueryString; +import io.jooby.value.ValueFactory; public class QueryStringValue extends HashValue implements QueryString { private String queryString; - public QueryStringValue(Context ctx, String queryString) { - super(ctx); + public QueryStringValue(ValueFactory valueFactory, String queryString) { + super(valueFactory); this.queryString = queryString; } @@ -22,12 +22,13 @@ protected boolean allowEmptyBean() { } @Override - protected T toNullable(@NonNull Context ctx, @NonNull Class type, boolean allowEmpty) { + protected T toNullable( + @NonNull ValueFactory valueFactory, @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); + return ValueConverters.convert(this, type, valueFactory, allowEmpty); } @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..f0b216a702 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -48,6 +48,7 @@ import io.jooby.internal.handler.ServerSentEventHandler; import io.jooby.internal.handler.WebSocketHandler; import io.jooby.problem.ProblemDetailsHandler; +import io.jooby.value.ValueFactory; import jakarta.inject.Provider; public class RouterImpl implements Router { @@ -174,6 +175,8 @@ public Stack executor(Executor executor) { private boolean stopped; + private ValueFactory valueFactory = new ValueFactory(); + public RouterImpl() { stack.addLast(new Stack(chi, null)); @@ -467,23 +470,14 @@ 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; - } - - @NonNull @Override - public List getConverters() { - return converters; + public ValueFactory getValueFactory() { + return valueFactory; } @NonNull @Override - public List getBeanConverters() { - return beanConverters; + public Router setValueFactory(@NonNull ValueFactory valueFactory) { + this.valueFactory = valueFactory; + return this; } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/internal/SessionImpl.java b/jooby/src/main/java/io/jooby/internal/SessionImpl.java index b45f84e07e..b4c25ba547 100644 --- a/jooby/src/main/java/io/jooby/internal/SessionImpl.java +++ b/jooby/src/main/java/io/jooby/internal/SessionImpl.java @@ -78,7 +78,7 @@ 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.getRouter().getValueFactory(), name, attributes.get(name)); } @Override @@ -92,7 +92,9 @@ public Session setId(@Nullable String id) { public @NonNull ValueNode remove(@NonNull String name) { String value = attributes.remove(name); updateState(); - return value == null ? Value.missing(name) : Value.value(ctx, name, value); + return value == null + ? Value.missing(name) + : Value.value(ctx.getRouter().getValueFactory(), name, value); } @Override diff --git a/jooby/src/main/java/io/jooby/internal/SingleValue.java b/jooby/src/main/java/io/jooby/internal/SingleValue.java index 6fd0c38b2b..c0b169cdcb 100644 --- a/jooby/src/main/java/io/jooby/internal/SingleValue.java +++ b/jooby/src/main/java/io/jooby/internal/SingleValue.java @@ -18,19 +18,19 @@ 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.ValueFactory; public class SingleValue implements ValueNode { - private final Context ctx; + private final ValueFactory factory; private final String name; private 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; } @@ -87,12 +87,12 @@ public Optional toOptional(@NonNull Class type) { @NonNull @Override public T to(@NonNull Class type) { - return ctx.convert(this, type); + return (T) factory.convert(type, this); } @Nullable @Override public T toNullable(@NonNull Class type) { - return ctx.convertOrNull(this, type); + return (T) factory.convert(type, this); } @Override 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 index 4057ed5a58..f504d96467 100644 --- a/jooby/src/main/java/io/jooby/internal/ValueConverters.java +++ b/jooby/src/main/java/io/jooby/internal/ValueConverters.java @@ -8,8 +8,6 @@ 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; @@ -17,6 +15,7 @@ import io.jooby.internal.converter.StringConstructorConverter; import io.jooby.internal.converter.ValueOfConverter; import io.jooby.internal.reflect.$Types; +import io.jooby.value.ValueFactory; public class ValueConverters { @@ -27,7 +26,7 @@ public static List> defaultConverters() { return converters; } - public static T convert(ValueNode value, Type type, Router router) { + public static T convert(ValueNode value, Type type, ValueFactory router) { Class rawType = $Types.getRawType(type); if (List.class.isAssignableFrom(rawType)) { return (T) Collections.singletonList(convert(value, $Types.parameterizedType0(type), router)); @@ -41,7 +40,8 @@ public static T convert(ValueNode value, Type type, Router router) { return convert(value, rawType, router, false); } - public static T convert(ValueNode value, Class type, Router router, boolean allowEmptyBean) { + public static T convert( + ValueNode value, Class type, ValueFactory router, boolean allowEmptyBean) { if (type == String.class) { return (T) value.valueOrNull(); } @@ -83,19 +83,23 @@ public static T convert(ValueNode value, Class type, Router router, boolean 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); - } - } - } + var converter = router.get(type); + if (converter != null) { + return (T) converter.convert(type, value); + } + // 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); diff --git a/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java b/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java index 9676b397b6..29eed503f3 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java +++ b/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java @@ -29,9 +29,16 @@ import io.jooby.StatusCode; import io.jooby.Value; import io.jooby.ValueConverter; +import io.jooby.value.Converter; +import io.jooby.value.ValueFactory; -public enum BuiltinConverter implements ValueConverter { +public enum BuiltinConverter implements ValueConverter, Converter { BigDecimal { + @Override + public void register(ValueFactory factory) { + factory.put(BigDecimal.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == BigDecimal.class; @@ -39,10 +46,20 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { return new BigDecimal(value.value()); } }, BigInteger { + @Override + public void register(ValueFactory factory) { + factory.put(BigInteger.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == BigInteger.class; @@ -50,10 +67,20 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { return new BigInteger(value.value()); } }, Charset { + @Override + public void register(ValueFactory factory) { + factory.put(Charset.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == Charset.class; @@ -61,6 +88,11 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { String charset = value.value(); return switch (charset.toLowerCase()) { case "utf-8" -> StandardCharsets.UTF_8; @@ -74,6 +106,11 @@ public boolean supports(@NonNull Class type) { } }, Date { + @Override + public void register(ValueFactory factory) { + factory.put(Date.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == Date.class; @@ -81,6 +118,11 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { try { // must be millis return new Date(Long.parseLong(value.value())); @@ -92,6 +134,11 @@ public boolean supports(@NonNull Class type) { } }, Duration { + @Override + public void register(ValueFactory factory) { + factory.put(Duration.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == Duration.class; @@ -99,6 +146,11 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { try { return java.time.Duration.parse(value.value()); } catch (DateTimeParseException x) { @@ -158,6 +210,11 @@ private static long parseDuration(String value) { } }, Period { + @Override + public void register(ValueFactory factory) { + factory.put(Period.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == Period.class; @@ -165,6 +222,11 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { try { return java.time.Period.from((Duration) Duration.convert(value, type)); } catch (DateTimeException x) { @@ -225,6 +287,11 @@ private static Period periodOf(int n, ChronoUnit unit) { } }, Instant { + @Override + public void register(ValueFactory factory) { + factory.put(Instant.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == Instant.class; @@ -232,6 +299,11 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { try { return java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); } catch (NumberFormatException x) { @@ -240,6 +312,11 @@ public boolean supports(@NonNull Class type) { } }, LocalDate { + @Override + public void register(ValueFactory factory) { + factory.put(LocalDate.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == LocalDate.class; @@ -247,6 +324,11 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { try { // must be millis var instant = java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); @@ -258,6 +340,11 @@ public boolean supports(@NonNull Class type) { } }, LocalDateTime { + @Override + public void register(ValueFactory factory) { + factory.put(LocalDateTime.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == LocalDateTime.class; @@ -265,6 +352,11 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { try { // must be millis var instant = java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); @@ -276,6 +368,11 @@ public boolean supports(@NonNull Class type) { } }, StatusCode { + @Override + public void register(ValueFactory factory) { + factory.put(StatusCode.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == StatusCode.class; @@ -283,10 +380,20 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { return io.jooby.StatusCode.valueOf(value.intValue()); } }, TimeZone { + @Override + public void register(ValueFactory factory) { + factory.put(TimeZone.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == TimeZone.class; @@ -294,10 +401,20 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { return java.util.TimeZone.getTimeZone(value.value()); } }, URI { + @Override + public void register(ValueFactory factory) { + factory.put(URI.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == URI.class || type == URL.class; @@ -305,6 +422,11 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { try { var uri = java.net.URI.create(value.value()); if (type == URL.class) { @@ -317,6 +439,11 @@ public boolean supports(@NonNull Class type) { } }, UUID { + @Override + public void register(ValueFactory factory) { + factory.put(UUID.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == UUID.class; @@ -324,10 +451,20 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { return java.util.UUID.fromString(value.value()); } }, ZoneId { + @Override + public void register(ValueFactory factory) { + factory.put(ZoneId.class, this); + } + @Override public boolean supports(@NonNull Class type) { return type == ZoneId.class; @@ -335,11 +472,18 @@ public boolean supports(@NonNull Class type) { @Override public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { + return convert(type, value); + } + + @Override + public Object convert(@NonNull Class type, @NonNull Value value) { var zoneId = value.value(); return java.time.ZoneId.of(java.time.ZoneId.SHORT_IDS.getOrDefault(zoneId, zoneId)); } }; + public abstract void register(ValueFactory factory); + private static String getUnits(String s) { int i = s.length() - 1; while (i >= 0) { 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..acbc1fcb81 --- /dev/null +++ b/jooby/src/main/java/io/jooby/value/Converter.java @@ -0,0 +1,27 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.value; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Value; + +/** + * 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 to specific type. + * + * @param type Requested type. + * @param value Value value. + * @return Converted value. + */ + Object convert(@NonNull Class type, @NonNull Value value); +} 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..6b0101cab4 --- /dev/null +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -0,0 +1,51 @@ +/* + * 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 java.util.*; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Value; +import io.jooby.internal.converter.BuiltinConverter; +import io.jooby.internal.reflect.$Types; + +public class ValueFactory { + + private final Map, Converter> converterMap = new HashMap<>(); + + public ValueFactory() { + for (var converter : BuiltinConverter.values()) { + converter.register(this); + } + } + + public Converter get(Class type) { + return converterMap.get(type); + } + + public ValueFactory put(Class type, Converter converter) { + converterMap.put(type, converter); + return this; + } + + public Object convert(@NonNull Type type, @NonNull Value value) { + if (type instanceof Class clazz) { + var converter = get(clazz); + return converter.convert(clazz, value); + } else { + var rawType = $Types.getRawType(type); + if (List.class.isAssignableFrom(rawType)) { + return List.of(convert($Types.parameterizedType0(type), value)); + } else if (Set.class.isAssignableFrom(rawType)) { + return Set.of(convert($Types.parameterizedType0(type), value)); + } else if (Optional.class.isAssignableFrom(rawType)) { + return Optional.of(convert($Types.parameterizedType0(type), value)); + } + throw new UnsupportedOperationException("Unsupported type: " + type); + } + } +} diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index b3c120d7aa..e8c2696895 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -13,6 +13,7 @@ exports io.jooby.handler; exports io.jooby.validation; exports io.jooby.problem; + exports io.jooby.value; uses io.jooby.MvcFactory; uses io.jooby.Server; diff --git a/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java b/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java index 9dc0e05973..0dae20ad93 100644 --- a/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java +++ b/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java @@ -5,58 +5,46 @@ */ 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; +import io.jooby.value.ValueFactory; 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; + public static ValueFactory testContext(ValueConverter... converters) { + var result = new ValueFactory(); + return result; + // 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.getValueFactory()); + // 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.getValueFactory()); + // }); + // return ctx; } } 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..9c7e6f012a 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 @@ -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(getRouter().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(getRouter().getValueFactory()); formParam(request, formdata); @@ -253,7 +253,8 @@ public Formdata form() { @NonNull @Override public Value header(@NonNull String name) { - return Value.create(this, name, request.getHeaders().getValuesList(name)); + return Value.create( + getRouter().getValueFactory(), name, request.getHeaders().getValuesList(name)); } @NonNull @Override @@ -263,7 +264,7 @@ public ValueNode header() { for (HttpField header : request.getHeaders()) { headerMap.put(header.getName(), header.getValueList()); } - headers = Value.headers(this, headerMap); + headers = Value.headers(getRouter().getValueFactory(), headerMap); } return headers; } 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..0108802753 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 @@ -240,7 +240,8 @@ 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(getRouter().getValueFactory(), q >= 0 ? uri.substring(q + 1) : null); } return query; } @@ -248,7 +249,7 @@ public QueryString query() { @NonNull @Override public Formdata form() { if (formdata == null) { - formdata = Formdata.create(this); + formdata = Formdata.create(getRouter().getValueFactory()); decodeForm(formdata); } return formdata; @@ -256,7 +257,7 @@ public Formdata form() { @NonNull @Override public Value header(@NonNull String name) { - return Value.create(this, name, req.headers().getAll(name)); + return Value.create(getRouter().getValueFactory(), name, req.headers().getAll(name)); } @NonNull @Override @@ -347,7 +348,7 @@ public ValueNode header() { for (String name : names) { headerMap.put(name, headers.getAll(name)); } - this.headers = Value.headers(this, headerMap); + this.headers = Value.headers(getRouter().getValueFactory(), headerMap); } return headers; } 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..1ab462b62b 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 @@ -59,6 +59,7 @@ import io.jooby.buffer.DataBufferFactory; import io.jooby.buffer.DefaultDataBufferFactory; import io.jooby.exception.TypeMismatchException; +import io.jooby.value.ValueFactory; /** Unit test friendly context implementation. Allows to set context properties. */ public class MockContext implements DefaultContext { @@ -75,7 +76,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; @@ -275,7 +278,7 @@ public MockContext setPathMap(@NonNull Map pathMap) { @NonNull @Override public QueryString query() { - return QueryString.create(this, queryString); + return QueryString.create(valueFactory, queryString); } @NonNull @Override @@ -296,7 +299,7 @@ public String queryString() { @NonNull @Override public ValueNode header() { - return Value.headers(this, headers); + return Value.headers(valueFactory, headers); } /** 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..8cfb0a3bcb 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 @@ -15,11 +15,13 @@ import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.Session; import io.jooby.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-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..74c6ff2361 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 @@ -257,7 +257,8 @@ 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( + getRouter().getValueFactory(), name, exchange.getRequestHeaders().get(name)); } @NonNull @Override @@ -270,7 +271,7 @@ public ValueNode header() { HeaderValues values = map.get(name); headerMap.put(name.toString(), values); } - headers = Value.headers(this, headerMap); + headers = Value.headers(getRouter().getValueFactory(), headerMap); } return headers; } @@ -278,7 +279,7 @@ public ValueNode header() { @NonNull @Override public QueryString query() { if (query == null) { - query = QueryString.create(this, exchange.getQueryString()); + query = QueryString.create(getRouter().getValueFactory(), exchange.getQueryString()); } return query; } @@ -286,7 +287,7 @@ public QueryString query() { @NonNull @Override public Formdata form() { if (formdata == null) { - formdata = Formdata.create(this); + formdata = Formdata.create(getRouter().getValueFactory()); formData(formdata, exchange.getAttachment(FORM_DATA)); } return formdata; From e042e6b5de14a55c54a860aa9a55e0b1add1db8b Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 18 Jun 2025 19:52:12 -0300 Subject: [PATCH 02/60] value api: fix tests + code cleanup --- jooby/src/main/java/io/jooby/Value.java | 7 +- .../java/io/jooby/internal/ArrayValue.java | 27 ++- .../java/io/jooby/internal/HashValue.java | 11 +- .../java/io/jooby/internal/SingleValue.java | 9 +- .../io/jooby/internal/ValueConverters.java | 152 ++++++++-------- .../internal/converter/BuiltinConverter.java | 67 +++---- .../converter/ReflectiveBeanConverter.java | 1 - .../internal/converter/RuntimeConverter.java | 12 ++ .../internal/converter/StandardConverter.java | 167 ++++++++++++++++++ .../main/java/io/jooby/value/Converter.java | 4 +- .../java/io/jooby/value/ValueFactory.java | 97 ++++++++-- jooby/src/test/java/io/jooby/Issue2525.java | 27 +-- .../jooby/internal/ValueConverterHelper.java | 3 +- .../java/io/jooby/apt/MockContextHelper.java | 33 ++-- .../test/java/tests/HandlerCompilerTest.java | 2 +- .../src/test/java/tests/i2325/Issue2325.java | 6 +- .../src/test/java/tests/i2325/VC2325.java | 15 +- .../test/java/tests/i2405/Converter2405.java | 14 +- .../src/test/java/tests/i2405/Issue2405.java | 3 +- .../main/java/io/jooby/test/MockContext.java | 8 + .../test/java/io/jooby/test/LookupTest.java | 26 +-- .../src/test/java/io/jooby/test/UnitTest.java | 2 +- .../test/java/io/jooby/i2325/Issue2325.java | 3 +- .../src/test/java/io/jooby/i2325/VC2325.java | 17 +- .../test/java/io/jooby/i2557/Issue2557.java | 10 +- .../test/java/io/jooby/test/FeaturedTest.java | 32 ++-- .../src/test/java/io/jooby/test/MvcTest.java | 3 +- .../io/jooby/test/MyValueBeanConverter.java | 18 +- .../kotlin/io/jooby/FeaturedKotlinTest.kt | 9 +- 29 files changed, 540 insertions(+), 245 deletions(-) create mode 100644 jooby/src/main/java/io/jooby/internal/converter/RuntimeConverter.java create mode 100644 jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java diff --git a/jooby/src/main/java/io/jooby/Value.java b/jooby/src/main/java/io/jooby/Value.java index 7aa8c76b61..099465aa28 100644 --- a/jooby/src/main/java/io/jooby/Value.java +++ b/jooby/src/main/java/io/jooby/Value.java @@ -10,7 +10,6 @@ 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; @@ -63,7 +62,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); } @@ -382,7 +381,7 @@ default boolean isObject() { * @return List of items. */ @NonNull default List toList(@NonNull Class type) { - return Collections.singletonList(to(type)); + return List.of(to(type)); } /** @@ -393,7 +392,7 @@ default boolean isObject() { * @return Set of items. */ @NonNull default Set toSet(@NonNull Class type) { - return Collections.singleton(to(type)); + return Set.of(to(type)); } /** diff --git a/jooby/src/main/java/io/jooby/internal/ArrayValue.java b/jooby/src/main/java/io/jooby/internal/ArrayValue.java index c4c88c6acf..42356b00b9 100644 --- a/jooby/src/main/java/io/jooby/internal/ArrayValue.java +++ b/jooby/src/main/java/io/jooby/internal/ArrayValue.java @@ -91,12 +91,12 @@ public String toString() { @NonNull @Override public T to(@NonNull Class type) { - return (T) factory.convert(type, list.get(0)); + return ValueConverters.convert(list.get(0), type, factory); } @Nullable @Override public T toNullable(@NonNull Class type) { - return list.isEmpty() ? null : (T) factory.convert(type, list.get(0)); + return list.isEmpty() ? null : ValueConverters.convert(list.get(0), type, factory); } @NonNull @Override @@ -120,24 +120,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/HashValue.java b/jooby/src/main/java/io/jooby/internal/HashValue.java index 268eeaa3f3..15c1f33857 100644 --- a/jooby/src/main/java/io/jooby/internal/HashValue.java +++ b/jooby/src/main/java/io/jooby/internal/HashValue.java @@ -25,6 +25,7 @@ import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.FileUpload; import io.jooby.ValueNode; +import io.jooby.exception.TypeMismatchException; import io.jooby.value.ValueFactory; public class HashValue implements ValueNode { @@ -240,7 +241,11 @@ public Optional toOptional(@NonNull Class type) { @NonNull @Override public T to(@NonNull Class type) { - return (T) factory.convert(type, this); + var result = ValueConverters.convert(this, type, factory); + if (result == null) { + throw new TypeMismatchException(name(), type); + } + return (T) result; } @Nullable @Override @@ -296,7 +301,9 @@ private > C toCollection(@NonNull Class type, C co } } } else { - addItem(factory, this, type, collection); + var item = new HashValue(factory, this.name); + item.hash = hash; + addItem(factory, item, type, collection); } } return collection; diff --git a/jooby/src/main/java/io/jooby/internal/SingleValue.java b/jooby/src/main/java/io/jooby/internal/SingleValue.java index c0b169cdcb..37fc7554b0 100644 --- a/jooby/src/main/java/io/jooby/internal/SingleValue.java +++ b/jooby/src/main/java/io/jooby/internal/SingleValue.java @@ -19,6 +19,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.ValueNode; +import io.jooby.exception.TypeMismatchException; import io.jooby.value.ValueFactory; public class SingleValue implements ValueNode { @@ -87,12 +88,16 @@ public Optional toOptional(@NonNull Class type) { @NonNull @Override public T to(@NonNull Class type) { - return (T) factory.convert(type, this); + var result = ValueConverters.convert(this, type, factory); + if (result == null) { + throw new TypeMismatchException(name(), type); + } + return (T) result; } @Nullable @Override public T toNullable(@NonNull Class type) { - return (T) factory.convert(type, this); + return ValueConverters.convert(this, type, factory); } @Override diff --git a/jooby/src/main/java/io/jooby/internal/ValueConverters.java b/jooby/src/main/java/io/jooby/internal/ValueConverters.java index f504d96467..49cd7581d0 100644 --- a/jooby/src/main/java/io/jooby/internal/ValueConverters.java +++ b/jooby/src/main/java/io/jooby/internal/ValueConverters.java @@ -11,10 +11,8 @@ 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; import io.jooby.value.ValueFactory; public class ValueConverters { @@ -27,82 +25,86 @@ public static List> defaultConverters() { } public static T convert(ValueNode value, Type type, ValueFactory 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); + // 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, type, router, false); + // return (T) router.convert(type, value); } public static T convert( - ValueNode value, Class type, ValueFactory 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())); - } - - var converter = router.get(type); - if (converter != null) { - return (T) converter.convert(type, value); - } - // 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); + ValueNode value, Type type, ValueFactory router, boolean allowEmptyBean) { + return (T) router.convert(type, value); + // 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())); + // } + // + // var converter = router.get(type); + // if (converter != null) { + // return (T) converter.convert(type, value); + // } + // // 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) { diff --git a/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java b/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java index 29eed503f3..52104fb5c6 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java +++ b/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java @@ -8,6 +8,7 @@ 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; @@ -35,7 +36,7 @@ public enum BuiltinConverter implements ValueConverter, Converter { BigDecimal { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(BigDecimal.class, this); } @@ -50,13 +51,13 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { return new BigDecimal(value.value()); } }, BigInteger { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(BigInteger.class, this); } @@ -71,13 +72,13 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { return new BigInteger(value.value()); } }, Charset { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(Charset.class, this); } @@ -92,7 +93,7 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { String charset = value.value(); return switch (charset.toLowerCase()) { case "utf-8" -> StandardCharsets.UTF_8; @@ -107,7 +108,7 @@ public Object convert(@NonNull Class type, @NonNull Value value) { }, Date { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(Date.class, this); } @@ -122,7 +123,7 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { try { // must be millis return new Date(Long.parseLong(value.value())); @@ -135,7 +136,7 @@ public Object convert(@NonNull Class type, @NonNull Value value) { }, Duration { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(Duration.class, this); } @@ -150,7 +151,7 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { try { return java.time.Duration.parse(value.value()); } catch (DateTimeParseException x) { @@ -211,7 +212,7 @@ private static long parseDuration(String value) { }, Period { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(Period.class, this); } @@ -226,9 +227,9 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { try { - return java.time.Period.from((Duration) Duration.convert(value, type)); + return java.time.Period.from((Duration) Duration.convert(type, value)); } catch (DateTimeException x) { return parsePeriod(value.value()); } @@ -288,7 +289,7 @@ private static Period periodOf(int n, ChronoUnit unit) { }, Instant { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(Instant.class, this); } @@ -303,7 +304,7 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { try { return java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); } catch (NumberFormatException x) { @@ -313,7 +314,7 @@ public Object convert(@NonNull Class type, @NonNull Value value) { }, LocalDate { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(LocalDate.class, this); } @@ -328,7 +329,7 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { try { // must be millis var instant = java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); @@ -341,7 +342,7 @@ public Object convert(@NonNull Class type, @NonNull Value value) { }, LocalDateTime { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(LocalDateTime.class, this); } @@ -356,7 +357,7 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { try { // must be millis var instant = java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); @@ -369,7 +370,7 @@ public Object convert(@NonNull Class type, @NonNull Value value) { }, StatusCode { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(StatusCode.class, this); } @@ -384,13 +385,13 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { return io.jooby.StatusCode.valueOf(value.intValue()); } }, TimeZone { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(TimeZone.class, this); } @@ -405,13 +406,13 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { return java.util.TimeZone.getTimeZone(value.value()); } }, URI { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(URI.class, this); } @@ -426,7 +427,7 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { try { var uri = java.net.URI.create(value.value()); if (type == URL.class) { @@ -440,7 +441,7 @@ public Object convert(@NonNull Class type, @NonNull Value value) { }, UUID { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(UUID.class, this); } @@ -455,13 +456,13 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { return java.util.UUID.fromString(value.value()); } }, ZoneId { @Override - public void register(ValueFactory factory) { + protected void add(ValueFactory factory) { factory.put(ZoneId.class, this); } @@ -476,13 +477,19 @@ public boolean supports(@NonNull Class type) { } @Override - public Object convert(@NonNull Class type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value) { var zoneId = value.value(); return java.time.ZoneId.of(java.time.ZoneId.SHORT_IDS.getOrDefault(zoneId, zoneId)); } }; - public abstract void register(ValueFactory factory); + protected abstract void add(ValueFactory factory); + + public static void register(ValueFactory factory) { + for (var converter : values()) { + converter.add(factory); + } + } private static String getUnits(String s) { int i = s.length() - 1; diff --git a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java index 207487b963..1a395cc6c0 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java +++ b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java @@ -45,7 +45,6 @@ public void invoke(Object instance) throws InvocationTargetException, IllegalAcc method.invoke(instance, arg); } } - ; private static final String AMBIGUOUS_CONSTRUCTOR = "Ambiguous constructor found. Expecting a single constructor or only one annotated with " diff --git a/jooby/src/main/java/io/jooby/internal/converter/RuntimeConverter.java b/jooby/src/main/java/io/jooby/internal/converter/RuntimeConverter.java new file mode 100644 index 0000000000..54f8edd9db --- /dev/null +++ b/jooby/src/main/java/io/jooby/internal/converter/RuntimeConverter.java @@ -0,0 +1,12 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.converter; + +import io.jooby.value.Converter; + +public interface RuntimeConverter extends Converter { + boolean supports(Class type); +} diff --git a/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java b/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java new file mode 100644 index 0000000000..99e8dbbbd8 --- /dev/null +++ b/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java @@ -0,0 +1,167 @@ +/* + * 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.Type; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Value; +import io.jooby.value.Converter; +import io.jooby.value.ValueFactory; + +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) { + return value.valueOrNull(); + } + }, + Int { + @Override + protected void add(ValueFactory factory) { + factory.put(int.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.intValue(); + } + }, + IntNullable { + @Override + protected void add(ValueFactory factory) { + factory.put(Integer.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.isMissing() ? null : value.intValue(); + } + }, + Long { + @Override + protected void add(ValueFactory factory) { + factory.put(long.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.longValue(); + } + }, + LongNullable { + @Override + protected void add(ValueFactory factory) { + factory.put(Long.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.isMissing() ? null : value.longValue(); + } + }, + Float { + @Override + protected void add(ValueFactory factory) { + factory.put(float.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.floatValue(); + } + }, + FloatNullable { + @Override + protected void add(ValueFactory factory) { + factory.put(Float.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.isMissing() ? null : value.floatValue(); + } + }, + Double { + @Override + protected void add(ValueFactory factory) { + factory.put(double.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.doubleValue(); + } + }, + DoubleNullable { + @Override + protected void add(ValueFactory factory) { + factory.put(Double.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.isMissing() ? null : value.doubleValue(); + } + }, + Boolean { + @Override + protected void add(ValueFactory factory) { + factory.put(boolean.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.booleanValue(); + } + }, + BooleanNullable { + @Override + protected void add(ValueFactory factory) { + factory.put(Boolean.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.isMissing() ? null : value.booleanValue(); + } + }, + Byte { + @Override + protected void add(ValueFactory factory) { + factory.put(byte.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.byteValue(); + } + }, + ByteNullable { + @Override + protected void add(ValueFactory factory) { + factory.put(Byte.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return value.isMissing() ? null : value.byteValue(); + } + }; + + 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/Converter.java b/jooby/src/main/java/io/jooby/value/Converter.java index acbc1fcb81..038111c0ec 100644 --- a/jooby/src/main/java/io/jooby/value/Converter.java +++ b/jooby/src/main/java/io/jooby/value/Converter.java @@ -5,6 +5,8 @@ */ package io.jooby.value; +import java.lang.reflect.Type; + import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Value; @@ -23,5 +25,5 @@ public interface Converter { * @param value Value value. * @return Converted value. */ - Object convert(@NonNull Class type, @NonNull Value value); + Object convert(@NonNull Type type, @NonNull Value value); } diff --git a/jooby/src/main/java/io/jooby/value/ValueFactory.java b/jooby/src/main/java/io/jooby/value/ValueFactory.java index 6b0101cab4..ed54ad67f0 100644 --- a/jooby/src/main/java/io/jooby/value/ValueFactory.java +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -5,47 +5,110 @@ */ 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 io.jooby.QueryString; +import io.jooby.SneakyThrows; import io.jooby.Value; +import io.jooby.ValueNode; import io.jooby.internal.converter.BuiltinConverter; +import io.jooby.internal.converter.ReflectiveBeanConverter; +import io.jooby.internal.converter.StandardConverter; import io.jooby.internal.reflect.$Types; public class ValueFactory { - private final Map, Converter> converterMap = new HashMap<>(); + private final Map converterMap = new HashMap<>(); + + private MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + + private MethodType methodType = MethodType.methodType(Object.class, String.class); public ValueFactory() { - for (var converter : BuiltinConverter.values()) { - converter.register(this); - } + StandardConverter.register(this); + BuiltinConverter.register(this); } - public Converter get(Class type) { + public Converter get(Type type) { return converterMap.get(type); } - public ValueFactory put(Class type, Converter converter) { + public ValueFactory put(Type type, Converter converter) { converterMap.put(type, converter); return this; } public Object convert(@NonNull Type type, @NonNull Value value) { - if (type instanceof Class clazz) { - var converter = get(clazz); - return converter.convert(clazz, value); + var converter = converterMap.get(type); + if (converter != null) { + // Specific converter at type level. + return converter.convert(type, value); + } + var rawType = $Types.getRawType(type); + // Is it a container? + if (List.class.isAssignableFrom(rawType)) { + return List.of(convert($Types.parameterizedType0(type), value)); + } else if (Set.class.isAssignableFrom(rawType)) { + return Set.of(convert($Types.parameterizedType0(type), value)); + } else if (Optional.class.isAssignableFrom(rawType)) { + return Optional.of(convert($Types.parameterizedType0(type), value)); } else { - var rawType = $Types.getRawType(type); - if (List.class.isAssignableFrom(rawType)) { - return List.of(convert($Types.parameterizedType0(type), value)); - } else if (Set.class.isAssignableFrom(rawType)) { - return Set.of(convert($Types.parameterizedType0(type), value)); - } else if (Optional.class.isAssignableFrom(rawType)) { - return Optional.of(convert($Types.parameterizedType0(type), value)); + // dynamic conversion + if (Enum.class.isAssignableFrom(rawType)) { + return 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 valueOf.invoke(value.value()); + } catch (Throwable ex) { + throw SneakyThrows.propagate(ex); + } + } + } + // anything else fallback to reflective + var reflective = new ReflectiveBeanConverter(); + // TODO: review emptyBean flag + return reflective.convert((ValueNode) value, rawType, value instanceof QueryString); + } + } + + 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 new UnsupportedOperationException("Unsupported type: " + type); + throw x; } } } diff --git a/jooby/src/test/java/io/jooby/Issue2525.java b/jooby/src/test/java/io/jooby/Issue2525.java index d4e5e2fe4c..e7fec344b0 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,15 @@ import org.junit.jupiter.api.Test; import io.jooby.internal.UrlParser; -import io.jooby.internal.ValueConverterHelper; +import io.jooby.value.Converter; +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) { return new MyID2525(value.value()); } } @@ -99,7 +97,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 +124,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/internal/ValueConverterHelper.java b/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java index 0dae20ad93..078a091313 100644 --- a/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java +++ b/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java @@ -5,12 +5,11 @@ */ package io.jooby.internal; -import io.jooby.ValueConverter; import io.jooby.value.ValueFactory; public class ValueConverterHelper { - public static ValueFactory testContext(ValueConverter... converters) { + public static ValueFactory testContext() { var result = new ValueFactory(); return result; // List beans = new ArrayList<>(); 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/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/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..8c25ea916d 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,16 @@ */ 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; +import io.jooby.value.Converter; +public class VC2325 implements Converter { @Override - public Object convert(Value value, Class type) { + public Object convert(@NotNull Type type, @NotNull Value value) { 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..1d2bd3a098 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,17 @@ */ package tests.i2405; +import java.lang.reflect.Type; + +import org.jetbrains.annotations.NotNull; + import io.jooby.Value; -import io.jooby.ValueConverter; +import io.jooby.value.Converter; -public class Converter2405 implements ValueConverter { - @Override - public boolean supports(Class type) { - return type == Bean2405.class; - } +public class Converter2405 implements Converter { @Override - public Object convert(Value value, Class type) { + public Object convert(@NotNull Type type, @NotNull Value value) { 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-test/src/main/java/io/jooby/test/MockContext.java b/modules/jooby-test/src/main/java/io/jooby/test/MockContext.java index 1ab462b62b..655579e099 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 @@ -848,4 +848,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/test/java/io/jooby/test/LookupTest.java b/modules/jooby-test/src/test/java/io/jooby/test/LookupTest.java index 8a109187d7..2218b8048c 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,9 @@ 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.ValueFactory; public class LookupTest { @@ -35,7 +35,7 @@ public class LookupTest { @BeforeEach public void makeContext() { - context = new MockContext(); + context = mockContext(); Map pathMap = new HashMap<>(); pathMap.put("foo", "path-bar"); @@ -55,7 +55,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 +91,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 +109,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/UnitTest.java b/modules/jooby-test/src/test/java/io/jooby/test/UnitTest.java index f9d277b3fb..be56b27b41 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 @@ -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); 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..08fed37420 100644 --- a/tests/src/test/java/io/jooby/i2325/VC2325.java +++ b/tests/src/test/java/io/jooby/i2325/VC2325.java @@ -5,19 +5,18 @@ */ package io.jooby.i2325; +import java.lang.reflect.Type; + import org.jetbrains.annotations.NotNull; +import io.jooby.QueryString; import io.jooby.Value; -import io.jooby.ValueConverter; - -public class VC2325 implements ValueConverter { - @Override - public boolean supports(@NotNull Class type) { - return type == MyID2325.class; - } +import io.jooby.value.Converter; +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) { + 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/i2557/Issue2557.java b/tests/src/test/java/io/jooby/i2557/Issue2557.java index 26ab79227d..f5c00f1228 100644 --- a/tests/src/test/java/io/jooby/i2557/Issue2557.java +++ b/tests/src/test/java/io/jooby/i2557/Issue2557.java @@ -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/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index a824fb43ba..8da1d6efeb 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -3790,7 +3790,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 +3806,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()); + // }); }); } diff --git a/tests/src/test/java/io/jooby/test/MvcTest.java b/tests/src/test/java/io/jooby/test/MvcTest.java index eaac172aa3..eb7127b05f 100644 --- a/tests/src/test/java/io/jooby/test/MvcTest.java +++ b/tests/src/test/java/io/jooby/test/MvcTest.java @@ -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..d9a541b9b9 100644 --- a/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java +++ b/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java @@ -5,20 +5,20 @@ */ package io.jooby.test; -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.BeanConverter; +import java.lang.reflect.Type; + +import org.jetbrains.annotations.NotNull; + +import io.jooby.Value; import io.jooby.ValueNode; +import io.jooby.value.Converter; -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) { MyValue result = new MyValue(); - result.setString(value.get("string").value()); + result.setString(((ValueNode) value).get("string").value()); return result; } } 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'" + ) ) } From a802108ba4744982551227b98a4ffb73905c9adf Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 19 Jun 2025 10:29:54 -0300 Subject: [PATCH 03/60] value api: fix tests --- jooby/src/main/java/io/jooby/Body.java | 5 +-- .../main/java/io/jooby/DefaultContext.java | 5 ++- .../java/io/jooby/internal/HashValue.java | 42 +++++++------------ .../io/jooby/internal/QueryStringValue.java | 13 +++--- .../java/io/jooby/value/ValueFactory.java | 21 ++++++++-- .../test/java/io/jooby/test/FeaturedTest.java | 31 ++------------ 6 files changed, 47 insertions(+), 70 deletions(-) diff --git a/jooby/src/main/java/io/jooby/Body.java b/jooby/src/main/java/io/jooby/Body.java index ffc0fb0f53..ab4c662a9e 100644 --- a/jooby/src/main/java/io/jooby/Body.java +++ b/jooby/src/main/java/io/jooby/Body.java @@ -10,7 +10,6 @@ import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.file.Path; -import java.util.Collections; import java.util.List; import java.util.Set; @@ -88,11 +87,11 @@ default List toList(@NonNull Class type) { } default @NonNull @Override List toList() { - return Collections.singletonList(value()); + return List.of(value()); } default @NonNull @Override Set toSet() { - return Collections.singleton(value()); + return Set.of(value()); } @Override diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 41d446849c..d12f83ebe5 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -432,9 +432,10 @@ default boolean isSecure() { default @NonNull T decode(@NonNull Type type, @NonNull MediaType contentType) { try { if (MediaType.text.equals(contentType)) { - T result = ValueConverters.convert(body(), type, getRouter().getValueFactory()); - return result; + //noinspection unchecked + return (T) getRouter().getValueFactory().convert(type, body()); } + //noinspection unchecked return (T) decoder(contentType).decode(this, type); } catch (Exception x) { throw SneakyThrows.propagate(x); diff --git a/jooby/src/main/java/io/jooby/internal/HashValue.java b/jooby/src/main/java/io/jooby/internal/HashValue.java index 15c1f33857..a9b771c57b 100644 --- a/jooby/src/main/java/io/jooby/internal/HashValue.java +++ b/jooby/src/main/java/io/jooby/internal/HashValue.java @@ -25,12 +25,11 @@ import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.FileUpload; import io.jooby.ValueNode; -import io.jooby.exception.TypeMismatchException; import io.jooby.value.ValueFactory; public class HashValue implements ValueNode { protected static final Map EMPTY = Collections.emptyMap(); - private final ValueFactory factory; + protected final ValueFactory factory; protected Map hash = EMPTY; private final String name; private boolean arrayLike; @@ -192,13 +191,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); } @@ -236,26 +233,23 @@ 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) { - var result = ValueConverters.convert(this, type, factory); - if (result == null) { - throw new TypeMismatchException(name(), type); - } - return (T) result; + //noinspection unchecked + return (T) factory.convert(type, this); } @Nullable @Override - public final T toNullable(@NonNull Class type) { - return toNullable(factory, type, allowEmptyBean()); + public T toNullable(@NonNull Class type) { + return toNullable(factory, type); } - protected T toNullable( - @NonNull ValueFactory factory, @NonNull Class type, boolean allowEmpty) { - return ValueConverters.convert(this, type, factory, allowEmpty); + private T toNullable(@NonNull ValueFactory factory, @NonNull Class type) { + //noinspection unchecked + return (T) factory.convertOrNull(type, this); } @Override @@ -301,9 +295,9 @@ private > C toCollection(@NonNull Class type, C co } } } else { - var item = new HashValue(factory, this.name); - item.hash = hash; - addItem(factory, item, type, collection); + // var item = new HashValue(factory, this.name); + // item.hash = hash; + addItem(factory, this, type, collection); } } return collection; @@ -311,13 +305,9 @@ private > C toCollection(@NonNull Class type, C co private static void addItem( ValueFactory factory, HashValue node, Class type, Collection container) { - var item = node.toNullable(factory, type, false); + 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/QueryStringValue.java b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java index 79f4999592..b0b37fee65 100644 --- a/jooby/src/main/java/io/jooby/internal/QueryStringValue.java +++ b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java @@ -6,6 +6,7 @@ package io.jooby.internal; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.QueryString; import io.jooby.value.ValueFactory; @@ -17,18 +18,14 @@ public QueryStringValue(ValueFactory valueFactory, String queryString) { this.queryString = queryString; } - protected boolean allowEmptyBean() { - return true; - } - - @Override - protected T toNullable( - @NonNull ValueFactory valueFactory, @NonNull Class type, boolean allowEmpty) { + @Nullable @Override + public T toNullable(@NonNull Class type) { // 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, valueFactory, allowEmpty); + //noinspection unchecked + return (T) factory.convertOrEmpty(type, this); } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/value/ValueFactory.java b/jooby/src/main/java/io/jooby/value/ValueFactory.java index ed54ad67f0..7788c7e01a 100644 --- a/jooby/src/main/java/io/jooby/value/ValueFactory.java +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -12,10 +12,10 @@ import java.util.*; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.QueryString; import io.jooby.SneakyThrows; import io.jooby.Value; import io.jooby.ValueNode; +import io.jooby.exception.TypeMismatchException; import io.jooby.internal.converter.BuiltinConverter; import io.jooby.internal.converter.ReflectiveBeanConverter; import io.jooby.internal.converter.StandardConverter; @@ -44,6 +44,22 @@ public ValueFactory put(Type type, Converter converter) { } public Object convert(@NonNull Type type, @NonNull Value value) { + var result = convert(type, value, false); + if (result == null) { + throw new TypeMismatchException(value.name(), type); + } + return result; + } + + public Object convertOrNull(@NonNull Type type, @NonNull Value value) { + return convert(type, value, false); + } + + public Object convertOrEmpty(@NonNull Type type, @NonNull Value value) { + return convert(type, value, true); + } + + private Object convert(@NonNull Type type, @NonNull Value value, boolean allowEmptyBean) { var converter = converterMap.get(type); if (converter != null) { // Specific converter at type level. @@ -75,8 +91,7 @@ public Object convert(@NonNull Type type, @NonNull Value value) { } // anything else fallback to reflective var reflective = new ReflectiveBeanConverter(); - // TODO: review emptyBean flag - return reflective.convert((ValueNode) value, rawType, value instanceof QueryString); + return reflective.convert((ValueNode) value, rawType, allowEmptyBean); } } diff --git a/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index 8da1d6efeb..63eb81caa4 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,7 +64,6 @@ 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; @@ -1182,30 +1180,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 +1207,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", From 0be56e5424667229443db3b27f632c2b6516615e Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 19 Jun 2025 10:42:41 -0300 Subject: [PATCH 04/60] value api: remove internal class ValueConverters --- .../main/java/io/jooby/DefaultContext.java | 3 +- .../main/java/io/jooby/ValueConverter.java | 3 +- .../java/io/jooby/internal/ArrayValue.java | 6 +- .../java/io/jooby/internal/RouterImpl.java | 2 +- .../java/io/jooby/internal/SingleValue.java | 21 +-- .../io/jooby/internal/ValueConverters.java | 125 ------------------ .../java/io/jooby/value/ValueFactory.java | 26 ++-- 7 files changed, 26 insertions(+), 160 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/internal/ValueConverters.java diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index d12f83ebe5..0d58d3278c 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -37,7 +37,6 @@ import io.jooby.internal.MissingValue; import io.jooby.internal.SingleValue; import io.jooby.internal.UrlParser; -import io.jooby.internal.ValueConverters; /*** * Like {@link Context} but with couple of default methods. @@ -425,7 +424,7 @@ default boolean isSecure() { @Override default @NonNull T convertOrNull(@NonNull ValueNode value, @NonNull Class type) { - return ValueConverters.convert(value, type, getRouter().getValueFactory()); + return getRouter().getValueFactory().convertOrNull(type, value); } @Override diff --git a/jooby/src/main/java/io/jooby/ValueConverter.java b/jooby/src/main/java/io/jooby/ValueConverter.java index 6d8a87e675..38dc1937c8 100644 --- a/jooby/src/main/java/io/jooby/ValueConverter.java +++ b/jooby/src/main/java/io/jooby/ValueConverter.java @@ -8,7 +8,6 @@ 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 @@ -40,6 +39,6 @@ public interface ValueConverter { * @return Immutable list of defaults/built-in {@link ValueConverter}. */ static List> defaults() { - return ValueConverters.defaultConverters(); + return null; } } diff --git a/jooby/src/main/java/io/jooby/internal/ArrayValue.java b/jooby/src/main/java/io/jooby/internal/ArrayValue.java index 42356b00b9..44b95eaacd 100644 --- a/jooby/src/main/java/io/jooby/internal/ArrayValue.java +++ b/jooby/src/main/java/io/jooby/internal/ArrayValue.java @@ -91,12 +91,12 @@ public String toString() { @NonNull @Override public T to(@NonNull Class type) { - return ValueConverters.convert(list.get(0), type, factory); + return factory.convert(type, list.get(0)); } @Nullable @Override public T toNullable(@NonNull Class type) { - return list.isEmpty() ? null : ValueConverters.convert(list.get(0), type, factory); + return list.isEmpty() ? null : factory.convertOrNull(type, list.get(0)); } @NonNull @Override @@ -107,7 +107,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(); } diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index f0b216a702..701a2963dd 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -180,7 +180,7 @@ public Stack executor(Executor executor) { public RouterImpl() { stack.addLast(new Stack(chi, null)); - converters = new LinkedList<>(ValueConverters.defaultConverters()); + converters = new LinkedList<>(); beanConverters = new ArrayList<>(3); } diff --git a/jooby/src/main/java/io/jooby/internal/SingleValue.java b/jooby/src/main/java/io/jooby/internal/SingleValue.java index 37fc7554b0..4d8a25c691 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; @@ -19,7 +15,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.ValueNode; -import io.jooby.exception.TypeMismatchException; import io.jooby.value.ValueFactory; public class SingleValue implements ValueNode { @@ -68,7 +63,7 @@ public String toString() { @Override public Iterator iterator() { - return Collections.singletonList(this).iterator(); + return List.of(this).iterator(); } @NonNull @Override @@ -88,30 +83,26 @@ public Optional toOptional(@NonNull Class type) { @NonNull @Override public T to(@NonNull Class type) { - var result = ValueConverters.convert(this, type, factory); - if (result == null) { - throw new TypeMismatchException(name(), type); - } - return (T) result; + return factory.convert(type, this); } @Nullable @Override public T toNullable(@NonNull Class type) { - return ValueConverters.convert(this, type, factory); + return factory.convertOrNull(type, this); } @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/ValueConverters.java b/jooby/src/main/java/io/jooby/internal/ValueConverters.java deleted file mode 100644 index 49cd7581d0..0000000000 --- a/jooby/src/main/java/io/jooby/internal/ValueConverters.java +++ /dev/null @@ -1,125 +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.ValueConverter; -import io.jooby.ValueNode; -import io.jooby.internal.converter.BuiltinConverter; -import io.jooby.internal.converter.StringConstructorConverter; -import io.jooby.internal.converter.ValueOfConverter; -import io.jooby.value.ValueFactory; - -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, ValueFactory 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, type, router, false); - // return (T) router.convert(type, value); - } - - public static T convert( - ValueNode value, Type type, ValueFactory router, boolean allowEmptyBean) { - return (T) router.convert(type, value); - // 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())); - // } - // - // var converter = router.get(type); - // if (converter != null) { - // return (T) converter.convert(type, value); - // } - // // 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/value/ValueFactory.java b/jooby/src/main/java/io/jooby/value/ValueFactory.java index 7788c7e01a..11d312dbc1 100644 --- a/jooby/src/main/java/io/jooby/value/ValueFactory.java +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -43,47 +43,49 @@ public ValueFactory put(Type type, Converter converter) { return this; } - public Object convert(@NonNull Type type, @NonNull Value value) { - var result = convert(type, value, false); + public T convert(@NonNull Type type, @NonNull Value value) { + T result = convert(type, value, false); if (result == null) { throw new TypeMismatchException(value.name(), type); } + return result; } - public Object convertOrNull(@NonNull Type type, @NonNull Value value) { + public T convertOrNull(@NonNull Type type, @NonNull Value value) { return convert(type, value, false); } - public Object convertOrEmpty(@NonNull Type type, @NonNull Value value) { + public T convertOrEmpty(@NonNull Type type, @NonNull Value value) { return convert(type, value, true); } - private Object convert(@NonNull Type type, @NonNull Value value, boolean allowEmptyBean) { + @SuppressWarnings("unchecked") + private T convert(@NonNull Type type, @NonNull Value value, boolean allowEmptyBean) { var converter = converterMap.get(type); if (converter != null) { // Specific converter at type level. - return converter.convert(type, value); + return (T) converter.convert(type, value); } var rawType = $Types.getRawType(type); // Is it a container? if (List.class.isAssignableFrom(rawType)) { - return List.of(convert($Types.parameterizedType0(type), value)); + return (T) List.of(convert($Types.parameterizedType0(type), value)); } else if (Set.class.isAssignableFrom(rawType)) { - return Set.of(convert($Types.parameterizedType0(type), value)); + return (T) Set.of(convert($Types.parameterizedType0(type), value)); } else if (Optional.class.isAssignableFrom(rawType)) { - return Optional.of(convert($Types.parameterizedType0(type), value)); + return (T) Optional.of(convert($Types.parameterizedType0(type), value)); } else { // dynamic conversion if (Enum.class.isAssignableFrom(rawType)) { - return enumValue(value, (Class) 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 valueOf.invoke(value.value()); + return (T) valueOf.invoke(value.value()); } catch (Throwable ex) { throw SneakyThrows.propagate(ex); } @@ -91,7 +93,7 @@ private Object convert(@NonNull Type type, @NonNull Value value, boolean allowEm } // anything else fallback to reflective var reflective = new ReflectiveBeanConverter(); - return reflective.convert((ValueNode) value, rawType, allowEmptyBean); + return (T) reflective.convert((ValueNode) value, rawType, allowEmptyBean); } } From e293f853eb7a8f7b844694f66d15a6d8a7a739c1 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 19 Jun 2025 10:52:33 -0300 Subject: [PATCH 05/60] value api: remove Value/BeanConverter - both are replaced by Converter --- .../src/main/java/io/jooby/BeanConverter.java | 34 ----- jooby/src/main/java/io/jooby/Value.java | 2 +- .../main/java/io/jooby/ValueConverter.java | 44 ------ .../java/io/jooby/internal/RouterImpl.java | 7 - .../java/io/jooby/internal/SingleValue.java | 2 +- .../internal/converter/BuiltinConverter.java | 143 +----------------- .../converter/FromStringConverter.java | 52 ------- .../internal/converter/RuntimeConverter.java | 12 -- .../converter/StringConstructorConverter.java | 26 ---- .../internal/converter/ValueOfConverter.java | 32 ---- .../jooby/internal/DurationConverterTest.java | 4 +- .../src/test/java/tests/i2525/VC2525.java | 22 --- 12 files changed, 5 insertions(+), 375 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/BeanConverter.java delete mode 100644 jooby/src/main/java/io/jooby/ValueConverter.java delete mode 100644 jooby/src/main/java/io/jooby/internal/converter/FromStringConverter.java delete mode 100644 jooby/src/main/java/io/jooby/internal/converter/RuntimeConverter.java delete mode 100644 jooby/src/main/java/io/jooby/internal/converter/StringConstructorConverter.java delete mode 100644 jooby/src/main/java/io/jooby/internal/converter/ValueOfConverter.java delete mode 100644 modules/jooby-apt/src/test/java/tests/i2525/VC2525.java 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/Value.java b/jooby/src/main/java/io/jooby/Value.java index 099465aa28..22b6ee20c9 100644 --- a/jooby/src/main/java/io/jooby/Value.java +++ b/jooby/src/main/java/io/jooby/Value.java @@ -367,7 +367,7 @@ default boolean isObject() { */ @NonNull default Optional toOptional(@NonNull Class type) { try { - return Optional.ofNullable(to(type)); + return Optional.ofNullable(toNullable(type)); } catch (MissingValueException x) { return Optional.empty(); } 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 38dc1937c8..0000000000 --- a/jooby/src/main/java/io/jooby/ValueConverter.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; - -import java.util.List; - -import edu.umd.cs.findbugs.annotations.NonNull; - -/** - * 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 null; - } -} diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 701a2963dd..c44241e39c 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -157,10 +157,6 @@ public Stack executor(Executor executor) { private Cookie flashCookie = new Cookie("jooby.flash").setHttpOnly(true); - private LinkedList converters; - - private List beanConverters; - private ContextInitializer preDispatchInitializer; private ContextInitializer postDispatchInitializer; @@ -179,9 +175,6 @@ public Stack executor(Executor executor) { public RouterImpl() { stack.addLast(new Stack(chi, null)); - - converters = new LinkedList<>(); - beanConverters = new ArrayList<>(3); } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/internal/SingleValue.java b/jooby/src/main/java/io/jooby/internal/SingleValue.java index 4d8a25c691..2fe485a252 100644 --- a/jooby/src/main/java/io/jooby/internal/SingleValue.java +++ b/jooby/src/main/java/io/jooby/internal/SingleValue.java @@ -78,7 +78,7 @@ 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 diff --git a/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java b/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java index 52104fb5c6..08b45e3e0d 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java +++ b/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java @@ -29,27 +29,16 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.Value; -import io.jooby.ValueConverter; import io.jooby.value.Converter; import io.jooby.value.ValueFactory; -public enum BuiltinConverter implements ValueConverter, Converter { +public enum BuiltinConverter implements Converter { BigDecimal { @Override protected void add(ValueFactory factory) { factory.put(BigDecimal.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == BigDecimal.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { return new BigDecimal(value.value()); @@ -61,16 +50,6 @@ protected void add(ValueFactory factory) { factory.put(BigInteger.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == BigInteger.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { return new BigInteger(value.value()); @@ -82,16 +61,6 @@ protected void add(ValueFactory factory) { factory.put(Charset.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == Charset.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { String charset = value.value(); @@ -112,16 +81,6 @@ protected void add(ValueFactory factory) { factory.put(Date.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == Date.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { try { @@ -140,16 +99,6 @@ protected void add(ValueFactory factory) { factory.put(Duration.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == Duration.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { try { @@ -216,16 +165,6 @@ protected void add(ValueFactory factory) { factory.put(Period.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == Period.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { try { @@ -293,16 +232,6 @@ protected void add(ValueFactory factory) { factory.put(Instant.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == Instant.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { try { @@ -318,16 +247,6 @@ protected void add(ValueFactory factory) { factory.put(LocalDate.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == LocalDate.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { try { @@ -346,16 +265,6 @@ protected void add(ValueFactory factory) { factory.put(LocalDateTime.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == LocalDateTime.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { try { @@ -374,16 +283,6 @@ protected void add(ValueFactory factory) { factory.put(StatusCode.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == StatusCode.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { return io.jooby.StatusCode.valueOf(value.intValue()); @@ -395,16 +294,6 @@ protected void add(ValueFactory factory) { factory.put(TimeZone.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == TimeZone.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { return java.util.TimeZone.getTimeZone(value.value()); @@ -416,16 +305,6 @@ protected void add(ValueFactory factory) { factory.put(URI.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == URI.class || type == URL.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { try { @@ -445,16 +324,6 @@ protected void add(ValueFactory factory) { factory.put(UUID.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == UUID.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { return java.util.UUID.fromString(value.value()); @@ -466,16 +335,6 @@ protected void add(ValueFactory factory) { factory.put(ZoneId.class, this); } - @Override - public boolean supports(@NonNull Class type) { - return type == ZoneId.class; - } - - @Override - public @NonNull Object convert(@NonNull Value value, @NonNull Class type) { - return convert(type, value); - } - @Override public Object convert(@NonNull Type type, @NonNull Value value) { var zoneId = value.value(); 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/RuntimeConverter.java b/jooby/src/main/java/io/jooby/internal/converter/RuntimeConverter.java deleted file mode 100644 index 54f8edd9db..0000000000 --- a/jooby/src/main/java/io/jooby/internal/converter/RuntimeConverter.java +++ /dev/null @@ -1,12 +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 io.jooby.value.Converter; - -public interface RuntimeConverter extends Converter { - boolean supports(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/test/java/io/jooby/internal/DurationConverterTest.java b/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java index 899ac2a942..8ac65742e0 100644 --- a/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java +++ b/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java @@ -53,11 +53,11 @@ public void convertPeriod() { } private Duration duration(String value) { - return (Duration) BuiltinConverter.Duration.convert(value(value), Duration.class); + return (Duration) BuiltinConverter.Duration.convert(Duration.class, value(value)); } private Period period(String value) { - return (Period) BuiltinConverter.Period.convert(value(value), Period.class); + return (Period) BuiltinConverter.Period.convert(Period.class, value(value)); } private Value value(String value) { 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()); - } -} From be75181917f1504268a1254b51610b83adde41ed Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 19 Jun 2025 10:57:54 -0300 Subject: [PATCH 06/60] value api: merge builtin converter into standard converter --- .../internal/converter/BuiltinConverter.java | 362 ------------------ .../internal/converter/StandardConverter.java | 340 ++++++++++++++++ .../java/io/jooby/value/ValueFactory.java | 4 - jooby/src/main/java/module-info.java | 1 + 4 files changed, 341 insertions(+), 366 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java diff --git a/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java b/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java deleted file mode 100644 index 08b45e3e0d..0000000000 --- a/jooby/src/main/java/io/jooby/internal/converter/BuiltinConverter.java +++ /dev/null @@ -1,362 +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 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; -import java.net.URI; -import java.net.URL; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.time.*; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.time.temporal.ChronoUnit; -import java.util.Date; -import java.util.TimeZone; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.SneakyThrows; -import io.jooby.StatusCode; -import io.jooby.Value; -import io.jooby.value.Converter; -import io.jooby.value.ValueFactory; - -public enum BuiltinConverter implements Converter { - BigDecimal { - @Override - protected void add(ValueFactory factory) { - factory.put(BigDecimal.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - return new BigDecimal(value.value()); - } - }, - BigInteger { - @Override - protected void add(ValueFactory factory) { - factory.put(BigInteger.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - return new BigInteger(value.value()); - } - }, - Charset { - @Override - protected void add(ValueFactory factory) { - factory.put(Charset.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - String charset = value.value(); - return switch (charset.toLowerCase()) { - case "utf-8" -> StandardCharsets.UTF_8; - case "us-ascii" -> StandardCharsets.US_ASCII; - case "iso-8859-1" -> StandardCharsets.ISO_8859_1; - case "utf-16" -> StandardCharsets.UTF_16; - case "utf-16be" -> StandardCharsets.UTF_16BE; - case "utf-16le" -> StandardCharsets.UTF_16LE; - default -> java.nio.charset.Charset.forName(charset); - }; - } - }, - Date { - @Override - protected void add(ValueFactory factory) { - factory.put(Date.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - try { - // must be millis - return new Date(Long.parseLong(value.value())); - } catch (NumberFormatException x) { - // must be YYYY-MM-dd - var date = java.time.LocalDate.parse(value.value(), DateTimeFormatter.ISO_LOCAL_DATE); - return java.util.Date.from(date.atStartOfDay(java.time.ZoneId.systemDefault()).toInstant()); - } - } - }, - Duration { - @Override - protected void add(ValueFactory factory) { - factory.put(Duration.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - try { - return java.time.Duration.parse(value.value()); - } catch (DateTimeParseException x) { - var millis = MILLISECONDS.convert(parseDuration(value.value()), NANOSECONDS); - return java.time.Duration.ofMillis(millis); - } - } - - /** - * Parses a duration string. If no units are specified in the string, it is assumed to be in - * milliseconds. The returned duration is in nanoseconds. The purpose of this function is to - * implement the duration-related methods in the ConfigObject interface. - * - * @param value the string to parse - * @return duration in nanoseconds - */ - private static long parseDuration(String value) { - var unitString = getUnits(value); - var numberString = value.substring(0, value.length() - unitString.length()); - - // this would be caught later anyway, but the error message - // is more helpful if we check it here. - if (numberString.isEmpty()) { - throw new DateTimeParseException( - "No number in duration value: '" + numberString + "'", value, 0); - } - - // note that this is deliberately case-sensitive - var units = - switch (unitString) { - case "ms", "milli", "millis", "millisecond", "milliseconds", "" -> MILLISECONDS; - case "us", "micro", "micros", "microsecond", "microseconds" -> TimeUnit.MICROSECONDS; - case "ns", "nano", "nanos", "nanosecond", "nanoseconds" -> NANOSECONDS; - case "s", "second", "seconds" -> TimeUnit.SECONDS; - case "m", "minute", "minutes" -> TimeUnit.MINUTES; - case "h", "hour", "hours" -> TimeUnit.HOURS; - case "d", "day", "days" -> TimeUnit.DAYS; - default -> - throw new DateTimeParseException( - "Could not parse time unit '" + unitString + "'", - value, - value.length() - unitString.length()); - }; - try { - // 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)); - } else { - long nanosInUnit = units.toNanos(1); - return (long) (Double.parseDouble(numberString) * nanosInUnit); - } - } catch (NumberFormatException e) { - throw new DateTimeParseException( - "Could not parse duration number '" + numberString + "'", numberString, 0); - } - } - }, - Period { - @Override - protected void add(ValueFactory factory) { - factory.put(Period.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - try { - return java.time.Period.from((Duration) Duration.convert(type, value)); - } catch (DateTimeException x) { - return parsePeriod(value.value()); - } - } - - /** - * Parses a period string. If no units are specified in the string, it is assumed to be in days. - * The returned period is in days. The purpose of this function is to implement the - * period-related methods in the ConfigObject interface. - * - * @param value the string to parse path to include in exceptions - * @return duration in days - */ - public static Period parsePeriod(String value) { - var unitString = getUnits(value); - var numberString = value.substring(0, value.length() - unitString.length()); - - // this would be caught later anyway, but the error message - // is more helpful if we check it here. - if (numberString.isEmpty()) - throw new DateTimeParseException( - "No number in period value '" + numberString + "'", numberString, 0); - - var units = - switch (unitString) { - case "d", "day", "days", "" -> ChronoUnit.DAYS; - case "w", "week", "weeks" -> ChronoUnit.WEEKS; - case "m", "mo", "month", "months" -> ChronoUnit.MONTHS; - case "y", "year", "years" -> ChronoUnit.YEARS; - default -> - throw new DateTimeParseException( - "Could not parse time unit '" + unitString + "' (try d, w, mo, y)", - value, - value.length() - unitString.length()); - }; - try { - return periodOf(Integer.parseInt(numberString), units); - } catch (NumberFormatException e) { - throw new DateTimeParseException( - "Could not parse duration number '" + numberString + "'", numberString, 0); - } - } - - private static Period periodOf(int n, ChronoUnit unit) { - if (unit.isTimeBased()) { - throw new DateTimeException(unit + " cannot be converted to a java.time.Period"); - } - - return switch (unit) { - case DAYS -> java.time.Period.ofDays(n); - case WEEKS -> java.time.Period.ofWeeks(n); - case MONTHS -> java.time.Period.ofMonths(n); - case YEARS -> java.time.Period.ofYears(n); - default -> throw new DateTimeException(unit + " cannot be converted to a java.time.Period"); - }; - } - }, - Instant { - @Override - protected void add(ValueFactory factory) { - factory.put(Instant.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - try { - return java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); - } catch (NumberFormatException x) { - return DateTimeFormatter.ISO_INSTANT.parse(value.value(), java.time.Instant::from); - } - } - }, - LocalDate { - @Override - protected void add(ValueFactory factory) { - factory.put(LocalDate.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - try { - // must be millis - var instant = java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); - return instant.atZone(java.time.ZoneId.systemDefault()).toLocalDate(); - } catch (NumberFormatException x) { - // must be YYYY-MM-dd - return java.time.LocalDate.parse(value.value(), DateTimeFormatter.ISO_LOCAL_DATE); - } - } - }, - LocalDateTime { - @Override - protected void add(ValueFactory factory) { - factory.put(LocalDateTime.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - try { - // must be millis - var instant = java.time.Instant.ofEpochMilli(Long.parseLong(value.value())); - return instant.atZone(java.time.ZoneId.systemDefault()).toLocalDateTime(); - } catch (NumberFormatException x) { - // must be YYYY-MM-dd - return java.time.LocalDateTime.parse(value.value(), DateTimeFormatter.ISO_LOCAL_DATE_TIME); - } - } - }, - StatusCode { - @Override - protected void add(ValueFactory factory) { - factory.put(StatusCode.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - return io.jooby.StatusCode.valueOf(value.intValue()); - } - }, - TimeZone { - @Override - protected void add(ValueFactory factory) { - factory.put(TimeZone.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - return java.util.TimeZone.getTimeZone(value.value()); - } - }, - URI { - @Override - protected void add(ValueFactory factory) { - factory.put(URI.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - try { - var uri = java.net.URI.create(value.value()); - if (type == URL.class) { - return uri.toURL(); - } - return uri; - } catch (MalformedURLException x) { - throw SneakyThrows.propagate(x); - } - } - }, - UUID { - @Override - protected void add(ValueFactory factory) { - factory.put(UUID.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - return java.util.UUID.fromString(value.value()); - } - }, - ZoneId { - @Override - protected void add(ValueFactory factory) { - factory.put(ZoneId.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value) { - var zoneId = value.value(); - return java.time.ZoneId.of(java.time.ZoneId.SHORT_IDS.getOrDefault(zoneId, zoneId)); - } - }; - - protected abstract void add(ValueFactory factory); - - public static void register(ValueFactory factory) { - for (var converter : values()) { - converter.add(factory); - } - } - - private static String getUnits(String s) { - int i = s.length() - 1; - while (i >= 0) { - char c = s.charAt(i); - if (!Character.isLetter(c)) break; - i -= 1; - } - return s.substring(i + 1); - } -} diff --git a/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java b/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java index 99e8dbbbd8..cec464031f 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java +++ b/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java @@ -5,9 +5,31 @@ */ package io.jooby.internal.converter; +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; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.TimeUnit; import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.SneakyThrows; +import io.jooby.StatusCode; import io.jooby.Value; import io.jooby.value.Converter; import io.jooby.value.ValueFactory; @@ -155,8 +177,326 @@ protected void add(ValueFactory factory) { public Object convert(@NonNull Type type, @NonNull Value value) { return value.isMissing() ? null : value.byteValue(); } + }, + BigDecimal { + @Override + protected void add(ValueFactory factory) { + factory.put(BigDecimal.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return new BigDecimal(value.value()); + } + }, + BigInteger { + @Override + protected void add(ValueFactory factory) { + factory.put(BigInteger.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return new BigInteger(value.value()); + } + }, + Charset { + @Override + protected void add(ValueFactory factory) { + factory.put(Charset.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + String charset = value.value(); + return switch (charset.toLowerCase()) { + case "utf-8" -> StandardCharsets.UTF_8; + case "us-ascii" -> StandardCharsets.US_ASCII; + case "iso-8859-1" -> StandardCharsets.ISO_8859_1; + case "utf-16" -> StandardCharsets.UTF_16; + case "utf-16be" -> StandardCharsets.UTF_16BE; + case "utf-16le" -> StandardCharsets.UTF_16LE; + default -> java.nio.charset.Charset.forName(charset); + }; + } + }, + Date { + @Override + protected void add(ValueFactory factory) { + factory.put(Date.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + try { + // must be millis + 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); + return java.util.Date.from(date.atStartOfDay(java.time.ZoneId.systemDefault()).toInstant()); + } + } + }, + Duration { + @Override + protected void add(ValueFactory factory) { + factory.put(Duration.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + try { + return java.time.Duration.parse(value.value()); + } catch (DateTimeParseException x) { + var millis = MILLISECONDS.convert(parseDuration(value.value()), NANOSECONDS); + return java.time.Duration.ofMillis(millis); + } + } + + /** + * Parses a duration string. If no units are specified in the string, it is assumed to be in + * milliseconds. The returned duration is in nanoseconds. The purpose of this function is to + * implement the duration-related methods in the ConfigObject interface. + * + * @param value the string to parse + * @return duration in nanoseconds + */ + private static long parseDuration(String value) { + var unitString = getUnits(value); + var numberString = value.substring(0, value.length() - unitString.length()); + + // this would be caught later anyway, but the error message + // is more helpful if we check it here. + if (numberString.isEmpty()) { + throw new DateTimeParseException( + "No number in duration value: '" + numberString + "'", value, 0); + } + + // note that this is deliberately case-sensitive + var units = + switch (unitString) { + case "ms", "milli", "millis", "millisecond", "milliseconds", "" -> MILLISECONDS; + case "us", "micro", "micros", "microsecond", "microseconds" -> TimeUnit.MICROSECONDS; + case "ns", "nano", "nanos", "nanosecond", "nanoseconds" -> NANOSECONDS; + case "s", "second", "seconds" -> TimeUnit.SECONDS; + case "m", "minute", "minutes" -> TimeUnit.MINUTES; + case "h", "hour", "hours" -> TimeUnit.HOURS; + case "d", "day", "days" -> TimeUnit.DAYS; + default -> + throw new DateTimeParseException( + "Could not parse time unit '" + unitString + "'", + value, + value.length() - unitString.length()); + }; + try { + // 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(parseLong(numberString)); + } else { + long nanosInUnit = units.toNanos(1); + return (long) (parseDouble(numberString) * nanosInUnit); + } + } catch (NumberFormatException e) { + throw new DateTimeParseException( + "Could not parse duration number '" + numberString + "'", numberString, 0); + } + } + }, + Period { + @Override + protected void add(ValueFactory factory) { + factory.put(Period.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + try { + return java.time.Period.from((Duration) Duration.convert(type, value)); + } catch (DateTimeException x) { + return parsePeriod(value.value()); + } + } + + /** + * Parses a period string. If no units are specified in the string, it is assumed to be in days. + * The returned period is in days. The purpose of this function is to implement the + * period-related methods in the ConfigObject interface. + * + * @param value the string to parse path to include in exceptions + * @return duration in days + */ + public static Period parsePeriod(String value) { + var unitString = getUnits(value); + var numberString = value.substring(0, value.length() - unitString.length()); + + // this would be caught later anyway, but the error message + // is more helpful if we check it here. + if (numberString.isEmpty()) + throw new DateTimeParseException( + "No number in period value '" + numberString + "'", numberString, 0); + + var units = + switch (unitString) { + case "d", "day", "days", "" -> ChronoUnit.DAYS; + case "w", "week", "weeks" -> ChronoUnit.WEEKS; + case "m", "mo", "month", "months" -> ChronoUnit.MONTHS; + case "y", "year", "years" -> ChronoUnit.YEARS; + default -> + throw new DateTimeParseException( + "Could not parse time unit '" + unitString + "' (try d, w, mo, y)", + value, + value.length() - unitString.length()); + }; + try { + return periodOf(Integer.parseInt(numberString), units); + } catch (NumberFormatException e) { + throw new DateTimeParseException( + "Could not parse duration number '" + numberString + "'", numberString, 0); + } + } + + private static Period periodOf(int n, ChronoUnit unit) { + if (unit.isTimeBased()) { + throw new DateTimeException(unit + " cannot be converted to a java.time.Period"); + } + + return switch (unit) { + case DAYS -> java.time.Period.ofDays(n); + case WEEKS -> java.time.Period.ofWeeks(n); + case MONTHS -> java.time.Period.ofMonths(n); + case YEARS -> java.time.Period.ofYears(n); + default -> throw new DateTimeException(unit + " cannot be converted to a java.time.Period"); + }; + } + }, + Instant { + @Override + protected void add(ValueFactory factory) { + factory.put(Instant.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + try { + return java.time.Instant.ofEpochMilli(parseLong(value.value())); + } catch (NumberFormatException x) { + return DateTimeFormatter.ISO_INSTANT.parse(value.value(), java.time.Instant::from); + } + } + }, + LocalDate { + @Override + protected void add(ValueFactory factory) { + factory.put(LocalDate.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + try { + // must be millis + 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 + return java.time.LocalDate.parse(value.value(), DateTimeFormatter.ISO_LOCAL_DATE); + } + } + }, + LocalDateTime { + @Override + protected void add(ValueFactory factory) { + factory.put(LocalDateTime.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + try { + // must be millis + 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 + return java.time.LocalDateTime.parse(value.value(), DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + } + }, + StatusCode { + @Override + protected void add(ValueFactory factory) { + factory.put(StatusCode.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return io.jooby.StatusCode.valueOf(value.intValue()); + } + }, + TimeZone { + @Override + protected void add(ValueFactory factory) { + factory.put(TimeZone.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return java.util.TimeZone.getTimeZone(value.value()); + } + }, + URI { + @Override + protected void add(ValueFactory factory) { + factory.put(URI.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + try { + var uri = java.net.URI.create(value.value()); + if (type == URL.class) { + return uri.toURL(); + } + return uri; + } catch (MalformedURLException x) { + throw SneakyThrows.propagate(x); + } + } + }, + UUID { + @Override + protected void add(ValueFactory factory) { + factory.put(UUID.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + return java.util.UUID.fromString(value.value()); + } + }, + ZoneId { + @Override + protected void add(ValueFactory factory) { + factory.put(ZoneId.class, this); + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value) { + var zoneId = value.value(); + return java.time.ZoneId.of(java.time.ZoneId.SHORT_IDS.getOrDefault(zoneId, zoneId)); + } }; + private static String getUnits(String s) { + int i = s.length() - 1; + while (i >= 0) { + char c = s.charAt(i); + if (!Character.isLetter(c)) break; + i -= 1; + } + return s.substring(i + 1); + } + protected abstract void add(ValueFactory factory); public static void register(ValueFactory factory) { diff --git a/jooby/src/main/java/io/jooby/value/ValueFactory.java b/jooby/src/main/java/io/jooby/value/ValueFactory.java index 11d312dbc1..2fbda4c8a0 100644 --- a/jooby/src/main/java/io/jooby/value/ValueFactory.java +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -16,7 +16,6 @@ import io.jooby.Value; import io.jooby.ValueNode; import io.jooby.exception.TypeMismatchException; -import io.jooby.internal.converter.BuiltinConverter; import io.jooby.internal.converter.ReflectiveBeanConverter; import io.jooby.internal.converter.StandardConverter; import io.jooby.internal.reflect.$Types; @@ -27,11 +26,8 @@ public class ValueFactory { private MethodHandles.Lookup lookup = MethodHandles.publicLookup(); - private MethodType methodType = MethodType.methodType(Object.class, String.class); - public ValueFactory() { StandardConverter.register(this); - BuiltinConverter.register(this); } public Converter get(Type type) { diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index e8c2696895..9326ab4a22 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -33,4 +33,5 @@ * Optional dependency for rate limiting */ requires static io.github.bucket4j.core; + requires io.jooby; } From 5e4c455ada897bf22471afee824a883eefcad9bb Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 19 Jun 2025 11:51:55 -0300 Subject: [PATCH 07/60] value api: replace `convertXXX` with hint parameter --- .../main/java/io/jooby/DefaultContext.java | 20 ++++--- .../java/io/jooby/internal/ArrayValue.java | 3 +- .../java/io/jooby/internal/HashValue.java | 7 +-- .../io/jooby/internal/QueryStringValue.java | 4 +- .../java/io/jooby/internal/SingleValue.java | 3 +- .../internal/converter/StandardConverter.java | 57 ++++++++++--------- .../java/io/jooby/value/ConversionHint.java | 34 +++++++++++ .../main/java/io/jooby/value/Converter.java | 3 +- .../java/io/jooby/value/ValueFactory.java | 22 ++++--- jooby/src/main/java/module-info.java | 1 - jooby/src/test/java/io/jooby/Issue2525.java | 3 +- .../jooby/internal/DurationConverterTest.java | 9 ++- .../src/test/java/tests/i2325/VC2325.java | 3 +- .../test/java/tests/i2405/Converter2405.java | 4 +- .../src/test/java/io/jooby/i2325/VC2325.java | 3 +- .../io/jooby/test/MyValueBeanConverter.java | 3 +- 16 files changed, 113 insertions(+), 66 deletions(-) create mode 100644 jooby/src/main/java/io/jooby/value/ConversionHint.java diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 0d58d3278c..4d11a6ba29 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -37,6 +37,8 @@ import io.jooby.internal.MissingValue; import io.jooby.internal.SingleValue; import io.jooby.internal.UrlParser; +import io.jooby.value.ConversionHint; +import io.jooby.value.ValueFactory; /*** * Like {@link Context} but with couple of default methods. @@ -133,7 +135,7 @@ default FlashMap flashOrNull() { */ @Override default @NonNull Value flash(@NonNull String name) { - return Value.create(getRouter().getValueFactory(), name, flash().get(name)); + return Value.create(getValueFactory(), name, flash().get(name)); } @Override @@ -184,9 +186,7 @@ default FlashMap flashOrNull() { @Override default @NonNull Value cookie(@NonNull String name) { String value = cookieMap().get(name); - return value == null - ? Value.missing(name) - : Value.value(getRouter().getValueFactory(), name, value); + return value == null ? Value.missing(name) : Value.value(getValueFactory(), name, value); } @Override @@ -194,7 +194,7 @@ default FlashMap flashOrNull() { String value = pathMap().get(name); return value == null ? new MissingValue(name) - : new SingleValue(getRouter().getValueFactory(), name, UrlParser.decodePathSegment(value)); + : new SingleValue(getValueFactory(), name, UrlParser.decodePathSegment(value)); } @Override @@ -204,7 +204,7 @@ default FlashMap flashOrNull() { @Override @NonNull default ValueNode path() { - var path = new HashValue(getRouter().getValueFactory(), null); + var path = new HashValue(getValueFactory(), null); for (Map.Entry entry : pathMap().entrySet()) { path.put(entry.getKey(), entry.getValue()); } @@ -422,9 +422,13 @@ default boolean isSecure() { return body().to(type); } + default ValueFactory getValueFactory() { + return getRouter().getValueFactory(); + } + @Override default @NonNull T convertOrNull(@NonNull ValueNode value, @NonNull Class type) { - return getRouter().getValueFactory().convertOrNull(type, value); + return getValueFactory().convert(type, value, ConversionHint.Nullable); } @Override @@ -432,7 +436,7 @@ default boolean isSecure() { try { if (MediaType.text.equals(contentType)) { //noinspection unchecked - return (T) getRouter().getValueFactory().convert(type, body()); + return (T) getValueFactory().convert(type, body()); } //noinspection unchecked return (T) decoder(contentType).decode(this, type); diff --git a/jooby/src/main/java/io/jooby/internal/ArrayValue.java b/jooby/src/main/java/io/jooby/internal/ArrayValue.java index 44b95eaacd..688dc3c7ce 100644 --- a/jooby/src/main/java/io/jooby/internal/ArrayValue.java +++ b/jooby/src/main/java/io/jooby/internal/ArrayValue.java @@ -19,6 +19,7 @@ import io.jooby.ValueNode; import io.jooby.exception.MissingValueException; import io.jooby.exception.TypeMismatchException; +import io.jooby.value.ConversionHint; import io.jooby.value.ValueFactory; public class ArrayValue implements ValueNode { @@ -96,7 +97,7 @@ public T to(@NonNull Class type) { @Nullable @Override public T toNullable(@NonNull Class type) { - return list.isEmpty() ? null : factory.convertOrNull(type, list.get(0)); + return list.isEmpty() ? null : factory.convert(type, list.get(0), ConversionHint.Nullable); } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/internal/HashValue.java b/jooby/src/main/java/io/jooby/internal/HashValue.java index a9b771c57b..c37a1921cc 100644 --- a/jooby/src/main/java/io/jooby/internal/HashValue.java +++ b/jooby/src/main/java/io/jooby/internal/HashValue.java @@ -25,6 +25,7 @@ import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.FileUpload; import io.jooby.ValueNode; +import io.jooby.value.ConversionHint; import io.jooby.value.ValueFactory; public class HashValue implements ValueNode { @@ -238,8 +239,7 @@ public Optional toOptional(@NonNull Class type) { @NonNull @Override public T to(@NonNull Class type) { - //noinspection unchecked - return (T) factory.convert(type, this); + return factory.convert(type, this); } @Nullable @Override @@ -248,8 +248,7 @@ public T toNullable(@NonNull Class type) { } private T toNullable(@NonNull ValueFactory factory, @NonNull Class type) { - //noinspection unchecked - return (T) factory.convertOrNull(type, this); + return factory.convert(type, this, ConversionHint.Nullable); } @Override diff --git a/jooby/src/main/java/io/jooby/internal/QueryStringValue.java b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java index b0b37fee65..a68b5c48d4 100644 --- a/jooby/src/main/java/io/jooby/internal/QueryStringValue.java +++ b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java @@ -8,6 +8,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.QueryString; +import io.jooby.value.ConversionHint; import io.jooby.value.ValueFactory; public class QueryStringValue extends HashValue implements QueryString { @@ -24,8 +25,7 @@ public T toNullable(@NonNull Class type) { // GET /search? // with class Search (q="*") // so q is defaulted to "*" - //noinspection unchecked - return (T) factory.convertOrEmpty(type, this); + return factory.convert(type, this, ConversionHint.Empty); } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/internal/SingleValue.java b/jooby/src/main/java/io/jooby/internal/SingleValue.java index 2fe485a252..d145fa14e8 100644 --- a/jooby/src/main/java/io/jooby/internal/SingleValue.java +++ b/jooby/src/main/java/io/jooby/internal/SingleValue.java @@ -15,6 +15,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.ValueNode; +import io.jooby.value.ConversionHint; import io.jooby.value.ValueFactory; public class SingleValue implements ValueNode { @@ -88,7 +89,7 @@ public T to(@NonNull Class type) { @Nullable @Override public T toNullable(@NonNull Class type) { - return factory.convertOrNull(type, this); + return factory.convert(type, this, ConversionHint.Nullable); } @Override diff --git a/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java b/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java index cec464031f..e60ede3bb9 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java +++ b/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java @@ -31,6 +31,7 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.Value; +import io.jooby.value.ConversionHint; import io.jooby.value.Converter; import io.jooby.value.ValueFactory; @@ -42,7 +43,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.valueOrNull(); } }, @@ -53,7 +54,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.intValue(); } }, @@ -64,7 +65,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.isMissing() ? null : value.intValue(); } }, @@ -75,7 +76,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.longValue(); } }, @@ -86,7 +87,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.isMissing() ? null : value.longValue(); } }, @@ -97,7 +98,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.floatValue(); } }, @@ -108,7 +109,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.isMissing() ? null : value.floatValue(); } }, @@ -119,7 +120,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.doubleValue(); } }, @@ -130,7 +131,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.isMissing() ? null : value.doubleValue(); } }, @@ -141,7 +142,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.booleanValue(); } }, @@ -152,7 +153,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.isMissing() ? null : value.booleanValue(); } }, @@ -163,7 +164,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.byteValue(); } }, @@ -174,7 +175,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return value.isMissing() ? null : value.byteValue(); } }, @@ -185,7 +186,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return new BigDecimal(value.value()); } }, @@ -196,7 +197,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return new BigInteger(value.value()); } }, @@ -207,7 +208,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { String charset = value.value(); return switch (charset.toLowerCase()) { case "utf-8" -> StandardCharsets.UTF_8; @@ -227,7 +228,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { // must be millis return new Date(parseLong(value.value())); @@ -245,7 +246,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { return java.time.Duration.parse(value.value()); } catch (DateTimeParseException x) { @@ -311,9 +312,9 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { - return java.time.Period.from((Duration) Duration.convert(type, value)); + return java.time.Period.from((Duration) Duration.convert(type, value, hint)); } catch (DateTimeException x) { return parsePeriod(value.value()); } @@ -378,7 +379,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { return java.time.Instant.ofEpochMilli(parseLong(value.value())); } catch (NumberFormatException x) { @@ -393,7 +394,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { // must be millis var instant = java.time.Instant.ofEpochMilli(parseLong(value.value())); @@ -411,7 +412,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { try { // must be millis var instant = java.time.Instant.ofEpochMilli(parseLong(value.value())); @@ -429,7 +430,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return io.jooby.StatusCode.valueOf(value.intValue()); } }, @@ -440,7 +441,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return java.util.TimeZone.getTimeZone(value.value()); } }, @@ -451,7 +452,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + 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) { @@ -470,7 +471,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { return java.util.UUID.fromString(value.value()); } }, @@ -481,7 +482,7 @@ protected void add(ValueFactory factory) { } @Override - public Object convert(@NonNull Type type, @NonNull Value value) { + 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)); } 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..f8fdfb5f2c --- /dev/null +++ b/jooby/src/main/java/io/jooby/value/ConversionHint.java @@ -0,0 +1,34 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.value; + +import 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 index 038111c0ec..06adc4b3cb 100644 --- a/jooby/src/main/java/io/jooby/value/Converter.java +++ b/jooby/src/main/java/io/jooby/value/Converter.java @@ -23,7 +23,8 @@ public interface Converter { * * @param type Requested type. * @param value Value value. + * @param hint Requested hint. * @return Converted value. */ - Object convert(@NonNull Type type, @NonNull Value value); + Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint); } diff --git a/jooby/src/main/java/io/jooby/value/ValueFactory.java b/jooby/src/main/java/io/jooby/value/ValueFactory.java index 2fbda4c8a0..fcddabbd03 100644 --- a/jooby/src/main/java/io/jooby/value/ValueFactory.java +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -22,6 +22,12 @@ public class ValueFactory { + public enum ConversionType { + Strict, + Nullable, + Empty, + } + private final Map converterMap = new HashMap<>(); private MethodHandles.Lookup lookup = MethodHandles.publicLookup(); @@ -40,7 +46,7 @@ public ValueFactory put(Type type, Converter converter) { } public T convert(@NonNull Type type, @NonNull Value value) { - T result = convert(type, value, false); + T result = convert(type, value, ConversionHint.Strict); if (result == null) { throw new TypeMismatchException(value.name(), type); } @@ -48,20 +54,12 @@ public T convert(@NonNull Type type, @NonNull Value value) { return result; } - public T convertOrNull(@NonNull Type type, @NonNull Value value) { - return convert(type, value, false); - } - - public T convertOrEmpty(@NonNull Type type, @NonNull Value value) { - return convert(type, value, true); - } - @SuppressWarnings("unchecked") - private T convert(@NonNull Type type, @NonNull Value value, boolean allowEmptyBean) { + public T convert(@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); + return (T) converter.convert(type, value, hint); } var rawType = $Types.getRawType(type); // Is it a container? @@ -89,7 +87,7 @@ private T convert(@NonNull Type type, @NonNull Value value, boolean allowEmp } // anything else fallback to reflective var reflective = new ReflectiveBeanConverter(); - return (T) reflective.convert((ValueNode) value, rawType, allowEmptyBean); + return (T) reflective.convert((ValueNode) value, rawType, hint == ConversionHint.Empty); } } diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index 9326ab4a22..e8c2696895 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -33,5 +33,4 @@ * Optional dependency for rate limiting */ requires static io.github.bucket4j.core; - requires io.jooby; } diff --git a/jooby/src/test/java/io/jooby/Issue2525.java b/jooby/src/test/java/io/jooby/Issue2525.java index e7fec344b0..8f22ca280d 100644 --- a/jooby/src/test/java/io/jooby/Issue2525.java +++ b/jooby/src/test/java/io/jooby/Issue2525.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; import io.jooby.internal.UrlParser; +import io.jooby.value.ConversionHint; import io.jooby.value.Converter; import io.jooby.value.ValueFactory; import jakarta.inject.Inject; @@ -24,7 +25,7 @@ public class Issue2525 { public class VC2525 implements Converter { @Override - public Object convert(@NotNull Type type, @NotNull Value value) { + public Object convert(@NotNull Type type, @NotNull Value value, ConversionHint hint) { return new MyID2525(value.value()); } } diff --git a/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java b/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java index 8ac65742e0..f41f39ba91 100644 --- a/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java +++ b/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java @@ -15,7 +15,8 @@ import org.mockito.Mockito; import io.jooby.Value; -import io.jooby.internal.converter.BuiltinConverter; +import io.jooby.internal.converter.StandardConverter; +import io.jooby.value.ConversionHint; public class DurationConverterTest { @@ -53,11 +54,13 @@ public void convertPeriod() { } private Duration duration(String value) { - return (Duration) BuiltinConverter.Duration.convert(Duration.class, value(value)); + return (Duration) + StandardConverter.Duration.convert(Duration.class, value(value), ConversionHint.Strict); } private Period period(String value) { - return (Period) BuiltinConverter.Period.convert(Period.class, value(value)); + return (Period) + StandardConverter.Period.convert(Period.class, value(value), ConversionHint.Strict); } private Value value(String value) { 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 8c25ea916d..f3604f25db 100644 --- a/modules/jooby-apt/src/test/java/tests/i2325/VC2325.java +++ b/modules/jooby-apt/src/test/java/tests/i2325/VC2325.java @@ -10,11 +10,12 @@ import org.jetbrains.annotations.NotNull; import io.jooby.Value; +import io.jooby.value.ConversionHint; import io.jooby.value.Converter; public class VC2325 implements Converter { @Override - public Object convert(@NotNull Type type, @NotNull Value value) { + public Object convert(@NotNull Type type, @NotNull Value value, 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 1d2bd3a098..b92c2b6364 100644 --- a/modules/jooby-apt/src/test/java/tests/i2405/Converter2405.java +++ b/modules/jooby-apt/src/test/java/tests/i2405/Converter2405.java @@ -9,13 +9,15 @@ import org.jetbrains.annotations.NotNull; +import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Value; +import io.jooby.value.ConversionHint; import io.jooby.value.Converter; public class Converter2405 implements Converter { @Override - public Object convert(@NotNull Type type, @NotNull Value value) { + public Object convert(@NotNull Type type, @NotNull Value value, @NonNull ConversionHint hint) { return new Bean2405(value.value()); } } diff --git a/tests/src/test/java/io/jooby/i2325/VC2325.java b/tests/src/test/java/io/jooby/i2325/VC2325.java index 08fed37420..9aa85586a2 100644 --- a/tests/src/test/java/io/jooby/i2325/VC2325.java +++ b/tests/src/test/java/io/jooby/i2325/VC2325.java @@ -11,11 +11,12 @@ import io.jooby.QueryString; import io.jooby.Value; +import io.jooby.value.ConversionHint; import io.jooby.value.Converter; public class VC2325 implements Converter { @Override - public Object convert(@NotNull Type type, @NotNull Value value) { + public Object convert(@NotNull Type type, @NotNull Value value, 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/test/MyValueBeanConverter.java b/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java index d9a541b9b9..1b90fb9013 100644 --- a/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java +++ b/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java @@ -11,12 +11,13 @@ import io.jooby.Value; import io.jooby.ValueNode; +import io.jooby.value.ConversionHint; import io.jooby.value.Converter; public class MyValueBeanConverter implements Converter { @Override - public Object convert(@NotNull Type type, @NotNull Value value) { + public Object convert(@NotNull Type type, @NotNull Value value, ConversionHint hint) { MyValue result = new MyValue(); result.setString(((ValueNode) value).get("string").value()); return result; From afa3b3f9df798e9adf72dd6bde382d4e1afd9e56 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 19 Jun 2025 13:07:47 -0300 Subject: [PATCH 08/60] value api: reduce API by removing `ValueNode` - we don't need it --- jooby/pom.xml | 1 - jooby/src/main/java/io/jooby/Body.java | 2 +- jooby/src/main/java/io/jooby/Context.java | 22 +- .../main/java/io/jooby/DefaultContext.java | 8 +- jooby/src/main/java/io/jooby/Formdata.java | 4 +- .../main/java/io/jooby/ForwardingContext.java | 44 ++-- jooby/src/main/java/io/jooby/ParamLookup.java | 13 -- jooby/src/main/java/io/jooby/QueryString.java | 2 +- jooby/src/main/java/io/jooby/Value.java | 200 +++++++++++++++-- jooby/src/main/java/io/jooby/ValueNode.java | 212 ------------------ .../main/java/io/jooby/annotation/Header.java | 37 --- .../java/io/jooby/internal/ArrayValue.java | 14 +- .../java/io/jooby/internal/ByteArrayBody.java | 6 +- .../main/java/io/jooby/internal/FileBody.java | 6 +- .../java/io/jooby/internal/HashValue.java | 36 +-- .../java/io/jooby/internal/HeadersValue.java | 12 +- .../io/jooby/internal/InputStreamBody.java | 6 +- .../java/io/jooby/internal/MissingValue.java | 8 +- .../java/io/jooby/internal/MultipartNode.java | 9 +- .../java/io/jooby/internal/SessionImpl.java | 3 +- .../java/io/jooby/internal/SingleValue.java | 12 +- .../converter/ReflectiveBeanConverter.java | 23 +- .../jooby/validation/ValidationContext.java | 10 +- .../java/io/jooby/value/ValueFactory.java | 3 +- .../test/java/io/jooby/ValueResolveTest.java | 6 +- .../src/test/java/tests/i1807/Issue1807.java | 4 +- .../io/jooby/internal/jetty/JettyContext.java | 5 +- .../src/main/kotlin/io/jooby/kt/Kooby.kt | 3 +- .../io/jooby/internal/netty/NettyBody.java | 5 +- .../io/jooby/internal/netty/NettyContext.java | 5 +- .../main/java/io/jooby/test/MockContext.java | 5 +- .../src/test/java/io/jooby/test/UnitTest.java | 4 +- .../internal/undertow/UndertowContext.java | 5 +- .../io/jooby/test/MyValueBeanConverter.java | 6 +- 34 files changed, 320 insertions(+), 421 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/ValueNode.java delete mode 100644 jooby/src/main/java/io/jooby/annotation/Header.java diff --git a/jooby/pom.xml b/jooby/pom.xml index 125e277cbb..4e94a3270d 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -94,7 +94,6 @@ 3.4.15 test - diff --git a/jooby/src/main/java/io/jooby/Body.java b/jooby/src/main/java/io/jooby/Body.java index ab4c662a9e..09a762c9a3 100644 --- a/jooby/src/main/java/io/jooby/Body.java +++ b/jooby/src/main/java/io/jooby/Body.java @@ -29,7 +29,7 @@ * @author edgar * @since 2.0.0 */ -public interface Body extends ValueNode { +public interface Body extends Value { /** * HTTP body as string. diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java index 690e2a58ab..35c053aeb9 100644 --- a/jooby/src/main/java/io/jooby/Context.java +++ b/jooby/src/main/java/io/jooby/Context.java @@ -164,7 +164,7 @@ private static Selector single() { * @param Generic type. * @return Converted value. */ - default @NonNull T convert(@NonNull ValueNode value, @NonNull Class type) { + default @NonNull T convert(@NonNull Value value, @NonNull Class type) { T result = convertOrNull(value, type); if (result == null) { throw new TypeMismatchException(value.name(), type); @@ -180,7 +180,7 @@ private static Selector single() { * @param Generic type. * @return Converted value or null. */ - @Nullable T convertOrNull(@NonNull ValueNode value, @NonNull Class type); + @Nullable T convertOrNull(@NonNull Value value, @NonNull Class type); /* * ********************************************************************************************** @@ -328,11 +328,11 @@ private static Selector single() { @NonNull 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(); + @NonNull Value path(); /** * Path map represent all the path keys with their values. @@ -367,9 +367,9 @@ 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(); @@ -389,7 +389,7 @@ private static Selector single() { * @param name Parameter name. * @return A query value. */ - @NonNull ValueNode query(@NonNull String name); + @NonNull Value query(@NonNull String name); /** * Query string with the leading ? or empty string. This is the raw query string, @@ -428,11 +428,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(); + @NonNull Value header(); /** * Get a header that matches the given name. @@ -764,7 +764,7 @@ default boolean isPreflight() { * @param name Field name. * @return Multipart value. */ - @NonNull ValueNode form(@NonNull String name); + @NonNull Value form(@NonNull String name); /** * Convert form data to the given type. diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 4d11a6ba29..de435c0a39 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -203,7 +203,7 @@ default FlashMap flashOrNull() { } @Override - @NonNull default ValueNode path() { + @NonNull default Value path() { var path = new HashValue(getValueFactory(), null); for (Map.Entry entry : pathMap().entrySet()) { path.put(entry.getKey(), entry.getValue()); @@ -212,7 +212,7 @@ default FlashMap flashOrNull() { } @Override - @NonNull default ValueNode query(@NonNull String name) { + @NonNull default Value query(@NonNull String name) { return query().get(name); } @@ -383,7 +383,7 @@ default boolean isSecure() { } @Override - @NonNull default ValueNode form(@NonNull String name) { + @NonNull default Value form(@NonNull String name) { return form().get(name); } @@ -427,7 +427,7 @@ default ValueFactory getValueFactory() { } @Override - default @NonNull T convertOrNull(@NonNull ValueNode value, @NonNull Class type) { + default @NonNull T convertOrNull(@NonNull Value value, @NonNull Class type) { return getValueFactory().convert(type, value, ConversionHint.Nullable); } diff --git a/jooby/src/main/java/io/jooby/Formdata.java b/jooby/src/main/java/io/jooby/Formdata.java index 87c6df2aae..7c38be453a 100644 --- a/jooby/src/main/java/io/jooby/Formdata.java +++ b/jooby/src/main/java/io/jooby/Formdata.java @@ -21,7 +21,7 @@ * @author edgar * @since 2.0.0 */ -public interface Formdata extends ValueNode { +public interface Formdata extends Value { /** * Add a form field. @@ -30,7 +30,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. diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index f8c68b7189..366dd28d21 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -108,12 +108,12 @@ public long getSize() { } @Override - @NonNull public ValueNode get(int index) { + @NonNull public Value get(int index) { return delegate.get(index); } @Override - @NonNull public ValueNode get(@NonNull String name) { + @NonNull public Value get(@NonNull String name) { return delegate.get(name); } @@ -123,7 +123,7 @@ public int size() { } @Override - @NonNull public Iterator iterator() { + @NonNull public Iterator iterator() { return delegate.iterator(); } @@ -153,12 +153,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(); } @@ -310,20 +310,20 @@ public boolean isObject() { } } - 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) { + @NonNull public Value get(@NonNull int index) { return delegate.get(index); } @Override - @NonNull public ValueNode get(@NonNull String name) { + @NonNull public Value get(@NonNull String name) { return delegate.get(name); } @@ -333,7 +333,7 @@ public int size() { } @Override - @NonNull public Iterator iterator() { + @NonNull public Iterator iterator() { return delegate.iterator(); } @@ -363,12 +363,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(); } @@ -545,7 +545,7 @@ public boolean isObject() { } } - public static class ForwardingQueryString extends ForwardingValueNode implements QueryString { + public static class ForwardingQueryString extends ForwardingValue implements QueryString { public ForwardingQueryString(QueryString queryString) { super(queryString); } @@ -556,13 +556,13 @@ public String 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); } @@ -751,7 +751,7 @@ public T path(@NonNull Class type) { } @NonNull @Override - public ValueNode path() { + public Value path() { return ctx.path(); } @@ -772,7 +772,7 @@ public ValueNode path() { } @NonNull @Override - public ValueNode query(@NonNull String name) { + public Value query(@NonNull String name) { return ctx.query(name); } @@ -792,7 +792,7 @@ public Map queryMap() { } @Override - @NonNull public ValueNode header() { + @NonNull public Value header() { return ctx.header(); } @@ -916,7 +916,7 @@ public Context setScheme(@NonNull String scheme) { } @NonNull @Override - public ValueNode form(@NonNull String name) { + public Value form(@NonNull String name) { return ctx.form(name); } @@ -961,12 +961,12 @@ public T body(@NonNull Type type) { } @NonNull @Override - public T convert(@NonNull ValueNode value, @NonNull Class type) { + public T convert(@NonNull Value value, @NonNull Class type) { return ctx.convert(value, type); } @Nullable @Override - public T convertOrNull(@NonNull ValueNode value, @NonNull Class type) { + public T convertOrNull(@NonNull Value value, @NonNull Class type) { return ctx.convertOrNull(value, type); } diff --git a/jooby/src/main/java/io/jooby/ParamLookup.java b/jooby/src/main/java/io/jooby/ParamLookup.java index 81e09ade5f..d0a1901ad5 100644 --- a/jooby/src/main/java/io/jooby/ParamLookup.java +++ b/jooby/src/main/java/io/jooby/ParamLookup.java @@ -5,8 +5,6 @@ */ package io.jooby; -import java.util.Optional; - /** * Fluent interface allowing to conveniently search context parameters in multiple sources. * @@ -117,16 +115,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/QueryString.java b/jooby/src/main/java/io/jooby/QueryString.java index fe716cb8c7..e475dcae89 100644 --- a/jooby/src/main/java/io/jooby/QueryString.java +++ b/jooby/src/main/java/io/jooby/QueryString.java @@ -16,7 +16,7 @@ * @author edgar * @since 2.0.0 */ -public interface QueryString extends ValueNode { +public interface QueryString extends Value { /** * Query string with the leading ? or empty string. diff --git a/jooby/src/main/java/io/jooby/Value.java b/jooby/src/main/java/io/jooby/Value.java index 22b6ee20c9..54ae56ab4e 100644 --- a/jooby/src/main/java/io/jooby/Value.java +++ b/jooby/src/main/java/io/jooby/Value.java @@ -9,12 +9,8 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeParseException; -import java.util.Collection; -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; @@ -48,7 +44,183 @@ * @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. + */ + @NonNull Value get(int index); + + /** + * Get a value that matches the given name. + * + * @param name Field name. + * @return Field value. + */ + @NonNull 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. + */ + default int size() { + return 0; + } + + /** + * Value iterator. + * + * @return Value iterator. + */ + @NonNull default 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("\\."); + 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.length() == 0) { + return expression; + } + if (offset < expression.length()) { + buffer.append(expression.substring(offset)); + } + return buffer.toString(); + } + /** * Convert this value to long (if possible). * @@ -444,7 +616,7 @@ default boolean isObject() { * @param name Name of missing value. * @return Missing value. */ - static @NonNull ValueNode missing(@NonNull String name) { + static @NonNull Value missing(@NonNull String name) { return new MissingValue(name); } @@ -456,7 +628,7 @@ default boolean isObject() { * @param value Value. * @return Single value. */ - static @NonNull ValueNode value( + static @NonNull Value value( @NonNull ValueFactory valueFactory, @NonNull String name, @NonNull String value) { return new SingleValue(valueFactory, name, value); } @@ -469,7 +641,7 @@ default boolean isObject() { * @param values Field values. * @return Array value. */ - static @NonNull ValueNode array( + static @NonNull Value array( @NonNull ValueFactory valueFactory, @NonNull String name, @NonNull List values) { return new ArrayValue(valueFactory, name).add(values); } @@ -485,7 +657,7 @@ default boolean isObject() { * @param values Field values. * @return A value. */ - static @NonNull ValueNode create( + static @NonNull Value create( ValueFactory valueFactory, @NonNull String name, @Nullable List values) { if (values == null || values.size() == 0) { return missing(name); @@ -507,7 +679,7 @@ default boolean isObject() { * @param value Field values. * @return A value. */ - static @NonNull ValueNode create( + static @NonNull Value create( ValueFactory valueFactory, @NonNull String name, @Nullable String value) { if (value == null) { return missing(name); @@ -522,7 +694,7 @@ default boolean isObject() { * @param values Map values. * @return A hash/object value. */ - static @NonNull ValueNode hash( + static @NonNull Value hash( ValueFactory valueFactory, @NonNull Map> values) { var node = new HashValue(valueFactory, null); node.put(values); @@ -546,7 +718,7 @@ default boolean isObject() { * @param values Map values. * @return A hash/object value. */ - static @NonNull ValueNode headers( + static @NonNull Value headers( ValueFactory valueFactory, @NonNull Map> values) { HeadersValue node = new HeadersValue(valueFactory); node.put(values); 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/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/internal/ArrayValue.java b/jooby/src/main/java/io/jooby/internal/ArrayValue.java index 688dc3c7ce..d6777cd7e1 100644 --- a/jooby/src/main/java/io/jooby/internal/ArrayValue.java +++ b/jooby/src/main/java/io/jooby/internal/ArrayValue.java @@ -16,18 +16,18 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.ValueNode; +import io.jooby.Value; import io.jooby.exception.MissingValueException; import io.jooby.exception.TypeMismatchException; import io.jooby.value.ConversionHint; import io.jooby.value.ValueFactory; -public class ArrayValue implements ValueNode { +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(ValueFactory factory, String name) { this.factory = factory; @@ -39,7 +39,7 @@ public String name() { return name; } - public ArrayValue add(ValueNode value) { + public ArrayValue add(Value value) { this.list.add(value); return this; } @@ -56,7 +56,7 @@ public ArrayValue add(String value) { } @Override - public @NonNull ValueNode get(int index) { + public @NonNull Value get(int index) { try { return list.get(index); } catch (IndexOutOfBoundsException x) { @@ -65,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); } @@ -86,7 +86,7 @@ public String toString() { } @Override - public @NonNull Iterator iterator() { + public @NonNull Iterator iterator() { return list.iterator(); } diff --git a/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java b/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java index 6ffb551ab6..c24e7f0f61 100644 --- a/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java +++ b/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java @@ -20,7 +20,7 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.ValueNode; +import io.jooby.Value; public class ByteArrayBody implements Body { private static final byte[] EMPTY = new byte[0]; @@ -70,12 +70,12 @@ public List toList() { } @NonNull @Override - public ValueNode get(int index) { + public Value get(int index) { return index == 0 ? this : get(Integer.toString(index)); } @NonNull @Override - public ValueNode get(@NonNull String name) { + public Value get(@NonNull String name) { return new MissingValue(name); } diff --git a/jooby/src/main/java/io/jooby/internal/FileBody.java b/jooby/src/main/java/io/jooby/internal/FileBody.java index 0e5959d6f2..5adff254fd 100644 --- a/jooby/src/main/java/io/jooby/internal/FileBody.java +++ b/jooby/src/main/java/io/jooby/internal/FileBody.java @@ -22,7 +22,7 @@ import io.jooby.Context; import io.jooby.MediaType; import io.jooby.SneakyThrows; -import io.jooby.ValueNode; +import io.jooby.Value; public class FileBody implements Body { private Context ctx; @@ -85,12 +85,12 @@ public List toList() { } @Override - public @NonNull ValueNode get(int index) { + public @NonNull Value get(int index) { return index == 0 ? this : get(Integer.toString(index)); } @NonNull @Override - public ValueNode get(@NonNull String name) { + public Value get(@NonNull String name) { return new MissingValue(name); } diff --git a/jooby/src/main/java/io/jooby/internal/HashValue.java b/jooby/src/main/java/io/jooby/internal/HashValue.java index c37a1921cc..bbb113d0cd 100644 --- a/jooby/src/main/java/io/jooby/internal/HashValue.java +++ b/jooby/src/main/java/io/jooby/internal/HashValue.java @@ -24,14 +24,14 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.FileUpload; -import io.jooby.ValueNode; +import io.jooby.Value; import io.jooby.value.ConversionHint; import io.jooby.value.ValueFactory; -public class HashValue implements ValueNode { - protected static final Map EMPTY = Collections.emptyMap(); +public class HashValue implements Value { + protected static final Map EMPTY = Collections.emptyMap(); protected final ValueFactory factory; - protected Map hash = EMPTY; + protected Map hash = EMPTY; private final String name; private boolean arrayLike; @@ -54,11 +54,11 @@ public void put(String path, String value) { put(path, Collections.singletonList(value)); } - public void put(String path, ValueNode node) { + public void put(String path, Value node) { put( path, (name, scope) -> { - ValueNode existing = scope.get(name); + Value existing = scope.get(name); if (existing == null) { scope.put(name, node); } else { @@ -79,7 +79,7 @@ public void put(String path, Collection values) { path, (name, scope) -> { for (String value : values) { - ValueNode existing = scope.get(name); + Value existing = scope.get(name); if (existing == null) { scope.put(name, new SingleValue(factory, name, decode(value))); } else { @@ -100,7 +100,7 @@ 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(); @@ -143,7 +143,7 @@ private void useIndexes() { return; } this.arrayLike = true; - TreeMap ordered = new TreeMap<>(); + TreeMap ordered = new TreeMap<>(); ordered.putAll(hash); hash.clear(); this.hash = ordered; @@ -158,7 +158,7 @@ private boolean isNumber(String value) { return true; } - protected Map hash() { + protected Map hash() { if (hash == EMPTY) { hash = new LinkedHashMap<>(); } @@ -169,8 +169,8 @@ protected Map hash() { 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) { + Value value = hash.get(name); if (value == null) { return new MissingValue(scope(name)); } @@ -182,7 +182,7 @@ private String scope(String name) { } @Override - public @NonNull ValueNode get(int index) { + public @NonNull Value get(int index) { return get(Integer.toString(index)); } @@ -205,7 +205,7 @@ public String value() { } @Override - public Iterator iterator() { + public Iterator iterator() { return hash.values().iterator(); } @@ -254,10 +254,10 @@ private T toNullable(@NonNull ValueFactory factory, @NonNull Class type) @Override public Map> toMultimap() { Map> result = new LinkedHashMap<>(hash.size()); - Set> entries = hash.entrySet(); + Set> entries = hash.entrySet(); String scope = name == null ? "" : name + "."; - for (Map.Entry entry : entries) { - ValueNode value = entry.getValue(); + for (Map.Entry entry : entries) { + Value value = entry.getValue(); value .toMultimap() .forEach( @@ -283,7 +283,7 @@ 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) { diff --git a/jooby/src/main/java/io/jooby/internal/HeadersValue.java b/jooby/src/main/java/io/jooby/internal/HeadersValue.java index 41132ccd79..5f8caec99d 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.ValueNode; +import io.jooby.Value; import io.jooby.value.ValueFactory; -public class HeadersValue extends HashValue implements ValueNode { +public class HeadersValue extends HashValue implements Value { 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/InputStreamBody.java b/jooby/src/main/java/io/jooby/internal/InputStreamBody.java index 8daf0e33a9..88f5f0e36f 100644 --- a/jooby/src/main/java/io/jooby/internal/InputStreamBody.java +++ b/jooby/src/main/java/io/jooby/internal/InputStreamBody.java @@ -23,7 +23,7 @@ import io.jooby.MediaType; import io.jooby.ServerOptions; import io.jooby.SneakyThrows; -import io.jooby.ValueNode; +import io.jooby.Value; public class InputStreamBody implements Body { private Context ctx; @@ -77,12 +77,12 @@ public String value() { } @NonNull @Override - public ValueNode get(int index) { + public Value get(int index) { return index == 0 ? this : get(Integer.toString(index)); } @NonNull @Override - public ValueNode get(@NonNull String name) { + public Value get(@NonNull String name) { return new MissingValue(name); } diff --git a/jooby/src/main/java/io/jooby/internal/MissingValue.java b/jooby/src/main/java/io/jooby/internal/MissingValue.java index 3cb585fb0c..4f818bc57d 100644 --- a/jooby/src/main/java/io/jooby/internal/MissingValue.java +++ b/jooby/src/main/java/io/jooby/internal/MissingValue.java @@ -14,10 +14,10 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.ValueNode; +import io.jooby.Value; import io.jooby.exception.MissingValueException; -public class MissingValue implements ValueNode { +public class MissingValue implements Value { private String name; public MissingValue(String name) { @@ -30,12 +30,12 @@ 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 @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 2987438297..90ec58d939 100644 --- a/jooby/src/main/java/io/jooby/internal/MultipartNode.java +++ b/jooby/src/main/java/io/jooby/internal/MultipartNode.java @@ -6,7 +6,6 @@ package io.jooby.internal; import java.util.*; -import java.util.stream.Collectors; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.FileUpload; @@ -15,25 +14,25 @@ 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(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/SessionImpl.java b/jooby/src/main/java/io/jooby/internal/SessionImpl.java index b4c25ba547..984b82319e 100644 --- a/jooby/src/main/java/io/jooby/internal/SessionImpl.java +++ b/jooby/src/main/java/io/jooby/internal/SessionImpl.java @@ -15,7 +15,6 @@ import io.jooby.Session; import io.jooby.SessionStore; import io.jooby.Value; -import io.jooby.ValueNode; public class SessionImpl implements Session { @@ -89,7 +88,7 @@ public Session setId(@Nullable String id) { } @Override - public @NonNull ValueNode remove(@NonNull String name) { + public @NonNull Value remove(@NonNull String name) { String value = attributes.remove(name); updateState(); return value == null diff --git a/jooby/src/main/java/io/jooby/internal/SingleValue.java b/jooby/src/main/java/io/jooby/internal/SingleValue.java index d145fa14e8..cdfb9c92a5 100644 --- a/jooby/src/main/java/io/jooby/internal/SingleValue.java +++ b/jooby/src/main/java/io/jooby/internal/SingleValue.java @@ -14,11 +14,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.ValueNode; +import io.jooby.Value; import io.jooby.value.ConversionHint; import io.jooby.value.ValueFactory; -public class SingleValue implements ValueNode { +public class SingleValue implements Value { private final ValueFactory factory; @@ -38,12 +38,12 @@ public String name() { } @Override - public @NonNull ValueNode get(int index) { + public @NonNull Value get(int index) { return index == 0 ? this : 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); } @@ -63,8 +63,8 @@ public String toString() { } @Override - public Iterator iterator() { - return List.of(this).iterator(); + public Iterator iterator() { + return List.of(this).iterator(); } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java index 1a395cc6c0..39dbd2bb18 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java +++ b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java @@ -30,7 +30,6 @@ 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; @@ -52,7 +51,7 @@ public void invoke(Object instance) throws InvocationTargetException, IllegalAcc private static final Object[] NO_ARGS = new Object[0]; - public Object convert(@NonNull ValueNode node, @NonNull Class type, boolean allowEmptyBean) { + public Object convert(@NonNull Value node, @NonNull Class type, boolean allowEmptyBean) { try { return newInstance(type, node, allowEmptyBean); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException x) { @@ -62,13 +61,13 @@ public Object convert(@NonNull ValueNode node, @NonNull Class type, boolean allo } } - private static Object newInstance(Class type, ValueNode node, boolean allowEmptyBean) + private static Object newInstance(Class type, Value node, boolean allowEmptyBean) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException { Constructor[] constructors = type.getConstructors(); - Set state = new HashSet<>(); + Set state = new HashSet<>(); Constructor constructor; if (constructors.length == 0) { constructor = type.getDeclaredConstructor(); @@ -122,7 +121,7 @@ private static Constructor selectConstructor(Constructor[] constructors) { } } - public static Object[] inject(ValueNode scope, Executable method, Consumer state) { + public static Object[] inject(Value scope, Executable method, Consumer state) { Parameter[] parameters = method.getParameters(); if (parameters.length == 0) { return NO_ARGS; @@ -131,7 +130,7 @@ public static Object[] inject(ValueNode scope, Executable method, Consumer 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) { @@ -173,11 +172,11 @@ 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); + Value value = node.get(name); if (nodes.add(value)) { Method method = findSetter(methods, name); if (method != null) { @@ -198,7 +197,7 @@ private static List setters(Class type, ValueNode node, Set n return result; } - private static Object value(Parameter parameter, ValueNode node, ValueNode value) { + private static Object value(Parameter parameter, Value node, Value value) { try { if (isFileUpload(node, parameter)) { Formdata formdata = (Formdata) node; @@ -262,7 +261,7 @@ 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())); } diff --git a/jooby/src/main/java/io/jooby/validation/ValidationContext.java b/jooby/src/main/java/io/jooby/validation/ValidationContext.java index 90599b2b91..7cf306a5f8 100644 --- a/jooby/src/main/java/io/jooby/validation/ValidationContext.java +++ b/jooby/src/main/java/io/jooby/validation/ValidationContext.java @@ -24,10 +24,10 @@ */ 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; } @@ -113,7 +113,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 +168,7 @@ public T body(@NonNull Class type) { } @NonNull @Override - public ValueNode path() { + public Value path() { return new ValidatedValue(ctx, super.path()); } @@ -198,7 +198,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/ValueFactory.java b/jooby/src/main/java/io/jooby/value/ValueFactory.java index fcddabbd03..590c9d9704 100644 --- a/jooby/src/main/java/io/jooby/value/ValueFactory.java +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -14,7 +14,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.SneakyThrows; import io.jooby.Value; -import io.jooby.ValueNode; import io.jooby.exception.TypeMismatchException; import io.jooby.internal.converter.ReflectiveBeanConverter; import io.jooby.internal.converter.StandardConverter; @@ -87,7 +86,7 @@ public T convert(@NonNull Type type, @NonNull Value value, @NonNull Conversi } // anything else fallback to reflective var reflective = new ReflectiveBeanConverter(); - return (T) reflective.convert((ValueNode) value, rawType, hint == ConversionHint.Empty); + return (T) reflective.convert(value, rawType, hint == ConversionHint.Empty); } } diff --git a/jooby/src/test/java/io/jooby/ValueResolveTest.java b/jooby/src/test/java/io/jooby/ValueResolveTest.java index 36eefa5d2a..9a85e3aeb9 100644 --- a/jooby/src/test/java/io/jooby/ValueResolveTest.java +++ b/jooby/src/test/java/io/jooby/ValueResolveTest.java @@ -17,7 +17,7 @@ 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 +52,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/modules/jooby-apt/src/test/java/tests/i1807/Issue1807.java b/modules/jooby-apt/src/test/java/tests/i1807/Issue1807.java index 89bcf75aaa..51a99f2d46 100644 --- a/modules/jooby-apt/src/test/java/tests/i1807/Issue1807.java +++ b/modules/jooby-apt/src/test/java/tests/i1807/Issue1807.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; import io.jooby.Formdata; -import io.jooby.ValueNode; +import io.jooby.Value; import io.jooby.apt.ProcessorRunner; import io.jooby.test.MockContext; import io.jooby.test.MockRouter; @@ -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-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java index 9c7e6f012a..ec4008e5b2 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 @@ -70,7 +70,6 @@ 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; @@ -83,7 +82,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; @@ -258,7 +257,7 @@ public Value header(@NonNull String name) { } @NonNull @Override - public ValueNode header() { + public Value header() { if (headers == null) { Map> headerMap = new LinkedHashMap<>(); for (HttpField header : request.getHeaders()) { 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..5b792c30cb 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 @@ -25,7 +25,6 @@ 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 java.util.function.Supplier import kotlin.reflect.KClass @@ -84,7 +83,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) } 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..256a38b6f8 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 @@ -25,7 +25,6 @@ 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 { @@ -84,12 +83,12 @@ public String value() { } @NonNull @Override - public ValueNode get(int index) { + public Value get(int index) { return index == 0 ? this : get(Integer.toString(index)); } @NonNull @Override - public ValueNode get(@NonNull String name) { + public Value get(@NonNull String name) { return Value.missing(name); } 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 0108802753..abd30059dd 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 @@ -68,7 +68,6 @@ 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.netty.buffer.ByteBuf; @@ -113,7 +112,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; @@ -340,7 +339,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(); 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 655579e099..4d69d6a2b4 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 @@ -53,7 +53,6 @@ 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; @@ -298,7 +297,7 @@ public String queryString() { } @NonNull @Override - public ValueNode header() { + public Value header() { return Value.headers(valueFactory, headers); } @@ -816,7 +815,7 @@ public Router getRouter() { } @NonNull @Override - public T convert(@NonNull ValueNode value, @NonNull Class type) { + public T convert(@NonNull Value value, @NonNull Class type) { return DefaultContext.super.convert(value, type); } 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 be56b27b41..ebd07d59b9 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,7 +20,7 @@ import io.jooby.Formdata; import io.jooby.Jooby; import io.jooby.StatusCode; -import io.jooby.ValueNode; +import io.jooby.Value; import io.jooby.WebSocketMessage; import io.reactivex.rxjava3.core.Single; @@ -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-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java index 74c6ff2361..1fe19564f4 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 @@ -57,7 +57,6 @@ 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.undertow.Handlers; @@ -76,7 +75,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; @@ -262,7 +261,7 @@ public Value header(@NonNull String name) { } @NonNull @Override - public ValueNode header() { + public Value header() { HeaderMap map = exchange.getRequestHeaders(); if (headers == null) { Map> headerMap = new LinkedHashMap<>(); diff --git a/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java b/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java index 1b90fb9013..49d2db49db 100644 --- a/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java +++ b/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java @@ -9,17 +9,17 @@ import org.jetbrains.annotations.NotNull; +import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Value; -import io.jooby.ValueNode; import io.jooby.value.ConversionHint; import io.jooby.value.Converter; public class MyValueBeanConverter implements Converter { @Override - public Object convert(@NotNull Type type, @NotNull Value value, ConversionHint hint) { + public Object convert(@NotNull Type type, @NotNull Value value, @NonNull ConversionHint hint) { MyValue result = new MyValue(); - result.setString(((ValueNode) value).get("string").value()); + result.setString(value.get("string").value()); return result; } } From c4744699545f608cceb6705bfcac115c756fe0cc Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 19 Jun 2025 19:31:04 -0300 Subject: [PATCH 09/60] value api: move Value to `value` package - code cleanup --- jooby/src/main/java/io/jooby/Body.java | 23 ++ jooby/src/main/java/io/jooby/Context.java | 43 +-- .../main/java/io/jooby/DefaultContext.java | 133 ++++--- jooby/src/main/java/io/jooby/Formdata.java | 1 + .../main/java/io/jooby/ForwardingContext.java | 341 +++++++++--------- jooby/src/main/java/io/jooby/ParamLookup.java | 2 + jooby/src/main/java/io/jooby/ParamSource.java | 2 + jooby/src/main/java/io/jooby/QueryString.java | 1 + jooby/src/main/java/io/jooby/Session.java | 1 + .../main/java/io/jooby/WebSocketMessage.java | 1 + .../java/io/jooby/internal/ArrayValue.java | 2 +- .../java/io/jooby/internal/ByteArrayBody.java | 11 - .../main/java/io/jooby/internal/FileBody.java | 11 - .../java/io/jooby/internal/FlashMapImpl.java | 2 +- .../java/io/jooby/internal/HashValue.java | 64 ++-- .../java/io/jooby/internal/HeadersValue.java | 2 +- .../io/jooby/internal/InputStreamBody.java | 11 - .../java/io/jooby/internal/MissingValue.java | 21 +- .../io/jooby/internal/ParamLookupImpl.java | 2 +- .../java/io/jooby/internal/SessionImpl.java | 2 +- .../java/io/jooby/internal/SingleValue.java | 10 +- .../converter/ReflectiveBeanConverter.java | 2 +- .../internal/converter/StandardConverter.java | 2 +- .../jooby/validation/ValidationContext.java | 1 + .../java/io/jooby/value/ConversionHint.java | 2 - .../main/java/io/jooby/value/Converter.java | 1 - .../main/java/io/jooby/{ => value}/Value.java | 90 +++-- .../java/io/jooby/value/ValueFactory.java | 1 - jooby/src/test/java/io/jooby/Issue2525.java | 1 + .../src/test/java/io/jooby/ValueLikeTest.java | 36 ++ .../test/java/io/jooby/ValueResolveTest.java | 1 + jooby/src/test/java/io/jooby/ValueTest.java | 12 +- .../test/java/io/jooby/ValueToBeanTest.java | 6 +- .../jooby/internal/DurationConverterTest.java | 2 +- .../jooby/internal/ValueConverterHelper.java | 49 --- .../source/ParamSourceCheckerContext.java | 2 +- .../src/test/java/tests/i1807/Issue1807.java | 2 +- .../src/test/java/tests/i2325/VC2325.java | 2 +- .../test/java/tests/i2405/Converter2405.java | 2 +- .../io/jooby/internal/jetty/JettyContext.java | 2 +- .../src/main/kotlin/io/jooby/kt/Kooby.kt | 2 +- .../io/jooby/internal/netty/NettyBody.java | 13 +- .../io/jooby/internal/netty/NettyContext.java | 2 +- .../jooby/internal/openapi/RequestParser.java | 3 +- .../io/jooby/internal/pac4j/Pac4jSession.java | 1 + .../internal/pac4j/SessionStoreImpl.java | 3 +- .../jooby/internal/pac4j/WebContextImpl.java | 2 +- .../main/java/io/jooby/test/MockContext.java | 179 +++++---- .../main/java/io/jooby/test/MockSession.java | 2 +- .../test/java/io/jooby/test/LookupTest.java | 1 + .../src/test/java/io/jooby/test/UnitTest.java | 2 +- .../internal/undertow/UndertowContext.java | 2 +- .../test/java/io/jooby/i1570/Issue1570.java | 2 +- .../src/test/java/io/jooby/i2325/VC2325.java | 2 +- .../io/jooby/test/MyValueBeanConverter.java | 2 +- 55 files changed, 530 insertions(+), 590 deletions(-) rename jooby/src/main/java/io/jooby/{ => value}/Value.java (89%) create mode 100644 jooby/src/test/java/io/jooby/ValueLikeTest.java delete mode 100644 jooby/src/test/java/io/jooby/internal/ValueConverterHelper.java diff --git a/jooby/src/main/java/io/jooby/Body.java b/jooby/src/main/java/io/jooby/Body.java index 09a762c9a3..d5269ba336 100644 --- a/jooby/src/main/java/io/jooby/Body.java +++ b/jooby/src/main/java/io/jooby/Body.java @@ -10,6 +10,7 @@ import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.file.Path; +import java.util.Iterator; import java.util.List; import java.util.Set; @@ -19,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.. @@ -45,6 +48,26 @@ public interface Body extends Value { 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. * diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java index 35c053aeb9..dce1a889ef 100644 --- a/jooby/src/main/java/io/jooby/Context.java +++ b/jooby/src/main/java/io/jooby/Context.java @@ -32,11 +32,12 @@ 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.value.Value; +import io.jooby.value.ValueFactory; /** * HTTP context allows you to interact with the HTTP Request and manipulate the HTTP Response. @@ -135,16 +136,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(); + DataBufferFactory getBufferFactory(); /** * Forward executing to another route. We use the given path to find a matching route. @@ -154,33 +155,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 Value 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 Value value, @NonNull Class type); + Object forward(@NonNull String path); /* * ********************************************************************************************** @@ -193,7 +168,7 @@ private static Selector single() { * * @return Flash map. */ - @NonNull FlashMap flash(); + FlashMap flash(); /** * Flash map or null when no flash cookie exists. @@ -202,13 +177,15 @@ 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. diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index de435c0a39..67914cc3fd 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -37,7 +37,7 @@ import io.jooby.internal.MissingValue; import io.jooby.internal.SingleValue; import io.jooby.internal.UrlParser; -import io.jooby.value.ConversionHint; +import io.jooby.value.Value; import io.jooby.value.ValueFactory; /*** @@ -48,44 +48,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()); } @@ -108,13 +109,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( @@ -134,12 +135,12 @@ default FlashMap flashOrNull() { * @return Flash attribute. */ @Override - default @NonNull Value flash(@NonNull String 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); @@ -148,7 +149,7 @@ default FlashMap flashOrNull() { } @Override - default @NonNull Session session() { + default Session session() { Session session = sessionOrNull(); if (session == null) { SessionStore store = getRouter().getSessionStore(); @@ -173,7 +174,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); @@ -184,13 +185,13 @@ 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(getValueFactory(), name, value); } @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) @@ -198,12 +199,12 @@ default FlashMap flashOrNull() { } @Override - @NonNull default T path(@NonNull Class type) { + default T path(@NonNull Class type) { return path().to(type); } @Override - @NonNull default Value path() { + default Value path() { var path = new HashValue(getValueFactory(), null); for (Map.Entry entry : pathMap().entrySet()) { path.put(entry.getKey(), entry.getValue()); @@ -212,32 +213,32 @@ default FlashMap flashOrNull() { } @Override - @NonNull default Value 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) { + default T query(@NonNull Class type) { return query().to(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(); } @@ -281,12 +282,12 @@ 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) { + default String getRequestURL(@NonNull String path) { String scheme = getScheme(); String host = getHost(); int port = getPort(); @@ -311,7 +312,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()); } @@ -340,7 +341,7 @@ default long getRequestLength() { } @Override - default @NonNull String getServerHost() { + default String getServerHost() { String host = getRouter().getServerOptions().getHost(); return host.equals("0.0.0.0") ? "localhost" : host; } @@ -368,7 +369,7 @@ default int getPort() { } @Override - default @NonNull String getHost() { + default String getHost() { String hostAndPort = getHostAndPort(); if (hostAndPort != null) { int index = hostAndPort.indexOf(':'); @@ -383,42 +384,42 @@ default boolean isSecure() { } @Override - @NonNull default Value 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); } @@ -427,16 +428,10 @@ default ValueFactory getValueFactory() { } @Override - default @NonNull T convertOrNull(@NonNull Value value, @NonNull Class type) { - return getValueFactory().convert(type, value, ConversionHint.Nullable); - } - - @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)) { - //noinspection unchecked - return (T) getValueFactory().convert(type, body()); + return getValueFactory().convert(type, body()); } //noinspection unchecked return (T) decoder(contentType).decode(this, type); @@ -446,22 +441,22 @@ default ValueFactory getValueFactory() { } @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); } @@ -472,17 +467,17 @@ default ValueFactory getValueFactory() { } @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(); @@ -501,13 +496,13 @@ default ValueFactory getValueFactory() { } @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); @@ -515,7 +510,7 @@ default ValueFactory getValueFactory() { } @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); @@ -524,30 +519,30 @@ default ValueFactory getValueFactory() { } @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) @@ -559,18 +554,18 @@ default ValueFactory getValueFactory() { } @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]); @@ -579,12 +574,12 @@ default ValueFactory getValueFactory() { } @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(); @@ -601,7 +596,7 @@ default ValueFactory getValueFactory() { } @Override - default @NonNull Context send(@NonNull Path file) { + default Context send(@NonNull Path file) { try { setDefaultResponseType(MediaType.byFile(file)); return send(FileChannel.open(file)); @@ -611,7 +606,7 @@ default ValueFactory getValueFactory() { } @Override - @NonNull default Context sendError(@NonNull Throwable cause) { + default Context sendError(@NonNull Throwable cause) { sendError(cause, getRouter().errorCode(cause)); return this; } @@ -624,7 +619,7 @@ default ValueFactory getValueFactory() { * @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()) { @@ -659,7 +654,7 @@ default ValueFactory getValueFactory() { return this; } - @NonNull default DataBufferFactory getBufferFactory() { + default DataBufferFactory getBufferFactory() { return getRouter().getBufferFactory(); } } diff --git a/jooby/src/main/java/io/jooby/Formdata.java b/jooby/src/main/java/io/jooby/Formdata.java index 7c38be453a..c2bed23c49 100644 --- a/jooby/src/main/java/io/jooby/Formdata.java +++ b/jooby/src/main/java/io/jooby/Formdata.java @@ -10,6 +10,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.internal.MultipartNode; +import io.jooby.value.Value; import io.jooby.value.ValueFactory; /** diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index 366dd28d21..66b8496a8d 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -26,6 +26,8 @@ import io.jooby.buffer.DataBuffer; import io.jooby.buffer.DataBufferFactory; import io.jooby.exception.RegistryException; +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 Value get(int index) { + public Value get(int index) { return delegate.get(index); } @Override - @NonNull public Value 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, @@ -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,22 +292,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 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(); } } @@ -318,12 +320,12 @@ public ForwardingValue(Value delegate) { } @Override - @NonNull public Value get(@NonNull int index) { + public Value get(@NonNull int index) { return delegate.get(index); } @Override - @NonNull public Value 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, @@ -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,12 +537,12 @@ 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(); } } @@ -550,7 +552,7 @@ public ForwardingQueryString(QueryString queryString) { super(queryString); } - @NonNull @Override + @Override public String queryString() { return ((QueryString) delegate).queryString(); } @@ -581,17 +583,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 +615,7 @@ public T getUser() { return ctx.getUser(); } - @NonNull @Override + @Override public Context setUser(@Nullable Object user) { ctx.setUser(user); return this; @@ -623,7 +625,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 +645,7 @@ public boolean isSecure() { } @Override - @NonNull public Map getAttributes() { + public Map getAttributes() { return ctx.getAttributes(); } @@ -652,23 +654,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 + @Override public DataBufferFactory getBufferFactory() { return ctx.getBufferFactory(); } - @NonNull @Override + @Override public FlashMap flash() { return ctx.flash(); } @@ -678,17 +680,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 +700,110 @@ 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 Value path(@NonNull String name) { return ctx.path(name); } - @NonNull @Override + @Override public T path(@NonNull Class type) { return ctx.path(type); } - @NonNull @Override + @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 + @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 Value 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 +823,7 @@ public MediaType getRequestType() { return ctx.getRequestType(); } - @NonNull @Override + @Override public MediaType getRequestType(MediaType defaults) { return ctx.getRequestType(defaults); } @@ -832,22 +834,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 +860,7 @@ public int getServerPort() { return ctx.getServerPort(); } - @NonNull @Override + @Override public String getServerHost() { return ctx.getServerHost(); } @@ -868,114 +870,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 + @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 Value value, @NonNull Class type) { - return ctx.convert(value, type); - } - - @Nullable @Override - public T convertOrNull(@NonNull Value 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 +983,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 +1059,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 +1187,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 + @Override public Context send(@NonNull DataBuffer data) { ctx.send(data); 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 +1305,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/ParamLookup.java b/jooby/src/main/java/io/jooby/ParamLookup.java index d0a1901ad5..b57fc32ec3 100644 --- a/jooby/src/main/java/io/jooby/ParamLookup.java +++ b/jooby/src/main/java/io/jooby/ParamLookup.java @@ -5,6 +5,8 @@ */ package io.jooby; +import io.jooby.value.Value; + /** * Fluent interface allowing to conveniently search context parameters in multiple sources. * 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 e475dcae89..b6d9356236 100644 --- a/jooby/src/main/java/io/jooby/QueryString.java +++ b/jooby/src/main/java/io/jooby/QueryString.java @@ -8,6 +8,7 @@ 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; /** 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/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/internal/ArrayValue.java b/jooby/src/main/java/io/jooby/internal/ArrayValue.java index d6777cd7e1..554e69654c 100644 --- a/jooby/src/main/java/io/jooby/internal/ArrayValue.java +++ b/jooby/src/main/java/io/jooby/internal/ArrayValue.java @@ -16,10 +16,10 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.Value; 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 Value { diff --git a/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java b/jooby/src/main/java/io/jooby/internal/ByteArrayBody.java index c24e7f0f61..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.Value; 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 Value get(int index) { - return index == 0 ? this : get(Integer.toString(index)); - } - - @NonNull @Override - public Value get(@NonNull String name) { - return new MissingValue(name); - } - @Override public String name() { return "body"; diff --git a/jooby/src/main/java/io/jooby/internal/FileBody.java b/jooby/src/main/java/io/jooby/internal/FileBody.java index 5adff254fd..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.Value; public class FileBody implements Body { private Context ctx; @@ -84,16 +83,6 @@ public List toList() { return Collections.singletonList(value()); } - @Override - public @NonNull Value get(int index) { - return index == 0 ? this : get(Integer.toString(index)); - } - - @NonNull @Override - public Value 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 bbb113d0cd..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; @@ -24,12 +23,12 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.FileUpload; -import io.jooby.Value; 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 = Collections.emptyMap(); + protected static final Map EMPTY = Map.of(); protected final ValueFactory factory; protected Map hash = EMPTY; private final String name; @@ -51,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, Value node) { put( path, (name, scope) -> { - Value existing = scope.get(name); + var existing = scope.get(name); if (existing == null) { scope.put(name, node); } else { @@ -78,10 +77,10 @@ public void put(String path, Collection values) { put( path, (name, scope) -> { - for (String value : values) { - Value existing = scope.get(name); + for (var value : values) { + var existing = scope.get(name); if (existing == null) { - scope.put(name, new SingleValue(factory, name, decode(value))); + scope.put(name, new SingleValue(factory, name, value)); } else { ArrayValue list; if (existing instanceof ArrayValue) { @@ -90,36 +89,32 @@ public void put(String path, Collection values) { 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) { // 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(); } @@ -130,7 +125,7 @@ private void put(String path, BiConsumer> consumer) { } } } - String key = path.substring(nameStart, nameEnd); + var key = path.substring(nameStart, nameEnd); if (isNumber(key)) { target.useIndexes(); } @@ -143,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; } @@ -170,7 +167,7 @@ protected Map hash() { } public @NonNull Value get(@NonNull String name) { - Value value = hash.get(name); + var value = hash.get(name); if (value == null) { return new MissingValue(scope(name)); } @@ -253,16 +250,15 @@ private T toNullable(@NonNull ValueFactory factory, @NonNull Class type) @Override public Map> toMultimap() { - Map> result = new LinkedHashMap<>(hash.size()); - Set> entries = hash.entrySet(); - String scope = name == null ? "" : name + "."; - for (Map.Entry entry : entries) { - Value 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; @@ -274,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()); } } @@ -294,8 +290,6 @@ private > C toCollection(@NonNull Class type, C co } } } else { - // var item = new HashValue(factory, this.name); - // item.hash = hash; addItem(factory, this, type, collection); } } diff --git a/jooby/src/main/java/io/jooby/internal/HeadersValue.java b/jooby/src/main/java/io/jooby/internal/HeadersValue.java index 5f8caec99d..8ed7148ca2 100644 --- a/jooby/src/main/java/io/jooby/internal/HeadersValue.java +++ b/jooby/src/main/java/io/jooby/internal/HeadersValue.java @@ -11,7 +11,7 @@ import java.util.TreeMap; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Value; +import io.jooby.value.Value; import io.jooby.value.ValueFactory; public class HeadersValue extends HashValue implements Value { diff --git a/jooby/src/main/java/io/jooby/internal/InputStreamBody.java b/jooby/src/main/java/io/jooby/internal/InputStreamBody.java index 88f5f0e36f..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.Value; public class InputStreamBody implements Body { private Context ctx; @@ -76,16 +75,6 @@ public String value() { return value(StandardCharsets.UTF_8); } - @NonNull @Override - public Value get(int index) { - return index == 0 ? this : get(Integer.toString(index)); - } - - @NonNull @Override - public Value get(@NonNull String name) { - return new MissingValue(name); - } - @Override public String name() { return "body"; diff --git a/jooby/src/main/java/io/jooby/internal/MissingValue.java b/jooby/src/main/java/io/jooby/internal/MissingValue.java index 4f818bc57d..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.Value; import io.jooby.exception.MissingValueException; +import io.jooby.value.Value; public class MissingValue implements Value { - private String name; + private final String name; public MissingValue(String name) { this.name = name; @@ -34,6 +29,16 @@ public String name() { return this.name.equals(name) ? this : new MissingValue(this.name + "." + name); } + @Override + 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/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/SessionImpl.java b/jooby/src/main/java/io/jooby/internal/SessionImpl.java index 984b82319e..146fe85c10 100644 --- a/jooby/src/main/java/io/jooby/internal/SessionImpl.java +++ b/jooby/src/main/java/io/jooby/internal/SessionImpl.java @@ -14,7 +14,7 @@ import io.jooby.Context; import io.jooby.Session; import io.jooby.SessionStore; -import io.jooby.Value; +import io.jooby.value.Value; public class SessionImpl implements Session { diff --git a/jooby/src/main/java/io/jooby/internal/SingleValue.java b/jooby/src/main/java/io/jooby/internal/SingleValue.java index cdfb9c92a5..5137bf5632 100644 --- a/jooby/src/main/java/io/jooby/internal/SingleValue.java +++ b/jooby/src/main/java/io/jooby/internal/SingleValue.java @@ -14,8 +14,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.Value; import io.jooby.value.ConversionHint; +import io.jooby.value.Value; import io.jooby.value.ValueFactory; public class SingleValue implements Value { @@ -39,7 +39,7 @@ public String name() { @Override public @NonNull Value get(int index) { - return index == 0 ? this : get(Integer.toString(index)); + return get(Integer.toString(index)); } @Override @@ -53,7 +53,7 @@ public int size() { } @Override - public String value() { + public @NonNull String value() { return value; } @@ -63,8 +63,8 @@ public String toString() { } @Override - public Iterator iterator() { - return List.of(this).iterator(); + public @NonNull Iterator iterator() { + return List.of((Value) this).iterator(); } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java index 39dbd2bb18..b2c813a485 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java +++ b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java @@ -29,11 +29,11 @@ import io.jooby.FileUpload; import io.jooby.Formdata; import io.jooby.Usage; -import io.jooby.Value; import io.jooby.annotation.EmptyBean; import io.jooby.exception.BadRequestException; import io.jooby.exception.ProvisioningException; import io.jooby.internal.reflect.$Types; +import io.jooby.value.Value; import jakarta.inject.Inject; import jakarta.inject.Named; diff --git a/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java b/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java index e60ede3bb9..8c00cd7a78 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java +++ b/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java @@ -30,9 +30,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.SneakyThrows; import io.jooby.StatusCode; -import io.jooby.Value; import io.jooby.value.ConversionHint; import io.jooby.value.Converter; +import io.jooby.value.Value; import io.jooby.value.ValueFactory; public enum StandardConverter implements Converter { diff --git a/jooby/src/main/java/io/jooby/validation/ValidationContext.java b/jooby/src/main/java/io/jooby/validation/ValidationContext.java index 7cf306a5f8..2da78518d2 100644 --- a/jooby/src/main/java/io/jooby/validation/ValidationContext.java +++ b/jooby/src/main/java/io/jooby/validation/ValidationContext.java @@ -15,6 +15,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.*; +import io.jooby.value.Value; /** * Wrap a context and run {@link BeanValidator#validate(Context, Object)} over HTTP request objects. diff --git a/jooby/src/main/java/io/jooby/value/ConversionHint.java b/jooby/src/main/java/io/jooby/value/ConversionHint.java index f8fdfb5f2c..c9c62cb1af 100644 --- a/jooby/src/main/java/io/jooby/value/ConversionHint.java +++ b/jooby/src/main/java/io/jooby/value/ConversionHint.java @@ -5,8 +5,6 @@ */ package io.jooby.value; -import 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. diff --git a/jooby/src/main/java/io/jooby/value/Converter.java b/jooby/src/main/java/io/jooby/value/Converter.java index 06adc4b3cb..525d937810 100644 --- a/jooby/src/main/java/io/jooby/value/Converter.java +++ b/jooby/src/main/java/io/jooby/value/Converter.java @@ -8,7 +8,6 @@ import java.lang.reflect.Type; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Value; /** * Value converter for values that come from config, query, path, form, path parameters into more diff --git a/jooby/src/main/java/io/jooby/Value.java b/jooby/src/main/java/io/jooby/value/Value.java similarity index 89% rename from jooby/src/main/java/io/jooby/Value.java rename to jooby/src/main/java/io/jooby/value/Value.java index 54ae56ab4e..ad914f3fe0 100644 --- a/jooby/src/main/java/io/jooby/Value.java +++ b/jooby/src/main/java/io/jooby/value/Value.java @@ -3,7 +3,7 @@ * 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; @@ -15,6 +15,9 @@ 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; @@ -23,7 +26,6 @@ import io.jooby.internal.MissingValue; import io.jooby.internal.MultipartNode; import io.jooby.internal.SingleValue; -import io.jooby.value.ValueFactory; /** * Unified API for HTTP value. This API plays two role: @@ -51,7 +53,7 @@ public interface Value extends Iterable { * @param index Position. * @return A value at the given position. */ - @NonNull Value get(int index); + Value get(int index); /** * Get a value that matches the given name. @@ -59,25 +61,21 @@ public interface Value extends Iterable { * @param name Field name. * @return Field value. */ - @NonNull Value get(@NonNull String name); + 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. */ - default int size() { - return 0; - } + int size(); /** * Value iterator. * * @return Value iterator. */ - @NonNull default Iterator iterator() { - return Collections.emptyIterator(); - } + Iterator iterator(); /** * Process the given expression and resolve value references. @@ -92,7 +90,7 @@ default int size() { * @param expression Text expression. * @return Resolved text. */ - @NonNull default String resolve(@NonNull String expression) { + default String resolve(@NonNull String expression) { return resolve(expression, "${", "}"); } @@ -110,7 +108,7 @@ default int size() { * @param ignoreMissing On missing values, keep the expression as it is. * @return Resolved text. */ - @NonNull default String resolve(@NonNull String expression, boolean ignoreMissing) { + default String resolve(@NonNull String expression, boolean ignoreMissing) { return resolve(expression, ignoreMissing, "${", "}"); } @@ -129,7 +127,7 @@ default int size() { * @param endDelim End delimiter. * @return Resolved text. */ - @NonNull default String resolve( + default String resolve( @NonNull String expression, @NonNull String startDelim, @NonNull String endDelim) { return resolve(expression, false, startDelim, endDelim); } @@ -150,12 +148,12 @@ default int size() { * @param endDelim End delimiter. * @return Resolved text. */ - @NonNull default String resolve( + default String resolve( @NonNull String expression, boolean ignoreMissing, @NonNull String startDelim, @NonNull String endDelim) { - if (expression.length() == 0) { + if (expression.isEmpty()) { return ""; } @@ -212,7 +210,7 @@ default int size() { offset = end + endDelim.length(); start = expression.indexOf(startDelim, offset); } - if (buffer.length() == 0) { + if (buffer.isEmpty()) { return expression; } if (offset < expression.length()) { @@ -391,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) { @@ -415,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()); } @@ -424,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. @@ -447,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); } @@ -459,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())); @@ -470,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) { @@ -524,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(); @@ -537,7 +535,7 @@ 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(toNullable(type)); } catch (MissingValueException x) { @@ -552,7 +550,7 @@ default boolean isObject() { * @param Item type. * @return List of items. */ - @NonNull default List toList(@NonNull Class type) { + default List toList(@NonNull Class type) { return List.of(to(type)); } @@ -563,7 +561,7 @@ default boolean isObject() { * @param Item type. * @return Set of items. */ - @NonNull default Set toSet(@NonNull Class type) { + default Set toSet(@NonNull Class type) { return Set.of(to(type)); } @@ -575,7 +573,7 @@ default boolean isObject() { * @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 @@ -592,14 +590,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; @@ -616,7 +614,7 @@ default boolean isObject() { * @param name Name of missing value. * @return Missing value. */ - static @NonNull Value missing(@NonNull String name) { + static Value missing(@NonNull String name) { return new MissingValue(name); } @@ -628,7 +626,7 @@ default boolean isObject() { * @param value Value. * @return Single value. */ - static @NonNull Value value( + static Value value( @NonNull ValueFactory valueFactory, @NonNull String name, @NonNull String value) { return new SingleValue(valueFactory, name, value); } @@ -641,7 +639,7 @@ default boolean isObject() { * @param values Field values. * @return Array value. */ - static @NonNull Value array( + static Value array( @NonNull ValueFactory valueFactory, @NonNull String name, @NonNull List values) { return new ArrayValue(valueFactory, name).add(values); } @@ -657,9 +655,9 @@ default boolean isObject() { * @param values Field values. * @return A value. */ - static @NonNull Value create( - ValueFactory valueFactory, @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) { @@ -679,8 +677,8 @@ default boolean isObject() { * @param value Field values. * @return A value. */ - static @NonNull Value create( - ValueFactory valueFactory, @NonNull String name, @Nullable String value) { + static Value create( + @NonNull ValueFactory valueFactory, @NonNull String name, @Nullable String value) { if (value == null) { return missing(name); } @@ -694,8 +692,8 @@ default boolean isObject() { * @param values Map values. * @return A hash/object value. */ - static @NonNull Value hash( - ValueFactory valueFactory, @NonNull Map> values) { + static Value hash( + @NonNull ValueFactory valueFactory, @NonNull Map> values) { var node = new HashValue(valueFactory, null); node.put(values); return node; @@ -707,7 +705,7 @@ default boolean isObject() { * @param valueFactory Current context. * @return A hash/object value. */ - static @NonNull Formdata formdata(ValueFactory valueFactory) { + static Formdata formdata(@NonNull ValueFactory valueFactory) { return new MultipartNode(valueFactory); } @@ -718,9 +716,9 @@ default boolean isObject() { * @param values Map values. * @return A hash/object value. */ - static @NonNull Value headers( - ValueFactory valueFactory, @NonNull Map> values) { - HeadersValue node = new HeadersValue(valueFactory); + 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 index 590c9d9704..68f1cb7b50 100644 --- a/jooby/src/main/java/io/jooby/value/ValueFactory.java +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -13,7 +13,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.SneakyThrows; -import io.jooby.Value; import io.jooby.exception.TypeMismatchException; import io.jooby.internal.converter.ReflectiveBeanConverter; import io.jooby.internal.converter.StandardConverter; diff --git a/jooby/src/test/java/io/jooby/Issue2525.java b/jooby/src/test/java/io/jooby/Issue2525.java index 8f22ca280d..1aca03ccf6 100644 --- a/jooby/src/test/java/io/jooby/Issue2525.java +++ b/jooby/src/test/java/io/jooby/Issue2525.java @@ -18,6 +18,7 @@ import io.jooby.internal.UrlParser; import io.jooby.value.ConversionHint; import io.jooby.value.Converter; +import io.jooby.value.Value; import io.jooby.value.ValueFactory; import jakarta.inject.Inject; 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 9a85e3aeb9..fcc79ef96c 100644 --- a/jooby/src/test/java/io/jooby/ValueResolveTest.java +++ b/jooby/src/test/java/io/jooby/ValueResolveTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import io.jooby.internal.HashValue; +import io.jooby.value.Value; public class ValueResolveTest { 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/DurationConverterTest.java b/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java index f41f39ba91..ed3fbc1937 100644 --- a/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java +++ b/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java @@ -14,9 +14,9 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import io.jooby.Value; import io.jooby.internal.converter.StandardConverter; import io.jooby.value.ConversionHint; +import io.jooby.value.Value; public class DurationConverterTest { 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 078a091313..0000000000 --- a/jooby/src/test/java/io/jooby/internal/ValueConverterHelper.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.internal; - -import io.jooby.value.ValueFactory; - -public class ValueConverterHelper { - - public static ValueFactory testContext() { - var result = new ValueFactory(); - return result; - // 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.getValueFactory()); - // 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.getValueFactory()); - // }); - // return ctx; - } -} 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/tests/i1807/Issue1807.java b/modules/jooby-apt/src/test/java/tests/i1807/Issue1807.java index 51a99f2d46..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.Value; import io.jooby.apt.ProcessorRunner; import io.jooby.test.MockContext; import io.jooby.test.MockRouter; +import io.jooby.value.Value; public class Issue1807 { 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 f3604f25db..bdbd952df0 100644 --- a/modules/jooby-apt/src/test/java/tests/i2325/VC2325.java +++ b/modules/jooby-apt/src/test/java/tests/i2325/VC2325.java @@ -9,9 +9,9 @@ import org.jetbrains.annotations.NotNull; -import io.jooby.Value; import io.jooby.value.ConversionHint; import io.jooby.value.Converter; +import io.jooby.value.Value; public class VC2325 implements Converter { @Override 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 b92c2b6364..67012df1a4 100644 --- a/modules/jooby-apt/src/test/java/tests/i2405/Converter2405.java +++ b/modules/jooby-apt/src/test/java/tests/i2405/Converter2405.java @@ -10,9 +10,9 @@ import org.jetbrains.annotations.NotNull; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Value; import io.jooby.value.ConversionHint; import io.jooby.value.Converter; +import io.jooby.value.Value; public class Converter2405 implements Converter { 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 ec4008e5b2..8b23e884b1 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 @@ -69,9 +69,9 @@ import io.jooby.SessionStore; import io.jooby.SneakyThrows; import io.jooby.StatusCode; -import io.jooby.Value; import io.jooby.WebSocket; import io.jooby.buffer.DataBuffer; +import io.jooby.value.Value; public class JettyContext implements DefaultContext, Callback { private final int bufferSize; 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 5b792c30cb..f2b397f1e7 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 @@ -24,8 +24,8 @@ import io.jooby.RouterOption import io.jooby.Server import io.jooby.ServerOptions import io.jooby.ServiceRegistry -import io.jooby.Value import io.jooby.handler.Cors +import io.jooby.value.Value import java.util.function.Supplier import kotlin.reflect.KClass import kotlin.reflect.KProperty 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 256a38b6f8..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,13 +24,12 @@ import io.jooby.Context; import io.jooby.MediaType; import io.jooby.SneakyThrows; -import io.jooby.Value; 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; @@ -82,16 +81,6 @@ public String value() { return value(StandardCharsets.UTF_8); } - @NonNull @Override - public Value get(int index) { - return index == 0 ? this : get(Integer.toString(index)); - } - - @NonNull @Override - public Value 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/NettyContext.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java index abd30059dd..ac635a6fc5 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 @@ -67,9 +67,9 @@ import io.jooby.Session; import io.jooby.SneakyThrows; import io.jooby.StatusCode; -import io.jooby.Value; import io.jooby.WebSocket; import io.jooby.buffer.DataBuffer; +import io.jooby.value.Value; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; 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-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-test/src/main/java/io/jooby/test/MockContext.java b/modules/jooby-test/src/main/java/io/jooby/test/MockContext.java index 4d69d6a2b4..34b7f87972 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,12 +52,12 @@ import io.jooby.ServerSentEmitter; import io.jooby.Session; import io.jooby.StatusCode; -import io.jooby.Value; 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.value.Value; import io.jooby.value.ValueFactory; /** Unit test friendly context implementation. Allows to set context properties. */ @@ -121,12 +121,12 @@ public class MockContext implements DefaultContext { private DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); - @NonNull @Override + @Override public String getMethod() { return method; } - @NonNull @Override + @Override public Context setPort(int port) { this.port = port; return this; @@ -137,7 +137,7 @@ public int getPort() { return port; } - @NonNull @Override + @Override public DataBufferFactory getBufferFactory() { return bufferFactory; } @@ -149,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); } @@ -168,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; } @@ -178,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) { @@ -198,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; } @@ -226,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; } @@ -254,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); @@ -264,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(valueFactory, queryString); } - @NonNull @Override + @Override public String queryString() { return queryString; } @@ -291,12 +291,12 @@ 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 + @Override public Value header() { return Value.headers(valueFactory, headers); } @@ -307,7 +307,7 @@ public Value 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; } @@ -319,18 +319,18 @@ public Value 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()); } @@ -347,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)) @@ -355,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)) @@ -370,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."); @@ -383,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."); @@ -411,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; } @@ -422,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; } @@ -433,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); } @@ -444,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); } @@ -459,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; @@ -494,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; @@ -511,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); @@ -546,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); @@ -559,7 +559,7 @@ public OutputStream responseStream() { return out; } - @NonNull @Override + @Override public Sender responseSender() { responseStarted = true; return new Sender() { @@ -570,7 +570,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { return this; } - @NonNull @Override + @Override public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { response.setResult(data); callback.onComplete(MockContext.this, null); @@ -584,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()); @@ -635,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()); @@ -643,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); @@ -651,7 +651,7 @@ public MockContext send(@NonNull byte[] data) { return this; } - @NonNull @Override + @Override public MockContext send(@NonNull byte[]... data) { responseStarted = true; this.response @@ -661,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()); @@ -669,7 +669,7 @@ public MockContext send(@NonNull ByteBuffer data) { return this; } - @NonNull @Override + @Override public Context send(@NonNull DataBuffer data) { responseStarted = true; this.response.setResult(data).setContentLength(data.readableByteCount()); @@ -677,7 +677,7 @@ public Context send(@NonNull DataBuffer data) { return this; } - @NonNull @Override + @Override public Context send(@NonNull ByteBuffer[] data) { responseStarted = true; this.response @@ -687,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); @@ -695,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); @@ -703,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); @@ -711,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); @@ -719,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); @@ -727,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)); @@ -747,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) { @@ -765,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; @@ -792,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; } @@ -809,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 Value 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; 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 8cfb0a3bcb..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,7 +14,7 @@ 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. */ 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 2218b8048c..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 @@ -27,6 +27,7 @@ import io.jooby.*; import io.jooby.exception.MissingValueException; +import io.jooby.value.Value; import io.jooby.value.ValueFactory; public class LookupTest { 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 ebd07d59b9..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.Value; import io.jooby.WebSocketMessage; +import io.jooby.value.Value; import io.reactivex.rxjava3.core.Single; public class UnitTest { 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 1fe19564f4..dad2266a87 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 @@ -56,9 +56,9 @@ import io.jooby.SessionStore; import io.jooby.SneakyThrows; import io.jooby.StatusCode; -import io.jooby.Value; import io.jooby.WebSocket; import io.jooby.buffer.DataBuffer; +import io.jooby.value.Value; import io.undertow.Handlers; import io.undertow.io.IoCallback; import io.undertow.io.Sender; 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/i2325/VC2325.java b/tests/src/test/java/io/jooby/i2325/VC2325.java index 9aa85586a2..0c51ad7831 100644 --- a/tests/src/test/java/io/jooby/i2325/VC2325.java +++ b/tests/src/test/java/io/jooby/i2325/VC2325.java @@ -10,9 +10,9 @@ import org.jetbrains.annotations.NotNull; import io.jooby.QueryString; -import io.jooby.Value; import io.jooby.value.ConversionHint; import io.jooby.value.Converter; +import io.jooby.value.Value; public class VC2325 implements Converter { @Override diff --git a/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java b/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java index 49d2db49db..50a946689d 100644 --- a/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java +++ b/tests/src/test/java/io/jooby/test/MyValueBeanConverter.java @@ -10,9 +10,9 @@ import org.jetbrains.annotations.NotNull; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Value; import io.jooby.value.ConversionHint; import io.jooby.value.Converter; +import io.jooby.value.Value; public class MyValueBeanConverter implements Converter { From 23fe055de4d85608a3978d6c9411e2d827748f10 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 19 Jun 2025 20:49:14 -0300 Subject: [PATCH 10/60] value api: make reflective bean converter a converter and use method handle for invocation - ReflectiveBeanConverter should take MethodHandles.Lookup fix #3498 --- .../converter/ReflectiveBeanConverter.java | 98 ++++++++++--------- .../java/io/jooby/value/ValueFactory.java | 44 ++++++--- jooby/src/test/java/io/jooby/Issue2525.java | 2 +- .../src/test/java/tests/i2325/VC2325.java | 2 +- .../src/test/java/io/jooby/i2325/VC2325.java | 2 +- 5 files changed, 85 insertions(+), 63 deletions(-) diff --git a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java index b2c813a485..16144e69ad 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java +++ b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java @@ -7,14 +7,8 @@ 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.lang.invoke.MethodHandles; +import java.lang.reflect.*; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -33,15 +27,18 @@ import io.jooby.exception.BadRequestException; import io.jooby.exception.ProvisioningException; import io.jooby.internal.reflect.$Types; +import io.jooby.value.ConversionHint; +import io.jooby.value.Converter; import io.jooby.value.Value; import jakarta.inject.Inject; import jakarta.inject.Named; -public class ReflectiveBeanConverter { +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); } } @@ -49,40 +46,45 @@ public void invoke(Object instance) throws InvocationTargetException, IllegalAcc "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 MethodHandles.Lookup lookup; - public Object convert(@NonNull Value node, @NonNull Class type, boolean allowEmptyBean) { + public ReflectiveBeanConverter(MethodHandles.Lookup lookup) { + this.lookup = lookup; + } + + @Override + public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { + return convert(value, $Types.parameterizedType0(type), hint == ConversionHint.Empty); + } + + private Object convert(@NonNull Value node, @NonNull Class type, boolean allowEmptyBean) { try { return newInstance(type, node, allowEmptyBean); - } catch (InstantiationException | IllegalAccessException | NoSuchMethodException x) { - throw propagate(x); } catch (InvocationTargetException x) { throw propagate(x.getCause()); + } catch (Throwable x) { + throw propagate(x); } } - private static Object newInstance(Class type, Value node, boolean allowEmptyBean) - throws IllegalAccessException, - InstantiationException, - InvocationTargetException, - NoSuchMethodException { - Constructor[] constructors = type.getConstructors(); + private Object newInstance(Class type, Value node, boolean allowEmptyBean) throws Throwable { + var constructors = type.getConstructors(); Set state = new HashSet<>(); - Constructor constructor; + 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); + var args = inject(node, constructor, state::add); + var 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); + var handle = lookup.unreflectConstructor(constructor); + var instance = handle.invokeWithArguments(args); + for (var setter : setters) { + setter.invoke(lookup, instance); } return instance; } @@ -97,9 +99,9 @@ private static Constructor selectConstructor(Constructor[] constructors) { } else { Constructor injectConstructor = null; Constructor defaultConstructor = null; - for (Constructor constructor : constructors) { + 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; @@ -113,7 +115,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); } @@ -121,23 +123,23 @@ private static Constructor selectConstructor(Constructor[] constructors) { } } - public static Object[] inject(Value 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]; + var args = new ArrayList<>(parameters.length); for (int i = 0; i < parameters.length; i++) { - Parameter parameter = parameters[i]; - String name = paramName(parameter); - Value param = scope.get(name); + var parameter = parameters[i]; + var name = paramName(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; } @@ -176,13 +178,13 @@ private static List setters(Class type, Value node, Set nodes) { var methods = type.getMethods(); var result = new ArrayList(); for (String name : names(node)) { - Value 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; @@ -200,9 +202,9 @@ private static List setters(Class type, Value node, Set nodes) { 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())) { @@ -222,7 +224,7 @@ private static Object value(Parameter parameter, Value node, Value 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; } @@ -252,7 +254,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; @@ -273,7 +275,7 @@ private static boolean isFileUpload(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/value/ValueFactory.java b/jooby/src/main/java/io/jooby/value/ValueFactory.java index 68f1cb7b50..56195d524b 100644 --- a/jooby/src/main/java/io/jooby/value/ValueFactory.java +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -12,6 +12,7 @@ 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.TypeMismatchException; import io.jooby.internal.converter.ReflectiveBeanConverter; @@ -28,28 +29,44 @@ public enum ConversionType { private final Map converterMap = new HashMap<>(); - private MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + private ConversionHint defaultHint = ConversionHint.Strict; - public ValueFactory() { + private MethodHandles.Lookup lookup; + + private Converter fallback; + + public ValueFactory(@NonNull MethodHandles.Lookup lookup) { + this.lookup = lookup; + this.fallback = new ReflectiveBeanConverter(lookup); StandardConverter.register(this); } - public Converter get(Type type) { + public ValueFactory() { + this(MethodHandles.publicLookup()); + } + + public @NonNull ValueFactory lookup(@NonNull MethodHandles.Lookup lookup) { + this.lookup = lookup; + this.fallback = new ReflectiveBeanConverter(lookup); + return this; + } + + public @NonNull ValueFactory hint(@NonNull ConversionHint defaultHint) { + this.defaultHint = defaultHint; + return this; + } + + public @Nullable Converter get(Type type) { return converterMap.get(type); } - public ValueFactory put(Type type, Converter converter) { + public @NonNull ValueFactory put(@NonNull Type type, @NonNull Converter converter) { converterMap.put(type, converter); return this; } public T convert(@NonNull Type type, @NonNull Value value) { - T result = convert(type, value, ConversionHint.Strict); - if (result == null) { - throw new TypeMismatchException(value.name(), type); - } - - return result; + return convert(type, value, defaultHint); } @SuppressWarnings("unchecked") @@ -84,8 +101,11 @@ public T convert(@NonNull Type type, @NonNull Value value, @NonNull Conversi } } // anything else fallback to reflective - var reflective = new ReflectiveBeanConverter(); - return (T) reflective.convert(value, rawType, hint == ConversionHint.Empty); + var result = (T) fallback.convert(type, value, hint); + if (result == null && hint == ConversionHint.Strict) { + throw new TypeMismatchException(value.name(), type); + } + return result; } } diff --git a/jooby/src/test/java/io/jooby/Issue2525.java b/jooby/src/test/java/io/jooby/Issue2525.java index 1aca03ccf6..455b319b23 100644 --- a/jooby/src/test/java/io/jooby/Issue2525.java +++ b/jooby/src/test/java/io/jooby/Issue2525.java @@ -26,7 +26,7 @@ public class Issue2525 { public class VC2525 implements Converter { @Override - public Object convert(@NotNull Type type, @NotNull Value value, ConversionHint hint) { + public Object convert(@NotNull Type type, @NotNull Value value, @NotNull ConversionHint hint) { return new MyID2525(value.value()); } } 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 bdbd952df0..625c6c9499 100644 --- a/modules/jooby-apt/src/test/java/tests/i2325/VC2325.java +++ b/modules/jooby-apt/src/test/java/tests/i2325/VC2325.java @@ -15,7 +15,7 @@ public class VC2325 implements Converter { @Override - public Object convert(@NotNull Type type, @NotNull Value value, ConversionHint hint) { + public Object convert(@NotNull Type type, @NotNull Value value, @NotNull ConversionHint hint) { return new MyID2325(value.value()); } } diff --git a/tests/src/test/java/io/jooby/i2325/VC2325.java b/tests/src/test/java/io/jooby/i2325/VC2325.java index 0c51ad7831..a950fdf505 100644 --- a/tests/src/test/java/io/jooby/i2325/VC2325.java +++ b/tests/src/test/java/io/jooby/i2325/VC2325.java @@ -16,7 +16,7 @@ public class VC2325 implements Converter { @Override - public Object convert(@NotNull Type type, @NotNull Value value, ConversionHint hint) { + 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); } From 707fb7fe0338f7c44cabcfeccdbdb4d9b8842223 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 21 Jun 2025 10:22:53 -0300 Subject: [PATCH 11/60] value api: remove `@EmptyBean` annotation and make `QueryString.toNullable` truly nullable - Add `QueryString.toEmpty` for backwards compatibility --- jooby/src/main/java/io/jooby/Body.java | 24 +- jooby/src/main/java/io/jooby/Context.java | 236 +++++++++--------- .../main/java/io/jooby/DefaultContext.java | 35 ++- .../main/java/io/jooby/ForwardingContext.java | 5 + jooby/src/main/java/io/jooby/QueryString.java | 32 ++- .../java/io/jooby/annotation/EmptyBean.java | 15 -- .../java/io/jooby/internal/ArrayValue.java | 2 +- .../io/jooby/internal/QueryStringValue.java | 11 +- .../converter/ReflectiveBeanConverter.java | 25 +- .../jooby/validation/ValidationContext.java | 19 +- .../main/java/io/jooby/value/Converter.java | 2 +- jooby/src/main/java/io/jooby/value/Value.java | 2 + .../java/io/jooby/value/ValueFactory.java | 6 - .../java/io/jooby/value/ValueHintTest.java | 59 +++++ .../internal/apt/ParameterGenerator.java | 43 ++-- .../src/test/java/tests/i3476/Issue3476.java | 2 +- .../BeanValidationGeneratorTest.java | 2 +- .../test/java/io/jooby/i2557/Issue2557.java | 6 +- 18 files changed, 302 insertions(+), 224 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/annotation/EmptyBean.java create mode 100644 jooby/src/test/java/io/jooby/value/ValueHintTest.java diff --git a/jooby/src/main/java/io/jooby/Body.java b/jooby/src/main/java/io/jooby/Body.java index d5269ba336..2cd7103886 100644 --- a/jooby/src/main/java/io/jooby/Body.java +++ b/jooby/src/main/java/io/jooby/Body.java @@ -40,7 +40,7 @@ public interface Body extends Value { * @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"); @@ -95,30 +95,30 @@ default Iterator iterator() { * * @return Body as 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() { + default @Override List toList() { return List.of(value()); } - default @NonNull @Override Set toSet() { + 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); } @@ -133,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. @@ -155,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); } @@ -167,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); } @@ -178,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); } @@ -189,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 dce1a889ef..b281ae7cf1 100644 --- a/jooby/src/main/java/io/jooby/Context.java +++ b/jooby/src/main/java/io/jooby/Context.java @@ -117,7 +117,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()}. @@ -192,7 +192,7 @@ private static Selector single() { * * @return Session. */ - @NonNull Session session(); + Session session(); /** * Find a session attribute using the given name. If there is no session or attribute under that @@ -201,7 +201,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. @@ -216,21 +216,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. @@ -238,14 +238,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. @@ -261,14 +261,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(); } @@ -277,7 +277,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. @@ -285,7 +285,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. @@ -293,7 +293,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. @@ -302,14 +302,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 Value} object. * * @return A value object. */ - @NonNull Value path(); + Value path(); /** * Path map represent all the path keys with their values. @@ -328,7 +328,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. @@ -336,7 +336,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 @@ -348,7 +348,7 @@ private static Selector single() { * * @return Query string as {@link Value} object. */ - @NonNull QueryString query(); + QueryString query(); /** * Get a query parameter that matches the given name. @@ -366,7 +366,7 @@ private static Selector single() { * @param name Parameter name. * @return A query value. */ - @NonNull Value query(@NonNull String name); + Value query(@NonNull String name); /** * Query string with the leading ? or empty string. This is the raw query string, @@ -375,7 +375,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. @@ -384,12 +384,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 * @@ -397,7 +397,7 @@ private static Selector single() { * * @return Query string as map. */ - @NonNull Map queryMap(); + Map queryMap(); /* ********************************************************************************************** * Header API @@ -409,7 +409,7 @@ private static Selector single() { * * @return Request headers as {@link Value}. */ - @NonNull Value header(); + Value header(); /** * Get a header that matches the given name. @@ -417,14 +417,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 @@ -467,7 +467,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. @@ -494,7 +494,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") @@ -510,7 +510,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); } @@ -532,7 +532,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() @@ -547,7 +547,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))); @@ -567,7 +567,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. @@ -577,7 +577,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. @@ -588,7 +588,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. @@ -599,7 +599,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. @@ -607,7 +607,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 @@ -620,7 +620,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). @@ -630,7 +630,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 @@ -642,7 +642,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 @@ -661,14 +661,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 @@ -677,7 +677,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. @@ -691,7 +691,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 @@ -706,7 +706,7 @@ default boolean isPreflight() { * * @return HTTP scheme in lower case. */ - @NonNull String getScheme(); + String getScheme(); /** * Set HTTP scheme in lower case. @@ -714,7 +714,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 @@ -729,7 +729,7 @@ default boolean isPreflight() { * * @return Multipart value. */ - @NonNull Formdata form(); + Formdata form(); /** * Get a form field that matches the given name. @@ -741,7 +741,7 @@ default boolean isPreflight() { * @param name Field name. * @return Multipart value. */ - @NonNull Value form(@NonNull String name); + Value form(@NonNull String name); /** * Convert form data to the given type. @@ -753,7 +753,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. @@ -763,14 +763,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. @@ -780,7 +780,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. @@ -790,7 +790,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 @@ -863,7 +863,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. @@ -872,7 +872,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. @@ -881,7 +881,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. @@ -891,7 +891,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 @@ -904,7 +904,7 @@ default ParamLookup lookup() { * @param contentType Content type. * @return MessageDecoder. */ - @NonNull MessageDecoder decoder(@NonNull MediaType contentType); + MessageDecoder decoder(@NonNull MediaType contentType); /* ********************************************************************************************** * Dispatch methods @@ -939,7 +939,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. @@ -962,7 +962,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 @@ -974,7 +974,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. @@ -984,7 +984,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. @@ -994,7 +994,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); /* * ********************************************************************************************** @@ -1009,7 +1009,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. @@ -1018,7 +1018,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. @@ -1027,7 +1027,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. @@ -1036,7 +1036,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. @@ -1044,14 +1044,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. @@ -1059,7 +1059,7 @@ default ParamLookup lookup() { * @param length Response length. * @return This context. */ - @NonNull Context setResponseLength(long length); + Context setResponseLength(long length); /** * Get response header. @@ -1082,7 +1082,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. @@ -1090,7 +1090,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. @@ -1098,7 +1098,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. @@ -1107,7 +1107,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 @@ -1116,14 +1116,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. @@ -1131,7 +1131,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. @@ -1139,14 +1139,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. @@ -1154,14 +1154,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. @@ -1169,7 +1169,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. @@ -1179,8 +1179,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; /** @@ -1190,21 +1190,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. @@ -1212,7 +1212,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. @@ -1221,7 +1221,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. @@ -1230,7 +1230,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. @@ -1240,9 +1240,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. @@ -1253,10 +1252,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; /** @@ -1265,7 +1262,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. @@ -1274,7 +1271,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. @@ -1282,7 +1279,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. @@ -1291,7 +1288,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. @@ -1299,7 +1296,7 @@ default ParamLookup lookup() { * @param data Response. * @return This context. */ - @NonNull Context send(@NonNull byte[] data); + Context send(@NonNull byte[] data); /** * Send response data. @@ -1307,7 +1304,7 @@ default ParamLookup lookup() { * @param data Response. * @return This context. */ - @NonNull Context send(@NonNull ByteBuffer data); + Context send(@NonNull ByteBuffer data); /** * Send response data. @@ -1315,7 +1312,7 @@ default ParamLookup lookup() { * @param data Response. * @return This context. */ - @NonNull Context send(@NonNull DataBuffer data); + Context send(@NonNull DataBuffer data); /** * Send response data. @@ -1323,7 +1320,7 @@ default ParamLookup lookup() { * @param data Response. * @return This context. */ - @NonNull Context send(@NonNull byte[]... data); + Context send(@NonNull byte[]... data); /** * Send response data. @@ -1331,7 +1328,7 @@ default ParamLookup lookup() { * @param data Response. * @return This context. */ - @NonNull Context send(@NonNull ByteBuffer[] data); + Context send(@NonNull ByteBuffer[] data); /** * Send response data. @@ -1339,7 +1336,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. @@ -1347,7 +1344,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. @@ -1355,7 +1352,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. @@ -1363,7 +1360,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. @@ -1371,7 +1368,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. @@ -1379,7 +1376,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)}. @@ -1387,7 +1384,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. @@ -1396,7 +1393,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. @@ -1420,7 +1417,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. @@ -1428,7 +1425,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 @@ -1442,7 +1439,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); } @@ -1461,11 +1458,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/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 67914cc3fd..f706568e78 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -25,7 +25,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; import org.slf4j.Logger; @@ -224,7 +223,7 @@ default String queryString() { @Override default T query(@NonNull Class type) { - return query().to(type); + return query().toEmpty(type); } @Override @@ -248,25 +247,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)) { @@ -288,15 +287,15 @@ default String getRequestURL() { @Override default String getRequestURL(@NonNull String path) { - String scheme = getScheme(); - String host = getHost(); + 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); } @@ -324,10 +323,10 @@ default long getRequestLength() { } @Override - default @Nullable String getHostAndPort() { + default String getHostAndPort() { Optional header = getRouter().isTrustProxy() ? header("X-Forwarded-Host").toOptional() : Optional.empty(); - String value = + var value = header.orElseGet( () -> ofNullable(header("Host").valueOrNull()) @@ -342,13 +341,13 @@ default long getRequestLength() { @Override default String getServerHost() { - String host = getRouter().getServerOptions().getHost(); + var host = getRouter().getServerOptions().getHost(); return host.equals("0.0.0.0") ? "localhost" : host; } @Override default int getServerPort() { - ServerOptions options = getRouter().getServerOptions(); + var options = getRouter().getServerOptions(); return isSecure() // Buggy proxy where it report a https scheme but there is no HTTPS configured option ? ofNullable(options.getSecurePort()).orElse(options.getPort()) @@ -357,7 +356,7 @@ default int getServerPort() { @Override default int getPort() { - String hostAndPort = getHostAndPort(); + var hostAndPort = getHostAndPort(); if (hostAndPort != null) { int index = hostAndPort.indexOf(':'); if (index > 0) { @@ -647,7 +646,7 @@ default Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { } } } - /** rethrow fatal exceptions: */ + /* rethrow fatal exceptions: */ if (SneakyThrows.isFatal(cause)) { throw SneakyThrows.propagate(cause); } diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index 66b8496a8d..6da79b3f44 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -552,6 +552,11 @@ public ForwardingQueryString(QueryString queryString) { super(queryString); } + @Override + public T toEmpty(@NonNull Class type) { + return ((QueryString) delegate).toEmpty(type); + } + @Override public String queryString() { return ((QueryString) delegate).queryString(); diff --git a/jooby/src/main/java/io/jooby/QueryString.java b/jooby/src/main/java/io/jooby/QueryString.java index b6d9356236..f9c5bfe8e9 100644 --- a/jooby/src/main/java/io/jooby/QueryString.java +++ b/jooby/src/main/java/io/jooby/QueryString.java @@ -24,7 +24,34 @@ public interface QueryString extends Value { * * @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. + * + *
{@code
+   * var factory = ctx.getValueFactory();
+   * var nullable = factory.convert(MyBean.class, ctx.query(), ConversionHint.Nullable);
+   * }
+ * + * @param type Type to convert. + * @return Non null result. + * @param Type result. + */ + @Nullable T toNullable(@NonNull Class type); + + /** + * 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. @@ -39,8 +66,7 @@ public interface QueryString extends Value { * @param queryString Query string. * @return A query string. */ - static @NonNull QueryString create( - @NonNull ValueFactory valueFactory, @Nullable String queryString) { + static QueryString create(@NonNull ValueFactory valueFactory, @Nullable String queryString) { return UrlParser.queryString(valueFactory, queryString); } } 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/internal/ArrayValue.java b/jooby/src/main/java/io/jooby/internal/ArrayValue.java index 554e69654c..5aa984398b 100644 --- a/jooby/src/main/java/io/jooby/internal/ArrayValue.java +++ b/jooby/src/main/java/io/jooby/internal/ArrayValue.java @@ -92,7 +92,7 @@ public String toString() { @NonNull @Override public T to(@NonNull Class type) { - return factory.convert(type, list.get(0)); + return factory.convert(type, list.get(0), ConversionHint.Strict); } @Nullable @Override diff --git a/jooby/src/main/java/io/jooby/internal/QueryStringValue.java b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java index a68b5c48d4..164321d37f 100644 --- a/jooby/src/main/java/io/jooby/internal/QueryStringValue.java +++ b/jooby/src/main/java/io/jooby/internal/QueryStringValue.java @@ -6,25 +6,20 @@ package io.jooby.internal; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; 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(ValueFactory valueFactory, String queryString) { super(valueFactory); this.queryString = queryString; } - @Nullable @Override - public T toNullable(@NonNull Class type) { - // NOTE: 2.x backward compatible. Make sure Query object are almost always created - // GET /search? - // with class Search (q="*") - // so q is defaulted to "*" + @Override + public @NonNull T toEmpty(@NonNull Class type) { return factory.convert(type, this, ConversionHint.Empty); } diff --git a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java index 16144e69ad..d72fdc3699 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java +++ b/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java @@ -9,21 +9,13 @@ import java.lang.invoke.MethodHandles; import java.lang.reflect.*; -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.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.annotation.EmptyBean; import io.jooby.exception.BadRequestException; import io.jooby.exception.ProvisioningException; import io.jooby.internal.reflect.$Types; @@ -33,6 +25,13 @@ import jakarta.inject.Inject; import jakarta.inject.Named; +/** + * Creates an object from {@link Value}. Value might come from HTTP Context (Query, Path, Form, + * etc.) or from configuration value. + * + * @author edgar + * @since 1.0.0 + */ public class ReflectiveBeanConverter implements Converter { private record Setter(Method method, Object arg) { @@ -46,7 +45,7 @@ public void invoke(MethodHandles.Lookup lookup, Object instance) throws Throwabl "Ambiguous constructor found. Expecting a single constructor or only one annotated with " + Inject.class.getName(); - private MethodHandles.Lookup lookup; + private final MethodHandles.Lookup lookup; public ReflectiveBeanConverter(MethodHandles.Lookup lookup) { this.lookup = lookup; @@ -78,7 +77,7 @@ private Object newInstance(Class type, Value node, boolean allowEmptyBean) throw } var args = inject(node, constructor, state::add); var setters = setters(type, node, state); - if (!allowEmptyBean(type, allowEmptyBean) && state.stream().allMatch(Value::isMissing)) { + if (!allowEmptyBean && state.stream().allMatch(Value::isMissing)) { return null; } var handle = lookup.unreflectConstructor(constructor); @@ -89,10 +88,6 @@ private Object newInstance(Class type, Value node, boolean allowEmptyBean) throw return instance; } - private static boolean allowEmptyBean(Class type, boolean defaults) { - return type.getAnnotation(EmptyBean.class) != null || defaults; - } - private static Constructor selectConstructor(Constructor[] constructors) { if (constructors.length == 1) { return constructors[0]; diff --git a/jooby/src/main/java/io/jooby/validation/ValidationContext.java b/jooby/src/main/java/io/jooby/validation/ValidationContext.java index 2da78518d2..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,7 @@ 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; /** @@ -35,13 +36,18 @@ public ValidatedValue(Context ctx, Value delegate) { @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 @@ -102,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(); @@ -180,7 +191,7 @@ public Body body() { @NonNull @Override public T query(@NonNull Class type) { - return query().to(type); + return query().toEmpty(type); } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/value/Converter.java b/jooby/src/main/java/io/jooby/value/Converter.java index 525d937810..19aa4ac002 100644 --- a/jooby/src/main/java/io/jooby/value/Converter.java +++ b/jooby/src/main/java/io/jooby/value/Converter.java @@ -18,7 +18,7 @@ */ public interface Converter { /** - * Convert to specific type. + * Convert a {@link Value} specific type. * * @param type Requested type. * @param value Value value. diff --git a/jooby/src/main/java/io/jooby/value/Value.java b/jooby/src/main/java/io/jooby/value/Value.java index ad914f3fe0..3edd77fad0 100644 --- a/jooby/src/main/java/io/jooby/value/Value.java +++ b/jooby/src/main/java/io/jooby/value/Value.java @@ -569,6 +569,8 @@ default Set toSet(@NonNull Class 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. diff --git a/jooby/src/main/java/io/jooby/value/ValueFactory.java b/jooby/src/main/java/io/jooby/value/ValueFactory.java index 56195d524b..e5a3bff03a 100644 --- a/jooby/src/main/java/io/jooby/value/ValueFactory.java +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -21,12 +21,6 @@ public class ValueFactory { - public enum ConversionType { - Strict, - Nullable, - Empty, - } - private final Map converterMap = new HashMap<>(); private ConversionHint defaultHint = ConversionHint.Strict; 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/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/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/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/tests/src/test/java/io/jooby/i2557/Issue2557.java b/tests/src/test/java/io/jooby/i2557/Issue2557.java index f5c00f1228..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", From 379f8fe8f9a14b404f97848ccad0529eee79258d Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 21 Jun 2025 12:05:53 -0300 Subject: [PATCH 12/60] value api: javadoc for ValueFactory --- jooby/src/main/java/io/jooby/QueryString.java | 16 --- .../java/io/jooby/internal/SingleValue.java | 2 +- .../java/io/jooby/value/ValueFactory.java | 118 ++++++++++++++++-- 3 files changed, 112 insertions(+), 24 deletions(-) diff --git a/jooby/src/main/java/io/jooby/QueryString.java b/jooby/src/main/java/io/jooby/QueryString.java index f9c5bfe8e9..02a91bbe73 100644 --- a/jooby/src/main/java/io/jooby/QueryString.java +++ b/jooby/src/main/java/io/jooby/QueryString.java @@ -26,22 +26,6 @@ public interface QueryString extends Value { */ 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. - * - *

{@code
-   * var factory = ctx.getValueFactory();
-   * var nullable = factory.convert(MyBean.class, ctx.query(), ConversionHint.Nullable);
-   * }
- * - * @param type Type to convert. - * @return Non null result. - * @param Type result. - */ - @Nullable T toNullable(@NonNull Class type); - /** * 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 diff --git a/jooby/src/main/java/io/jooby/internal/SingleValue.java b/jooby/src/main/java/io/jooby/internal/SingleValue.java index 5137bf5632..306b44db3f 100644 --- a/jooby/src/main/java/io/jooby/internal/SingleValue.java +++ b/jooby/src/main/java/io/jooby/internal/SingleValue.java @@ -24,7 +24,7 @@ public class SingleValue implements Value { private final String name; - private String value; + private final String value; public SingleValue(ValueFactory factory, String name, String value) { this.factory = factory; diff --git a/jooby/src/main/java/io/jooby/value/ValueFactory.java b/jooby/src/main/java/io/jooby/value/ValueFactory.java index e5a3bff03a..8b407d692f 100644 --- a/jooby/src/main/java/io/jooby/value/ValueFactory.java +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -14,11 +14,27 @@ 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.converter.ReflectiveBeanConverter; import io.jooby.internal.converter.StandardConverter; 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<>(); @@ -29,42 +45,134 @@ public class ValueFactory { 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; } - public T convert(@NonNull Type type, @NonNull Value value) { + /** + * 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") - public T convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { + 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. @@ -95,11 +203,7 @@ public T convert(@NonNull Type type, @NonNull Value value, @NonNull Conversi } } // anything else fallback to reflective - var result = (T) fallback.convert(type, value, hint); - if (result == null && hint == ConversionHint.Strict) { - throw new TypeMismatchException(value.name(), type); - } - return result; + return (T) fallback.convert(type, value, hint); } } From bcc8ae435297145724e0a6ef87fe3a6fbeadd85f Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 21 Jun 2025 12:41:19 -0300 Subject: [PATCH 13/60] value api: move reflective and standard converter to public package - javadoc for reflective converter --- .../ReflectiveBeanConverter.java | 122 ++++++++++++------ .../StandardConverter.java | 86 +++--------- .../java/io/jooby/value/ValueFactory.java | 2 - .../jooby/internal/DurationConverterTest.java | 2 +- 4 files changed, 100 insertions(+), 112 deletions(-) rename jooby/src/main/java/io/jooby/{internal/converter => value}/ReflectiveBeanConverter.java (69%) rename jooby/src/main/java/io/jooby/{internal/converter => value}/StandardConverter.java (89%) 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 69% rename from jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java rename to jooby/src/main/java/io/jooby/value/ReflectiveBeanConverter.java index d72fdc3699..933051068a 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/ReflectiveBeanConverter.java +++ b/jooby/src/main/java/io/jooby/value/ReflectiveBeanConverter.java @@ -3,7 +3,7 @@ * 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; @@ -18,10 +18,8 @@ import io.jooby.Usage; import io.jooby.exception.BadRequestException; import io.jooby.exception.ProvisioningException; +import io.jooby.exception.TypeMismatchException; import io.jooby.internal.reflect.$Types; -import io.jooby.value.ConversionHint; -import io.jooby.value.Converter; -import io.jooby.value.Value; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -29,6 +27,8 @@ * 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 */ @@ -47,18 +47,76 @@ public void invoke(MethodHandles.Lookup lookup, Object instance) throws Throwabl 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; } + /** + * 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) { - return convert(value, $Types.parameterizedType0(type), hint == ConversionHint.Empty); - } - - private Object convert(@NonNull Value node, @NonNull Class type, boolean allowEmptyBean) { + 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); + 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) { @@ -66,34 +124,12 @@ private Object convert(@NonNull Value node, @NonNull Class type, boolean allowEm } } - private Object newInstance(Class type, Value node, boolean allowEmptyBean) throws Throwable { - var constructors = type.getConstructors(); - Set state = new HashSet<>(); - Constructor constructor; - if (constructors.length == 0) { - constructor = type.getDeclaredConstructor(); - } else { - constructor = selectConstructor(constructors); - } - var args = inject(node, constructor, state::add); - var setters = setters(type, node, state); - if (!allowEmptyBean && state.stream().allMatch(Value::isMissing)) { - return null; - } - var handle = lookup.unreflectConstructor(constructor); - var instance = handle.invokeWithArguments(args); - for (var setter : setters) { - setter.invoke(lookup, instance); - } - return instance; - } - - 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; + Constructor injectConstructor = null; + Constructor defaultConstructor = null; for (var constructor : constructors) { if (Modifier.isPublic(constructor.getModifiers())) { var inject = constructor.getAnnotation(Inject.class); @@ -124,9 +160,8 @@ public static List inject(Value scope, Executable method, Consumer(parameters.length); - for (int i = 0; i < parameters.length; i++) { - var parameter = parameters[i]; - var name = paramName(parameter); + for (var parameter : parameters) { + var name = parameterName(parameter); var param = scope.get(name); var arg = value(parameter, scope, param); if (arg == null) { @@ -139,9 +174,9 @@ public static List inject(Value scope, Executable method, Consumer 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()) { @@ -169,7 +204,7 @@ private static Set names(Value node) { return names; } - private static List setters(Class type, Value 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)) { @@ -194,6 +229,7 @@ private static List setters(Class type, Value node, Set nodes) { return result; } + @SuppressWarnings("unchecked") private static Object value(Parameter parameter, Value node, Value value) { try { if (isFileUpload(node, parameter)) { @@ -263,7 +299,7 @@ private static boolean isFileUpload(Value node, Parameter parameter) { || isFileUpload($Types.parameterizedType0(parameter.getParameterizedType())); } - private static boolean isFileUpload(Class type) { + private static boolean isFileUpload(Class type) { return FileUpload.class == type; } diff --git a/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java b/jooby/src/main/java/io/jooby/value/StandardConverter.java similarity index 89% rename from jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java rename to jooby/src/main/java/io/jooby/value/StandardConverter.java index 8c00cd7a78..595853257b 100644 --- a/jooby/src/main/java/io/jooby/internal/converter/StandardConverter.java +++ b/jooby/src/main/java/io/jooby/value/StandardConverter.java @@ -3,7 +3,7 @@ * 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; @@ -30,10 +30,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.SneakyThrows; import io.jooby.StatusCode; -import io.jooby.value.ConversionHint; -import io.jooby.value.Converter; -import io.jooby.value.Value; -import io.jooby.value.ValueFactory; public enum StandardConverter implements Converter { String { @@ -51,21 +47,14 @@ public Object convert(@NonNull Type type, @NonNull Value value, @NonNull Convers @Override protected void add(ValueFactory factory) { factory.put(int.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { - return value.intValue(); - } - }, - IntNullable { - @Override - protected void add(ValueFactory factory) { 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(); } }, @@ -73,21 +62,14 @@ public Object convert(@NonNull Type type, @NonNull Value value, @NonNull Convers @Override protected void add(ValueFactory factory) { factory.put(long.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { - return value.longValue(); - } - }, - LongNullable { - @Override - protected void add(ValueFactory factory) { 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(); } }, @@ -95,21 +77,14 @@ public Object convert(@NonNull Type type, @NonNull Value value, @NonNull Convers @Override protected void add(ValueFactory factory) { factory.put(float.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { - return value.floatValue(); - } - }, - FloatNullable { - @Override - protected void add(ValueFactory factory) { 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(); } }, @@ -117,21 +92,14 @@ public Object convert(@NonNull Type type, @NonNull Value value, @NonNull Convers @Override protected void add(ValueFactory factory) { factory.put(double.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { - return value.doubleValue(); - } - }, - DoubleNullable { - @Override - protected void add(ValueFactory factory) { 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(); } }, @@ -139,21 +107,14 @@ public Object convert(@NonNull Type type, @NonNull Value value, @NonNull Convers @Override protected void add(ValueFactory factory) { factory.put(boolean.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { - return value.booleanValue(); - } - }, - BooleanNullable { - @Override - protected void add(ValueFactory factory) { 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(); } }, @@ -161,21 +122,14 @@ public Object convert(@NonNull Type type, @NonNull Value value, @NonNull Convers @Override protected void add(ValueFactory factory) { factory.put(byte.class, this); - } - - @Override - public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { - return value.byteValue(); - } - }, - ByteNullable { - @Override - protected void add(ValueFactory factory) { 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(); } }, @@ -209,7 +163,7 @@ protected void add(ValueFactory factory) { @Override public Object convert(@NonNull Type type, @NonNull Value value, @NonNull ConversionHint hint) { - String charset = value.value(); + var charset = value.value(); return switch (charset.toLowerCase()) { case "utf-8" -> StandardCharsets.UTF_8; case "us-ascii" -> StandardCharsets.US_ASCII; diff --git a/jooby/src/main/java/io/jooby/value/ValueFactory.java b/jooby/src/main/java/io/jooby/value/ValueFactory.java index 8b407d692f..85bbe75761 100644 --- a/jooby/src/main/java/io/jooby/value/ValueFactory.java +++ b/jooby/src/main/java/io/jooby/value/ValueFactory.java @@ -16,8 +16,6 @@ import io.jooby.SneakyThrows; import io.jooby.exception.ProvisioningException; import io.jooby.exception.TypeMismatchException; -import io.jooby.internal.converter.ReflectiveBeanConverter; -import io.jooby.internal.converter.StandardConverter; import io.jooby.internal.reflect.$Types; /** diff --git a/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java b/jooby/src/test/java/io/jooby/internal/DurationConverterTest.java index ed3fbc1937..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,8 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import io.jooby.internal.converter.StandardConverter; import io.jooby.value.ConversionHint; +import io.jooby.value.StandardConverter; import io.jooby.value.Value; public class DurationConverterTest { From 94f37c0edc9fd7e9e05cf12166d9debfc52513c6 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 26 Jun 2025 13:04:17 -0300 Subject: [PATCH 14/60] output api: - introduce more simply buffer/output api under `io.jooby.output` - buffer: simplify buffer API fix #3604 --- jooby/src/main/java/io/jooby/Context.java | 12 ++ .../main/java/io/jooby/DefaultContext.java | 11 +- .../main/java/io/jooby/ForwardingContext.java | 13 ++ jooby/src/main/java/io/jooby/Jooby.java | 12 ++ .../main/java/io/jooby/MessageEncoder.java | 12 +- jooby/src/main/java/io/jooby/Router.java | 5 + jooby/src/main/java/io/jooby/Sender.java | 3 + .../main/java/io/jooby/ServerSentMessage.java | 99 +++++++-- .../main/java/io/jooby/TemplateEngine.java | 6 +- jooby/src/main/java/io/jooby/WebSocket.java | 13 ++ .../java/io/jooby/internal/HeadContext.java | 14 ++ .../io/jooby/internal/HttpMessageEncoder.java | 71 +++---- .../java/io/jooby/internal/RouterImpl.java | 15 ++ .../io/jooby/internal/WebSocketSender.java | 11 + .../internal/handler/ChunkedSubscriber.java | 15 +- .../jooby/output/ByteBufferChunkedOutput.java | 105 +++++++++ .../jooby/output/ByteBufferOutputFactory.java | 35 +++ .../io/jooby/output/ByteBufferOutputImpl.java | 197 +++++++++++++++++ .../java/io/jooby/output/ChunkedOutput.java | 13 ++ .../src/main/java/io/jooby/output/Output.java | 201 ++++++++++++++++++ .../java/io/jooby/output/OutputFactory.java | 34 +++ .../io/jooby/output/OutputOutputStream.java | 56 +++++ .../java/io/jooby/output/OutputWriter.java | 71 +++++++ .../java/io/jooby/output/WrappedOutput.java | 74 +++++++ jooby/src/main/java/module-info.java | 1 + jooby/src/test/java/io/jooby/Issue3607.java | 7 +- .../java/io/jooby/ServerSentMessageTest.java | 50 ++--- .../io/jooby/output/BufferedOutputTest.java | 115 ++++++++++ .../jooby/avaje/jsonb/AvajeJsonbModule.java | 8 +- .../avaje/jsonb/DataBufferJsonOutput.java | 12 +- .../avaje/jsonb/AvajeJsonbModuleTest.java | 2 +- .../freemarker/FreemarkerTemplateEngine.java | 6 +- .../freemarker/FreemarkerModuleTest.java | 12 +- .../main/java/io/jooby/gson/GsonModule.java | 6 +- .../test/java/io/jooby/gson/Issue3434.java | 8 +- .../handlebars/HandlebarsTemplateEngine.java | 6 +- .../handlebars/HandlebarsModuleTest.java | 4 +- .../java/io/jooby/jackson/JacksonModule.java | 6 +- .../jooby/jackson/JacksonJsonModuleTest.java | 10 +- .../jooby/internal/jetty/JettyCallbacks.java | 48 +++++ .../io/jooby/internal/jetty/JettyContext.java | 7 + .../io/jooby/internal/jetty/JettySender.java | 8 + .../jetty/JettyServerSentEmitter.java | 4 +- .../jooby/internal/jetty/JettyWebSocket.java | 13 +- .../jetty/WebSocketDataBufferCallback.java | 17 +- .../jstachio/JStachioMessageEncoder.java | 11 +- .../jooby/internal/jte/DataBufferOutput.java | 6 +- .../jooby/internal/jte/JteModelEncoder.java | 6 +- .../java/io/jooby/jte/JteTemplateEngine.java | 6 +- .../internal/jte/DataBufferOutputTest.java | 20 +- .../src/test/java/io/jooby/jte/Issue3599.java | 20 +- .../src/test/java/io/jooby/jte/Issue3602.java | 12 +- .../io/jooby/internal/netty/NettyContext.java | 7 + .../io/jooby/internal/netty/NettySender.java | 17 ++ .../netty/NettyServerSentEmitter.java | 15 +- .../jooby/internal/netty/NettyWebSocket.java | 20 ++ .../main/java/io/jooby/netty/NettyServer.java | 3 + .../netty/output/NettyBufferedOutput.java | 81 +++++++ .../netty/output/NettyChunkedOutput.java | 90 ++++++++ .../io/jooby/netty/output/NettyOutput.java | 36 ++++ .../netty/output/NettyOutputFactory.java | 69 ++++++ .../io/jooby/pebble/PebbleTemplateEngine.java | 6 +- .../io/jooby/pebble/PebbleModuleTest.java | 14 +- .../io/jooby/rocker/DataBufferOutput.java | 16 +- .../io/jooby/rocker/RockerMessageEncoder.java | 4 +- .../java/io/jooby/rocker/RockerModule.java | 2 +- .../main/java/io/jooby/test/MockContext.java | 25 +++ .../java/io/jooby/test/MockWebSocket.java | 11 + .../thymeleaf/ThymeleafTemplateEngine.java | 11 +- .../internal/undertow/UndertowContext.java | 7 + .../undertow/UndertowOutputCallback.java | 44 ++++ .../internal/undertow/UndertowSender.java | 7 + .../UndertowServerSentConnection.java | 10 +- .../internal/undertow/UndertowWebSocket.java | 20 +- .../java/io/jooby/yasson/YassonModule.java | 11 +- .../io/jooby/yasson/YassonModuleTest.java | 6 +- .../test/java/io/jooby/i2613/Issue2613.java | 7 +- .../test/java/io/jooby/test/FeaturedTest.java | 12 +- .../src/test/java/io/jooby/test/MvcTest.java | 4 +- 79 files changed, 1826 insertions(+), 258 deletions(-) create mode 100644 jooby/src/main/java/io/jooby/output/ByteBufferChunkedOutput.java create mode 100644 jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java create mode 100644 jooby/src/main/java/io/jooby/output/ByteBufferOutputImpl.java create mode 100644 jooby/src/main/java/io/jooby/output/ChunkedOutput.java create mode 100644 jooby/src/main/java/io/jooby/output/Output.java create mode 100644 jooby/src/main/java/io/jooby/output/OutputFactory.java create mode 100644 jooby/src/main/java/io/jooby/output/OutputOutputStream.java create mode 100644 jooby/src/main/java/io/jooby/output/OutputWriter.java create mode 100644 jooby/src/main/java/io/jooby/output/WrappedOutput.java create mode 100644 jooby/src/test/java/io/jooby/output/BufferedOutputTest.java create mode 100644 modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyBufferedOutput.java create mode 100644 modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyChunkedOutput.java create mode 100644 modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutput.java create mode 100644 modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java create mode 100644 modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowOutputCallback.java diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java index b281ae7cf1..8c44141ef3 100644 --- a/jooby/src/main/java/io/jooby/Context.java +++ b/jooby/src/main/java/io/jooby/Context.java @@ -36,6 +36,8 @@ 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; @@ -147,6 +149,8 @@ private static Selector single() { DataBufferFactory getBufferFactory(); + OutputFactory getOutputFactory(); + /** * Forward executing to another route. We use the given path to find a matching route. * @@ -1314,6 +1318,14 @@ Context responseWriter( */ Context send(@NonNull DataBuffer data); + /** + * Send response data. + * + * @param output Output. + * @return This context. + */ + Context send(@NonNull Output output); + /** * Send response data. * diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index f706568e78..0cd092c1ea 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -36,6 +36,8 @@ import io.jooby.internal.MissingValue; import io.jooby.internal.SingleValue; import io.jooby.internal.UrlParser; +import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.OutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -478,8 +480,8 @@ default Context setResponseCode(@NonNull StatusCode statusCode) { @Override 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()) { @@ -656,4 +658,9 @@ default Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { default DataBufferFactory getBufferFactory() { return getRouter().getBufferFactory(); } + + @Override + default OutputFactory getOutputFactory() { + return new ByteBufferOutputFactory(); + } } diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index 6da79b3f44..304829aaef 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -26,6 +26,8 @@ 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; @@ -675,6 +677,11 @@ public DataBufferFactory getBufferFactory() { return ctx.getBufferFactory(); } + @Override + public OutputFactory getOutputFactory() { + return ctx.getOutputFactory(); + } + @Override public FlashMap flash() { return ctx.flash(); @@ -1234,6 +1241,12 @@ public Context send(@NonNull DataBuffer data) { return this; } + @Override + public Context send(@NonNull Output output) { + ctx.send(output); + return this; + } + @Override public Context send(@NonNull byte[]... data) { ctx.send(data); diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 28422b99dc..e687ce6ba7 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -52,6 +52,7 @@ 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 io.jooby.value.ValueFactory; import jakarta.inject.Provider; @@ -930,6 +931,17 @@ public Jooby setValueFactory(@NonNull ValueFactory valueFactory) { return this; } + @NonNull @Override + public OutputFactory getOutputFactory() { + return router.getOutputFactory(); + } + + @NonNull @Override + public Jooby setOutputFactory(@NonNull OutputFactory outputFactory) { + router.setOutputFactory(outputFactory); + return this; + } + @NonNull @Override public Jooby setHiddenMethod(@NonNull Function> provider) { router.setHiddenMethod(provider); diff --git a/jooby/src/main/java/io/jooby/MessageEncoder.java b/jooby/src/main/java/io/jooby/MessageEncoder.java index cb2ef858c5..b79ef124dd 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()); }; @@ -35,8 +33,8 @@ public interface MessageEncoder { * * @param ctx Web context. * @param value Value to render. - * @return Value as byte array or null if given object isn't supported it. + * @return Value as a byte array 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/Router.java b/jooby/src/main/java/io/jooby/Router.java index 12d6de0730..6bbbdca5a0 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -37,6 +37,7 @@ import io.jooby.exception.MissingValueException; import io.jooby.handler.AssetHandler; import io.jooby.handler.AssetSource; +import io.jooby.output.OutputFactory; import io.jooby.value.ValueFactory; import jakarta.inject.Provider; @@ -538,6 +539,10 @@ default Object execute(@NonNull Context context) { @NonNull DataBufferFactory getBufferFactory(); + @NonNull OutputFactory getOutputFactory(); + + @NonNull Router setOutputFactory(@NonNull OutputFactory outputFactory); + @NonNull Router setBufferFactory(@NonNull DataBufferFactory bufferFactory); /** diff --git a/jooby/src/main/java/io/jooby/Sender.java b/jooby/src/main/java/io/jooby/Sender.java index 2461268bf6..54c015bd27 100644 --- a/jooby/src/main/java/io/jooby/Sender.java +++ b/jooby/src/main/java/io/jooby/Sender.java @@ -11,6 +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; /** * Non-blocking sender. Reactive responses uses this class to send partial data in non-blocking @@ -96,6 +97,8 @@ interface Callback { @NonNull Sender write(@NonNull DataBuffer data, @NonNull Callback callback); + @NonNull Sender write(@NonNull Output output, @NonNull Callback callback); + /** Close the sender. */ void close(); } diff --git a/jooby/src/main/java/io/jooby/ServerSentMessage.java b/jooby/src/main/java/io/jooby/ServerSentMessage.java index 041a45e9b5..4e09fe5932 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.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; 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.*; /** * 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.newBufferedOutput(); 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 = message.split(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,68 @@ public ServerSentMessage(@NonNull Object data) { throw SneakyThrows.propagate(x); } } + + class ServerSentMessageOutput implements Output { + private final Output delegate; + + public ServerSentMessageOutput(Output delegate) { + this.delegate = delegate; + } + + @Override + public ByteBuffer asByteBuffer() { + return delegate.asByteBuffer(); + } + + @Override + public String asString(@NonNull Charset charset) { + return delegate.asString(charset); + } + + @Override + public void accept(SneakyThrows.Consumer consumer) { + delegate.accept(consumer); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public Output write(byte b) { + return write(new byte[] {b}, 0, 1); + } + + @Override + public Output write(byte[] source) { + return write(ByteBuffer.wrap(source)); + } + + @Override + public Output write(byte[] source, int offset, int length) { + var begin = offset; + var len = offset + length; + for (int i = offset; i < len; i++) { + var ch = source[i]; + if (ch == '\n') { + delegate.write(source, begin, i + 1); + if (i < len - 1) { + delegate.write(DATA); + } + begin = i + 1; + } + } + if (begin < len) { + delegate.write(source, begin, len); + } + return this; + } + + @Override + public void send(Context ctx) {} + + @Override + public void close() throws IOException {} + } } 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/WebSocket.java b/jooby/src/main/java/io/jooby/WebSocket.java index ca4f1f9a96..e8bc040ad9 100644 --- a/jooby/src/main/java/io/jooby/WebSocket.java +++ b/jooby/src/main/java/io/jooby/WebSocket.java @@ -12,6 +12,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: @@ -247,6 +248,12 @@ interface WriteCallback { @NonNull WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callback); + default @NonNull WebSocket send(@NonNull Output message) { + return send(message, WriteCallback.NOOP); + } + + @NonNull WebSocket send(@NonNull Output message, @NonNull WriteCallback callback); + /** * Send a binary message to client. * @@ -299,6 +306,12 @@ interface WriteCallback { @NonNull WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback); + default @NonNull WebSocket sendBinary(@NonNull Output message) { + return sendBinary(message, WriteCallback.NOOP); + } + + @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/internal/HeadContext.java b/jooby/src/main/java/io/jooby/internal/HeadContext.java index dd9a0f3ede..0b666556d8 100644 --- a/jooby/src/main/java/io/jooby/internal/HeadContext.java +++ b/jooby/src/main/java/io/jooby/internal/HeadContext.java @@ -20,6 +20,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 { /** @@ -73,6 +74,14 @@ public Context send(@NonNull DataBuffer data) { return this; } + @NonNull @Override + public Context send(@NonNull Output output) { + ctx.setResponseLength(output.size()); + checkSizeHeaders(); + ctx.send(StatusCode.OK); + return this; + } + @NonNull @Override public Context send(@NonNull FileChannel file) { try { @@ -190,6 +199,11 @@ public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { return this; } + @NonNull @Override + public Sender write(@NonNull Output output, @NonNull Callback callback) { + return this; + } + @Override public void close() {} } diff --git a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java index e6a7e8ca50..5fc8de2670 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,67 @@ 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); + if (value instanceof ByteBuffer buffer) { + ctx.send(buffer); return null; } 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/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index c44241e39c..a5eb82cbfb 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -47,6 +47,8 @@ import io.jooby.exception.StatusCodeException; import io.jooby.internal.handler.ServerSentEventHandler; import io.jooby.internal.handler.WebSocketHandler; +import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.OutputFactory; import io.jooby.problem.ProblemDetailsHandler; import io.jooby.value.ValueFactory; import jakarta.inject.Provider; @@ -173,6 +175,8 @@ public Stack executor(Executor executor) { private ValueFactory valueFactory = new ValueFactory(); + private OutputFactory outputFactory = new ByteBufferOutputFactory(); + public RouterImpl() { stack.addLast(new Stack(chi, null)); } @@ -473,6 +477,17 @@ public Router setValueFactory(@NonNull ValueFactory valueFactory) { return this; } + @NonNull @Override + public OutputFactory getOutputFactory() { + return outputFactory; + } + + @NonNull @Override + public Router setOutputFactory(@NonNull OutputFactory outputFactory) { + this.outputFactory = outputFactory; + return this; + } + @NonNull @Override public Route ws(@NonNull String pattern, @NonNull WebSocket.Initializer handler) { return route(WS, pattern, new WebSocketHandler(handler)).setHandle(handler); diff --git a/jooby/src/main/java/io/jooby/internal/WebSocketSender.java b/jooby/src/main/java/io/jooby/internal/WebSocketSender.java index fe98fdc1cc..1ed89edf65 100644 --- a/jooby/src/main/java/io/jooby/internal/WebSocketSender.java +++ b/jooby/src/main/java/io/jooby/internal/WebSocketSender.java @@ -20,6 +20,7 @@ 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 { @@ -78,6 +79,16 @@ public Context send(@NonNull DataBuffer data) { return this; } + @Override + public Context send(@NonNull Output output) { + if (binary) { + ws.sendBinary(output, callback); + } else { + ws.send(output, callback); + } + return this; + } + @NonNull @Override public Context render(@NonNull Object value) { DefaultContext.super.render(value); 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..80fc654f76 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().newChunkedOutput(); buffer.write(c); - buffer.write(data); + data.accept(buffer::write); return buffer; } diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferChunkedOutput.java b/jooby/src/main/java/io/jooby/output/ByteBufferChunkedOutput.java new file mode 100644 index 0000000000..ba54e24344 --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/ByteBufferChunkedOutput.java @@ -0,0 +1,105 @@ +/* + * 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.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.SneakyThrows; + +class ByteBufferChunkedOutput implements ChunkedOutput { + private final List chunks = new ArrayList<>(); + private int size = 0; + private int bufferSizeHint = BUFFER_SIZE; + + @Override + public List getChunks() { + return chunks; + } + + @Override + public int size() { + return size; + } + + @Override + public Output write(byte b) { + addChunk(ByteBuffer.wrap(new byte[] {b})); + return this; + } + + @Override + public Output write(byte[] source) { + addChunk(ByteBuffer.wrap(source)); + return this; + } + + @Override + public Output write(byte[] source, int offset, int length) { + addChunk(ByteBuffer.wrap(source, offset, length)); + return this; + } + + @Override + public void close() throws IOException { + // NOOP + } + + /** + * Expensive operation. + * + * @return A byte buffer. + */ + @Override + public ByteBuffer asByteBuffer() { + var buf = ByteBuffer.allocate(size); + chunks.forEach(buf::put); + buf.flip(); + return buf; + } + + @Override + public String asString(@NonNull Charset charset) { + var sb = new StringBuilder(); + chunks.forEach(bytes -> sb.append(charset.decode(bytes))); + return sb.toString(); + } + + @Override + public void accept(SneakyThrows.Consumer consumer) { + chunks.forEach(consumer); + } + + @Override + public int bufferSizeHint() { + return bufferSizeHint; + } + + @Override + public String toString() { + return asString(StandardCharsets.UTF_8); + } + + @Override + public void send(Context ctx) { + ctx.send(chunks.toArray(new ByteBuffer[0])); + } + + private void addChunk(ByteBuffer chunk) { + chunks.add(chunk); + int length = chunk.remaining(); + size += length; + if (bufferSizeHint < length) { + bufferSizeHint = length; + } + } +} diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java b/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java new file mode 100644 index 0000000000..a988e0539b --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java @@ -0,0 +1,35 @@ +/* + * 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; + +public class ByteBufferOutputFactory implements OutputFactory { + @Override + public Output newBufferedOutput(int size) { + return new ByteBufferOutputImpl(size); + } + + @Override + public ChunkedOutput newChunkedOutput() { + return new ByteBufferChunkedOutput(); + } + + @Override + public Output wrap(ByteBuffer buffer) { + return new WrappedOutput(buffer); + } + + @Override + public Output wrap(byte[] bytes) { + return new WrappedOutput(bytes); + } + + @Override + public Output wrap(byte[] bytes, int offset, int length) { + return new WrappedOutput(bytes, offset, length); + } +} diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferOutputImpl.java b/jooby/src/main/java/io/jooby/output/ByteBufferOutputImpl.java new file mode 100644 index 0000000000..a3623c92e4 --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/ByteBufferOutputImpl.java @@ -0,0 +1,197 @@ +/* + * 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.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.SneakyThrows; + +class ByteBufferOutputImpl implements Output { + private static final int MAX_CAPACITY = Integer.MAX_VALUE; + + private static final int CAPACITY_THRESHOLD = 1024 * 1024 * 4; + + private ByteBuffer buffer; + + private int capacity; + + private int readPosition; + + private int writePosition; + + public ByteBufferOutputImpl(boolean direct, int capacity) { + this.buffer = allocate(capacity, direct); + this.capacity = this.buffer.remaining(); + } + + public ByteBufferOutputImpl(boolean direct) { + this(direct, BUFFER_SIZE); + } + + public ByteBufferOutputImpl(int bufferSize) { + this(false, bufferSize); + } + + public ByteBufferOutputImpl() { + this(BUFFER_SIZE); + } + + @Override + public int size() { + return this.writePosition - this.readPosition; + } + + private void ensureWritable(int length) { + if (length > writableByteCount()) { + int newCapacity = calculateCapacity(this.writePosition + length); + setCapacity(newCapacity); + } + } + + @Override + public void accept(SneakyThrows.Consumer consumer) { + consumer.accept(asByteBuffer()); + } + + private int writableByteCount() { + return this.capacity - this.writePosition; + } + + @Override + public ByteBuffer asByteBuffer() { + return this.buffer.duplicate().position(this.readPosition).limit(this.writePosition); + } + + @Override + public String asString(@NonNull Charset charset) { + return charset.decode(asByteBuffer()).toString(); + } + + @Override + public Output write(byte b) { + ensureWritable(1); + this.buffer.put(this.writePosition, b); + this.writePosition += 1; + return this; + } + + @Override + public Output write(byte[] source) { + return write(source, 0, source.length); + } + + @Override + public Output 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 Output 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 void close() throws IOException { + this.buffer.clear(); + } + + @Override + public void send(Context ctx) { + ctx.send(asByteBuffer()); + } + + @Override + public String toString() { + return asString(StandardCharsets.UTF_8); + } + + /** 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 Math.min(newCapacity, MAX_CAPACITY); + } + } + + 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.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(); + setNativeBuffer(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; + } + setNativeBuffer(newBuffer); + } + } + + private void setNativeBuffer(ByteBuffer buffer) { + this.buffer = buffer; + this.capacity = buffer.capacity(); + } + + 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/ChunkedOutput.java b/jooby/src/main/java/io/jooby/output/ChunkedOutput.java new file mode 100644 index 0000000000..f9a4e4622a --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/ChunkedOutput.java @@ -0,0 +1,13 @@ +/* + * 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.List; + +public interface ChunkedOutput extends Output { + List getChunks(); +} 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..72f473de49 --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/Output.java @@ -0,0 +1,201 @@ +/* + * 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.Closeable; +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 java.util.ArrayList; +import java.util.Iterator; +import java.util.function.IntPredicate; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.SneakyThrows; + +public interface Output extends Closeable { + /** Default buffer size: 4k. */ + int BUFFER_SIZE = 4096; + + default OutputStream asOutputStream() { + return new OutputOutputStream(this); + } + + default Writer asWriter() { + return asWriter(StandardCharsets.UTF_8); + } + + default Writer asWriter(@NonNull Charset charset) { + return new OutputWriter(this, charset); + } + + ByteBuffer asByteBuffer(); + + String asString(@NonNull Charset charset); + + default void toByteBuffer(ByteBuffer dest) { + toByteBuffer(0, dest, dest.position(), size()); + } + + default Iterator split(IntPredicate predicate) { + // TODO: fix me for chunks + var buffer = asByteBuffer(); + var chunks = new ArrayList(); + var offset = 0; + for (int i = 0; i < buffer.remaining(); i++) { + var b = buffer.get(i); + if (predicate.test(b)) { + chunks.add(buffer.duplicate().position(offset).limit(i + 1)); + offset = i + 1; + } + } + if (offset < buffer.remaining()) { + chunks.add(buffer.duplicate().position(offset)); + } + return chunks.iterator(); + } + + /** + * 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 + */ + default void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { + dest = dest.duplicate().clear(); + dest.put(destPos, asByteBuffer(), srcPos, length); + } + + void accept(SneakyThrows.Consumer consumer); + + default Iterator iterator() { + var list = new ArrayList(); + accept(list::add); + return list.iterator(); + } + + /** + * Total size in number of bytes of the output. + * + * @return size + */ + int size(); + + /** + * The recommend buffer size to use for extracting with + * + * @return buffer size to use which by default is {@link #size()}. + */ + default int bufferSizeHint() { + return size(); + } + + /** + * Write a single byte into this buffer at the current writing position. + * + * @param b the byte to be written + * @return this output + */ + Output 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 + */ + Output 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 + */ + Output 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 Output 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 Output 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 Output 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); + } + } + + default Output write(@NonNull CharBuffer source, @NonNull Charset charset) { + if (!source.isEmpty()) { + return write(charset.encode(source)); + } + return this; + } + + static Output wrap(ByteBuffer buffer) { + return new WrappedOutput(buffer); + } + + static Output wrap(byte[] bytes) { + return new WrappedOutput(bytes); + } + + static Output wrap(byte[] bytes, int offset, int length) { + return new WrappedOutput(bytes, offset, length); + } + + static Output wrap(String value) { + return wrap(value, StandardCharsets.UTF_8); + } + + static Output wrap(String value, Charset charset) { + return wrap(value.getBytes(charset)); + } + + 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..e7b55814fe --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/OutputFactory.java @@ -0,0 +1,34 @@ +/* + * 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; + +public interface OutputFactory { + Output newBufferedOutput(int size); + + default Output newBufferedOutput() { + return newBufferedOutput(Output.BUFFER_SIZE); + } + + default Output wrap(String value) { + return wrap(value, StandardCharsets.UTF_8); + } + + default Output wrap(String value, Charset charset) { + return wrap(value.getBytes(charset)); + } + + Output wrap(ByteBuffer buffer); + + Output wrap(byte[] bytes); + + Output wrap(byte[] bytes, int offset, int length); + + ChunkedOutput newChunkedOutput(); +} diff --git a/jooby/src/main/java/io/jooby/output/OutputOutputStream.java b/jooby/src/main/java/io/jooby/output/OutputOutputStream.java new file mode 100644 index 0000000000..aac1aae899 --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/OutputOutputStream.java @@ -0,0 +1,56 @@ +/* + * 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.IOException; +import java.io.OutputStream; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * An {@link OutputStream} that writes to a {@link io.jooby.output.Output}. + * + * @see io.jooby.output.Output#asOutputStream() + */ +final class OutputOutputStream extends OutputStream { + + private final Output output; + + private boolean closed; + + public OutputOutputStream(@NonNull Output dataBuffer) { + this.output = dataBuffer; + } + + @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; + output.close(); + } + + private void checkClosed() throws IOException { + if (this.closed) { + throw new IOException("OutputStream is closed"); + } + } +} diff --git a/jooby/src/main/java/io/jooby/output/OutputWriter.java b/jooby/src/main/java/io/jooby/output/OutputWriter.java new file mode 100644 index 0000000000..8e48dc4463 --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/OutputWriter.java @@ -0,0 +1,71 @@ +/* + * 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.IOException; +import java.io.Writer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +import edu.umd.cs.findbugs.annotations.NonNull; + +class OutputWriter extends Writer { + private final Output output; + private final Charset charset; + private boolean closed; + + public OutputWriter(@NonNull Output output, @NonNull Charset charset) { + this.output = output; + this.charset = charset; + } + + @Override + public void write(int c) throws IOException { + checkClosed(); + super.write(c); + } + + @Override + public void write(@NonNull char[] source) throws IOException { + write(source, 0, source.length); + } + + @Override + public void write(@NonNull char[] source, int off, int len) throws IOException { + checkClosed(); + output.write(CharBuffer.wrap(source, off, len), charset); + } + + @Override + public void write(@NonNull String source) throws IOException { + checkClosed(); + output.write(source, charset); + } + + @Override + public void write(@NonNull String source, int off, int len) throws IOException { + checkClosed(); + output.write(CharBuffer.wrap(source, off, off + len), charset); + } + + @Override + public void flush() throws IOException {} + + @Override + public void close() throws IOException { + if (this.closed) { + return; + } + this.closed = true; + output.close(); + } + + private void checkClosed() throws IOException { + if (this.closed) { + throw new IOException("Writer is closed"); + } + } +} diff --git a/jooby/src/main/java/io/jooby/output/WrappedOutput.java b/jooby/src/main/java/io/jooby/output/WrappedOutput.java new file mode 100644 index 0000000000..e2c920387f --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/WrappedOutput.java @@ -0,0 +1,74 @@ +/* + * 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.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.SneakyThrows; + +class WrappedOutput implements Output { + + private final ByteBuffer buffer; + + public WrappedOutput(byte[] source, int offset, int length) { + this(ByteBuffer.wrap(source, offset, length)); + } + + public WrappedOutput(byte[] source) { + this(source, 0, source.length); + } + + public WrappedOutput(ByteBuffer buffer) { + this.buffer = buffer; + } + + @Override + public Output write(byte b) { + throw new UnsupportedOperationException(); + } + + @Override + public Output write(byte[] source) { + throw new UnsupportedOperationException(); + } + + @Override + public Output write(byte[] source, int offset, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public void close() throws IOException {} + + @Override + public int size() { + return buffer.remaining(); + } + + @Override + public void accept(SneakyThrows.Consumer consumer) { + consumer.accept(buffer); + } + + @Override + public ByteBuffer asByteBuffer() { + return buffer.duplicate(); + } + + @Override + public String asString(@NonNull Charset charset) { + return charset.decode(asByteBuffer()).toString(); + } + + @Override + public void send(Context ctx) { + ctx.send(buffer); + } +} diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index e8c2696895..787fda19ae 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -14,6 +14,7 @@ exports io.jooby.validation; exports io.jooby.problem; exports io.jooby.value; + exports io.jooby.output; uses io.jooby.MvcFactory; uses io.jooby.Server; diff --git a/jooby/src/test/java/io/jooby/Issue3607.java b/jooby/src/test/java/io/jooby/Issue3607.java index 2cb31658af..1b4683f748 100644 --- a/jooby/src/test/java/io/jooby/Issue3607.java +++ b/jooby/src/test/java/io/jooby/Issue3607.java @@ -9,16 +9,15 @@ import org.junit.jupiter.api.Test; -import io.jooby.buffer.DataBuffer; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.output.Output; 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 Output.wrap(new byte[0]); } } diff --git a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java index a763db115d..85f6ab21ff 100644 --- a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java +++ b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java @@ -13,69 +13,69 @@ import org.junit.jupiter.api.Test; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.output.ByteBufferOutputFactory; 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 = new ByteBufferOutputFactory(); + 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", message.encode(ctx).asString(StandardCharsets.UTF_8)); } @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 = new ByteBufferOutputFactory(); + 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)); + message.encode(ctx).asString(StandardCharsets.UTF_8)); } @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 = new ByteBufferOutputFactory(); + 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", message.encode(ctx).asString(StandardCharsets.UTF_8)); } } diff --git a/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java b/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java new file mode 100644 index 0000000000..7e717cdd54 --- /dev/null +++ b/jooby/src/test/java/io/jooby/output/BufferedOutputTest.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; + +public class BufferedOutputTest { + + @Test + public void bufferedOutput() { + buffered( + 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!", buffered.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 ByteBufferOutputImpl(3)); + + buffered( + 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!", buffered.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 ByteBufferOutputImpl()); + + buffered( + buffered -> { + var bytes = "xxHello World!xx".getBytes(StandardCharsets.UTF_8); + buffered.write(bytes, 2, bytes.length - 4); + assertEquals("Hello World!", buffered.toString()); + assertEquals(12, buffered.size()); + }, + new ByteBufferOutputImpl()); + + buffered( + buffered -> { + buffered.write((byte) 'A'); + assertEquals("A", buffered.toString()); + assertEquals(1, buffered.size()); + }, + new ByteBufferOutputImpl()); + } + + private void buffered(SneakyThrows.Consumer consumer, Output... buffers) { + Stream.of(buffers).forEach(consumer); + } + + @Test + public void chunkedOutput() { + chunked( + 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!", chunked.toString()); + assertEquals(12, chunked.size()); + assertEquals(5, chunked.getChunks().size()); + }, + new ByteBufferChunkedOutput()); + } + + @Test + public void wrapOutput() throws IOException { + var bytes = "xxHello World!xx".getBytes(StandardCharsets.UTF_8); + try (var output = new WrappedOutput(bytes, 2, bytes.length - 4)) { + assertEquals("Hello World!", output.asString(StandardCharsets.UTF_8)); + assertEquals(12, output.size()); + } + } + + private void chunked(SneakyThrows.Consumer consumer, ChunkedOutput... chunks) { + Stream.of(chunks).forEach(consumer); + } +} 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..a03cbbc001 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.output.Output; /** * JSON module using Avaje-JsonB: 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().newBufferedOutput(); 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..b6d87579cd 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,7 @@ 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!", output.asString(StandardCharsets.UTF_8).trim()); } @Test @@ -99,7 +99,7 @@ public void renderWithLocale() throws Exception { "friday", engine .render(ctx, ModelAndView.map("locales.ftl").put("someDate", nextFriday)) - .toString(StandardCharsets.UTF_8) + .asString(StandardCharsets.UTF_8) .trim() .toLowerCase()); @@ -111,7 +111,7 @@ public void renderWithLocale() throws Exception { ModelAndView.map("locales.ftl") .put("someDate", nextFriday) .setLocale(new Locale("en", "GB"))) - .toString(StandardCharsets.UTF_8) + .asString(StandardCharsets.UTF_8) .trim() .toLowerCase()); @@ -123,7 +123,7 @@ public void renderWithLocale() throws Exception { ModelAndView.map("locales.ftl") .put("someDate", nextFriday) .setLocale(Locale.GERMAN)) - .toString(StandardCharsets.UTF_8) + .asString(StandardCharsets.UTF_8) .trim() .toLowerCase()); } @@ -142,7 +142,7 @@ 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!", output.asString(StandardCharsets.UTF_8).trim()); } @Test @@ -159,6 +159,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", output.asString(StandardCharsets.UTF_8).trim()); } } 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..bb4072ecb2 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().newBufferedOutput(); 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..0fe5d3abc9 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,7 @@ import com.google.gson.GsonBuilder; import io.jooby.Context; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.output.ByteBufferOutputFactory; public class Issue3434 { String text = @@ -139,12 +139,12 @@ public class Issue3434 { @Test void shouldEncodeUsingBufferWriter() { var gson = new GsonBuilder().create(); - var factory = new DefaultDataBufferFactory(); + var factory = new ByteBufferOutputFactory(); 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)), result.asString(StandardCharsets.UTF_8)); } } 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..90eec6c874 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().newBufferedOutput(); 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..0ebb761f16 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 @@ -57,7 +57,7 @@ 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!", output.asString(StandardCharsets.UTF_8).trim()); } @Test @@ -77,6 +77,6 @@ public void renderFileSystem() 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!", output.asString(StandardCharsets.UTF_8).trim()); } } 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..69ce073b2d 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,8 +153,8 @@ 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 buffer = ctx.getOutputFactory().newBufferedOutput(); ctx.setDefaultResponseType(mediaType); mapper.writer().writeValue(buffer.asOutputStream(), value); return buffer; 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..0ac235708b 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,19 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.output.ByteBufferOutputFactory; public class JacksonJsonModuleTest { @Test public void renderJson() throws Exception { Context ctx = mock(Context.class); - when(ctx.getBufferFactory()).thenReturn(new DefaultDataBufferFactory()); + when(ctx.getOutputFactory()).thenReturn(new ByteBufferOutputFactory()); 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\"}", buffer.asString(StandardCharsets.UTF_8)); verify(ctx).setDefaultResponseType(MediaType.json); } @@ -57,12 +57,12 @@ 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(new ByteBufferOutputFactory()); JacksonModule jackson = new JacksonModule(new XmlMapper()); var buffer = jackson.encode(ctx, mapOf("k", "v")); - assertEquals("v", buffer.toString(StandardCharsets.UTF_8)); + assertEquals("v", buffer.asString(StandardCharsets.UTF_8)); 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..57009ac222 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,13 @@ 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,6 +46,48 @@ public void failed(Throwable x) { } } + public static class OutputCallback implements Callback { + + private final Response response; + private final Callback cb; + private final Iterator it; + private boolean closeOnLast; + + public OutputCallback(Response response, Callback cb, Output buffer) { + this.response = response; + this.cb = cb; + this.it = buffer.iterator(); + } + + public void send(boolean closeOnLast) { + this.closeOnLast = closeOnLast; + if (it.hasNext()) { + var buffer = it.next(); + if (it.hasNext()) { + response.write(false, buffer, this); + } else { + sendLast(closeOnLast, buffer); + } + } else { + sendLast(closeOnLast, null); + } + } + + private void sendLast(boolean last, ByteBuffer buffer) { + response.write(last, buffer, cb); + } + + @Override + public void succeeded() { + send(closeOnLast); + } + + @Override + public void failed(Throwable x) { + cb.failed(x); + } + } + public static class DataBufferCallback implements Callback { private final Response response; @@ -99,6 +143,10 @@ public static DataBufferCallback fromDataBuffer( 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( Response response, Callback cb, ByteBuffer[] buffer) { return new ByteBufferArrayCallback(response, cb, buffer); 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 8b23e884b1..9ae34be076 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 @@ -71,6 +71,7 @@ import io.jooby.StatusCode; 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 { @@ -523,6 +524,12 @@ public Context send(@NonNull DataBuffer data) { return this; } + @NonNull @Override + public Context send(@NonNull Output output) { + output.send(this); + return this; + } + @NonNull @Override public Context send(@NonNull ByteBuffer data) { var length = response.getHeaders().getLongField(CONTENT_LENGTH); 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..7a1852df1d 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 @@ -6,6 +6,7 @@ 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; @@ -15,6 +16,7 @@ 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; @@ -37,6 +39,12 @@ public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { return this; } + @NonNull @Override + public Sender write(@NonNull Output output, @NonNull Callback callback) { + fromOutput(response, toJettyCallback(ctx, callback), output).send(false); + return this; + } + private static org.eclipse.jetty.util.Callback toJettyCallback( JettyContext ctx, Callback callback) { return new org.eclipse.jetty.util.Callback() { 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..96af855383 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 @@ -35,6 +35,7 @@ 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 { @@ -288,13 +289,23 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback @NonNull @Override public WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callback) { + return this; + } + + @NonNull @Override + public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage( - (remote, writeCallback) -> remote.sendText(message.toString(UTF_8), writeCallback), + (remote, writeCallback) -> remote.sendText(message.asString(UTF_8), writeCallback), new WriteCallbackAdaptor(this, callback)); } @NonNull @Override public WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback) { + return this; + } + + @NonNull @Override + public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage( (remote, writeCallback) -> new WebSocketDataBufferCallback(writeCallback, message, remote::sendBinary).send(), 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/WebSocketDataBufferCallback.java index 8ae2f82eb4..91bb9f7ba9 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/WebSocketDataBufferCallback.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 { - private final DataBuffer.ByteBufferIterator it; + private final Iterator it; private final Callback cb; private Consumer2 sender; public WebSocketDataBufferCallback( - Callback cb, DataBuffer buffer, Consumer2 sender) { + 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-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-jte/src/main/java/io/jooby/internal/jte/DataBufferOutput.java b/modules/jooby-jte/src/main/java/io/jooby/internal/jte/DataBufferOutput.java index 2d2dd38e95..6bdfcbdafc 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/DataBufferOutput.java @@ -9,13 +9,13 @@ import java.nio.charset.Charset; import gg.jte.TemplateOutput; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class DataBufferOutput implements TemplateOutput { - private final DataBuffer buffer; + private final Output buffer; private final Charset charset; - public DataBufferOutput(DataBuffer buffer, Charset charset) { + public DataBufferOutput(Output 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..7dbc046271 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,13 +11,13 @@ 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 buffer = ctx.getOutputFactory().newBufferedOutput(); var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); jte.render(output); return buffer; 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..4b6c0b91f1 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.output.Output; class JteTemplateEngine implements io.jooby.TemplateEngine { private final TemplateEngine jte; @@ -32,8 +32,8 @@ public List extensions() { } @Override - public DataBuffer render(Context ctx, ModelAndView modelAndView) { - var buffer = ctx.getBufferFactory().allocateBuffer(); + public Output render(Context ctx, ModelAndView modelAndView) { + var buffer = ctx.getOutputFactory().newBufferedOutput(); var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); var attributes = ctx.getAttributes(); if (modelAndView instanceof MapModelAndView mapModelAndView) { 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 index 9205930bf1..1e55b68eaf 100644 --- 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 @@ -11,34 +11,34 @@ import org.junit.jupiter.api.Test; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.output.ByteBufferOutputFactory; public class DataBufferOutputTest { @Test public void checkWriteContent() { - var factory = new DefaultDataBufferFactory(); - var buffer = factory.allocateBuffer(); + var factory = new ByteBufferOutputFactory(); + var buffer = factory.newBufferedOutput(); var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); output.writeContent("Hello"); - assertEquals("Hello", buffer.toString(StandardCharsets.UTF_8)); + assertEquals("Hello", buffer.asString(StandardCharsets.UTF_8)); } @Test public void checkWriteContentSubstring() { - var factory = new DefaultDataBufferFactory(); - var buffer = factory.allocateBuffer(); + var factory = new ByteBufferOutputFactory(); + var buffer = factory.newBufferedOutput(); 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)); + assertEquals("Hello World", buffer.asString(StandardCharsets.UTF_8)); } @Test public void checkWriteBinaryContent() { - var factory = new DefaultDataBufferFactory(); - var buffer = factory.allocateBuffer(); + var factory = new ByteBufferOutputFactory(); + var buffer = factory.newBufferedOutput(); var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); output.writeBinaryContent("Hello".getBytes(StandardCharsets.UTF_8)); - assertEquals("Hello", buffer.toString(StandardCharsets.UTF_8)); + assertEquals("Hello", buffer.asString(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..7a15dffb36 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.Output; +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(Output.class); + when(bufferFactory.newBufferedOutput()).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(Output.class); + when(bufferFactory.newBufferedOutput()).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..35e3d319ed 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.Output; +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(Output.class); + when(bufferFactory.newBufferedOutput()).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-netty/src/main/java/io/jooby/internal/netty/NettyContext.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java index ac635a6fc5..55a05ecbdb 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 @@ -69,6 +69,7 @@ import io.jooby.StatusCode; 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; @@ -599,6 +600,12 @@ public Context send(@NonNull DataBuffer data) { return this; } + @Override + @NonNull public Context send(@NonNull Output output) { + output.send(this); + return this; + } + public Context send(@NonNull ByteBuf data) { try { responseStarted = true; 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..c7675a0f4e 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 @@ -9,6 +9,9 @@ import io.jooby.Sender; import io.jooby.buffer.DataBuffer; import io.jooby.netty.buffer.NettyDataBuffer; +import io.jooby.netty.output.NettyOutput; +import io.jooby.output.Output; +import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; @@ -41,6 +44,20 @@ public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { return this; } + @NonNull @Override + public Sender write(@NonNull Output output, @NonNull Callback callback) { + ByteBuf buf; + if (output instanceof NettyOutput netty) { + buf = netty.byteBuf(); + } else { + buf = Unpooled.wrappedBuffer(output.asByteBuffer()); + } + context + .writeAndFlush(new DefaultHttpContent(buf)) + .addListener(newChannelFutureListener(ctx, callback)); + return this; + } + @Override public void close() { context.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT).addListener(ctx); 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..5293dd8442 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 @@ -18,7 +18,9 @@ import io.jooby.ServerSentEmitter; import io.jooby.ServerSentMessage; import io.jooby.SneakyThrows; -import io.jooby.netty.buffer.NettyDataBuffer; +import io.jooby.netty.output.NettyOutput; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.channel.EventLoop; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; @@ -64,9 +66,14 @@ 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); + ByteBuf byteBuf; + if (output instanceof NettyOutput outputNetty) { + byteBuf = outputNetty.byteBuf(); + } else { + byteBuf = Unpooled.wrappedBuffer(output.asByteBuffer()); + } + netty.ctx.writeAndFlush(byteBuf).addListener(this); } else { log.warn("server-sent-event closed: {}", id); } 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..26e3f68ce4 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 @@ -28,6 +28,8 @@ import io.jooby.WebSocketMessage; import io.jooby.buffer.DataBuffer; import io.jooby.netty.buffer.NettyDataBuffer; +import io.jooby.netty.output.NettyOutput; +import io.jooby.output.Output; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; @@ -148,6 +150,24 @@ public WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callba return sendMessage(((NettyDataBuffer) message).getNativeBuffer(), false, callback); } + @NonNull @Override + public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { + if (message instanceof NettyOutput netty) { + return sendMessage(netty.byteBuf(), false, callback); + } else { + return sendMessage(Unpooled.wrappedBuffer(message.asByteBuffer()), false, callback); + } + } + + @NonNull @Override + public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { + if (message instanceof NettyOutput netty) { + return sendMessage(netty.byteBuf(), false, callback); + } else { + return sendMessage(Unpooled.wrappedBuffer(message.asByteBuffer()), true, callback); + } + } + @Override public WebSocket render(Object value, @NonNull WriteCallback callback) { return renderMessage(value, false, callback); 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 06673efa70..530fb0e522 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 @@ -24,6 +24,7 @@ import io.jooby.SslOptions; import io.jooby.internal.netty.*; import io.jooby.netty.buffer.NettyDataBufferFactory; +import io.jooby.netty.output.NettyOutputFactory; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelOption; @@ -121,6 +122,8 @@ public Server start(@NonNull Jooby... application) { } // Make sure context use same buffer factory applications.forEach(app -> app.setBufferFactory(bufferFactory)); + var outputFactory = new NettyOutputFactory(ByteBufAllocator.DEFAULT); + applications.forEach(app -> app.setOutputFactory(outputFactory)); addShutdownHook(); diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyBufferedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyBufferedOutput.java new file mode 100644 index 0000000000..65b0b4813b --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyBufferedOutput.java @@ -0,0 +1,81 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.netty.output; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.SneakyThrows; +import io.jooby.internal.netty.NettyContext; +import io.jooby.output.Output; +import io.netty.buffer.ByteBuf; + +class NettyBufferedOutput implements NettyOutput { + + private final ByteBuf buffer; + + protected NettyBufferedOutput(ByteBuf buffer) { + this.buffer = buffer; + } + + @Override + public ByteBuf byteBuf() { + return this.buffer; + } + + @Override + public ByteBuffer asByteBuffer() { + return this.buffer.nioBuffer(); + } + + @Override + public String asString(@NonNull Charset charset) { + return this.buffer.toString(charset); + } + + @Override + public void accept(SneakyThrows.Consumer consumer) { + consumer.accept(asByteBuffer()); + } + + @Override + public int size() { + return buffer.readableBytes(); + } + + @Override + public Output write(byte b) { + buffer.writeByte(b); + return this; + } + + @Override + public Output write(byte[] source) { + buffer.writeBytes(source); + return this; + } + + @Override + public Output write(byte[] source, int offset, int length) { + this.buffer.writeBytes(source, offset, length); + return this; + } + + @Override + public void send(Context ctx) { + if (ctx instanceof NettyContext netty) { + netty.send(this.buffer); + } else { + ctx.send(this.buffer.nioBuffer()); + } + } + + @Override + public void close() throws IOException {} +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyChunkedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyChunkedOutput.java new file mode 100644 index 0000000000..fd9ecab19c --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyChunkedOutput.java @@ -0,0 +1,90 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.netty.output; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.SneakyThrows; +import io.jooby.internal.netty.NettyContext; +import io.jooby.output.ChunkedOutput; +import io.jooby.output.Output; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; + +class NettyChunkedOutput implements ChunkedOutput, NettyOutput { + + private final CompositeByteBuf buffer; + + protected NettyChunkedOutput(CompositeByteBuf buffer) { + this.buffer = buffer; + } + + @Override + public ByteBuf byteBuf() { + return this.buffer; + } + + @Override + public ByteBuffer asByteBuffer() { + return this.buffer.nioBuffer(); + } + + @Override + public String asString(@NonNull Charset charset) { + return this.buffer.toString(charset); + } + + @Override + public void accept(SneakyThrows.Consumer consumer) { + consumer.accept(asByteBuffer()); + } + + @Override + public int size() { + return buffer.readableBytes(); + } + + @Override + public Output write(byte b) { + buffer.writeByte(b); + return this; + } + + @Override + public Output write(byte[] source) { + buffer.writeBytes(source); + return this; + } + + @Override + public Output write(byte[] source, int offset, int length) { + this.buffer.writeBytes(source, offset, length); + return this; + } + + @Override + public void send(Context ctx) { + if (ctx instanceof NettyContext netty) { + netty.send(this.buffer); + } else { + ctx.send(this.buffer.nioBuffer()); + } + } + + @Override + public void close() throws IOException {} + + @Override + public List getChunks() { + return Arrays.asList(buffer.nioBuffers()); + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutput.java b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutput.java new file mode 100644 index 0000000000..af73a7a1e0 --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutput.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.netty.output; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.function.IntPredicate; + +import io.jooby.output.Output; +import io.netty.buffer.ByteBuf; + +public interface NettyOutput extends Output { + ByteBuf byteBuf(); + + @Override + default Iterator split(IntPredicate predicate) { + var buffer = byteBuf(); + var offset = buffer.readerIndex(); + var chunks = new ArrayList(); + for (int i = offset; i < size(); i++) { + var b = buffer.getByte(i); + if (predicate.test(b)) { + chunks.add(buffer.retainedSlice(offset, i + 1).nioBuffer()); + offset = i + 1; + } + } + if (offset < size()) { + chunks.add(buffer.retainedSlice(offset, size()).nioBuffer()); + } + return chunks.iterator(); + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java new file mode 100644 index 0000000000..5d7a5a7181 --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java @@ -0,0 +1,69 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.netty.output; + +import java.nio.ByteBuffer; + +import io.jooby.output.ChunkedOutput; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; +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 final ByteBufAllocator allocator; + + /** + * Create a new {@code NettyDataBufferFactory} based on the given factory. + * + * @param allocator the factory to use + * @see io.netty.buffer.PooledByteBufAllocator + * @see io.netty.buffer.UnpooledByteBufAllocator + */ + public NettyOutputFactory(ByteBufAllocator allocator) { + this.allocator = allocator; + } + + public NettyOutputFactory() { + this(ByteBufAllocator.DEFAULT); + } + + @Override + public Output newBufferedOutput(int size) { + return new NettyBufferedOutput(this.allocator.buffer(size)); + } + + @Override + public Output wrap(ByteBuffer buffer) { + return new NettyBufferedOutput(Unpooled.wrappedBuffer(buffer)); + } + + @Override + public Output wrap(byte[] bytes) { + return new NettyBufferedOutput(Unpooled.wrappedBuffer(bytes)); + } + + @Override + public Output wrap(byte[] bytes, int offset, int length) { + return new NettyBufferedOutput(Unpooled.wrappedBuffer(bytes, offset, length)); + } + + @Override + public ChunkedOutput newChunkedOutput() { + return new NettyChunkedOutput(allocator.compositeBuffer(48)); + } +} 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..48f2b55a9a 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().newBufferedOutput(); 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..f5b7b2b6e8 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 @@ -58,7 +58,7 @@ 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!", output.asString(StandardCharsets.UTF_8)); } @Test @@ -89,7 +89,7 @@ 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!", output.asString(StandardCharsets.UTF_8)); } @Test @@ -104,30 +104,30 @@ public void renderWithLocale() throws Exception { assertEquals( "Greetings!", - engine.render(ctx, ModelAndView.map("locales.peb")).toString(StandardCharsets.UTF_8)); + engine.render(ctx, ModelAndView.map("locales.peb")).asString(StandardCharsets.UTF_8)); assertEquals( "Hi!", engine .render(ctx, ModelAndView.map("locales.peb").setLocale(new Locale("en", "GB"))) - .toString(StandardCharsets.UTF_8)); + .asString(StandardCharsets.UTF_8)); assertEquals( "Grüß Gott!", engine .render(ctx, ModelAndView.map("locales.peb").setLocale(Locale.GERMAN)) - .toString(StandardCharsets.UTF_8)); + .asString(StandardCharsets.UTF_8)); assertEquals( "Grüß Gott!", engine .render(ctx, ModelAndView.map("locales.peb").setLocale(Locale.GERMANY)) - .toString(StandardCharsets.UTF_8)); + .asString(StandardCharsets.UTF_8)); assertEquals( "Servus!", engine .render(ctx, ModelAndView.map("locales.peb").setLocale(new Locale("de", "AT"))) - .toString(StandardCharsets.UTF_8)); + .asString(StandardCharsets.UTF_8)); } } diff --git a/modules/jooby-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java b/modules/jooby-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java index dccfb4e345..fdc53ab5bc 100644 --- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java +++ b/modules/jooby-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java @@ -10,8 +10,8 @@ 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.ChunkedOutput; +import io.jooby.output.OutputFactory; /** * Rocker output that uses a byte array to render the output. @@ -27,9 +27,9 @@ public class DataBufferOutput implements RockerOutput { private final ContentType contentType; /** The buffer where data is stored. */ - protected DataBuffer buffer; + protected ChunkedOutput buffer; - DataBufferOutput(Charset charset, ContentType contentType, DataBuffer buffer) { + DataBufferOutput(Charset charset, ContentType contentType, ChunkedOutput buffer) { this.charset = charset; this.contentType = contentType; this.buffer = buffer; @@ -59,7 +59,7 @@ public DataBufferOutput w(byte[] bytes) { @Override public int getByteLength() { - return buffer.readableByteCount(); + return buffer.size(); } /** @@ -67,13 +67,13 @@ public int getByteLength() { * * @return Byte buffer. */ - public DataBuffer toBuffer() { + public ChunkedOutput toBuffer() { return buffer; } static RockerOutputFactory factory( - Charset charset, DataBufferFactory factory, int bufferSize) { + Charset charset, OutputFactory factory, int bufferSize) { return (contentType, charsetName) -> - new DataBufferOutput(charset, contentType, factory.allocateBuffer(bufferSize)); + new DataBufferOutput(charset, contentType, factory.newChunkedOutput()); } } 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..d7a7d5c743 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,7 +11,7 @@ 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; @@ -21,7 +21,7 @@ class RockerMessageEncoder implements MessageEncoder { } @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()); 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..59ab96224e 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 @@ -97,7 +97,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 = DataBufferOutput.factory(charset, application.getOutputFactory(), bufferSize); runtime.setReloading(reloading); // renderer application.encoder(new RockerMessageEncoder(factory)); 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 34b7f87972..02a5c2f1bf 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 @@ -57,6 +57,9 @@ import io.jooby.buffer.DataBufferFactory; import io.jooby.buffer.DefaultDataBufferFactory; import io.jooby.exception.TypeMismatchException; +import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -121,6 +124,8 @@ public class MockContext implements DefaultContext { private DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); + private OutputFactory outputFactory = new ByteBufferOutputFactory(); + @Override public String getMethod() { return method; @@ -142,6 +147,11 @@ public DataBufferFactory getBufferFactory() { return bufferFactory; } + @Override + public OutputFactory getOutputFactory() { + return outputFactory; + } + /** * Set HTTP method. * @@ -577,6 +587,13 @@ public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { return this; } + @NonNull @Override + public Sender write(@NonNull Output output, @NonNull Callback callback) { + response.setResult(output); + callback.onComplete(MockContext.this, null); + return this; + } + @Override public void close() { listeners.run(MockContext.this); @@ -677,6 +694,14 @@ public Context send(@NonNull DataBuffer data) { return this; } + @Override + public Context send(@NonNull Output output) { + responseStarted = true; + this.response.setResult(output).setContentLength(output.size()); + listeners.run(this); + return this; + } + @Override public Context send(@NonNull ByteBuffer[] data) { responseStarted = true; 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..0391ea38d1 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 @@ -15,6 +15,7 @@ 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. @@ -96,6 +97,11 @@ public WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callba return sendObject(message, callback); } + @NonNull @Override + public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { + return sendObject(message, callback); + } + @NonNull @Override public WebSocket sendBinary(@NonNull String message, @NonNull WriteCallback callback) { return sendObject(message, callback); @@ -116,6 +122,11 @@ public WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback return sendObject(message, callback); } + @NonNull @Override + public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { + return sendObject(message, callback); + } + @NonNull @Override public WebSocket render(@NonNull Object value, @NonNull WriteCallback callback) { return sendObject(value, callback); 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..30ac63bcb8 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().newBufferedOutput(); var context = new Context(locale, model); var templateName = modelAndView.getView(); if (!templateName.startsWith("/")) { 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 dad2266a87..951923bcd4 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 @@ -58,6 +58,7 @@ import io.jooby.StatusCode; 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; @@ -504,6 +505,12 @@ public Context send(@NonNull DataBuffer data) { return this; } + @NonNull @Override + public Context send(@NonNull Output output) { + output.send(this); + return this; + } + @NonNull @Override public Context send(StatusCode statusCode) { exchange.setStatusCode(statusCode.value()); 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..d738b17d01 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 @@ -11,6 +11,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; @@ -35,6 +36,12 @@ public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { return this; } + @NonNull @Override + public Sender write(@NonNull Output output, @NonNull Callback callback) { + new UndertowOutputCallback(output, newIoCallback(ctx, callback)).send(exchange); + return this; + } + @Override public void close() { ctx.destroy(null); 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..67ec8c3834 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 @@ -24,7 +24,7 @@ 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; @@ -129,9 +129,9 @@ private void fillBuffer() { buffered.add(data); if (data.leftOverData == null) { var message = data.message.encode(context); - if (message.readableByteCount() < buffer.remaining()) { + if (message.size() < buffer.remaining()) { message.toByteBuffer(buffer); - buffer.position(buffer.position() + message.readableByteCount()); + buffer.position(buffer.position() + message.size()); data.endBufferPosition = buffer.position(); } else { queue.addFirst(data); @@ -142,7 +142,7 @@ private void fillBuffer() { 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(); @@ -247,7 +247,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..123a4c2cde 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; @@ -33,6 +34,7 @@ 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; @@ -87,7 +89,7 @@ public void onError(WebSocketChannel channel, Void context, Throwable cause) { } private static class WebSocketDataBufferCallback implements WebSocketCallback { - private final DataBuffer.ByteBufferIterator it; + private final Iterator it; private final boolean binary; private final WebSocketChannel channel; private final UndertowWebSocket ws; @@ -98,12 +100,12 @@ public WebSocketDataBufferCallback( 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() { @@ -223,11 +225,21 @@ public WebSocket sendBinary(@NonNull String message, @NonNull WriteCallback call @NonNull @Override public WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback) { + return this; // sendMessage(message, true, callback); + } + + @NonNull @Override + 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) { + return this; // sendMessage(message, false, callback); + } + + @NonNull @Override + public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage(message, false, callback); } @@ -236,7 +248,7 @@ 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(); 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..30af5a7500 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.newBufferedOutput(); + 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..25fb254e32 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,7 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.buffer.DefaultDataBufferFactory; +import io.jooby.output.ByteBufferOutputFactory; public class YassonModuleTest { @@ -39,11 +39,11 @@ public void render() { user.age = Integer.MAX_VALUE; Context ctx = mock(Context.class); - when(ctx.getBufferFactory()).thenReturn(new DefaultDataBufferFactory()); + when(ctx.getOutputFactory()).thenReturn(new ByteBufferOutputFactory()); var buffer = YassonModule.encode(ctx, user); assertEquals( "{\"age\":2147483647,\"id\":-1,\"name\":\"Lorem €@!?\"}", - buffer.toString(StandardCharsets.UTF_8)); + buffer.asString(StandardCharsets.UTF_8)); verify(ctx).setDefaultResponseType(MediaType.json); } 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/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index 63eb81caa4..dbeb49cb16 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -1006,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( @@ -1058,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", diff --git a/tests/src/test/java/io/jooby/test/MvcTest.java b/tests/src/test/java/io/jooby/test/MvcTest.java index eb7127b05f..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( From b40f3efc1cade0c35df8d4d274fd56baf7f271a5 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 26 Jun 2025 14:04:53 -0300 Subject: [PATCH 15/60] output api: completely remove DataBuffer from source code (happy to do it) - fix #3604 --- jooby/src/main/java/io/jooby/Context.java | 12 - .../main/java/io/jooby/DefaultContext.java | 5 - .../main/java/io/jooby/ForwardingContext.java | 13 - jooby/src/main/java/io/jooby/Jooby.java | 12 - jooby/src/main/java/io/jooby/Router.java | 5 - jooby/src/main/java/io/jooby/Sender.java | 11 +- jooby/src/main/java/io/jooby/WebSocket.java | 13 - .../src/main/java/io/jooby/buffer/Assert.java | 39 - .../io/jooby/buffer/CloseableDataBuffer.java | 24 - .../main/java/io/jooby/buffer/DataBuffer.java | 473 ------------ .../io/jooby/buffer/DataBufferFactory.java | 100 --- .../jooby/buffer/DataBufferInputStream.java | 97 --- .../buffer/DataBufferLimitException.java | 24 - .../jooby/buffer/DataBufferOutputStream.java | 58 -- .../java/io/jooby/buffer/DataBufferUtils.java | 170 ----- .../io/jooby/buffer/DataBufferWrapper.java | 227 ------ .../io/jooby/buffer/DataBufferWriter.java | 70 -- .../io/jooby/buffer/DefaultDataBuffer.java | 513 ------------- .../buffer/DefaultDataBufferFactory.java | 137 ---- .../jooby/buffer/LimitedDataBufferList.java | 136 ---- .../java/io/jooby/buffer/ObjectUtils.java | 19 - .../jooby/buffer/OutputStreamPublisher.java | 332 --------- .../io/jooby/buffer/PooledDataBuffer.java | 47 -- jooby/src/main/java/io/jooby/buffer/README | 18 - .../io/jooby/buffer/TouchableDataBuffer.java | 23 - .../java/io/jooby/buffer/package-info.java | 34 - .../java/io/jooby/internal/HeadContext.java | 14 - .../java/io/jooby/internal/RouterImpl.java | 22 +- .../io/jooby/internal/WebSocketSender.java | 11 - jooby/src/main/java/module-info.java | 2 - .../test/java/io/jooby/buffer/Issue3434.java | 8 +- .../jooby/internal/jetty/JettyCallbacks.java | 56 -- .../io/jooby/internal/jetty/JettyContext.java | 7 - .../io/jooby/internal/jetty/JettySender.java | 9 - .../jooby/internal/jetty/JettyWebSocket.java | 13 +- ...back.java => WebSocketOutputCallback.java} | 4 +- .../io/jooby/internal/netty/NettyContext.java | 7 - .../io/jooby/internal/netty/NettySender.java | 10 - .../jooby/internal/netty/NettyWebSocket.java | 12 - .../main/java/io/jooby/netty/NettyServer.java | 29 +- .../jooby/netty/buffer/NettyDataBuffer.java | 368 --------- .../netty/buffer/NettyDataBufferFactory.java | 169 ----- .../io/jooby/netty/buffer/package-info.java | 2 - .../netty/output/NettyOutputFactory.java | 4 + .../src/main/java/module-info.java | 5 - .../main/java/io/jooby/test/MockContext.java | 25 - .../java/io/jooby/test/MockWebSocket.java | 11 - .../internal/undertow/UndertowContext.java | 7 - .../undertow/UndertowDataBufferCallback.java | 59 -- .../internal/undertow/UndertowSender.java | 7 - .../internal/undertow/UndertowWebSocket.java | 17 +- .../avaje/jsonb/AvajeJsonbEncoderBench.java | 17 +- .../jsonb/DataBufferJsonOutputBench.java | 6 +- .../AbstractDataBufferAllocatingTests.java | 192 ----- .../java/io/jooby/buffer/DataBufferTests.java | 698 ------------------ .../jooby/buffer/DefaultDataBufferTests.java | 45 -- .../io/jooby/buffer/LeakAwareDataBuffer.java | 78 -- .../buffer/LeakAwareDataBufferFactory.java | 156 ---- .../LeakAwareDataBufferFactoryTests.java | 37 - .../buffer/LimitedDataBufferListTests.java | 49 -- .../jooby/buffer/PooledDataBufferTests.java | 87 --- .../java/io/jooby/junit/ServerTestRunner.java | 4 +- 62 files changed, 35 insertions(+), 4824 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/buffer/Assert.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/CloseableDataBuffer.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/DataBuffer.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/DataBufferFactory.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/DataBufferInputStream.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/DataBufferLimitException.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/DataBufferOutputStream.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/DataBufferUtils.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/DataBufferWrapper.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/DataBufferWriter.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/DefaultDataBuffer.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/DefaultDataBufferFactory.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/LimitedDataBufferList.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/ObjectUtils.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/OutputStreamPublisher.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/PooledDataBuffer.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/README delete mode 100644 jooby/src/main/java/io/jooby/buffer/TouchableDataBuffer.java delete mode 100644 jooby/src/main/java/io/jooby/buffer/package-info.java rename modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/{WebSocketDataBufferCallback.java => WebSocketOutputCallback.java} (89%) delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/netty/buffer/NettyDataBuffer.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/netty/buffer/NettyDataBufferFactory.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/netty/buffer/package-info.java delete mode 100644 modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowDataBufferCallback.java delete mode 100644 tests/src/test/java/io/jooby/buffer/AbstractDataBufferAllocatingTests.java delete mode 100644 tests/src/test/java/io/jooby/buffer/DataBufferTests.java delete mode 100644 tests/src/test/java/io/jooby/buffer/DefaultDataBufferTests.java delete mode 100644 tests/src/test/java/io/jooby/buffer/LeakAwareDataBuffer.java delete mode 100644 tests/src/test/java/io/jooby/buffer/LeakAwareDataBufferFactory.java delete mode 100644 tests/src/test/java/io/jooby/buffer/LeakAwareDataBufferFactoryTests.java delete mode 100644 tests/src/test/java/io/jooby/buffer/LimitedDataBufferListTests.java delete mode 100644 tests/src/test/java/io/jooby/buffer/PooledDataBufferTests.java diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java index 8c44141ef3..1b2b2c18be 100644 --- a/jooby/src/main/java/io/jooby/Context.java +++ b/jooby/src/main/java/io/jooby/Context.java @@ -30,8 +30,6 @@ 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.internal.LocaleUtils; import io.jooby.internal.ParamLookupImpl; import io.jooby.internal.ReadOnlyContext; @@ -147,8 +145,6 @@ private static Selector single() { */ Router getRouter(); - DataBufferFactory getBufferFactory(); - OutputFactory getOutputFactory(); /** @@ -1310,14 +1306,6 @@ Context responseWriter( */ Context send(@NonNull ByteBuffer data); - /** - * Send response data. - * - * @param data Response. - * @return This context. - */ - Context send(@NonNull DataBuffer data); - /** * Send response data. * diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 0cd092c1ea..d1675768f6 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -30,7 +30,6 @@ 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; @@ -655,10 +654,6 @@ default Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { return this; } - default DataBufferFactory getBufferFactory() { - return getRouter().getBufferFactory(); - } - @Override default OutputFactory getOutputFactory() { return new ByteBufferOutputFactory(); diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index 304829aaef..2e3d874e20 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -23,8 +23,6 @@ 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; @@ -672,11 +670,6 @@ public Router getRouter() { return ctx.getRouter(); } - @Override - public DataBufferFactory getBufferFactory() { - return ctx.getBufferFactory(); - } - @Override public OutputFactory getOutputFactory() { return ctx.getOutputFactory(); @@ -1235,12 +1228,6 @@ public Context send(@NonNull ByteBuffer data) { return this; } - @Override - public Context send(@NonNull DataBuffer data) { - ctx.send(data); - return this; - } - @Override public Context send(@NonNull Output output) { ctx.send(output); diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index e687ce6ba7..5551c5dfae 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -45,7 +45,6 @@ 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; @@ -741,17 +740,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()); diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 6bbbdca5a0..92c60a2158 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -33,7 +33,6 @@ 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; @@ -537,14 +536,10 @@ default Object execute(@NonNull Context context) { */ @NonNull Router setDefaultWorker(@NonNull Executor worker); - @NonNull DataBufferFactory getBufferFactory(); - @NonNull OutputFactory getOutputFactory(); @NonNull Router setOutputFactory(@NonNull OutputFactory outputFactory); - @NonNull Router setBufferFactory(@NonNull DataBufferFactory bufferFactory); - /** * Attach a filter to the route pipeline. * diff --git a/jooby/src/main/java/io/jooby/Sender.java b/jooby/src/main/java/io/jooby/Sender.java index 54c015bd27..a5dae5fb97 100644 --- a/jooby/src/main/java/io/jooby/Sender.java +++ b/jooby/src/main/java/io/jooby/Sender.java @@ -10,11 +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: @@ -87,17 +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); - - @NonNull Sender write(@NonNull Output output, @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/WebSocket.java b/jooby/src/main/java/io/jooby/WebSocket.java index e8bc040ad9..85a79c7ec0 100644 --- a/jooby/src/main/java/io/jooby/WebSocket.java +++ b/jooby/src/main/java/io/jooby/WebSocket.java @@ -11,7 +11,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.DataBuffer; import io.jooby.output.Output; /** @@ -242,12 +241,6 @@ interface WriteCallback { @NonNull WebSocket send(@NonNull ByteBuffer message, @NonNull WriteCallback callback); - default @NonNull WebSocket send(@NonNull DataBuffer message) { - return send(message, WriteCallback.NOOP); - } - - @NonNull WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callback); - default @NonNull WebSocket send(@NonNull Output message) { return send(message, WriteCallback.NOOP); } @@ -300,12 +293,6 @@ interface WriteCallback { @NonNull WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback callback); - default @NonNull WebSocket sendBinary(@NonNull DataBuffer message) { - return sendBinary(message, WriteCallback.NOOP); - } - - @NonNull WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback); - default @NonNull WebSocket sendBinary(@NonNull Output message) { return sendBinary(message, WriteCallback.NOOP); } 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/DataBufferWriter.java b/jooby/src/main/java/io/jooby/buffer/DataBufferWriter.java deleted file mode 100644 index 308fc2b6b5..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/DataBufferWriter.java +++ /dev/null @@ -1,70 +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.Writer; -import java.nio.CharBuffer; -import java.nio.charset.Charset; - -import edu.umd.cs.findbugs.annotations.NonNull; - -class DataBufferWriter extends Writer { - private final DataBuffer dataBuffer; - private final Charset charset; - private boolean closed; - - public DataBufferWriter(DataBuffer dataBuffer, Charset charset) { - this.dataBuffer = dataBuffer; - this.charset = charset; - } - - @Override - public void write(int c) throws IOException { - checkClosed(); - super.write(c); - } - - @Override - public void write(@NonNull char[] source) throws IOException { - write(source, 0, source.length); - } - - @Override - public void write(@NonNull char[] source, int off, int len) throws IOException { - checkClosed(); - dataBuffer.write(CharBuffer.wrap(source, off, len), charset); - } - - @Override - public void write(@NonNull String source) throws IOException { - checkClosed(); - dataBuffer.write(CharBuffer.wrap(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); - } - - @Override - public void flush() throws IOException {} - - @Override - public void close() { - if (this.closed) { - return; - } - this.closed = true; - } - - private void checkClosed() throws IOException { - if (this.closed) { - throw new IOException("DataBufferWriter is closed"); - } - } -} 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/internal/HeadContext.java b/jooby/src/main/java/io/jooby/internal/HeadContext.java index 0b666556d8..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,6 @@ 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,14 +65,6 @@ public Context send(@NonNull ByteBuffer data) { return this; } - @NonNull @Override - public Context send(@NonNull DataBuffer data) { - ctx.setResponseLength(data.readableByteCount()); - checkSizeHeaders(); - ctx.send(StatusCode.OK); - return this; - } - @NonNull @Override public Context send(@NonNull Output output) { ctx.setResponseLength(output.size()); @@ -194,11 +185,6 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { return this; } - @NonNull @Override - public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { - return this; - } - @NonNull @Override public Sender write(@NonNull Output output, @NonNull Callback callback) { return this; diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index a5eb82cbfb..9a7b4fccfe 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -23,7 +23,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; @@ -41,8 +40,6 @@ 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; @@ -164,7 +161,7 @@ public Stack executor(Executor executor) { private ContextInitializer postDispatchInitializer; private Set routerOptions = EnumSet.of(RouterOption.RESET_HEADERS_ON_ERROR); - private DataBufferFactory bufferFactory; + private boolean trustProxy; private boolean contextAsService; @@ -374,23 +371,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; diff --git a/jooby/src/main/java/io/jooby/internal/WebSocketSender.java b/jooby/src/main/java/io/jooby/internal/WebSocketSender.java index 1ed89edf65..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,6 @@ 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 { @@ -69,16 +68,6 @@ public Context send(@NonNull ByteBuffer data) { return this; } - @NonNull @Override - public Context send(@NonNull DataBuffer data) { - if (binary) { - ws.sendBinary(data, callback); - } else { - ws.send(data, callback); - } - return this; - } - @Override public Context send(@NonNull Output output) { if (binary) { diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index 787fda19ae..c73a21a69e 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -8,7 +8,6 @@ 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; @@ -20,7 +19,6 @@ 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/buffer/Issue3434.java b/jooby/src/test/java/io/jooby/buffer/Issue3434.java index a1874b9844..cd1a842bff 100644 --- a/jooby/src/test/java/io/jooby/buffer/Issue3434.java +++ b/jooby/src/test/java/io/jooby/buffer/Issue3434.java @@ -15,9 +15,11 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; +import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.OutputFactory; public class Issue3434 { - DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + OutputFactory factory = new ByteBufferOutputFactory(); @Test void shouldWriteCharBufferOnBufferWriter() throws IOException { @@ -142,10 +144,10 @@ void shouldWriteCharBufferOnBufferWriter() throws IOException { private String writeCharSequence(Charset charset, SneakyThrows.Consumer writer) throws IOException { - var buffer = factory.allocateBuffer(); + var buffer = factory.newBufferedOutput(); try (var out = buffer.asWriter(charset)) { writer.accept(out); - return buffer.toString(charset); + return buffer.asString(charset); } } } 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 57009ac222..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 @@ -11,7 +11,6 @@ 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 { @@ -88,61 +87,6 @@ public void failed(Throwable x) { } } - public static class DataBufferCallback implements Callback { - - private final Response response; - private final Callback cb; - private final DataBuffer.ByteBufferIterator it; - private boolean closeOnLast; - - public DataBufferCallback(Response response, Callback cb, DataBuffer buffer) { - this.response = response; - this.cb = cb; - this.it = buffer.readableByteBuffers(); - } - - public void send(boolean closeOnLast) { - this.closeOnLast = closeOnLast; - if (it.hasNext()) { - var buffer = it.next(); - if (it.hasNext()) { - response.write(false, buffer, this); - } else { - sendLast(closeOnLast, buffer); - } - } else { - sendLast(closeOnLast, null); - } - } - - private void sendLast(boolean last, ByteBuffer buffer) { - try { - response.write(last, buffer, cb); - } finally { - it.close(); - } - } - - @Override - public void succeeded() { - send(closeOnLast); - } - - @Override - public void failed(Throwable x) { - try { - cb.failed(x); - } finally { - it.close(); - } - } - } - - 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); } 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 9ae34be076..ffd8bf08f7 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 @@ -70,7 +70,6 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.WebSocket; -import io.jooby.buffer.DataBuffer; import io.jooby.output.Output; import io.jooby.value.Value; @@ -518,12 +517,6 @@ public Context send(@NonNull String data, @NonNull Charset charset) { return send(ByteBuffer.wrap(data.getBytes(charset))); } - @NonNull @Override - public Context send(@NonNull DataBuffer data) { - data.send(this); - return this; - } - @NonNull @Override public Context send(@NonNull Output output) { output.send(this); 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 7a1852df1d..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,17 +5,14 @@ */ 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 { @@ -33,12 +30,6 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { return this; } - @NonNull @Override - public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { - fromDataBuffer(response, toJettyCallback(ctx, callback), data).send(false); - return this; - } - @NonNull @Override public Sender write(@NonNull Output output, @NonNull Callback callback) { fromOutput(response, toJettyCallback(ctx, callback), output).send(false); 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 96af855383..d6ba8d3e25 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,6 @@ 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,11 +286,6 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback new WriteCallbackAdaptor(this, callback)); } - @NonNull @Override - public WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callback) { - return this; - } - @NonNull @Override public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage( @@ -299,16 +293,11 @@ public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) new WriteCallbackAdaptor(this, callback)); } - @NonNull @Override - public WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback) { - return this; - } - @NonNull @Override 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 89% 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 91bb9f7ba9..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 @@ -13,13 +13,13 @@ import io.jooby.SneakyThrows.Consumer2; import io.jooby.output.Output; -public class WebSocketDataBufferCallback implements Callback { +public class WebSocketOutputCallback implements Callback { private final Iterator it; private final Callback cb; private Consumer2 sender; - public WebSocketDataBufferCallback( + public WebSocketOutputCallback( Callback cb, Output buffer, Consumer2 sender) { this.cb = cb; this.it = buffer.iterator(); 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 55a05ecbdb..0bf4f3b017 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 @@ -68,7 +68,6 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.WebSocket; -import io.jooby.buffer.DataBuffer; import io.jooby.output.Output; import io.jooby.value.Value; import io.netty.buffer.ByteBuf; @@ -594,12 +593,6 @@ public final Context send(ByteBuffer data) { return send(wrappedBuffer(data)); } - @NonNull @Override - public Context send(@NonNull DataBuffer data) { - data.send(this); - return this; - } - @Override @NonNull public Context send(@NonNull Output output) { output.send(this); 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 c7675a0f4e..72877eb579 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 @@ -7,8 +7,6 @@ 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.netty.output.NettyOutput; import io.jooby.output.Output; import io.netty.buffer.ByteBuf; @@ -36,14 +34,6 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { return this; } - @NonNull @Override - public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { - context - .writeAndFlush(new DefaultHttpContent(((NettyDataBuffer) data).getNativeBuffer())) - .addListener(newChannelFutureListener(ctx, callback)); - return this; - } - @NonNull @Override public Sender write(@NonNull Output output, @NonNull Callback callback) { ByteBuf buf; 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 26e3f68ce4..c60c21d872 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 @@ -26,8 +26,6 @@ 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.netty.output.NettyOutput; import io.jooby.output.Output; import io.netty.buffer.ByteBuf; @@ -140,16 +138,6 @@ public WebSocket sendBinary(@NonNull byte[] message, @NonNull WriteCallback call return sendMessage(Unpooled.wrappedBuffer(message), true, callback); } - @NonNull @Override - public WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback) { - return sendMessage(((NettyDataBuffer) message).getNativeBuffer(), true, callback); - } - - @NonNull @Override - public WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callback) { - return sendMessage(((NettyDataBuffer) message).getNativeBuffer(), false, callback); - } - @NonNull @Override public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { if (message instanceof NettyOutput netty) { 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 530fb0e522..6af4d872da 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 @@ -23,7 +23,6 @@ import io.jooby.SneakyThrows; import io.jooby.SslOptions; import io.jooby.internal.netty.*; -import io.jooby.netty.buffer.NettyDataBufferFactory; import io.jooby.netty.output.NettyOutputFactory; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBufAllocator; @@ -54,21 +53,8 @@ public class NettyServer extends Server.Base { private ScheduledExecutorService dateLoop; private ExecutorService worker; - private NettyDataBufferFactory bufferFactory; private List applications; - /** - * Creates a server. - * - * @param bufferFactory Byte buffer allocator. - * @param worker Thread-pool to use. - */ - public NettyServer( - @NonNull NettyDataBufferFactory bufferFactory, @NonNull ExecutorService worker) { - this.worker = worker; - this.bufferFactory = bufferFactory; - } - /** * Creates a server. * @@ -78,15 +64,6 @@ 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() {} @@ -117,11 +94,7 @@ public Server start(@NonNull Jooby... application) { 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)); var outputFactory = new NettyOutputFactory(ByteBufAllocator.DEFAULT); applications.forEach(app -> app.setOutputFactory(outputFactory)); @@ -141,7 +114,7 @@ public Server start(@NonNull Jooby... application) { this.dateLoop = Executors.newSingleThreadScheduledExecutor(); var dateService = new NettyDateService(dateLoop); - var allocator = bufferFactory.getByteBufAllocator(); + var allocator = outputFactory.allocator(); var http2 = options.isHttp2() == Boolean.TRUE; /* Bootstrap: */ if (!options.isHttpsOnly()) { 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/io/jooby/netty/buffer/package-info.java b/modules/jooby-netty/src/main/java/io/jooby/netty/buffer/package-info.java deleted file mode 100644 index e892a0274d..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/buffer/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -@edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault -package io.jooby.netty.buffer; diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java index 5d7a5a7181..c1fa7a0dd1 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java @@ -42,6 +42,10 @@ public NettyOutputFactory() { this(ByteBufAllocator.DEFAULT); } + public ByteBufAllocator allocator() { + return allocator; + } + @Override public Output newBufferedOutput(int size) { return new NettyBufferedOutput(this.allocator.buffer(size)); 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-test/src/main/java/io/jooby/test/MockContext.java b/modules/jooby-test/src/main/java/io/jooby/test/MockContext.java index 02a5c2f1bf..2b044c3757 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 @@ -53,9 +53,6 @@ import io.jooby.Session; import io.jooby.StatusCode; 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.ByteBufferOutputFactory; import io.jooby.output.Output; @@ -122,8 +119,6 @@ public class MockContext implements DefaultContext { private int port = -1; - private DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); - private OutputFactory outputFactory = new ByteBufferOutputFactory(); @Override @@ -142,11 +137,6 @@ public int getPort() { return port; } - @Override - public DataBufferFactory getBufferFactory() { - return bufferFactory; - } - @Override public OutputFactory getOutputFactory() { return outputFactory; @@ -580,13 +570,6 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { return this; } - @Override - public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { - response.setResult(data); - callback.onComplete(MockContext.this, null); - return this; - } - @NonNull @Override public Sender write(@NonNull Output output, @NonNull Callback callback) { response.setResult(output); @@ -686,14 +669,6 @@ public MockContext send(@NonNull ByteBuffer data) { return this; } - @Override - public Context send(@NonNull DataBuffer data) { - responseStarted = true; - this.response.setResult(data).setContentLength(data.readableByteCount()); - listeners.run(this); - return this; - } - @Override public Context send(@NonNull Output output) { responseStarted = true; 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 0391ea38d1..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,6 @@ import io.jooby.SneakyThrows; import io.jooby.WebSocket; import io.jooby.WebSocketCloseStatus; -import io.jooby.buffer.DataBuffer; import io.jooby.output.Output; /** @@ -92,11 +91,6 @@ public WebSocket send(@NonNull ByteBuffer message, @NonNull WriteCallback callba return sendObject(message, callback); } - @NonNull @Override - public WebSocket send(@NonNull DataBuffer message, @NonNull WriteCallback callback) { - return sendObject(message, callback); - } - @NonNull @Override public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendObject(message, callback); @@ -117,11 +111,6 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback return sendObject(message, callback); } - @NonNull @Override - public WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback) { - return sendObject(message, callback); - } - @NonNull @Override public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { return sendObject(message, callback); 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 951923bcd4..a5da9314e6 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 @@ -57,7 +57,6 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.WebSocket; -import io.jooby.buffer.DataBuffer; import io.jooby.output.Output; import io.jooby.value.Value; import io.undertow.Handlers; @@ -499,12 +498,6 @@ public Context send(@NonNull ByteBuffer data) { return this; } - @NonNull @Override - public Context send(@NonNull DataBuffer data) { - data.send(this); - return this; - } - @NonNull @Override public Context send(@NonNull Output output) { output.send(this); 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/UndertowSender.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowSender.java index d738b17d01..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,6 @@ 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,12 +29,6 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { return this; } - @NonNull @Override - public Sender write(@NonNull DataBuffer data, @NonNull Callback callback) { - new UndertowDataBufferCallback(data, newIoCallback(ctx, callback)).send(exchange); - return this; - } - @NonNull @Override public Sender write(@NonNull Output output, @NonNull Callback callback) { new UndertowOutputCallback(output, newIoCallback(ctx, callback)).send(exchange); 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 123a4c2cde..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 @@ -33,7 +33,6 @@ 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; @@ -88,14 +87,14 @@ public void onError(WebSocketChannel channel, Void context, Throwable cause) { } } - private static class WebSocketDataBufferCallback implements WebSocketCallback { + 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, @@ -223,21 +222,11 @@ public WebSocket sendBinary(@NonNull String message, @NonNull WriteCallback call return sendMessage(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)), true, callback); } - @NonNull @Override - public WebSocket sendBinary(@NonNull DataBuffer message, @NonNull WriteCallback callback) { - return this; // sendMessage(message, true, callback); - } - @NonNull @Override 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) { - return this; // sendMessage(message, false, callback); - } - @NonNull @Override public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage(message, false, callback); @@ -251,7 +240,7 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback 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/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java b/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java index 24c77e164e..06e5b6d5d2 100644 --- a/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java +++ b/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java @@ -11,9 +11,9 @@ 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; +import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; @Fork(5) @Warmup(iterations = 5, time = 1) @@ -26,18 +26,18 @@ public class AvajeJsonbEncoderBench { private Jsonb jsonb; private Map message; - private DataBufferFactory factory; - private ThreadLocal cache = + private OutputFactory factory; + private ThreadLocal cache = ThreadLocal.withInitial( () -> { - return (DefaultDataBuffer) factory.allocateBuffer(1024); + return factory.newBufferedOutput(1024); }); @Setup public void setup() { message = Map.of("id", 98, "value", "Hello World"); jsonb = Jsonb.builder().build(); - factory = new DefaultDataBufferFactory(); + factory = new ByteBufferOutputFactory(); } @Benchmark @@ -49,6 +49,7 @@ public void withJsonBuffer() { public void withDataBufferOutputStream() { var buffer = cache.get(); jsonb.toJson(message, jsonb.writer(new DataBufferJsonOutputBench(buffer))); - buffer.getNativeBuffer().clear(); + // TODO: add clear to output or use close + buffer.asByteBuffer().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 index 7c134b5084..c790843722 100644 --- a/tests/src/test/java/io/jooby/avaje/jsonb/DataBufferJsonOutputBench.java +++ b/tests/src/test/java/io/jooby/avaje/jsonb/DataBufferJsonOutputBench.java @@ -9,13 +9,13 @@ import java.io.OutputStream; import io.avaje.json.stream.JsonOutput; -import io.jooby.buffer.DataBuffer; +import io.jooby.output.Output; public class DataBufferJsonOutputBench implements JsonOutput { - private DataBuffer buffer; + private Output buffer; - public DataBufferJsonOutputBench(DataBuffer buffer) { + public DataBufferJsonOutputBench(Output buffer) { this.buffer = buffer; } 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/junit/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index 18db7540cd..22cfd82e98 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -28,9 +28,9 @@ import io.jooby.SneakyThrows; import io.jooby.StartupSummary; import io.jooby.StatusCode; -import io.jooby.buffer.DefaultDataBufferFactory; import io.jooby.internal.MutedServer; import io.jooby.netty.NettyServer; +import io.jooby.output.ByteBufferOutputFactory; import io.jooby.test.WebClient; public class ServerTestRunner { @@ -92,7 +92,7 @@ public void ready(SneakyThrows.Consumer2 onReady) { System.setProperty("___server_name__", server.getName()); var app = provider.get(); if (!(server instanceof NettyServer)) { - app.setBufferFactory(new DefaultDataBufferFactory()); + app.setOutputFactory(new ByteBufferOutputFactory()); } Optional.ofNullable(executionMode).ifPresent(app::setExecutionMode); // Reduce log from maven build: From 492c516fc2d5d1a073c1eabeb2a75ecf60f243f4 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 26 Jun 2025 14:28:58 -0300 Subject: [PATCH 16/60] output api: remove public chunked version (no need to be public) - rename chunked to composite buffer (was truly a composite implementation) - replace output.close with clear --- .../main/java/io/jooby/DefaultContext.java | 4 +- .../main/java/io/jooby/ServerSentMessage.java | 67 ------------------- .../java/io/jooby/internal/RouterImpl.java | 4 +- .../internal/handler/ChunkedSubscriber.java | 2 +- ...fferOutputImpl.java => ByteBufferOut.java} | 24 ++++--- ...tput.java => ByteBufferWrappedOutput.java} | 19 ++++-- .../java/io/jooby/output/ChunkedOutput.java | 13 ---- ...put.java => CompsiteByteBufferOutput.java} | 17 ++--- ...Factory.java => DefaultOutputFactory.java} | 14 ++-- .../src/main/java/io/jooby/output/Output.java | 15 +++-- .../java/io/jooby/output/OutputFactory.java | 4 +- .../io/jooby/output/OutputOutputStream.java | 2 +- .../java/io/jooby/output/OutputWriter.java | 2 +- .../java/io/jooby/output/package-info.java | 2 + .../java/io/jooby/ServerSentMessageTest.java | 8 +-- .../test/java/io/jooby/buffer/Issue3434.java | 4 +- .../io/jooby/output/BufferedOutputTest.java | 44 ++++++------ .../test/java/io/jooby/gson/Issue3434.java | 4 +- .../jooby/jackson/JacksonJsonModuleTest.java | 6 +- .../internal/jte/DataBufferOutputTest.java | 8 +-- .../netty/output/NettyBufferedOutput.java | 18 ++--- ...dOutput.java => NettyCompositeOutput.java} | 16 ++--- .../netty/output/NettyOutputFactory.java | 5 +- .../io/jooby/rocker/DataBufferOutput.java | 10 +-- .../java/io/jooby/rocker/RockerHandler.java | 2 +- .../io/jooby/rocker/RockerMessageEncoder.java | 2 +- .../main/java/io/jooby/test/MockContext.java | 4 +- .../io/jooby/yasson/YassonModuleTest.java | 4 +- .../avaje/jsonb/AvajeJsonbEncoderBench.java | 6 +- ...nOutputBench.java => JsonOutputBench.java} | 16 ++--- .../java/io/jooby/junit/ServerTestRunner.java | 4 +- 31 files changed, 135 insertions(+), 215 deletions(-) rename jooby/src/main/java/io/jooby/output/{ByteBufferOutputImpl.java => ByteBufferOut.java} (91%) rename jooby/src/main/java/io/jooby/output/{WrappedOutput.java => ByteBufferWrappedOutput.java} (77%) delete mode 100644 jooby/src/main/java/io/jooby/output/ChunkedOutput.java rename jooby/src/main/java/io/jooby/output/{ByteBufferChunkedOutput.java => CompsiteByteBufferOutput.java} (86%) rename jooby/src/main/java/io/jooby/output/{ByteBufferOutputFactory.java => DefaultOutputFactory.java} (57%) create mode 100644 jooby/src/main/java/io/jooby/output/package-info.java rename modules/jooby-netty/src/main/java/io/jooby/netty/output/{NettyChunkedOutput.java => NettyCompositeOutput.java} (80%) rename tests/src/test/java/io/jooby/avaje/jsonb/{DataBufferJsonOutputBench.java => JsonOutputBench.java} (58%) diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index d1675768f6..ddc41d6c09 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -35,7 +35,7 @@ import io.jooby.internal.MissingValue; import io.jooby.internal.SingleValue; import io.jooby.internal.UrlParser; -import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.DefaultOutputFactory; import io.jooby.output.OutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -656,6 +656,6 @@ default Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { @Override default OutputFactory getOutputFactory() { - return new ByteBufferOutputFactory(); + return new DefaultOutputFactory(); } } diff --git a/jooby/src/main/java/io/jooby/ServerSentMessage.java b/jooby/src/main/java/io/jooby/ServerSentMessage.java index 4e09fe5932..9da10e459a 100644 --- a/jooby/src/main/java/io/jooby/ServerSentMessage.java +++ b/jooby/src/main/java/io/jooby/ServerSentMessage.java @@ -7,9 +7,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.util.function.IntPredicate; import edu.umd.cs.findbugs.annotations.NonNull; @@ -173,68 +170,4 @@ public ServerSentMessage(@NonNull Object data) { throw SneakyThrows.propagate(x); } } - - class ServerSentMessageOutput implements Output { - private final Output delegate; - - public ServerSentMessageOutput(Output delegate) { - this.delegate = delegate; - } - - @Override - public ByteBuffer asByteBuffer() { - return delegate.asByteBuffer(); - } - - @Override - public String asString(@NonNull Charset charset) { - return delegate.asString(charset); - } - - @Override - public void accept(SneakyThrows.Consumer consumer) { - delegate.accept(consumer); - } - - @Override - public int size() { - return delegate.size(); - } - - @Override - public Output write(byte b) { - return write(new byte[] {b}, 0, 1); - } - - @Override - public Output write(byte[] source) { - return write(ByteBuffer.wrap(source)); - } - - @Override - public Output write(byte[] source, int offset, int length) { - var begin = offset; - var len = offset + length; - for (int i = offset; i < len; i++) { - var ch = source[i]; - if (ch == '\n') { - delegate.write(source, begin, i + 1); - if (i < len - 1) { - delegate.write(DATA); - } - begin = i + 1; - } - } - if (begin < len) { - delegate.write(source, begin, len); - } - return this; - } - - @Override - public void send(Context ctx) {} - - @Override - public void close() throws IOException {} - } } diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 9a7b4fccfe..18f813fb53 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -44,7 +44,7 @@ import io.jooby.exception.StatusCodeException; import io.jooby.internal.handler.ServerSentEventHandler; import io.jooby.internal.handler.WebSocketHandler; -import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.DefaultOutputFactory; import io.jooby.output.OutputFactory; import io.jooby.problem.ProblemDetailsHandler; import io.jooby.value.ValueFactory; @@ -172,7 +172,7 @@ public Stack executor(Executor executor) { private ValueFactory valueFactory = new ValueFactory(); - private OutputFactory outputFactory = new ByteBufferOutputFactory(); + private OutputFactory outputFactory = new DefaultOutputFactory(); public RouterImpl() { stack.addLast(new Stack(chi, 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 80fc654f76..677f729b26 100644 --- a/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java +++ b/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java @@ -117,7 +117,7 @@ public void onComplete() { } private static Output prepend(Context ctx, Output data, byte c) { - var buffer = ctx.getOutputFactory().newChunkedOutput(); + var buffer = ctx.getOutputFactory().newCompositeOutput(); buffer.write(c); data.accept(buffer::write); return buffer; diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferOutputImpl.java b/jooby/src/main/java/io/jooby/output/ByteBufferOut.java similarity index 91% rename from jooby/src/main/java/io/jooby/output/ByteBufferOutputImpl.java rename to jooby/src/main/java/io/jooby/output/ByteBufferOut.java index a3623c92e4..29d799efb2 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferOutputImpl.java +++ b/jooby/src/main/java/io/jooby/output/ByteBufferOut.java @@ -5,16 +5,14 @@ */ package io.jooby.output; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -class ByteBufferOutputImpl implements Output { +class ByteBufferOut implements Output { private static final int MAX_CAPACITY = Integer.MAX_VALUE; private static final int CAPACITY_THRESHOLD = 1024 * 1024 * 4; @@ -27,20 +25,20 @@ class ByteBufferOutputImpl implements Output { private int writePosition; - public ByteBufferOutputImpl(boolean direct, int capacity) { + public ByteBufferOut(boolean direct, int capacity) { this.buffer = allocate(capacity, direct); this.capacity = this.buffer.remaining(); } - public ByteBufferOutputImpl(boolean direct) { + public ByteBufferOut(boolean direct) { this(direct, BUFFER_SIZE); } - public ByteBufferOutputImpl(int bufferSize) { + public ByteBufferOut(int bufferSize) { this(false, bufferSize); } - public ByteBufferOutputImpl() { + public ByteBufferOut() { this(BUFFER_SIZE); } @@ -114,8 +112,9 @@ public Output write(@NonNull ByteBuffer source) { } @Override - public void close() throws IOException { + public Output clear() { this.buffer.clear(); + return this; } @Override @@ -125,7 +124,14 @@ public void send(Context ctx) { @Override public String toString() { - return asString(StandardCharsets.UTF_8); + return "readPosition=" + + this.readPosition + + ", writePosition=" + + this.writePosition + + ", size=" + + this.size() + + ", capacity=" + + this.capacity; } /** Calculate the capacity of the buffer. */ diff --git a/jooby/src/main/java/io/jooby/output/WrappedOutput.java b/jooby/src/main/java/io/jooby/output/ByteBufferWrappedOutput.java similarity index 77% rename from jooby/src/main/java/io/jooby/output/WrappedOutput.java rename to jooby/src/main/java/io/jooby/output/ByteBufferWrappedOutput.java index e2c920387f..f823fe1247 100644 --- a/jooby/src/main/java/io/jooby/output/WrappedOutput.java +++ b/jooby/src/main/java/io/jooby/output/ByteBufferWrappedOutput.java @@ -5,7 +5,6 @@ */ package io.jooby.output; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -13,19 +12,19 @@ import io.jooby.Context; import io.jooby.SneakyThrows; -class WrappedOutput implements Output { +class ByteBufferWrappedOutput implements Output { private final ByteBuffer buffer; - public WrappedOutput(byte[] source, int offset, int length) { + public ByteBufferWrappedOutput(byte[] source, int offset, int length) { this(ByteBuffer.wrap(source, offset, length)); } - public WrappedOutput(byte[] source) { + public ByteBufferWrappedOutput(byte[] source) { this(source, 0, source.length); } - public WrappedOutput(ByteBuffer buffer) { + public ByteBufferWrappedOutput(ByteBuffer buffer) { this.buffer = buffer; } @@ -45,7 +44,10 @@ public Output write(byte[] source, int offset, int length) { } @Override - public void close() throws IOException {} + public Output clear() { + buffer.clear(); + return this; + } @Override public int size() { @@ -67,6 +69,11 @@ public String asString(@NonNull Charset charset) { return charset.decode(asByteBuffer()).toString(); } + @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/output/ChunkedOutput.java b/jooby/src/main/java/io/jooby/output/ChunkedOutput.java deleted file mode 100644 index f9a4e4622a..0000000000 --- a/jooby/src/main/java/io/jooby/output/ChunkedOutput.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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.List; - -public interface ChunkedOutput extends Output { - List getChunks(); -} diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferChunkedOutput.java b/jooby/src/main/java/io/jooby/output/CompsiteByteBufferOutput.java similarity index 86% rename from jooby/src/main/java/io/jooby/output/ByteBufferChunkedOutput.java rename to jooby/src/main/java/io/jooby/output/CompsiteByteBufferOutput.java index ba54e24344..e359e0697a 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferChunkedOutput.java +++ b/jooby/src/main/java/io/jooby/output/CompsiteByteBufferOutput.java @@ -5,10 +5,8 @@ */ package io.jooby.output; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -16,16 +14,11 @@ import io.jooby.Context; import io.jooby.SneakyThrows; -class ByteBufferChunkedOutput implements ChunkedOutput { +class CompsiteByteBufferOutput implements Output { private final List chunks = new ArrayList<>(); private int size = 0; private int bufferSizeHint = BUFFER_SIZE; - @Override - public List getChunks() { - return chunks; - } - @Override public int size() { return size; @@ -50,8 +43,10 @@ public Output write(byte[] source, int offset, int length) { } @Override - public void close() throws IOException { - // NOOP + public Output clear() { + chunks.forEach(ByteBuffer::clear); + chunks.clear(); + return this; } /** @@ -86,7 +81,7 @@ public int bufferSizeHint() { @Override public String toString() { - return asString(StandardCharsets.UTF_8); + return "chunks=" + chunks.size() + ", size=" + size; } @Override diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java b/jooby/src/main/java/io/jooby/output/DefaultOutputFactory.java similarity index 57% rename from jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java rename to jooby/src/main/java/io/jooby/output/DefaultOutputFactory.java index a988e0539b..588b2fedc1 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/DefaultOutputFactory.java @@ -7,29 +7,29 @@ import java.nio.ByteBuffer; -public class ByteBufferOutputFactory implements OutputFactory { +public class DefaultOutputFactory implements OutputFactory { @Override public Output newBufferedOutput(int size) { - return new ByteBufferOutputImpl(size); + return new ByteBufferOut(size); } @Override - public ChunkedOutput newChunkedOutput() { - return new ByteBufferChunkedOutput(); + public Output newCompositeOutput() { + return new CompsiteByteBufferOutput(); } @Override public Output wrap(ByteBuffer buffer) { - return new WrappedOutput(buffer); + return new ByteBufferWrappedOutput(buffer); } @Override public Output wrap(byte[] bytes) { - return new WrappedOutput(bytes); + return new ByteBufferWrappedOutput(bytes); } @Override public Output wrap(byte[] bytes, int offset, int length) { - return new WrappedOutput(bytes, offset, length); + return new ByteBufferWrappedOutput(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 index 72f473de49..45e92a2228 100644 --- a/jooby/src/main/java/io/jooby/output/Output.java +++ b/jooby/src/main/java/io/jooby/output/Output.java @@ -5,7 +5,6 @@ */ package io.jooby.output; -import java.io.Closeable; import java.io.OutputStream; import java.io.Writer; import java.nio.ByteBuffer; @@ -19,7 +18,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.SneakyThrows; -public interface Output extends Closeable { +public interface Output { /** Default buffer size: 4k. */ int BUFFER_SIZE = 4096; @@ -177,16 +176,20 @@ default Output write(@NonNull CharBuffer source, @NonNull Charset charset) { return this; } + void send(io.jooby.Context ctx); + + Output clear(); + static Output wrap(ByteBuffer buffer) { - return new WrappedOutput(buffer); + return new ByteBufferWrappedOutput(buffer); } static Output wrap(byte[] bytes) { - return new WrappedOutput(bytes); + return new ByteBufferWrappedOutput(bytes); } static Output wrap(byte[] bytes, int offset, int length) { - return new WrappedOutput(bytes, offset, length); + return new ByteBufferWrappedOutput(bytes, offset, length); } static Output wrap(String value) { @@ -196,6 +199,4 @@ static Output wrap(String value) { static Output wrap(String value, Charset charset) { return wrap(value.getBytes(charset)); } - - 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 index e7b55814fe..578f8dc406 100644 --- a/jooby/src/main/java/io/jooby/output/OutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/OutputFactory.java @@ -16,6 +16,8 @@ default Output newBufferedOutput() { return newBufferedOutput(Output.BUFFER_SIZE); } + Output newCompositeOutput(); + default Output wrap(String value) { return wrap(value, StandardCharsets.UTF_8); } @@ -29,6 +31,4 @@ default Output wrap(String value, Charset charset) { Output wrap(byte[] bytes); Output wrap(byte[] bytes, int offset, int length); - - ChunkedOutput newChunkedOutput(); } diff --git a/jooby/src/main/java/io/jooby/output/OutputOutputStream.java b/jooby/src/main/java/io/jooby/output/OutputOutputStream.java index aac1aae899..41bf612c11 100644 --- a/jooby/src/main/java/io/jooby/output/OutputOutputStream.java +++ b/jooby/src/main/java/io/jooby/output/OutputOutputStream.java @@ -45,7 +45,7 @@ public void close() throws IOException { return; } this.closed = true; - output.close(); + output.clear(); } private void checkClosed() throws IOException { diff --git a/jooby/src/main/java/io/jooby/output/OutputWriter.java b/jooby/src/main/java/io/jooby/output/OutputWriter.java index 8e48dc4463..e4e8025c0f 100644 --- a/jooby/src/main/java/io/jooby/output/OutputWriter.java +++ b/jooby/src/main/java/io/jooby/output/OutputWriter.java @@ -60,7 +60,7 @@ public void close() throws IOException { return; } this.closed = true; - output.close(); + output.clear(); } private void checkClosed() throws IOException { diff --git a/jooby/src/main/java/io/jooby/output/package-info.java b/jooby/src/main/java/io/jooby/output/package-info.java new file mode 100644 index 0000000000..6dbbe0ce94 --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/package-info.java @@ -0,0 +1,2 @@ +@edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault +package io.jooby.output; diff --git a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java index 85f6ab21ff..0cd7f1b128 100644 --- a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java +++ b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; -import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.DefaultOutputFactory; public class ServerSentMessageTest { @@ -22,7 +22,7 @@ public void shouldFormatMessage() throws Exception { var data = "some"; var ctx = mock(Context.class); - var bufferFactory = new ByteBufferOutputFactory(); + var bufferFactory = new DefaultOutputFactory(); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) @@ -42,7 +42,7 @@ public void shouldFormatMultiLineMessage() throws Exception { var data = "line 1\n line ,a .. 2\nline ...abc 3"; var ctx = mock(Context.class); - var bufferFactory = new ByteBufferOutputFactory(); + var bufferFactory = new DefaultOutputFactory(); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) @@ -64,7 +64,7 @@ public void shouldFormatMessageEndingWithNL() throws Exception { var data = "line 1\n"; var ctx = mock(Context.class); - var bufferFactory = new ByteBufferOutputFactory(); + var bufferFactory = new DefaultOutputFactory(); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) diff --git a/jooby/src/test/java/io/jooby/buffer/Issue3434.java b/jooby/src/test/java/io/jooby/buffer/Issue3434.java index cd1a842bff..c840a527e4 100644 --- a/jooby/src/test/java/io/jooby/buffer/Issue3434.java +++ b/jooby/src/test/java/io/jooby/buffer/Issue3434.java @@ -15,11 +15,11 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; -import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.DefaultOutputFactory; import io.jooby.output.OutputFactory; public class Issue3434 { - OutputFactory factory = new ByteBufferOutputFactory(); + OutputFactory factory = new DefaultOutputFactory(); @Test void shouldWriteCharBufferOnBufferWriter() throws IOException { diff --git a/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java b/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java index 7e717cdd54..d0948b418f 100644 --- a/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java +++ b/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java @@ -20,13 +20,13 @@ public class BufferedOutputTest { @Test public void bufferedOutput() { - buffered( + 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!", buffered.toString()); + assertEquals("Hello World!", buffered.asString(StandardCharsets.UTF_8)); assertEquals(12, buffered.size()); assertEquals( "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); @@ -36,13 +36,13 @@ public void bufferedOutput() { StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(30, buffered.size()); }, - new ByteBufferOutputImpl(3)); + new ByteBufferOut(3)); - buffered( + 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!", buffered.toString()); + assertEquals("Hello World!", buffered.asString(StandardCharsets.UTF_8)); assertEquals(12, buffered.size()); assertEquals( "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); @@ -52,33 +52,33 @@ public void bufferedOutput() { StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(30, buffered.size()); }, - new ByteBufferOutputImpl()); + new ByteBufferOut()); - buffered( + output( buffered -> { var bytes = "xxHello World!xx".getBytes(StandardCharsets.UTF_8); buffered.write(bytes, 2, bytes.length - 4); - assertEquals("Hello World!", buffered.toString()); + assertEquals("Hello World!", buffered.asString(StandardCharsets.UTF_8)); assertEquals(12, buffered.size()); }, - new ByteBufferOutputImpl()); + new ByteBufferOut()); - buffered( + output( buffered -> { buffered.write((byte) 'A'); - assertEquals("A", buffered.toString()); + assertEquals("A", buffered.asString(StandardCharsets.UTF_8)); assertEquals(1, buffered.size()); }, - new ByteBufferOutputImpl()); + new ByteBufferOut()); } - private void buffered(SneakyThrows.Consumer consumer, Output... buffers) { + private void output(SneakyThrows.Consumer consumer, Output... buffers) { Stream.of(buffers).forEach(consumer); } @Test public void chunkedOutput() { - chunked( + output( chunked -> { var buffer = ByteBuffer.allocateDirect(6); buffer.put("rld!".getBytes(StandardCharsets.UTF_8)); @@ -93,23 +93,17 @@ public void chunkedOutput() { assertEquals(8, chunked.size()); chunked.write(buffer); assertEquals(12, chunked.size()); - assertEquals("Hello World!", chunked.toString()); + assertEquals("Hello World!", chunked.asString(StandardCharsets.UTF_8)); assertEquals(12, chunked.size()); - assertEquals(5, chunked.getChunks().size()); }, - new ByteBufferChunkedOutput()); + new CompsiteByteBufferOutput()); } @Test public void wrapOutput() throws IOException { var bytes = "xxHello World!xx".getBytes(StandardCharsets.UTF_8); - try (var output = new WrappedOutput(bytes, 2, bytes.length - 4)) { - assertEquals("Hello World!", output.asString(StandardCharsets.UTF_8)); - assertEquals(12, output.size()); - } - } - - private void chunked(SneakyThrows.Consumer consumer, ChunkedOutput... chunks) { - Stream.of(chunks).forEach(consumer); + var output = new ByteBufferWrappedOutput(bytes, 2, bytes.length - 4); + assertEquals("Hello World!", output.asString(StandardCharsets.UTF_8)); + assertEquals(12, output.size()); } } 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 0fe5d3abc9..1affa4031a 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,7 @@ import com.google.gson.GsonBuilder; import io.jooby.Context; -import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.DefaultOutputFactory; public class Issue3434 { String text = @@ -139,7 +139,7 @@ public class Issue3434 { @Test void shouldEncodeUsingBufferWriter() { var gson = new GsonBuilder().create(); - var factory = new ByteBufferOutputFactory(); + var factory = new DefaultOutputFactory(); var ctx = mock(Context.class); when(ctx.getOutputFactory()).thenReturn(factory); 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 0ac235708b..a2deb15542 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,14 +21,14 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.DefaultOutputFactory; public class JacksonJsonModuleTest { @Test public void renderJson() throws Exception { Context ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(new ByteBufferOutputFactory()); + when(ctx.getOutputFactory()).thenReturn(new DefaultOutputFactory()); JacksonModule jackson = new JacksonModule(new ObjectMapper()); @@ -57,7 +57,7 @@ public void parseJson() throws Exception { @Test public void renderXml() throws Exception { Context ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(new ByteBufferOutputFactory()); + when(ctx.getOutputFactory()).thenReturn(new DefaultOutputFactory()); JacksonModule jackson = new JacksonModule(new XmlMapper()); 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 index 1e55b68eaf..650ea1304b 100644 --- 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 @@ -11,13 +11,13 @@ import org.junit.jupiter.api.Test; -import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.DefaultOutputFactory; public class DataBufferOutputTest { @Test public void checkWriteContent() { - var factory = new ByteBufferOutputFactory(); + var factory = new DefaultOutputFactory(); var buffer = factory.newBufferedOutput(); var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); output.writeContent("Hello"); @@ -26,7 +26,7 @@ public void checkWriteContent() { @Test public void checkWriteContentSubstring() { - var factory = new ByteBufferOutputFactory(); + var factory = new DefaultOutputFactory(); var buffer = factory.newBufferedOutput(); var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); output.writeContent(" Hello World! ", 1, " Hello World! ".length() - 2); @@ -35,7 +35,7 @@ public void checkWriteContentSubstring() { @Test public void checkWriteBinaryContent() { - var factory = new ByteBufferOutputFactory(); + var factory = new DefaultOutputFactory(); var buffer = factory.newBufferedOutput(); var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); output.writeBinaryContent("Hello".getBytes(StandardCharsets.UTF_8)); diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyBufferedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyBufferedOutput.java index 65b0b4813b..3e238643eb 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyBufferedOutput.java +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyBufferedOutput.java @@ -5,7 +5,6 @@ */ package io.jooby.netty.output; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -25,17 +24,17 @@ protected NettyBufferedOutput(ByteBuf buffer) { } @Override - public ByteBuf byteBuf() { + @NonNull public ByteBuf byteBuf() { return this.buffer; } @Override - public ByteBuffer asByteBuffer() { + @NonNull public ByteBuffer asByteBuffer() { return this.buffer.nioBuffer(); } @Override - public String asString(@NonNull Charset charset) { + @NonNull public String asString(@NonNull Charset charset) { return this.buffer.toString(charset); } @@ -50,19 +49,19 @@ public int size() { } @Override - public Output write(byte b) { + @NonNull public Output write(byte b) { buffer.writeByte(b); return this; } @Override - public Output write(byte[] source) { + @NonNull public Output write(byte[] source) { buffer.writeBytes(source); return this; } @Override - public Output write(byte[] source, int offset, int length) { + @NonNull public Output write(byte[] source, int offset, int length) { this.buffer.writeBytes(source, offset, length); return this; } @@ -77,5 +76,8 @@ public void send(Context ctx) { } @Override - public void close() throws IOException {} + @NonNull public Output clear() { + this.buffer.clear(); + return this; + } } diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyChunkedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyCompositeOutput.java similarity index 80% rename from modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyChunkedOutput.java rename to modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyCompositeOutput.java index fd9ecab19c..e5cb7b3587 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyChunkedOutput.java +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyCompositeOutput.java @@ -5,26 +5,22 @@ */ package io.jooby.netty.output; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.List; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; import io.jooby.internal.netty.NettyContext; -import io.jooby.output.ChunkedOutput; import io.jooby.output.Output; import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; -class NettyChunkedOutput implements ChunkedOutput, NettyOutput { +class NettyCompositeOutput implements NettyOutput { private final CompositeByteBuf buffer; - protected NettyChunkedOutput(CompositeByteBuf buffer) { + protected NettyCompositeOutput(CompositeByteBuf buffer) { this.buffer = buffer; } @@ -81,10 +77,8 @@ public void send(Context ctx) { } @Override - public void close() throws IOException {} - - @Override - public List getChunks() { - return Arrays.asList(buffer.nioBuffers()); + public Output clear() { + this.buffer.clear(); + return this; } } diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java index c1fa7a0dd1..1a01cc70f7 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java @@ -7,7 +7,6 @@ import java.nio.ByteBuffer; -import io.jooby.output.ChunkedOutput; import io.jooby.output.Output; import io.jooby.output.OutputFactory; import io.netty.buffer.ByteBufAllocator; @@ -67,7 +66,7 @@ public Output wrap(byte[] bytes, int offset, int length) { } @Override - public ChunkedOutput newChunkedOutput() { - return new NettyChunkedOutput(allocator.compositeBuffer(48)); + public Output newCompositeOutput() { + return new NettyCompositeOutput(allocator.compositeBuffer(48)); } } diff --git a/modules/jooby-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java b/modules/jooby-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java index fdc53ab5bc..fac9fc1caa 100644 --- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java +++ b/modules/jooby-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java @@ -10,7 +10,7 @@ import com.fizzed.rocker.ContentType; import com.fizzed.rocker.RockerOutput; import com.fizzed.rocker.RockerOutputFactory; -import io.jooby.output.ChunkedOutput; +import io.jooby.output.Output; import io.jooby.output.OutputFactory; /** @@ -27,9 +27,9 @@ public class DataBufferOutput implements RockerOutput { private final ContentType contentType; /** The buffer where data is stored. */ - protected ChunkedOutput buffer; + protected Output buffer; - DataBufferOutput(Charset charset, ContentType contentType, ChunkedOutput buffer) { + DataBufferOutput(Charset charset, ContentType contentType, Output buffer) { this.charset = charset; this.contentType = contentType; this.buffer = buffer; @@ -67,13 +67,13 @@ public int getByteLength() { * * @return Byte buffer. */ - public ChunkedOutput toBuffer() { + public Output asOutput() { return buffer; } static RockerOutputFactory factory( Charset charset, OutputFactory factory, int bufferSize) { return (contentType, charsetName) -> - new DataBufferOutput(charset, contentType, factory.newChunkedOutput()); + new DataBufferOutput(charset, contentType, factory.newCompositeOutput()); } } 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 index 5d9caf922f..c59d69da43 100644 --- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerHandler.java +++ b/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerHandler.java @@ -24,7 +24,7 @@ public Route.Handler apply(@NonNull Route.Handler next) { try { RockerModel template = (RockerModel) next.apply(ctx); ctx.setResponseType(MediaType.html); - return ctx.send(template.render(factory).toBuffer()); + return ctx.send(template.render(factory).asOutput()); } 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 d7a7d5c743..bf9575ac42 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 @@ -26,7 +26,7 @@ public Output encode(@NonNull Context ctx, @NonNull Object value) { 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-test/src/main/java/io/jooby/test/MockContext.java b/modules/jooby-test/src/main/java/io/jooby/test/MockContext.java index 2b044c3757..9e0a5f1192 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 @@ -54,7 +54,7 @@ import io.jooby.StatusCode; import io.jooby.WebSocket; import io.jooby.exception.TypeMismatchException; -import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.DefaultOutputFactory; import io.jooby.output.Output; import io.jooby.output.OutputFactory; import io.jooby.value.Value; @@ -119,7 +119,7 @@ public class MockContext implements DefaultContext { private int port = -1; - private OutputFactory outputFactory = new ByteBufferOutputFactory(); + private OutputFactory outputFactory = new DefaultOutputFactory(); @Override public String getMethod() { 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 25fb254e32..d33a3db5c4 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,7 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.DefaultOutputFactory; public class YassonModuleTest { @@ -39,7 +39,7 @@ public void render() { user.age = Integer.MAX_VALUE; Context ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(new ByteBufferOutputFactory()); + when(ctx.getOutputFactory()).thenReturn(new DefaultOutputFactory()); var buffer = YassonModule.encode(ctx, user); assertEquals( "{\"age\":2147483647,\"id\":-1,\"name\":\"Lorem €@!?\"}", diff --git a/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java b/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java index 06e5b6d5d2..f1d801f58b 100644 --- a/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java +++ b/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java @@ -11,7 +11,7 @@ import org.openjdk.jmh.annotations.*; import io.avaje.jsonb.Jsonb; -import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.DefaultOutputFactory; import io.jooby.output.Output; import io.jooby.output.OutputFactory; @@ -37,7 +37,7 @@ public class AvajeJsonbEncoderBench { public void setup() { message = Map.of("id", 98, "value", "Hello World"); jsonb = Jsonb.builder().build(); - factory = new ByteBufferOutputFactory(); + factory = new DefaultOutputFactory(); } @Benchmark @@ -48,7 +48,7 @@ public void withJsonBuffer() { @Benchmark public void withDataBufferOutputStream() { var buffer = cache.get(); - jsonb.toJson(message, jsonb.writer(new DataBufferJsonOutputBench(buffer))); + jsonb.toJson(message, jsonb.writer(new JsonOutputBench(buffer))); // TODO: add clear to output or use close buffer.asByteBuffer().clear(); } diff --git a/tests/src/test/java/io/jooby/avaje/jsonb/DataBufferJsonOutputBench.java b/tests/src/test/java/io/jooby/avaje/jsonb/JsonOutputBench.java similarity index 58% rename from tests/src/test/java/io/jooby/avaje/jsonb/DataBufferJsonOutputBench.java rename to tests/src/test/java/io/jooby/avaje/jsonb/JsonOutputBench.java index c790843722..a028fcc1d9 100644 --- a/tests/src/test/java/io/jooby/avaje/jsonb/DataBufferJsonOutputBench.java +++ b/tests/src/test/java/io/jooby/avaje/jsonb/JsonOutputBench.java @@ -11,27 +11,27 @@ import io.avaje.json.stream.JsonOutput; import io.jooby.output.Output; -public class DataBufferJsonOutputBench implements JsonOutput { +public class JsonOutputBench implements JsonOutput { - private Output buffer; + private final Output output; - public DataBufferJsonOutputBench(Output buffer) { - this.buffer = buffer; + public JsonOutputBench(Output output) { + this.output = output; } @Override public void write(byte[] content, int offset, int length) throws IOException { - buffer.write(content, offset, length); + output.write(content, offset, length); } @Override - public void flush() throws IOException {} + public void flush() {} @Override public OutputStream unwrapOutputStream() { - return this.buffer.asOutputStream(); + return this.output.asOutputStream(); } @Override - public void close() throws IOException {} + public void close() {} } diff --git a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index 22cfd82e98..068d18bddb 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -30,7 +30,7 @@ import io.jooby.StatusCode; import io.jooby.internal.MutedServer; import io.jooby.netty.NettyServer; -import io.jooby.output.ByteBufferOutputFactory; +import io.jooby.output.DefaultOutputFactory; import io.jooby.test.WebClient; public class ServerTestRunner { @@ -92,7 +92,7 @@ public void ready(SneakyThrows.Consumer2 onReady) { System.setProperty("___server_name__", server.getName()); var app = provider.get(); if (!(server instanceof NettyServer)) { - app.setOutputFactory(new ByteBufferOutputFactory()); + app.setOutputFactory(new DefaultOutputFactory()); } Optional.ofNullable(executionMode).ifPresent(app::setExecutionMode); // Reduce log from maven build: From 056d55f8c794fe9f0d29b6e03efb09aa82b78f66 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 26 Jun 2025 15:57:10 -0300 Subject: [PATCH 17/60] output api: code clean up + refactor --- .../java/io/jooby/output/ByteBufferOut.java | 7 ++ .../src/main/java/io/jooby/output/Output.java | 30 +++---- .../io/jooby/output/OutputOutputStream.java | 4 +- .../jooby/avaje/jsonb/AvajeJsonbModule.java | 4 +- ...sonOutput.java => BufferedJsonOutput.java} | 4 +- .../main/java/io/jooby/jetty/JettyServer.java | 3 - ...utput.java => BufferedTemplateOutput.java} | 4 +- .../jooby/internal/jte/JteModelEncoder.java | 3 +- .../java/io/jooby/jte/JteTemplateEngine.java | 4 +- ...t.java => BufferedTemplateOutputTest.java} | 8 +- .../netty}/NettyBufferedOutput.java | 15 +++- .../netty}/NettyOutputFactory.java | 33 +++----- .../io/jooby/internal/netty/NettySender.java | 12 +-- .../netty/NettyServerSentEmitter.java | 13 +-- .../jooby/internal/netty/NettyWebSocket.java | 15 +--- .../main/java/io/jooby/netty/NettyServer.java | 1 - .../netty/output/NettyCompositeOutput.java | 84 ------------------- .../io/jooby/netty/output/NettyOutput.java | 36 -------- .../io.jooby.buffer.DataBufferFactory | 1 - ...rOutput.java => BufferedRockerOutput.java} | 24 +++--- .../java/io/jooby/rocker/RockerHandler.java | 4 +- .../io/jooby/rocker/RockerMessageEncoder.java | 4 +- .../java/io/jooby/rocker/RockerModule.java | 6 +- .../UndertowServerSentConnection.java | 4 +- .../avaje/jsonb/AvajeJsonbEncoderBench.java | 5 +- 25 files changed, 92 insertions(+), 236 deletions(-) rename modules/jooby-avaje-jsonb/src/main/java/io/jooby/internal/avaje/jsonb/{DataBufferJsonOutput.java => BufferedJsonOutput.java} (87%) rename modules/jooby-jte/src/main/java/io/jooby/internal/jte/{DataBufferOutput.java => BufferedTemplateOutput.java} (84%) rename modules/jooby-jte/src/test/java/io/jooby/internal/jte/{DataBufferOutputTest.java => BufferedTemplateOutputTest.java} (81%) rename modules/jooby-netty/src/main/java/io/jooby/{netty/output => internal/netty}/NettyBufferedOutput.java (82%) rename modules/jooby-netty/src/main/java/io/jooby/{netty/output => internal/netty}/NettyOutputFactory.java (62%) delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyCompositeOutput.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutput.java delete mode 100644 modules/jooby-netty/src/main/resources/META-INF/services/io.jooby.buffer.DataBufferFactory rename modules/jooby-rocker/src/main/java/io/jooby/rocker/{DataBufferOutput.java => BufferedRockerOutput.java} (68%) diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferOut.java b/jooby/src/main/java/io/jooby/output/ByteBufferOut.java index 29d799efb2..096a11e510 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferOut.java +++ b/jooby/src/main/java/io/jooby/output/ByteBufferOut.java @@ -7,6 +7,8 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.util.Iterator; +import java.util.List; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; @@ -59,6 +61,11 @@ public void accept(SneakyThrows.Consumer consumer) { consumer.accept(asByteBuffer()); } + @Override + public Iterator iterator() { + return List.of(asByteBuffer()).iterator(); + } + private int writableByteCount() { return this.capacity - this.writePosition; } diff --git a/jooby/src/main/java/io/jooby/output/Output.java b/jooby/src/main/java/io/jooby/output/Output.java index 45e92a2228..0665d0a741 100644 --- a/jooby/src/main/java/io/jooby/output/Output.java +++ b/jooby/src/main/java/io/jooby/output/Output.java @@ -42,6 +42,21 @@ default void toByteBuffer(ByteBuffer dest) { toByteBuffer(0, dest, dest.position(), 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 + */ + default void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { + dest = dest.duplicate().clear(); + dest.put(destPos, asByteBuffer(), srcPos, length); + } + default Iterator split(IntPredicate predicate) { // TODO: fix me for chunks var buffer = asByteBuffer(); @@ -60,21 +75,6 @@ default Iterator split(IntPredicate predicate) { return chunks.iterator(); } - /** - * 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 - */ - default void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - dest = dest.duplicate().clear(); - dest.put(destPos, asByteBuffer(), srcPos, length); - } - void accept(SneakyThrows.Consumer consumer); default Iterator iterator() { diff --git a/jooby/src/main/java/io/jooby/output/OutputOutputStream.java b/jooby/src/main/java/io/jooby/output/OutputOutputStream.java index 41bf612c11..4dd2266ae0 100644 --- a/jooby/src/main/java/io/jooby/output/OutputOutputStream.java +++ b/jooby/src/main/java/io/jooby/output/OutputOutputStream.java @@ -21,8 +21,8 @@ final class OutputOutputStream extends OutputStream { private boolean closed; - public OutputOutputStream(@NonNull Output dataBuffer) { - this.output = dataBuffer; + public OutputOutputStream(@NonNull Output output) { + this.output = output; } @Override 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 a03cbbc001..4c21bdc16e 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,7 +18,7 @@ import io.jooby.MessageDecoder; import io.jooby.MessageEncoder; import io.jooby.ServiceRegistry; -import io.jooby.internal.avaje.jsonb.DataBufferJsonOutput; +import io.jooby.internal.avaje.jsonb.BufferedJsonOutput; import io.jooby.output.Output; /** @@ -109,7 +109,7 @@ public Output encode(@NonNull Context ctx, @NonNull Object value) { ctx.setDefaultResponseType(MediaType.json); var factory = ctx.getOutputFactory(); var buffer = factory.newBufferedOutput(); - try (var writer = jsonb.writer(new DataBufferJsonOutput(buffer))) { + try (var writer = jsonb.writer(new BufferedJsonOutput(buffer))) { jsonb.toJson(value, writer); return buffer; } diff --git a/modules/jooby-avaje-jsonb/src/main/java/io/jooby/internal/avaje/jsonb/DataBufferJsonOutput.java b/modules/jooby-avaje-jsonb/src/main/java/io/jooby/internal/avaje/jsonb/BufferedJsonOutput.java similarity index 87% rename from modules/jooby-avaje-jsonb/src/main/java/io/jooby/internal/avaje/jsonb/DataBufferJsonOutput.java rename to modules/jooby-avaje-jsonb/src/main/java/io/jooby/internal/avaje/jsonb/BufferedJsonOutput.java index 55867c6d7e..d10231c814 100644 --- a/modules/jooby-avaje-jsonb/src/main/java/io/jooby/internal/avaje/jsonb/DataBufferJsonOutput.java +++ b/modules/jooby-avaje-jsonb/src/main/java/io/jooby/internal/avaje/jsonb/BufferedJsonOutput.java @@ -11,10 +11,10 @@ import io.avaje.json.stream.JsonOutput; import io.jooby.output.Output; -public class DataBufferJsonOutput implements JsonOutput { +public class BufferedJsonOutput implements JsonOutput { private final Output output; - public DataBufferJsonOutput(Output output) { + public BufferedJsonOutput(Output output) { this.output = output; } 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 4317ca55f5..5dd5a03169 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 @@ -54,9 +54,6 @@ public class JettyServer extends io.jooby.Server.Base { private Consumer httpConfigurer; - // TODO: integrate buffer factory with Jetty. - // private DataBufferFactory bufferFactory; - public JettyServer(@NonNull QueuedThreadPool threadPool) { this.threadPool = threadPool; } 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 84% 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 6bdfcbdafc..54f0713bd8 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 @@ -11,11 +11,11 @@ import gg.jte.TemplateOutput; import io.jooby.output.Output; -public class DataBufferOutput implements TemplateOutput { +public class BufferedTemplateOutput implements TemplateOutput { private final Output buffer; private final Charset charset; - public DataBufferOutput(Output buffer, Charset charset) { + public BufferedTemplateOutput(Output 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 7dbc046271..e36ae3f54c 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 @@ -18,8 +18,7 @@ public class JteModelEncoder implements io.jooby.MessageEncoder { public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { if (value instanceof JteModel jte) { var buffer = ctx.getOutputFactory().newBufferedOutput(); - var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); - jte.render(output); + 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 4b6c0b91f1..36b81d2790 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,7 +14,7 @@ import io.jooby.Context; import io.jooby.MapModelAndView; import io.jooby.ModelAndView; -import io.jooby.internal.jte.DataBufferOutput; +import io.jooby.internal.jte.BufferedTemplateOutput; import io.jooby.output.Output; class JteTemplateEngine implements io.jooby.TemplateEngine { @@ -34,7 +34,7 @@ public List extensions() { @Override public Output render(Context ctx, ModelAndView modelAndView) { var buffer = ctx.getOutputFactory().newBufferedOutput(); - var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); + 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/DataBufferOutputTest.java b/modules/jooby-jte/src/test/java/io/jooby/internal/jte/BufferedTemplateOutputTest.java similarity index 81% rename from modules/jooby-jte/src/test/java/io/jooby/internal/jte/DataBufferOutputTest.java rename to modules/jooby-jte/src/test/java/io/jooby/internal/jte/BufferedTemplateOutputTest.java index 650ea1304b..6f16c7ac2e 100644 --- a/modules/jooby-jte/src/test/java/io/jooby/internal/jte/DataBufferOutputTest.java +++ b/modules/jooby-jte/src/test/java/io/jooby/internal/jte/BufferedTemplateOutputTest.java @@ -13,13 +13,13 @@ import io.jooby.output.DefaultOutputFactory; -public class DataBufferOutputTest { +public class BufferedTemplateOutputTest { @Test public void checkWriteContent() { var factory = new DefaultOutputFactory(); var buffer = factory.newBufferedOutput(); - var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); + var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeContent("Hello"); assertEquals("Hello", buffer.asString(StandardCharsets.UTF_8)); } @@ -28,7 +28,7 @@ public void checkWriteContent() { public void checkWriteContentSubstring() { var factory = new DefaultOutputFactory(); var buffer = factory.newBufferedOutput(); - var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); + var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeContent(" Hello World! ", 1, " Hello World! ".length() - 2); assertEquals("Hello World", buffer.asString(StandardCharsets.UTF_8)); } @@ -37,7 +37,7 @@ public void checkWriteContentSubstring() { public void checkWriteBinaryContent() { var factory = new DefaultOutputFactory(); var buffer = factory.newBufferedOutput(); - var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8); + var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeBinaryContent("Hello".getBytes(StandardCharsets.UTF_8)); assertEquals("Hello", buffer.asString(StandardCharsets.UTF_8)); } diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyBufferedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java similarity index 82% rename from modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyBufferedOutput.java rename to modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java index 3e238643eb..6b2783ad0f 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyBufferedOutput.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.netty.output; +package io.jooby.internal.netty; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -11,11 +11,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.internal.netty.NettyContext; import io.jooby.output.Output; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; -class NettyBufferedOutput implements NettyOutput { +public class NettyBufferedOutput implements Output { private final ByteBuf buffer; @@ -23,7 +23,14 @@ protected NettyBufferedOutput(ByteBuf buffer) { this.buffer = buffer; } - @Override + public static ByteBuf byteBuf(Output output) { + if (output instanceof NettyBufferedOutput netty) { + return netty.byteBuf(); + } else { + return Unpooled.wrappedBuffer(output.asByteBuffer()); + } + } + @NonNull public ByteBuf byteBuf() { return this.buffer; } diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.java similarity index 62% rename from modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java rename to modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.java index 1a01cc70f7..d3010157a4 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutputFactory.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.java @@ -3,17 +3,18 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.netty.output; +package io.jooby.internal.netty; import java.nio.ByteBuffer; +import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.output.Output; import io.jooby.output.OutputFactory; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.util.ResourceLeakDetector; -public class NettyOutputFactory implements OutputFactory { +public record NettyOutputFactory(ByteBufAllocator allocator) implements OutputFactory { private static final String LEAK_DETECTION = "io.netty.leakDetection.level"; static { @@ -24,49 +25,37 @@ public class NettyOutputFactory implements OutputFactory { ResourceLeakDetector.Level.valueOf(System.getProperty(LEAK_DETECTION))); } - private final ByteBufAllocator allocator; - /** - * Create a new {@code NettyDataBufferFactory} based on the given factory. + * Create a new {@code OutputFactory} based on the given factory. * * @param allocator the factory to use * @see io.netty.buffer.PooledByteBufAllocator * @see io.netty.buffer.UnpooledByteBufAllocator */ - public NettyOutputFactory(ByteBufAllocator allocator) { - this.allocator = allocator; - } - - public NettyOutputFactory() { - this(ByteBufAllocator.DEFAULT); - } - - public ByteBufAllocator allocator() { - return allocator; - } + public NettyOutputFactory {} @Override - public Output newBufferedOutput(int size) { + @NonNull public Output newBufferedOutput(int size) { return new NettyBufferedOutput(this.allocator.buffer(size)); } @Override - public Output wrap(ByteBuffer buffer) { + @NonNull public Output wrap(ByteBuffer buffer) { return new NettyBufferedOutput(Unpooled.wrappedBuffer(buffer)); } @Override - public Output wrap(byte[] bytes) { + @NonNull public Output wrap(byte[] bytes) { return new NettyBufferedOutput(Unpooled.wrappedBuffer(bytes)); } @Override - public Output wrap(byte[] bytes, int offset, int length) { + @NonNull public Output wrap(byte[] bytes, int offset, int length) { return new NettyBufferedOutput(Unpooled.wrappedBuffer(bytes, offset, length)); } @Override - public Output newCompositeOutput() { - return new NettyCompositeOutput(allocator.compositeBuffer(48)); + @NonNull public Output newCompositeOutput() { + return new NettyBufferedOutput(allocator.compositeBuffer(48)); } } 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 72877eb579..3e0f737062 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,11 +5,11 @@ */ package io.jooby.internal.netty; +import static io.jooby.internal.netty.NettyBufferedOutput.byteBuf; + import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Sender; -import io.jooby.netty.output.NettyOutput; import io.jooby.output.Output; -import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; @@ -36,14 +36,8 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { @NonNull @Override public Sender write(@NonNull Output output, @NonNull Callback callback) { - ByteBuf buf; - if (output instanceof NettyOutput netty) { - buf = netty.byteBuf(); - } else { - buf = Unpooled.wrappedBuffer(output.asByteBuffer()); - } context - .writeAndFlush(new DefaultHttpContent(buf)) + .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 5293dd8442..141cd97b8e 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.NettyBufferedOutput.byteBuf; + import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -18,9 +20,6 @@ import io.jooby.ServerSentEmitter; import io.jooby.ServerSentMessage; import io.jooby.SneakyThrows; -import io.jooby.netty.output.NettyOutput; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; import io.netty.channel.EventLoop; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; @@ -67,13 +66,7 @@ public Context getContext() { public ServerSentEmitter send(ServerSentMessage data) { if (checkOpen()) { var output = data.encode(netty); - ByteBuf byteBuf; - if (output instanceof NettyOutput outputNetty) { - byteBuf = outputNetty.byteBuf(); - } else { - byteBuf = Unpooled.wrappedBuffer(output.asByteBuffer()); - } - netty.ctx.writeAndFlush(byteBuf).addListener(this); + 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/NettyWebSocket.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWebSocket.java index c60c21d872..23df3bd3fc 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.NettyBufferedOutput.byteBuf; + import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -26,7 +28,6 @@ import io.jooby.WebSocketCloseStatus; import io.jooby.WebSocketConfigurer; import io.jooby.WebSocketMessage; -import io.jooby.netty.output.NettyOutput; import io.jooby.output.Output; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -140,20 +141,12 @@ public WebSocket sendBinary(@NonNull byte[] message, @NonNull WriteCallback call @NonNull @Override public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { - if (message instanceof NettyOutput netty) { - return sendMessage(netty.byteBuf(), false, callback); - } else { - return sendMessage(Unpooled.wrappedBuffer(message.asByteBuffer()), false, callback); - } + return sendMessage(byteBuf(message), false, callback); } @NonNull @Override public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { - if (message instanceof NettyOutput netty) { - return sendMessage(netty.byteBuf(), false, callback); - } else { - return sendMessage(Unpooled.wrappedBuffer(message.asByteBuffer()), true, callback); - } + return sendMessage(byteBuf(message), true, callback); } @Override 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 6af4d872da..cf9ecca69a 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 @@ -23,7 +23,6 @@ import io.jooby.SneakyThrows; import io.jooby.SslOptions; import io.jooby.internal.netty.*; -import io.jooby.netty.output.NettyOutputFactory; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelOption; diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyCompositeOutput.java b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyCompositeOutput.java deleted file mode 100644 index e5cb7b3587..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyCompositeOutput.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.netty.output; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; - -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Context; -import io.jooby.SneakyThrows; -import io.jooby.internal.netty.NettyContext; -import io.jooby.output.Output; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.CompositeByteBuf; - -class NettyCompositeOutput implements NettyOutput { - - private final CompositeByteBuf buffer; - - protected NettyCompositeOutput(CompositeByteBuf buffer) { - this.buffer = buffer; - } - - @Override - public ByteBuf byteBuf() { - return this.buffer; - } - - @Override - public ByteBuffer asByteBuffer() { - return this.buffer.nioBuffer(); - } - - @Override - public String asString(@NonNull Charset charset) { - return this.buffer.toString(charset); - } - - @Override - public void accept(SneakyThrows.Consumer consumer) { - consumer.accept(asByteBuffer()); - } - - @Override - public int size() { - return buffer.readableBytes(); - } - - @Override - public Output write(byte b) { - buffer.writeByte(b); - return this; - } - - @Override - public Output write(byte[] source) { - buffer.writeBytes(source); - return this; - } - - @Override - public Output write(byte[] source, int offset, int length) { - this.buffer.writeBytes(source, offset, length); - return this; - } - - @Override - public void send(Context ctx) { - if (ctx instanceof NettyContext netty) { - netty.send(this.buffer); - } else { - ctx.send(this.buffer.nioBuffer()); - } - } - - @Override - public Output clear() { - this.buffer.clear(); - return this; - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutput.java b/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutput.java deleted file mode 100644 index af73a7a1e0..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/output/NettyOutput.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.netty.output; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.function.IntPredicate; - -import io.jooby.output.Output; -import io.netty.buffer.ByteBuf; - -public interface NettyOutput extends Output { - ByteBuf byteBuf(); - - @Override - default Iterator split(IntPredicate predicate) { - var buffer = byteBuf(); - var offset = buffer.readerIndex(); - var chunks = new ArrayList(); - for (int i = offset; i < size(); i++) { - var b = buffer.getByte(i); - if (predicate.test(b)) { - chunks.add(buffer.retainedSlice(offset, i + 1).nioBuffer()); - offset = i + 1; - } - } - if (offset < size()) { - chunks.add(buffer.retainedSlice(offset, size()).nioBuffer()); - } - return chunks.iterator(); - } -} 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-rocker/src/main/java/io/jooby/rocker/DataBufferOutput.java b/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java similarity index 68% 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 fac9fc1caa..fee3bd3e01 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 @@ -18,7 +18,7 @@ * * @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 +27,12 @@ public class DataBufferOutput implements RockerOutput { private final ContentType contentType; /** The buffer where data is stored. */ - protected Output buffer; + protected Output output; - DataBufferOutput(Charset charset, ContentType contentType, Output buffer) { + BufferedRockerOutput(Charset charset, ContentType contentType, Output output) { this.charset = charset; this.contentType = contentType; - this.buffer = buffer; + this.output = output; } @Override @@ -46,20 +46,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.size(); + return output.size(); } /** @@ -68,12 +68,12 @@ public int getByteLength() { * @return Byte buffer. */ public Output asOutput() { - return buffer; + return output; } - static RockerOutputFactory factory( + static RockerOutputFactory factory( Charset charset, OutputFactory factory, int bufferSize) { return (contentType, charsetName) -> - new DataBufferOutput(charset, contentType, factory.newCompositeOutput()); + new BufferedRockerOutput(charset, contentType, factory.newCompositeOutput()); } } 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 index c59d69da43..a963904acd 100644 --- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerHandler.java +++ b/modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerHandler.java @@ -12,9 +12,9 @@ import io.jooby.Route; class RockerHandler implements Route.Filter { - private final RockerOutputFactory factory; + private final RockerOutputFactory factory; - RockerHandler(RockerOutputFactory factory) { + RockerHandler(RockerOutputFactory factory) { this.factory = factory; } 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 bf9575ac42..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 @@ -14,9 +14,9 @@ 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; } 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 59ab96224e..7dd3a36934 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 @@ -33,7 +33,7 @@ public RockerModule(@NonNull Charset charset, int bufferSize) { } public RockerModule(@NonNull Charset charset) { - this(charset, DataBufferOutput.BUFFER_SIZE); + this(charset, BufferedRockerOutput.BUFFER_SIZE); } public RockerModule() { @@ -78,7 +78,7 @@ public RockerModule() { /** * Allow simple reuse of raw byte buffers. It is usually used through ThreadLocal - * variable pointing to instance of {@link DataBufferOutput}. + * variable pointing to instance of {@link BufferedRockerOutput}. * * @param reuseBuffer True for reuse the buffer. Default is: false * @return This module. @@ -97,7 +97,7 @@ public void install(@NonNull Jooby application) { this.reloading == null ? (env.isActive("dev") && runtime.isReloadingPossible()) : this.reloading; - var factory = DataBufferOutput.factory(charset, application.getOutputFactory(), bufferSize); + var factory = BufferedRockerOutput.factory(charset, application.getOutputFactory(), bufferSize); runtime.setReloading(reloading); // renderer application.encoder(new RockerMessageEncoder(factory)); 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 67ec8c3834..bfd14b8321 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 @@ -122,10 +122,10 @@ 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); diff --git a/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java b/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java index f1d801f58b..94a63109b1 100644 --- a/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java +++ b/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java @@ -46,10 +46,9 @@ public void withJsonBuffer() { } @Benchmark - public void withDataBufferOutputStream() { + public void withBufferedOutput() { var buffer = cache.get(); jsonb.toJson(message, jsonb.writer(new JsonOutputBench(buffer))); - // TODO: add clear to output or use close - buffer.asByteBuffer().clear(); + buffer.clear(); } } From 0b127d881ca294ea9b361ff5224c89f3ef7f3977 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Fri, 27 Jun 2025 14:02:52 -0300 Subject: [PATCH 18/60] output api: doc, code cleanup + refactor - move split and transferTo(ByteBuffer) out of public API - use more performant version of send(String) --- .../main/java/io/jooby/ServerSentMessage.java | 47 +++++++- .../internal/handler/ChunkedSubscriber.java | 2 +- .../output/ByteArrayWrappedOutput.java | 73 ++++++++++++ .../output/ByteBufferOutput.java} | 19 ++-- .../output/ByteBufferWrappedOutput.java | 15 +-- .../output/CompsiteByteBufferOutput.java | 12 +- .../output/OutputOutputStream.java | 5 +- .../{ => internal}/output/OutputWriter.java | 5 +- .../jooby/internal/output/package-info.java | 4 + .../io/jooby/output/DefaultOutputFactory.java | 24 +++- .../jooby/output/ForwardingOutputFactory.java | 50 +++++++++ .../src/main/java/io/jooby/output/Output.java | 106 ++++++++++-------- .../java/io/jooby/output/OutputFactory.java | 96 +++++++++++++++- jooby/src/main/java/module-info.java | 1 + .../io/jooby/output/BufferedOutputTest.java | 12 +- modules/jooby-avaje-jsonb/pom.xml | 14 +++ .../avaje/jsonb/AvajeJsonbEncoderBench.java | 14 ++- modules/jooby-jackson/pom.xml | 14 +++ .../java/io/jooby/jackson/JacksonModule.java | 6 +- .../java/io/jooby/jackson/JacksonBench.java | 56 +++++++++ modules/jooby-netty/pom.xml | 15 +++ .../internal/netty/NettyBufferedOutput.java | 50 ++------- .../internal/netty/NettyByteBufOutput.java | 57 ++++++++++ .../io/jooby/internal/netty/NettyContext.java | 5 +- .../internal/netty/NettyOutputFactory.java | 18 ++- .../io/jooby/internal/netty/NettySender.java | 2 +- .../netty/NettyServerSentEmitter.java | 2 +- .../jooby/internal/netty/NettyWebSocket.java | 2 +- .../internal/netty/NettyWrappedOutput.java | 56 +++++++++ .../java/io/jooby/netty/ByteBufBench.java | 48 ++++++++ .../UndertowServerSentConnection.java | 38 ++++++- .../io/jooby/avaje/jsonb/JsonOutputBench.java | 37 ------ 32 files changed, 706 insertions(+), 199 deletions(-) create mode 100644 jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java rename jooby/src/main/java/io/jooby/{output/ByteBufferOut.java => internal/output/ByteBufferOutput.java} (91%) rename jooby/src/main/java/io/jooby/{ => internal}/output/ByteBufferWrappedOutput.java (78%) rename jooby/src/main/java/io/jooby/{ => internal}/output/CompsiteByteBufferOutput.java (89%) rename jooby/src/main/java/io/jooby/{ => internal}/output/OutputOutputStream.java (90%) rename jooby/src/main/java/io/jooby/{ => internal}/output/OutputWriter.java (93%) create mode 100644 jooby/src/main/java/io/jooby/internal/output/package-info.java create mode 100644 jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java rename {tests => modules/jooby-avaje-jsonb}/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java (74%) create mode 100644 modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java create mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufOutput.java create mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWrappedOutput.java create mode 100644 modules/jooby-netty/src/test/java/io/jooby/netty/ByteBufBench.java delete mode 100644 tests/src/test/java/io/jooby/avaje/jsonb/JsonOutputBench.java diff --git a/jooby/src/main/java/io/jooby/ServerSentMessage.java b/jooby/src/main/java/io/jooby/ServerSentMessage.java index 9da10e459a..8f36d26ab7 100644 --- a/jooby/src/main/java/io/jooby/ServerSentMessage.java +++ b/jooby/src/main/java/io/jooby/ServerSentMessage.java @@ -7,6 +7,9 @@ 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; @@ -156,7 +159,7 @@ public ServerSentMessage(@NonNull Object data) { IntPredicate nl = ch -> ch == '\n'; var message = encoder.encode(ctx, data); - var lines = message.split(nl); + var lines = split(message.iterator(), nl); while (lines.hasNext()) { buffer.write(lines.next()); if (lines.hasNext()) { @@ -170,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/internal/handler/ChunkedSubscriber.java b/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java index 677f729b26..6867ec8e1f 100644 --- a/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java +++ b/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java @@ -119,7 +119,7 @@ public void onComplete() { private static Output prepend(Context ctx, Output data, byte c) { var buffer = ctx.getOutputFactory().newCompositeOutput(); buffer.write(c); - data.accept(buffer::write); + data.transferTo(buffer::write); return buffer; } diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java b/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java new file mode 100644 index 0000000000..0da4015f8c --- /dev/null +++ b/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java @@ -0,0 +1,73 @@ +/* + * 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.nio.charset.Charset; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.SneakyThrows; +import io.jooby.output.Output; + +public class ByteArrayWrappedOutput implements Output { + + private final byte[] buffer; + + public ByteArrayWrappedOutput(byte[] source) { + this.buffer = source; + } + + @Override + public Output write(byte b) { + throw new UnsupportedOperationException(); + } + + @Override + public Output write(byte[] source) { + throw new UnsupportedOperationException(); + } + + @Override + public Output write(byte[] source, int offset, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public Output clear() { + return this; + } + + @Override + public int size() { + return buffer.length; + } + + @Override + public void transferTo(@NonNull SneakyThrows.Consumer consumer) { + consumer.accept(ByteBuffer.wrap(buffer)); + } + + @Override + public ByteBuffer asByteBuffer() { + return ByteBuffer.wrap(buffer); + } + + @Override + public String asString(@NonNull Charset charset) { + return charset.decode(asByteBuffer()).toString(); + } + + @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/output/ByteBufferOut.java b/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java similarity index 91% rename from jooby/src/main/java/io/jooby/output/ByteBufferOut.java rename to jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java index 096a11e510..5e6b79f417 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferOut.java +++ b/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.output; +package io.jooby.internal.output; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -13,8 +13,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; +import io.jooby.output.Output; -class ByteBufferOut implements Output { +public class ByteBufferOutput implements Output { private static final int MAX_CAPACITY = Integer.MAX_VALUE; private static final int CAPACITY_THRESHOLD = 1024 * 1024 * 4; @@ -27,20 +28,20 @@ class ByteBufferOut implements Output { private int writePosition; - public ByteBufferOut(boolean direct, int capacity) { + public ByteBufferOutput(boolean direct, int capacity) { this.buffer = allocate(capacity, direct); this.capacity = this.buffer.remaining(); } - public ByteBufferOut(boolean direct) { + public ByteBufferOutput(boolean direct) { this(direct, BUFFER_SIZE); } - public ByteBufferOut(int bufferSize) { + public ByteBufferOutput(int bufferSize) { this(false, bufferSize); } - public ByteBufferOut() { + public ByteBufferOutput() { this(BUFFER_SIZE); } @@ -57,12 +58,12 @@ private void ensureWritable(int length) { } @Override - public void accept(SneakyThrows.Consumer consumer) { + public void transferTo(@NonNull SneakyThrows.Consumer consumer) { consumer.accept(asByteBuffer()); } @Override - public Iterator iterator() { + public @NonNull Iterator iterator() { return List.of(asByteBuffer()).iterator(); } @@ -71,7 +72,7 @@ private int writableByteCount() { } @Override - public ByteBuffer asByteBuffer() { + public @NonNull ByteBuffer asByteBuffer() { return this.buffer.duplicate().position(this.readPosition).limit(this.writePosition); } diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferWrappedOutput.java b/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java similarity index 78% rename from jooby/src/main/java/io/jooby/output/ByteBufferWrappedOutput.java rename to jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java index f823fe1247..8c64efe090 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferWrappedOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.output; +package io.jooby.internal.output; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -11,19 +11,12 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; +import io.jooby.output.Output; -class ByteBufferWrappedOutput implements Output { +public class ByteBufferWrappedOutput implements Output { private final ByteBuffer buffer; - public ByteBufferWrappedOutput(byte[] source, int offset, int length) { - this(ByteBuffer.wrap(source, offset, length)); - } - - public ByteBufferWrappedOutput(byte[] source) { - this(source, 0, source.length); - } - public ByteBufferWrappedOutput(ByteBuffer buffer) { this.buffer = buffer; } @@ -55,7 +48,7 @@ public int size() { } @Override - public void accept(SneakyThrows.Consumer consumer) { + public void transferTo(@NonNull SneakyThrows.Consumer consumer) { consumer.accept(buffer); } diff --git a/jooby/src/main/java/io/jooby/output/CompsiteByteBufferOutput.java b/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java similarity index 89% rename from jooby/src/main/java/io/jooby/output/CompsiteByteBufferOutput.java rename to jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java index e359e0697a..e9d53689ab 100644 --- a/jooby/src/main/java/io/jooby/output/CompsiteByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.output; +package io.jooby.internal.output; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -13,8 +13,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; +import io.jooby.output.Output; -class CompsiteByteBufferOutput implements Output { +public class CompsiteByteBufferOutput implements Output { private final List chunks = new ArrayList<>(); private int size = 0; private int bufferSizeHint = BUFFER_SIZE; @@ -70,15 +71,10 @@ public String asString(@NonNull Charset charset) { } @Override - public void accept(SneakyThrows.Consumer consumer) { + public void transferTo(@NonNull SneakyThrows.Consumer consumer) { chunks.forEach(consumer); } - @Override - public int bufferSizeHint() { - return bufferSizeHint; - } - @Override public String toString() { return "chunks=" + chunks.size() + ", size=" + size; diff --git a/jooby/src/main/java/io/jooby/output/OutputOutputStream.java b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java similarity index 90% rename from jooby/src/main/java/io/jooby/output/OutputOutputStream.java rename to jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java index 4dd2266ae0..bae5482d19 100644 --- a/jooby/src/main/java/io/jooby/output/OutputOutputStream.java +++ b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java @@ -3,19 +3,20 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.output; +package io.jooby.internal.output; import java.io.IOException; import java.io.OutputStream; import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.output.Output; /** * An {@link OutputStream} that writes to a {@link io.jooby.output.Output}. * * @see io.jooby.output.Output#asOutputStream() */ -final class OutputOutputStream extends OutputStream { +public class OutputOutputStream extends OutputStream { private final Output output; diff --git a/jooby/src/main/java/io/jooby/output/OutputWriter.java b/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java similarity index 93% rename from jooby/src/main/java/io/jooby/output/OutputWriter.java rename to jooby/src/main/java/io/jooby/internal/output/OutputWriter.java index e4e8025c0f..f49779657d 100644 --- a/jooby/src/main/java/io/jooby/output/OutputWriter.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.output; +package io.jooby.internal.output; import java.io.IOException; import java.io.Writer; @@ -11,8 +11,9 @@ import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.output.Output; -class OutputWriter extends Writer { +public class OutputWriter extends Writer { private final Output output; private final Charset charset; private boolean closed; 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/DefaultOutputFactory.java b/jooby/src/main/java/io/jooby/output/DefaultOutputFactory.java index 588b2fedc1..ee05718228 100644 --- a/jooby/src/main/java/io/jooby/output/DefaultOutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/DefaultOutputFactory.java @@ -7,10 +7,22 @@ import java.nio.ByteBuffer; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.internal.output.ByteArrayWrappedOutput; +import io.jooby.internal.output.ByteBufferOutput; +import io.jooby.internal.output.ByteBufferWrappedOutput; +import io.jooby.internal.output.CompsiteByteBufferOutput; + +/** + * An output factory backed by {@link ByteBuffer}. + * + * @author edgar + * @since 4.0.0 + */ public class DefaultOutputFactory implements OutputFactory { @Override public Output newBufferedOutput(int size) { - return new ByteBufferOut(size); + return new ByteBufferOutput(size); } @Override @@ -19,17 +31,17 @@ public Output newCompositeOutput() { } @Override - public Output wrap(ByteBuffer buffer) { + public Output wrap(@NonNull ByteBuffer buffer) { return new ByteBufferWrappedOutput(buffer); } @Override - public Output wrap(byte[] bytes) { - return new ByteBufferWrappedOutput(bytes); + public Output wrap(@NonNull byte[] bytes) { + return new ByteArrayWrappedOutput(bytes); } @Override - public Output wrap(byte[] bytes, int offset, int length) { - return new ByteBufferWrappedOutput(bytes, offset, length); + public Output wrap(@NonNull byte[] bytes, int offset, int length) { + return new ByteBufferWrappedOutput(ByteBuffer.wrap(bytes, offset, length)); } } 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..7597ba5898 --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java @@ -0,0 +1,50 @@ +/* + * 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(OutputFactory delegate) { + this.delegate = delegate; + } + + @Override + public Output newBufferedOutput(int size) { + return delegate.newBufferedOutput(size); + } + + @Override + public Output newCompositeOutput() { + return delegate.newCompositeOutput(); + } + + @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 index 0665d0a741..3c9c327caa 100644 --- a/jooby/src/main/java/io/jooby/output/Output.java +++ b/jooby/src/main/java/io/jooby/output/Output.java @@ -13,73 +13,90 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Iterator; -import java.util.function.IntPredicate; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.SneakyThrows; - +import io.jooby.internal.output.ByteArrayWrappedOutput; +import io.jooby.internal.output.ByteBufferWrappedOutput; +import io.jooby.internal.output.OutputOutputStream; +import io.jooby.internal.output.OutputWriter; + +/** + * Buffered output used to support multiple implementations like byte array, byte buffer, netty + * buffers. + * + *

There are two implementations of output one is backed by a {@link ByteBuffer} and the other is + * a view of multiple {@link ByteBuffer} byffers. See {@link OutputFactory#newBufferedOutput()} and + * {@link OutputFactory#newCompositeOutput()} + * + * @author edgar + * @since 4.0.0 + */ public interface Output { /** Default buffer size: 4k. */ int BUFFER_SIZE = 4096; + /** + * 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); } + /** + * A view of internal bytes as {@link byte buffer} changes made to the buffer are reflected in + * this output. + * + * @return A byte byffer. + */ ByteBuffer asByteBuffer(); + /** + * A view of internal bytes as string. + * + * @param charset Charset to use. + * @return A string. + */ String asString(@NonNull Charset charset); - default void toByteBuffer(ByteBuffer dest) { - toByteBuffer(0, dest, dest.position(), 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. + * Transfers the entire buffered output (one or multiple buffers) to a consumer. * - * @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 + * @param consumer Consumer. */ - default void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - dest = dest.duplicate().clear(); - dest.put(destPos, asByteBuffer(), srcPos, length); - } - - default Iterator split(IntPredicate predicate) { - // TODO: fix me for chunks - var buffer = asByteBuffer(); - var chunks = new ArrayList(); - var offset = 0; - for (int i = 0; i < buffer.remaining(); i++) { - var b = buffer.get(i); - if (predicate.test(b)) { - chunks.add(buffer.duplicate().position(offset).limit(i + 1)); - offset = i + 1; - } - } - if (offset < buffer.remaining()) { - chunks.add(buffer.duplicate().position(offset)); - } - return chunks.iterator(); - } - - void accept(SneakyThrows.Consumer consumer); + void transferTo(@NonNull SneakyThrows.Consumer consumer); + /** + * An iterator over byte buffers. + * + * @return An iterator over byte buffers. + */ default Iterator iterator() { var list = new ArrayList(); - accept(list::add); + transferTo(list::add); return list.iterator(); } @@ -90,15 +107,6 @@ default Iterator iterator() { */ int size(); - /** - * The recommend buffer size to use for extracting with - * - * @return buffer size to use which by default is {@link #size()}. - */ - default int bufferSizeHint() { - return size(); - } - /** * Write a single byte into this buffer at the current writing position. * @@ -185,11 +193,11 @@ static Output wrap(ByteBuffer buffer) { } static Output wrap(byte[] bytes) { - return new ByteBufferWrappedOutput(bytes); + return new ByteArrayWrappedOutput(bytes); } static Output wrap(byte[] bytes, int offset, int length) { - return new ByteBufferWrappedOutput(bytes, offset, length); + return new ByteBufferWrappedOutput(ByteBuffer.wrap(bytes, offset, length)); } static Output wrap(String value) { diff --git a/jooby/src/main/java/io/jooby/output/OutputFactory.java b/jooby/src/main/java/io/jooby/output/OutputFactory.java index 578f8dc406..5bc7abfdbb 100644 --- a/jooby/src/main/java/io/jooby/output/OutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/OutputFactory.java @@ -5,30 +5,118 @@ */ package io.jooby.output; +import static java.lang.ThreadLocal.withInitial; + import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import edu.umd.cs.findbugs.annotations.NonNull; + public interface OutputFactory { + + /** + * Thread local for output buffer. Please note only store calls to {@link #newBufferedOutput()}, + * {@link #newCompositeOutput()} are not saved into thread local. + * + * @param factory Factory. + * @return Thread local factory. + */ + static OutputFactory threadLocal(OutputFactory factory, int bufferSize) { + return new ForwardingOutputFactory(factory) { + private final ThreadLocal threadLocal = + withInitial(() -> factory.newBufferedOutput(bufferSize)); + + @Override + public Output newBufferedOutput(int size) { + return threadLocal.get().clear(); + } + + @Override + public Output newBufferedOutput() { + return threadLocal.get().clear(); + } + }; + } + + /** + * Default output factory, backed by {@link ByteBuffer}. + * + * @return Default output factory. + */ + static OutputFactory create() { + return new DefaultOutputFactory(); + } + + /** + * Creates a new byte buffered output. + * + * @param size Output size. + * @return A byte buffered output. + */ Output newBufferedOutput(int size); + /** + * Creates a new byte buffered output with an initial size of {@link Output#BUFFER_SIZE}. + * + * @return A byte buffered output. + */ default Output newBufferedOutput() { return newBufferedOutput(Output.BUFFER_SIZE); } + /** + * A virtual buffer which shows multiple buffers as a single merged buffer. Useful for chunk of + * data. + * + * @return A new composite buffer. + */ Output newCompositeOutput(); + /** + * 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); } - default Output wrap(String value, Charset charset) { + /** + * 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)); } - Output wrap(ByteBuffer buffer); + /** + * Readonly buffer created from buffer. + * + * @param buffer Input buffer. + * @return Readonly buffer. + */ + Output wrap(@NonNull ByteBuffer buffer); - Output wrap(byte[] bytes); + /** + * Readonly buffer created from byte array. + * + * @param bytes Byte array. + * @return Readonly buffer. + */ + Output wrap(@NonNull byte[] bytes); - Output wrap(byte[] bytes, int offset, int length); + /** + * 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); } diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index c73a21a69e..d8e9bd0f34 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -14,6 +14,7 @@ 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; diff --git a/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java b/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java index d0948b418f..daeedab6bd 100644 --- a/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java +++ b/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java @@ -15,6 +15,8 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; +import io.jooby.internal.output.ByteBufferOutput; +import io.jooby.internal.output.CompsiteByteBufferOutput; public class BufferedOutputTest { @@ -36,7 +38,7 @@ public void bufferedOutput() { StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(30, buffered.size()); }, - new ByteBufferOut(3)); + new ByteBufferOutput(3)); output( buffered -> { @@ -52,7 +54,7 @@ public void bufferedOutput() { StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(30, buffered.size()); }, - new ByteBufferOut()); + new ByteBufferOutput()); output( buffered -> { @@ -61,7 +63,7 @@ public void bufferedOutput() { assertEquals("Hello World!", buffered.asString(StandardCharsets.UTF_8)); assertEquals(12, buffered.size()); }, - new ByteBufferOut()); + new ByteBufferOutput()); output( buffered -> { @@ -69,7 +71,7 @@ public void bufferedOutput() { assertEquals("A", buffered.asString(StandardCharsets.UTF_8)); assertEquals(1, buffered.size()); }, - new ByteBufferOut()); + new ByteBufferOutput()); } private void output(SneakyThrows.Consumer consumer, Output... buffers) { @@ -102,7 +104,7 @@ public void chunkedOutput() { @Test public void wrapOutput() throws IOException { var bytes = "xxHello World!xx".getBytes(StandardCharsets.UTF_8); - var output = new ByteBufferWrappedOutput(bytes, 2, bytes.length - 4); + var output = Output.wrap(bytes, 2, bytes.length - 4); assertEquals("Hello World!", output.asString(StandardCharsets.UTF_8)); assertEquals(12, output.size()); } diff --git a/modules/jooby-avaje-jsonb/pom.xml b/modules/jooby-avaje-jsonb/pom.xml index e0608a6b2d..e061273876 100644 --- a/modules/jooby-avaje-jsonb/pom.xml +++ b/modules/jooby-avaje-jsonb/pom.xml @@ -60,5 +60,19 @@ mockito-core test + + + org.openjdk.jmh + jmh-core + 1.37 + test + + + + org.openjdk.jmh + jmh-generator-annprocess + 1.37 + test + diff --git a/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java b/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java similarity index 74% rename from tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java rename to modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java index 94a63109b1..7edd0f4f28 100644 --- a/tests/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java +++ b/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java @@ -11,6 +11,7 @@ import org.openjdk.jmh.annotations.*; import io.avaje.jsonb.Jsonb; +import io.jooby.internal.avaje.jsonb.BufferedJsonOutput; import io.jooby.output.DefaultOutputFactory; import io.jooby.output.Output; import io.jooby.output.OutputFactory; @@ -46,9 +47,14 @@ public void withJsonBuffer() { } @Benchmark - public void withBufferedOutput() { - var buffer = cache.get(); - jsonb.toJson(message, jsonb.writer(new JsonOutputBench(buffer))); - buffer.clear(); + public void witCachedBufferedOutput() { + var buffer = cache.get().clear(); + jsonb.toJson(message, jsonb.writer(new BufferedJsonOutput(buffer))); + } + + @Benchmark + public void witBufferedOutput() { + var buffer = factory.newBufferedOutput(1024); + jsonb.toJson(message, jsonb.writer(new BufferedJsonOutput(buffer))); } } diff --git a/modules/jooby-jackson/pom.xml b/modules/jooby-jackson/pom.xml index 4c3d4bcfcf..2a3f051e37 100644 --- a/modules/jooby-jackson/pom.xml +++ b/modules/jooby-jackson/pom.xml @@ -64,5 +64,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 69ce073b2d..ff50761dbc 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 @@ -154,10 +154,10 @@ public void install(@NonNull Jooby application) { @Override public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { - var buffer = ctx.getOutputFactory().newBufferedOutput(); + var factory = ctx.getOutputFactory(); ctx.setDefaultResponseType(mediaType); - mapper.writer().writeValue(buffer.asOutputStream(), value); - return buffer; + // let jackson uses his own cache, so just 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..761869890e --- /dev/null +++ b/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java @@ -0,0 +1,56 @@ +/* + * 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.DefaultOutputFactory; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; + +@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.newBufferedOutput(1024)); + + @Setup + public void setup() { + message = Map.of("id", 98, "value", "Hello World"); + mapper = new ObjectMapper(); + factory = new DefaultOutputFactory(); + } + + @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-netty/pom.xml b/modules/jooby-netty/pom.xml index fc8200bdde..a43c158257 100644 --- a/modules/jooby-netty/pom.xml +++ b/modules/jooby-netty/pom.xml @@ -80,6 +80,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/NettyBufferedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java index 6b2783ad0f..3580cd4968 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java @@ -5,17 +5,14 @@ */ package io.jooby.internal.netty; -import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.nio.charset.Charset; 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 class NettyBufferedOutput implements Output { +public class NettyBufferedOutput implements NettyByteBufOutput { private final ByteBuf buffer; @@ -23,38 +20,10 @@ protected NettyBufferedOutput(ByteBuf buffer) { this.buffer = buffer; } - public static ByteBuf byteBuf(Output output) { - if (output instanceof NettyBufferedOutput netty) { - return netty.byteBuf(); - } else { - return Unpooled.wrappedBuffer(output.asByteBuffer()); - } - } - @NonNull public ByteBuf byteBuf() { return this.buffer; } - @Override - @NonNull public ByteBuffer asByteBuffer() { - return this.buffer.nioBuffer(); - } - - @Override - @NonNull public String asString(@NonNull Charset charset) { - return this.buffer.toString(charset); - } - - @Override - public void accept(SneakyThrows.Consumer consumer) { - consumer.accept(asByteBuffer()); - } - - @Override - public int size() { - return buffer.readableBytes(); - } - @Override @NonNull public Output write(byte b) { buffer.writeByte(b); @@ -74,12 +43,15 @@ public int size() { } @Override - public void send(Context ctx) { - if (ctx instanceof NettyContext netty) { - netty.send(this.buffer); - } else { - ctx.send(this.buffer.nioBuffer()); - } + @NonNull public Output write(@NonNull String source, @NonNull Charset charset) { + this.buffer.writeBytes(source.getBytes(charset)); + return this; + } + + @Override + public Output write(@NonNull CharBuffer source, @NonNull Charset charset) { + this.buffer.writeBytes(charset.encode(source)); + return this; } @Override 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..32ab202bcd --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufOutput.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.netty; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +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 NettyByteBufOutput extends Output { + @NonNull ByteBuf byteBuf(); + + @Override + @NonNull default ByteBuffer asByteBuffer() { + return byteBuf().nioBuffer(); + } + + @Override + @NonNull default String asString(@NonNull Charset charset) { + return byteBuf().toString(charset); + } + + @Override + default void transferTo(@NonNull SneakyThrows.Consumer consumer) { + consumer.accept(asByteBuffer()); + } + + @Override + default int size() { + return byteBuf().readableBytes(); + } + + @Override + default void send(Context ctx) { + if (ctx instanceof NettyContext netty) { + netty.send(byteBuf()); + } else { + ctx.send(asByteBuffer()); + } + } + + static ByteBuf byteBuf(Output output) { + if (output instanceof NettyByteBufOutput 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 0bf4f3b017..eb70df1274 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,6 @@ */ package io.jooby.internal.netty; -import static io.netty.buffer.Unpooled.copiedBuffer; 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; @@ -565,12 +564,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 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 index d3010157a4..a21dbbd062 100644 --- 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 @@ -6,6 +6,7 @@ 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.Output; @@ -40,18 +41,23 @@ public record NettyOutputFactory(ByteBufAllocator allocator) implements OutputFa } @Override - @NonNull public Output wrap(ByteBuffer buffer) { - return new NettyBufferedOutput(Unpooled.wrappedBuffer(buffer)); + @NonNull public Output wrap(@NonNull ByteBuffer buffer) { + return new NettyWrappedOutput(Unpooled.wrappedBuffer(buffer)); } @Override - @NonNull public Output wrap(byte[] bytes) { - return new NettyBufferedOutput(Unpooled.wrappedBuffer(bytes)); + public Output wrap(@NonNull String value, @NonNull Charset charset) { + return new NettyWrappedOutput(Unpooled.wrappedBuffer(value.getBytes(charset))); } @Override - @NonNull public Output wrap(byte[] bytes, int offset, int length) { - return new NettyBufferedOutput(Unpooled.wrappedBuffer(bytes, offset, length)); + @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)); } @Override 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 3e0f737062..512832b133 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,7 +5,7 @@ */ package io.jooby.internal.netty; -import static io.jooby.internal.netty.NettyBufferedOutput.byteBuf; +import static io.jooby.internal.netty.NettyByteBufOutput.byteBuf; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Sender; 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 141cd97b8e..bbc10f9ab0 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,7 +5,7 @@ */ package io.jooby.internal.netty; -import static io.jooby.internal.netty.NettyBufferedOutput.byteBuf; +import static io.jooby.internal.netty.NettyByteBufOutput.byteBuf; import java.util.UUID; import java.util.concurrent.TimeUnit; 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 23df3bd3fc..c0519d6258 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,7 +5,7 @@ */ package io.jooby.internal.netty; -import static io.jooby.internal.netty.NettyBufferedOutput.byteBuf; +import static io.jooby.internal.netty.NettyByteBufOutput.byteBuf; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; 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..e0857c41ae --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWrappedOutput.java @@ -0,0 +1,56 @@ +/* + * 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.Output; +import io.netty.buffer.ByteBuf; + +public class NettyWrappedOutput implements NettyByteBufOutput { + + private final ByteBuf buffer; + + protected NettyWrappedOutput(ByteBuf buffer) { + this.buffer = buffer; + } + + @NonNull public ByteBuf byteBuf() { + return this.buffer; + } + + @Override + @NonNull public Output write(byte b) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public Output write(byte[] source) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public Output write(byte[] source, int offset, int length) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public Output write(@NonNull String source, @NonNull Charset charset) { + throw new UnsupportedOperationException(); + } + + @Override + public Output write(@NonNull CharBuffer source, @NonNull Charset charset) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public Output clear() { + return this; + } +} 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-undertow/src/main/java/io/jooby/internal/undertow/UndertowServerSentConnection.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowServerSentConnection.java index bfd14b8321..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,6 +22,7 @@ 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.output.Output; @@ -130,13 +131,13 @@ private void fillBuffer() { if (data.leftOverData == null) { var message = data.message.encode(context); if (message.size() < buffer.remaining()) { - message.toByteBuffer(buffer); + 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; @@ -146,13 +147,13 @@ private void fillBuffer() { 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) { diff --git a/tests/src/test/java/io/jooby/avaje/jsonb/JsonOutputBench.java b/tests/src/test/java/io/jooby/avaje/jsonb/JsonOutputBench.java deleted file mode 100644 index a028fcc1d9..0000000000 --- a/tests/src/test/java/io/jooby/avaje/jsonb/JsonOutputBench.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.output.Output; - -public class JsonOutputBench implements JsonOutput { - - private final Output output; - - public JsonOutputBench(Output output) { - this.output = output; - } - - @Override - public void write(byte[] content, int offset, int length) throws IOException { - output.write(content, offset, length); - } - - @Override - public void flush() {} - - @Override - public OutputStream unwrapOutputStream() { - return this.output.asOutputStream(); - } - - @Override - public void close() {} -} From 857161452a2d3e9ed1fca1b838e10a3f19b9804d Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Fri, 27 Jun 2025 18:14:15 -0300 Subject: [PATCH 19/60] output api: remove wrap from Output - just keep them at OutputFactory --- .../main/java/io/jooby/DefaultContext.java | 3 +- .../java/io/jooby/internal/RouterImpl.java | 3 +- .../internal/output/ByteBufferOutput.java | 12 ---- ...tory.java => ByteBufferOutputFactory.java} | 30 ++++++++- .../jooby/output/ForwardingOutputFactory.java | 23 ++++++- .../src/main/java/io/jooby/output/Output.java | 22 ------- .../java/io/jooby/output/OutputFactory.java | 61 ++++++++++++++++--- jooby/src/test/java/io/jooby/Issue3607.java | 2 +- .../java/io/jooby/ServerSentMessageTest.java | 8 +-- .../test/java/io/jooby/buffer/Issue3434.java | 3 +- .../io/jooby/output/BufferedOutputTest.java | 11 ++-- .../avaje/jsonb/AvajeJsonbEncoderBench.java | 3 +- .../test/java/io/jooby/gson/Issue3434.java | 4 +- .../java/io/jooby/jackson/JacksonBench.java | 3 +- .../jooby/jackson/JacksonJsonModuleTest.java | 6 +- .../jte/BufferedTemplateOutputTest.java | 8 +-- .../internal/netty/NettyOutputFactory.java | 41 +++++++++---- .../main/java/io/jooby/netty/NettyServer.java | 2 +- .../main/java/io/jooby/test/MockContext.java | 3 +- .../io/jooby/yasson/YassonModuleTest.java | 4 +- .../java/io/jooby/junit/ServerTestRunner.java | 4 +- 21 files changed, 164 insertions(+), 92 deletions(-) rename jooby/src/main/java/io/jooby/output/{DefaultOutputFactory.java => ByteBufferOutputFactory.java} (60%) diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index ddc41d6c09..0365d4adf8 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -35,7 +35,6 @@ import io.jooby.internal.MissingValue; import io.jooby.internal.SingleValue; import io.jooby.internal.UrlParser; -import io.jooby.output.DefaultOutputFactory; import io.jooby.output.OutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -656,6 +655,6 @@ default Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { @Override default OutputFactory getOutputFactory() { - return new DefaultOutputFactory(); + return getRouter().getOutputFactory(); } } diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 18f813fb53..7d819569de 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -44,7 +44,6 @@ import io.jooby.exception.StatusCodeException; import io.jooby.internal.handler.ServerSentEventHandler; import io.jooby.internal.handler.WebSocketHandler; -import io.jooby.output.DefaultOutputFactory; import io.jooby.output.OutputFactory; import io.jooby.problem.ProblemDetailsHandler; import io.jooby.value.ValueFactory; @@ -172,7 +171,7 @@ public Stack executor(Executor executor) { private ValueFactory valueFactory = new ValueFactory(); - private OutputFactory outputFactory = new DefaultOutputFactory(); + private OutputFactory outputFactory = OutputFactory.create(false); public RouterImpl() { stack.addLast(new Stack(chi, null)); diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java b/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java index 5e6b79f417..41c05bb482 100644 --- a/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java @@ -33,18 +33,6 @@ public ByteBufferOutput(boolean direct, int capacity) { this.capacity = this.buffer.remaining(); } - public ByteBufferOutput(boolean direct) { - this(direct, BUFFER_SIZE); - } - - public ByteBufferOutput(int bufferSize) { - this(false, bufferSize); - } - - public ByteBufferOutput() { - this(BUFFER_SIZE); - } - @Override public int size() { return this.writePosition - this.readPosition; diff --git a/jooby/src/main/java/io/jooby/output/DefaultOutputFactory.java b/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java similarity index 60% rename from jooby/src/main/java/io/jooby/output/DefaultOutputFactory.java rename to jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java index ee05718228..977056189c 100644 --- a/jooby/src/main/java/io/jooby/output/DefaultOutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java @@ -19,10 +19,34 @@ * @author edgar * @since 4.0.0 */ -public class DefaultOutputFactory implements OutputFactory { +public class ByteBufferOutputFactory implements OutputFactory { + private int initialBufferSize; + private final boolean direct; + + public ByteBufferOutputFactory(boolean direct, int initialBufferSize) { + this.initialBufferSize = initialBufferSize; + this.direct = direct; + } + + @Override + public int getInitialBufferSize() { + return initialBufferSize; + } + + @Override + public OutputFactory setInitialBufferSize(int initialBufferSize) { + this.initialBufferSize = initialBufferSize; + return this; + } + + @Override + public boolean isDirect() { + return direct; + } + @Override - public Output newBufferedOutput(int size) { - return new ByteBufferOutput(size); + public Output newBufferedOutput(boolean direct, int size) { + return new ByteBufferOutput(direct, size); } @Override diff --git a/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java b/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java index 7597ba5898..fedf0ef74a 100644 --- a/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java @@ -19,15 +19,36 @@ public abstract class ForwardingOutputFactory implements OutputFactory { protected final OutputFactory delegate; - public ForwardingOutputFactory(OutputFactory delegate) { + public ForwardingOutputFactory(@NonNull OutputFactory delegate) { this.delegate = delegate; } + @Override + public int getInitialBufferSize() { + return delegate.getInitialBufferSize(); + } + + @Override + public OutputFactory setInitialBufferSize(int initialBufferSize) { + delegate.setInitialBufferSize(initialBufferSize); + return this; + } + + @Override + public boolean isDirect() { + return delegate.isDirect(); + } + @Override public Output newBufferedOutput(int size) { return delegate.newBufferedOutput(size); } + @Override + public Output newBufferedOutput(boolean direct, int size) { + return delegate.newBufferedOutput(direct, size); + } + @Override public Output newCompositeOutput() { return delegate.newCompositeOutput(); diff --git a/jooby/src/main/java/io/jooby/output/Output.java b/jooby/src/main/java/io/jooby/output/Output.java index 3c9c327caa..d9490cb530 100644 --- a/jooby/src/main/java/io/jooby/output/Output.java +++ b/jooby/src/main/java/io/jooby/output/Output.java @@ -16,8 +16,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.SneakyThrows; -import io.jooby.internal.output.ByteArrayWrappedOutput; -import io.jooby.internal.output.ByteBufferWrappedOutput; import io.jooby.internal.output.OutputOutputStream; import io.jooby.internal.output.OutputWriter; @@ -187,24 +185,4 @@ default Output write(@NonNull CharBuffer source, @NonNull Charset charset) { void send(io.jooby.Context ctx); Output clear(); - - static Output wrap(ByteBuffer buffer) { - return new ByteBufferWrappedOutput(buffer); - } - - static Output wrap(byte[] bytes) { - return new ByteArrayWrappedOutput(bytes); - } - - static Output wrap(byte[] bytes, int offset, int length) { - return new ByteBufferWrappedOutput(ByteBuffer.wrap(bytes, offset, length)); - } - - static Output wrap(String value) { - return wrap(value, StandardCharsets.UTF_8); - } - - static Output wrap(String value, Charset charset) { - return wrap(value.getBytes(charset)); - } } diff --git a/jooby/src/main/java/io/jooby/output/OutputFactory.java b/jooby/src/main/java/io/jooby/output/OutputFactory.java index 5bc7abfdbb..6e4ddeea50 100644 --- a/jooby/src/main/java/io/jooby/output/OutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/OutputFactory.java @@ -13,6 +13,12 @@ import edu.umd.cs.findbugs.annotations.NonNull; +/** + * Factory class for buffered {@link Output}. + * + * @author edgar + * @since 4.0.0 + */ public interface OutputFactory { /** @@ -22,10 +28,14 @@ public interface OutputFactory { * @param factory Factory. * @return Thread local factory. */ - static OutputFactory threadLocal(OutputFactory factory, int bufferSize) { + static OutputFactory threadLocal(OutputFactory factory) { return new ForwardingOutputFactory(factory) { - private final ThreadLocal threadLocal = - withInitial(() -> factory.newBufferedOutput(bufferSize)); + private final ThreadLocal threadLocal = withInitial(factory::newBufferedOutput); + + @Override + public Output newBufferedOutput(boolean direct, int size) { + return threadLocal.get().clear(); + } @Override public Output newBufferedOutput(int size) { @@ -44,25 +54,62 @@ public Output newBufferedOutput() { * * @return Default output factory. */ - static OutputFactory create() { - return new DefaultOutputFactory(); + static OutputFactory create(boolean direct, int bufferSize) { + return new ByteBufferOutputFactory(direct, bufferSize); + } + + /** + * Default output factory, backed by {@link ByteBuffer}. + * + * @return Default output factory. + */ + static OutputFactory create(boolean direct) { + return new ByteBufferOutputFactory(direct, Output.BUFFER_SIZE); } + /** + * 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 + */ + boolean isDirect(); + + /** + * Buffer of a default initial capacity. Default capacity is 1024 bytes. + * + * @return buffer of a default initial capacity. + */ + int getInitialBufferSize(); + + /** + * Set default buffer initial capacity. + * + * @param initialBufferSize Default initial buffer capacity. + * @return This buffer factory. + */ + OutputFactory setInitialBufferSize(int initialBufferSize); + /** * Creates a new byte buffered output. * + * @param direct True for direct buffers. * @param size Output size. * @return A byte buffered output. */ - Output newBufferedOutput(int size); + Output newBufferedOutput(boolean direct, int size); /** * Creates a new byte buffered output with an initial size of {@link Output#BUFFER_SIZE}. * + * @param size Output size. * @return A byte buffered output. */ + default Output newBufferedOutput(int size) { + return newBufferedOutput(isDirect(), size); + } + default Output newBufferedOutput() { - return newBufferedOutput(Output.BUFFER_SIZE); + return newBufferedOutput(isDirect(), Output.BUFFER_SIZE); } /** diff --git a/jooby/src/test/java/io/jooby/Issue3607.java b/jooby/src/test/java/io/jooby/Issue3607.java index 1b4683f748..7e6565f925 100644 --- a/jooby/src/test/java/io/jooby/Issue3607.java +++ b/jooby/src/test/java/io/jooby/Issue3607.java @@ -17,7 +17,7 @@ private static class TemplateEngineImpl implements TemplateEngine { @Override public Output render(Context ctx, ModelAndView modelAndView) throws Exception { // do nothing - return Output.wrap(new byte[0]); + return ctx.getOutputFactory().wrap(new byte[0]); } } diff --git a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java index 0cd7f1b128..b71f81c329 100644 --- a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java +++ b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; -import io.jooby.output.DefaultOutputFactory; +import io.jooby.output.OutputFactory; public class ServerSentMessageTest { @@ -22,7 +22,7 @@ public void shouldFormatMessage() throws Exception { var data = "some"; var ctx = mock(Context.class); - var bufferFactory = new DefaultOutputFactory(); + var bufferFactory = OutputFactory.create(false); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) @@ -42,7 +42,7 @@ public void shouldFormatMultiLineMessage() throws Exception { var data = "line 1\n line ,a .. 2\nline ...abc 3"; var ctx = mock(Context.class); - var bufferFactory = new DefaultOutputFactory(); + var bufferFactory = OutputFactory.create(false); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) @@ -64,7 +64,7 @@ public void shouldFormatMessageEndingWithNL() throws Exception { var data = "line 1\n"; var ctx = mock(Context.class); - var bufferFactory = new DefaultOutputFactory(); + var bufferFactory = OutputFactory.create(false); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) diff --git a/jooby/src/test/java/io/jooby/buffer/Issue3434.java b/jooby/src/test/java/io/jooby/buffer/Issue3434.java index c840a527e4..4839fdcd58 100644 --- a/jooby/src/test/java/io/jooby/buffer/Issue3434.java +++ b/jooby/src/test/java/io/jooby/buffer/Issue3434.java @@ -15,11 +15,10 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; -import io.jooby.output.DefaultOutputFactory; import io.jooby.output.OutputFactory; public class Issue3434 { - OutputFactory factory = new DefaultOutputFactory(); + OutputFactory factory = OutputFactory.create(false); @Test void shouldWriteCharBufferOnBufferWriter() throws IOException { diff --git a/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java b/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java index daeedab6bd..a6e6247679 100644 --- a/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java +++ b/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java @@ -16,6 +16,7 @@ import io.jooby.SneakyThrows; import io.jooby.internal.output.ByteBufferOutput; +import io.jooby.internal.output.ByteBufferWrappedOutput; import io.jooby.internal.output.CompsiteByteBufferOutput; public class BufferedOutputTest { @@ -38,7 +39,7 @@ public void bufferedOutput() { StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(30, buffered.size()); }, - new ByteBufferOutput(3)); + new ByteBufferOutput(false, 3)); output( buffered -> { @@ -54,7 +55,7 @@ public void bufferedOutput() { StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(30, buffered.size()); }, - new ByteBufferOutput()); + new ByteBufferOutput(false, 255)); output( buffered -> { @@ -63,7 +64,7 @@ public void bufferedOutput() { assertEquals("Hello World!", buffered.asString(StandardCharsets.UTF_8)); assertEquals(12, buffered.size()); }, - new ByteBufferOutput()); + new ByteBufferOutput(false, 255)); output( buffered -> { @@ -71,7 +72,7 @@ public void bufferedOutput() { assertEquals("A", buffered.asString(StandardCharsets.UTF_8)); assertEquals(1, buffered.size()); }, - new ByteBufferOutput()); + new ByteBufferOutput(false, 255)); } private void output(SneakyThrows.Consumer consumer, Output... buffers) { @@ -104,7 +105,7 @@ public void chunkedOutput() { @Test public void wrapOutput() throws IOException { var bytes = "xxHello World!xx".getBytes(StandardCharsets.UTF_8); - var output = Output.wrap(bytes, 2, bytes.length - 4); + var output = new ByteBufferWrappedOutput(ByteBuffer.wrap(bytes, 2, bytes.length - 4)); assertEquals("Hello World!", output.asString(StandardCharsets.UTF_8)); assertEquals(12, output.size()); } diff --git a/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java b/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java index 7edd0f4f28..8bbb9ffd8d 100644 --- a/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java +++ b/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java @@ -12,7 +12,6 @@ import io.avaje.jsonb.Jsonb; import io.jooby.internal.avaje.jsonb.BufferedJsonOutput; -import io.jooby.output.DefaultOutputFactory; import io.jooby.output.Output; import io.jooby.output.OutputFactory; @@ -38,7 +37,7 @@ public class AvajeJsonbEncoderBench { public void setup() { message = Map.of("id", 98, "value", "Hello World"); jsonb = Jsonb.builder().build(); - factory = new DefaultOutputFactory(); + factory = OutputFactory.create(false); } @Benchmark 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 1affa4031a..ac19294b97 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,7 @@ import com.google.gson.GsonBuilder; import io.jooby.Context; -import io.jooby.output.DefaultOutputFactory; +import io.jooby.output.OutputFactory; public class Issue3434 { String text = @@ -139,7 +139,7 @@ public class Issue3434 { @Test void shouldEncodeUsingBufferWriter() { var gson = new GsonBuilder().create(); - var factory = new DefaultOutputFactory(); + var factory = OutputFactory.create(false); var ctx = mock(Context.class); when(ctx.getOutputFactory()).thenReturn(factory); 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 index 761869890e..07bb9e732c 100644 --- a/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java +++ b/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java @@ -13,7 +13,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.jooby.output.DefaultOutputFactory; import io.jooby.output.Output; import io.jooby.output.OutputFactory; @@ -35,7 +34,7 @@ public class JacksonBench { public void setup() { message = Map.of("id", 98, "value", "Hello World"); mapper = new ObjectMapper(); - factory = new DefaultOutputFactory(); + factory = OutputFactory.create(false); } @Benchmark 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 a2deb15542..21af89c76e 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,14 +21,14 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.output.DefaultOutputFactory; +import io.jooby.output.OutputFactory; public class JacksonJsonModuleTest { @Test public void renderJson() throws Exception { Context ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(new DefaultOutputFactory()); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(false)); JacksonModule jackson = new JacksonModule(new ObjectMapper()); @@ -57,7 +57,7 @@ public void parseJson() throws Exception { @Test public void renderXml() throws Exception { Context ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(new DefaultOutputFactory()); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(false)); JacksonModule jackson = new JacksonModule(new XmlMapper()); 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 index 6f16c7ac2e..04a78bce7a 100644 --- 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 @@ -11,13 +11,13 @@ import org.junit.jupiter.api.Test; -import io.jooby.output.DefaultOutputFactory; +import io.jooby.output.OutputFactory; public class BufferedTemplateOutputTest { @Test public void checkWriteContent() { - var factory = new DefaultOutputFactory(); + var factory = OutputFactory.create(false); var buffer = factory.newBufferedOutput(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeContent("Hello"); @@ -26,7 +26,7 @@ public void checkWriteContent() { @Test public void checkWriteContentSubstring() { - var factory = new DefaultOutputFactory(); + var factory = OutputFactory.create(false); var buffer = factory.newBufferedOutput(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeContent(" Hello World! ", 1, " Hello World! ".length() - 2); @@ -35,7 +35,7 @@ public void checkWriteContentSubstring() { @Test public void checkWriteBinaryContent() { - var factory = new DefaultOutputFactory(); + var factory = OutputFactory.create(false); var buffer = factory.newBufferedOutput(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeBinaryContent("Hello".getBytes(StandardCharsets.UTF_8)); 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 index a21dbbd062..b91787e4d5 100644 --- 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 @@ -15,7 +15,7 @@ import io.netty.buffer.Unpooled; import io.netty.util.ResourceLeakDetector; -public record NettyOutputFactory(ByteBufAllocator allocator) implements OutputFactory { +public class NettyOutputFactory implements OutputFactory { private static final String LEAK_DETECTION = "io.netty.leakDetection.level"; static { @@ -26,18 +26,37 @@ public record NettyOutputFactory(ByteBufAllocator allocator) implements OutputFa ResourceLeakDetector.Level.valueOf(System.getProperty(LEAK_DETECTION))); } - /** - * Create a new {@code OutputFactory} based on the given factory. - * - * @param allocator the factory to use - * @see io.netty.buffer.PooledByteBufAllocator - * @see io.netty.buffer.UnpooledByteBufAllocator - */ - public NettyOutputFactory {} + private final ByteBufAllocator allocator; + private int initialBufferSize = Output.BUFFER_SIZE; + + public NettyOutputFactory(ByteBufAllocator allocator) { + this.allocator = allocator; + } + + public ByteBufAllocator getAllocator() { + return allocator; + } + + @Override + public boolean isDirect() { + return allocator.isDirectBufferPooled(); + } + + @Override + public int getInitialBufferSize() { + return initialBufferSize; + } + + @Override + public @NonNull OutputFactory setInitialBufferSize(int initialBufferSize) { + this.initialBufferSize = initialBufferSize; + return this; + } @Override - @NonNull public Output newBufferedOutput(int size) { - return new NettyBufferedOutput(this.allocator.buffer(size)); + public @NonNull Output newBufferedOutput(boolean direct, int size) { + return new NettyBufferedOutput( + direct ? this.allocator.directBuffer(size) : this.allocator.heapBuffer(size)); } @Override 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 cf9ecca69a..b250855afa 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 @@ -113,7 +113,7 @@ public Server start(@NonNull Jooby... application) { this.dateLoop = Executors.newSingleThreadScheduledExecutor(); var dateService = new NettyDateService(dateLoop); - var allocator = outputFactory.allocator(); + var allocator = outputFactory.getAllocator(); var http2 = options.isHttp2() == Boolean.TRUE; /* Bootstrap: */ if (!options.isHttpsOnly()) { 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 9e0a5f1192..f36886051a 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 @@ -54,7 +54,6 @@ import io.jooby.StatusCode; import io.jooby.WebSocket; import io.jooby.exception.TypeMismatchException; -import io.jooby.output.DefaultOutputFactory; import io.jooby.output.Output; import io.jooby.output.OutputFactory; import io.jooby.value.Value; @@ -119,7 +118,7 @@ public class MockContext implements DefaultContext { private int port = -1; - private OutputFactory outputFactory = new DefaultOutputFactory(); + private OutputFactory outputFactory = OutputFactory.create(false); @Override public String getMethod() { 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 d33a3db5c4..95444ea4b1 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,7 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.output.DefaultOutputFactory; +import io.jooby.output.OutputFactory; public class YassonModuleTest { @@ -39,7 +39,7 @@ public void render() { user.age = Integer.MAX_VALUE; Context ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(new DefaultOutputFactory()); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(false)); var buffer = YassonModule.encode(ctx, user); assertEquals( "{\"age\":2147483647,\"id\":-1,\"name\":\"Lorem €@!?\"}", diff --git a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index 068d18bddb..1b6d85d82e 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -30,7 +30,7 @@ import io.jooby.StatusCode; import io.jooby.internal.MutedServer; import io.jooby.netty.NettyServer; -import io.jooby.output.DefaultOutputFactory; +import io.jooby.output.OutputFactory; import io.jooby.test.WebClient; public class ServerTestRunner { @@ -92,7 +92,7 @@ public void ready(SneakyThrows.Consumer2 onReady) { System.setProperty("___server_name__", server.getName()); var app = provider.get(); if (!(server instanceof NettyServer)) { - app.setOutputFactory(new DefaultOutputFactory()); + app.setOutputFactory(OutputFactory.create(false)); } Optional.ofNullable(executionMode).ifPresent(app::setExecutionMode); // Reduce log from maven build: From 1a03a6f6cd6cb04da8e2944f3c929cb203cb3725 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Fri, 27 Jun 2025 19:49:10 -0300 Subject: [PATCH 20/60] netty: clean up netty source code --- .../src/main/java/io/jooby/internal/Chi.java | 8 +- jooby/src/test/java/io/jooby/Issue3607.java | 2 + .../test/java/io/jooby/internal/ChiTest.java | 11 + .../netty/AssembledFullHttpResponse.java | 96 --- .../internal/netty/AssembledHttpResponse.java | 165 ----- .../netty/AssembledLastHttpContent.java | 98 --- .../jooby/internal/netty/HeadersMultiMap.java | 642 ------------------ .../io/jooby/internal/netty/NettyContext.java | 28 +- .../internal/netty/NettyResponseEncoder.java | 12 +- .../main/java/io/jooby/netty/NettyServer.java | 5 +- .../io/jooby/undertow/UndertowServer.java | 6 +- 11 files changed, 35 insertions(+), 1038 deletions(-) delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledFullHttpResponse.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledHttpResponse.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/AssembledLastHttpContent.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/HeadersMultiMap.java diff --git a/jooby/src/main/java/io/jooby/internal/Chi.java b/jooby/src/main/java/io/jooby/internal/Chi.java index 8c977a2485..14af60b3aa 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); @@ -549,7 +549,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); @@ -890,7 +890,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/test/java/io/jooby/Issue3607.java b/jooby/src/test/java/io/jooby/Issue3607.java index 7e6565f925..e95efaccd1 100644 --- a/jooby/src/test/java/io/jooby/Issue3607.java +++ b/jooby/src/test/java/io/jooby/Issue3607.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test; import io.jooby.output.Output; +import io.jooby.output.OutputFactory; public class Issue3607 { @@ -24,6 +25,7 @@ public Output render(Context ctx, ModelAndView modelAndView) throws Exception @Test public void shouldNotGenerateEmptyFlashMap() throws Exception { var ctx = mock(Context.class); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(false)); var templateEngine = new TemplateEngineImpl(); templateEngine.encode(ctx, ModelAndView.map("index.html")); diff --git a/jooby/src/test/java/io/jooby/internal/ChiTest.java b/jooby/src/test/java/io/jooby/internal/ChiTest.java index 1c5d497ac9..b1b598bd8d 100644 --- a/jooby/src/test/java/io/jooby/internal/ChiTest.java +++ b/jooby/src/test/java/io/jooby/internal/ChiTest.java @@ -33,6 +33,17 @@ 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"))); + } + @Test public void routeCase() { Chi router = new Chi(); 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 deleted file mode 100644 index 3308db9fbe..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/HeadersMultiMap.java +++ /dev/null @@ -1,642 +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 static io.netty.handler.codec.http.HttpConstants.*; - -import java.util.*; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -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; - -/** - * A case-insensitive implementation that extends Netty {@link HttpHeaders} for convenience. - * - * @author Julien Viet - */ -public final class HeadersMultiMap extends HttpHeaders { - - /** - * Convert the {@code value} to a non null {@code CharSequence} - * - * @param value the value - * @return the char sequence - */ - private static CharSequence toValidCharSequence(Object value) { - if (value instanceof CharSequence) { - return (CharSequence) value; - } else { - // Throws NPE - return value.toString(); - } - } - - public static final boolean DISABLE_HEADER_VALIDATION = - Boolean.parseBoolean(System.getProperty("io.netty.disableHttpHeadersValidation", "false")); - static final BiConsumer HTTP_VALIDATOR; - - static { - if (DISABLE_HEADER_VALIDATION) { - HTTP_VALIDATOR = null; - } else { - var nameValidator = DefaultHttpHeadersFactory.headersFactory().getNameValidator(); - var valueValidator = DefaultHttpHeadersFactory.headersFactory().getValueValidator(); - HTTP_VALIDATOR = - (name, value) -> { - nameValidator.validateName(name); - valueValidator.validate(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(); - } - - private final BiConsumer validator; - private final HeadersMultiMap.MapEntry[] entries = new HeadersMultiMap.MapEntry[16]; - private final HeadersMultiMap.MapEntry head = new HeadersMultiMap.MapEntry(); - - public HeadersMultiMap() { - this(HTTP_VALIDATOR); - } - - public HeadersMultiMap(BiConsumer validator) { - this.validator = validator; - head.before = head.after = head; - } - - public HeadersMultiMap add(CharSequence name, CharSequence value) { - Objects.requireNonNull(value); - int h = AsciiString.hashCode(name); - int i = h & 0x0000000F; - add0(h, i, name, value); - return this; - } - - @Override - public HeadersMultiMap add(CharSequence name, Object value) { - return add(name, toValidCharSequence(value)); - } - - @Override - public HttpHeaders add(String name, Object value) { - return add((CharSequence) name, toValidCharSequence(value)); - } - - public HeadersMultiMap add(String name, String strVal) { - return add((CharSequence) name, strVal); - } - - @Override - public HeadersMultiMap add(CharSequence name, Iterable values) { - int h = AsciiString.hashCode(name); - int i = h & 0x0000000F; - for (Object vstr : values) { - add0(h, i, name, toValidCharSequence(vstr)); - } - return this; - } - - @Override - public HeadersMultiMap add(String name, Iterable values) { - return add((CharSequence) name, values); - } - - @Override - public HeadersMultiMap remove(CharSequence name) { - Objects.requireNonNull(name, "name"); - int h = AsciiString.hashCode(name); - int i = h & 0x0000000F; - remove0(h, i, name); - return this; - } - - @Override - public HeadersMultiMap remove(final String name) { - return remove((CharSequence) name); - } - - public HeadersMultiMap set(CharSequence name, CharSequence value) { - return set0(name, value); - } - - public HeadersMultiMap set(String name, String value) { - return set0(name, value); - } - - @Override - public HeadersMultiMap set(String name, Object value) { - return set0(name, toValidCharSequence(value)); - } - - @Override - public HeadersMultiMap set(CharSequence name, Object value) { - return set(name, toValidCharSequence(value)); - } - - @Override - public HeadersMultiMap set(CharSequence name, Iterable values) { - Objects.requireNonNull(values, "values"); - - int h = AsciiString.hashCode(name); - int i = h & 0x0000000F; - - remove0(h, i, name); - for (Object v : values) { - if (v == null) { - break; - } - add0(h, i, name, toValidCharSequence(v)); - } - - return this; - } - - @Override - public HeadersMultiMap set(String name, Iterable values) { - return set((CharSequence) name, values); - } - - @Override - public boolean containsValue(CharSequence name, CharSequence value, boolean ignoreCase) { - return containsInternal(name, value, false, ignoreCase); - } - - @Override - public boolean contains(CharSequence name, CharSequence value, boolean ignoreCase) { - return containsInternal(name, value, true, ignoreCase); - } - - private boolean containsInternal( - CharSequence name, CharSequence value, boolean equals, boolean ignoreCase) { - int h = AsciiString.hashCode(name); - int i = h & 0x0000000F; - HeadersMultiMap.MapEntry e = entries[i]; - while (e != null) { - CharSequence key = e.key; - if (e.hash == h && (name == key || AsciiString.contentEqualsIgnoreCase(name, key))) { - CharSequence other = e.getValue(); - if (equals) { - if ((ignoreCase && AsciiString.contentEqualsIgnoreCase(value, other)) - || (!ignoreCase && AsciiString.contentEquals(value, other))) { - return true; - } - } else { - int prev = 0; - while (true) { - final int idx = AsciiString.indexOf(other, ',', prev); - int to; - if (idx == -1) { - to = other.length(); - } else { - to = idx; - } - while (to > prev && other.charAt(to - 1) == ' ') { - to--; - } - int from = prev; - while (from < to && other.charAt(from) == ' ') { - from++; - } - int len = to - from; - if (len > 0 && AsciiString.regionMatches(other, ignoreCase, from, value, 0, len)) { - return true; - } else if (idx == -1) { - break; - } - prev = idx + 1; - } - } - } - e = e.next; - } - return false; - } - - @Override - public boolean contains(String name, String value, boolean ignoreCase) { - return contains((CharSequence) name, value, ignoreCase); - } - - @Override - public boolean contains(CharSequence name) { - return get0(name) != null; - } - - @Override - public boolean contains(String name) { - return contains((CharSequence) name); - } - - @Override - public String get(CharSequence name) { - Objects.requireNonNull(name, "name"); - CharSequence ret = get0(name); - return ret != null ? ret.toString() : null; - } - - @Override - public String get(String name) { - return get((CharSequence) name); - } - - @Override - public List getAll(CharSequence name) { - Objects.requireNonNull(name, "name"); - LinkedList values = null; - int h = AsciiString.hashCode(name); - int i = h & 0x0000000F; - HeadersMultiMap.MapEntry e = entries[i]; - while (e != null) { - CharSequence key = e.key; - if (e.hash == h && (name == key || AsciiString.contentEqualsIgnoreCase(name, key))) { - if (values == null) { - values = new LinkedList<>(); - } - values.addFirst(e.getValue().toString()); - } - e = e.next; - } - return values == null ? Collections.emptyList() : Collections.unmodifiableList(values); - } - - @Override - public List> entries() { - if (isEmpty()) { - return Collections.emptyList(); - } - List> entries = new ArrayList<>(this.entries.length); - forEach((Consumer>) entries::add); - return entries; - } - - @Override - public List getAll(String name) { - return getAll((CharSequence) name); - } - - @Override - public void forEach(Consumer> action) { - HeadersMultiMap.MapEntry e = head.after; - while (e != head) { - action.accept(e.stringEntry()); - e = e.after; - } - } - - public void forEach(BiConsumer action) { - HeadersMultiMap.MapEntry e = head.after; - while (e != head) { - action.accept(e.getKey().toString(), e.getValue().toString()); - e = e.after; - } - } - - @Override - public Iterator> iterator() { - return new Iterator>() { - MapEntry curr = head; - - @Override - public boolean hasNext() { - return curr.after != head; - } - - @Override - public Map.Entry next() { - MapEntry next = curr.after; - if (next == head) { - throw new NoSuchElementException(); - } - curr = next; - return new Map.Entry() { - @Override - public String getKey() { - return next.key.toString(); - } - - @Override - public String getValue() { - return next.value.toString(); - } - - @Override - public String setValue(String value) { - return next.setValue(value).toString(); - } - - @Override - public String toString() { - return getKey() + "=" + getValue(); - } - }; - } - }; - } - - @Override - public boolean isEmpty() { - return head == head.after; - } - - @Override - public Set names() { - Set names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - HeadersMultiMap.MapEntry e = head.after; - while (e != head) { - names.add(e.getKey().toString()); - e = e.after; - } - return names; - } - - @Override - public HeadersMultiMap clear() { - Arrays.fill(entries, null); - head.before = head.after = head; - return this; - } - - public String toString() { - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : this) { - sb.append(entry).append('\n'); - } - return sb.toString(); - } - - @Override - public Integer getInt(CharSequence name) { - return getValue(name, Integer::parseInt, null); - } - - private T getValue(CharSequence name, Function mapper, T defaultValue) { - var value = get(name); - return value == null ? defaultValue : mapper.apply(value); - } - - @Override - public int getInt(CharSequence name, int defaultValue) { - return getValue(name, Integer::parseInt, defaultValue); - } - - @Override - public Short getShort(CharSequence name) { - return getValue(name, Short::parseShort, null); - } - - @Override - public short getShort(CharSequence name, short defaultValue) { - return getValue(name, Short::parseShort, defaultValue); - } - - @Override - public Long getTimeMillis(CharSequence name) { - return getValue(name, dateToMillis(), null); - } - - @Override - public long getTimeMillis(CharSequence name, long defaultValue) { - return getValue(name, dateToMillis(), defaultValue); - } - - private static Function dateToMillis() { - return value -> { - var date = DateFormatter.parseHttpDate(value); - return date == null ? null : date.getTime(); - }; - } - - @Override - public Iterator> iteratorCharSequence() { - return new Iterator>() { - HeadersMultiMap.MapEntry current = head.after; - - @Override - public boolean hasNext() { - return current != head; - } - - @Override - public Map.Entry next() { - Map.Entry next = current; - current = current.after; - return next; - } - }; - } - - @Override - public HttpHeaders addInt(CharSequence name, int value) { - add(name, Integer.toString(value)); - return this; - } - - @Override - public HttpHeaders addShort(CharSequence name, short value) { - add(name, Short.toString(value)); - return this; - } - - @Override - public HttpHeaders setInt(CharSequence name, int value) { - return set(name, Integer.toString(value)); - } - - @Override - public HttpHeaders setShort(CharSequence name, short value) { - return set(name, Short.toString(value)); - } - - public void encode(ByteBuf buf) { - HeadersMultiMap.MapEntry current = head.after; - while (current != head) { - encoderHeader(current.key, current.value, buf); - current = current.after; - } - } - - private static final int COLON_AND_SPACE_SHORT = (COLON << 8) | SP; - static final int CRLF_SHORT = (CR << 8) | LF; - - static void encoderHeader(CharSequence name, CharSequence value, ByteBuf buf) { - final int nameLen = name.length(); - final int valueLen = value.length(); - final int entryLen = nameLen + valueLen + 4; - buf.ensureWritable(entryLen); - int offset = buf.writerIndex(); - writeAscii(buf, offset, name); - offset += nameLen; - ByteBufUtil.setShortBE(buf, offset, COLON_AND_SPACE_SHORT); - offset += 2; - writeAscii(buf, offset, value); - offset += valueLen; - ByteBufUtil.setShortBE(buf, offset, CRLF_SHORT); - offset += 2; - buf.writerIndex(offset); - } - - private static void writeAscii(ByteBuf buf, int offset, CharSequence value) { - if (value instanceof AsciiString) { - ByteBufUtil.copy((AsciiString) value, 0, buf, offset, value.length()); - } else { - buf.setCharSequence(offset, value, CharsetUtil.US_ASCII); - } - } - - private final class MapEntry implements Map.Entry { - final int hash; - final CharSequence key; - CharSequence value; - HeadersMultiMap.MapEntry next; - HeadersMultiMap.MapEntry before, after; - - MapEntry() { - this.hash = -1; - this.key = null; - this.value = null; - } - - MapEntry(int hash, CharSequence key, CharSequence value) { - this.hash = hash; - this.key = key; - this.value = value; - } - - void remove() { - before.after = after; - after.before = before; - after = null; - before = null; - } - - void addBefore(HeadersMultiMap.MapEntry e) { - after = e; - before = e.before; - before.after = this; - after.before = this; - } - - @Override - public CharSequence getKey() { - return key; - } - - @Override - public CharSequence getValue() { - return value; - } - - @Override - public CharSequence setValue(CharSequence value) { - Objects.requireNonNull(value, "value"); - if (validator != null) { - validator.accept("", value); - } - CharSequence oldValue = this.value; - this.value = value; - return oldValue; - } - - @Override - public String toString() { - return getKey() + "=" + getValue(); - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - private Map.Entry stringEntry() { - if (key instanceof String && value instanceof String) { - return (Map.Entry) this; - } else { - return new AbstractMap.SimpleEntry<>(key.toString(), value.toString()); - } - } - } - - private void remove0(int h, int i, CharSequence name) { - HeadersMultiMap.MapEntry e = entries[i]; - MapEntry prev = null; - while (e != null) { - MapEntry next = e.next; - CharSequence key = e.key; - if (e.hash == h && (name == key || AsciiString.contentEqualsIgnoreCase(name, key))) { - if (prev == null) { - entries[i] = next; - } else { - prev.next = next; - } - e.remove(); - } else { - prev = e; - } - e = next; - } - } - - private void add0(int h, int i, final CharSequence name, final CharSequence value) { - if (validator != null) { - validator.accept(name, value); - } - // Update the hash table. - HeadersMultiMap.MapEntry e = entries[i]; - HeadersMultiMap.MapEntry newEntry; - entries[i] = newEntry = new HeadersMultiMap.MapEntry(h, name, value); - newEntry.next = e; - - // Update the linked list. - newEntry.addBefore(head); - } - - private HeadersMultiMap set0(final CharSequence name, final CharSequence strVal) { - int h = AsciiString.hashCode(name); - int i = h & 0x0000000F; - remove0(h, i, name); - if (strVal != null) { - add0(h, i, name, strVal); - } - return this; - } - - private CharSequence get0(CharSequence name) { - int h = AsciiString.hashCode(name); - int i = h & 0x0000000F; - HeadersMultiMap.MapEntry e = entries[i]; - CharSequence value = null; - while (e != null) { - CharSequence key = e.key; - if (e.hash == h && (name == key || AsciiString.contentEqualsIgnoreCase(name, key))) { - value = e.getValue(); - } - e = e.next; - } - return value; - } -} 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 eb70df1274..8f9db1fb98 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 @@ -92,12 +92,13 @@ import io.netty.util.IllegalReferenceCountException; public class NettyContext implements DefaultContext, ChannelFutureListener { - + public static HttpHeadersFactory HEADERS = + DefaultHttpHeadersFactory.headersFactory().withValidation(false); 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(); + HttpHeaders setHeaders = HEADERS.newHeaders(); private int bufferSize; InterfaceHttpPostRequestDecoder decoder; DefaultHttpDataFactory httpDataFactory; @@ -436,7 +437,7 @@ 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 { @@ -553,7 +554,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); } @@ -602,9 +603,7 @@ public Context send(@NonNull ByteBuf data) { 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); + var response = new DefaultFullHttpResponse(HTTP_1_1, status, data, setHeaders, NO_TRAILING); if (ctx.channel().eventLoop().inEventLoop()) { needsFlush = true; ctx.write(response, promise(this)); @@ -629,7 +628,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() @@ -660,7 +659,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() @@ -689,7 +688,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()) { @@ -765,8 +764,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)); @@ -869,10 +868,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) { 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..8cccc6fea3 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 @@ -5,20 +5,10 @@ */ package io.jooby.internal.netty; -import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.*; public class NettyResponseEncoder extends HttpResponseEncoder { - @Override - protected void encodeHeaders(HttpHeaders headers, ByteBuf buf) { - if (headers instanceof HeadersMultiMap headersMultiMap) { - headersMultiMap.encode(buf); - } else { - super.encodeHeaders(headers, buf); - } - } - @Override public boolean acceptOutboundMessage(Object msg) throws Exception { // fast-path singleton(s) @@ -34,7 +24,7 @@ public boolean acceptOutboundMessage(Object msg) throws Exception { // 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) { + if (msgClass == DefaultFullHttpResponse.class || msgClass == DefaultHttpResponse.class) { return true; } return super.acceptOutboundMessage(msg) && !(msg instanceof HttpRequest); 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 b250855afa..c636c37903 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 @@ -173,14 +173,13 @@ 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(_8KB) .setMaxChunkSize(bufferSize) - .setHeadersFactory(headersFactory) - .setTrailersFactory(headersFactory); + .setHeadersFactory(NettyContext.HEADERS) + .setTrailersFactory(NettyContext.HEADERS); return new NettyPipeline( sslContext, dateService, 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 518dd39367..78f47062e9 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 @@ -116,16 +116,16 @@ public String getName() { Undertow.Builder builder = Undertow.builder() .setBufferSize(options.getBufferSize()) - /** Socket : */ + /* 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.ALLOW_EQUALS_IN_COOKIE_VALUE, true) .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); From 790c7fe6e4f3012125a763254dff8bde79a3de42 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 28 Jun 2025 09:59:44 -0300 Subject: [PATCH 21/60] netty: more code cleanup --- .../io/jooby/internal/netty/NettyHandler.java | 2 +- .../jooby/internal/netty/NettyPipeline.java | 2 +- .../internal/netty/NettyRequestDecoder.java | 13 +++++--- .../internal/netty/NettyResponseEncoder.java | 32 ------------------- 4 files changed, 11 insertions(+), 38 deletions(-) delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.java 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..5b0965354a 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 @@ -71,7 +71,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } context.setHeaders.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.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/NettyPipeline.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java index 44b12e46cc..a1441cbfc9 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java @@ -119,7 +119,7 @@ private void http11Upgrade( private void http11(ChannelPipeline p) { p.addLast("decoder", new NettyRequestDecoder(decoderConfig)); - p.addLast("encoder", new NettyResponseEncoder()); + p.addLast("encoder", new HttpResponseEncoder()); additionalHandlers(p); p.addLast("handler", createHandler()); } 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 deleted file mode 100644 index 8cccc6fea3..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.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.netty; - -import io.netty.buffer.Unpooled; -import io.netty.handler.codec.http.*; - -public class NettyResponseEncoder extends HttpResponseEncoder { - @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 == DefaultFullHttpResponse.class || msgClass == DefaultHttpResponse.class) { - return true; - } - return super.acceptOutboundMessage(msg) && !(msg instanceof HttpRequest); - } -} From c4194350224750eac3787395a5ff00d220d105aa Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 28 Jun 2025 18:56:08 -0300 Subject: [PATCH 22/60] output api: rename to buffered output + add buffer options - update server implementations --- jooby/src/main/java/io/jooby/Context.java | 8 +- .../main/java/io/jooby/DefaultContext.java | 4 +- .../main/java/io/jooby/ForwardingContext.java | 8 +- jooby/src/main/java/io/jooby/Jooby.java | 6 +- .../main/java/io/jooby/MessageEncoder.java | 4 +- jooby/src/main/java/io/jooby/Router.java | 6 +- jooby/src/main/java/io/jooby/Sender.java | 4 +- .../src/main/java/io/jooby/ServerOptions.java | 37 ++++---- .../main/java/io/jooby/ServerSentMessage.java | 2 +- .../main/java/io/jooby/TemplateEngine.java | 6 +- jooby/src/main/java/io/jooby/WebSocket.java | 10 +-- .../java/io/jooby/internal/HeadContext.java | 6 +- .../io/jooby/internal/HttpMessageEncoder.java | 4 +- .../java/io/jooby/internal/RouterImpl.java | 8 +- .../io/jooby/internal/WebSocketSender.java | 4 +- .../internal/handler/ChunkedSubscriber.java | 4 +- .../output/ByteArrayWrappedOutput.java | 12 +-- .../internal/output/ByteBufferOutput.java | 14 ++-- .../output/ByteBufferWrappedOutput.java | 12 +-- .../output/CompsiteByteBufferOutput.java | 16 ++-- .../internal/output/OutputOutputStream.java | 10 +-- .../jooby/internal/output/OutputWriter.java | 6 +- .../java/io/jooby/output/BufferOptions.java | 84 +++++++++++++++++++ .../{Output.java => BufferedOutput.java} | 25 +++--- ...actory.java => BufferedOutputFactory.java} | 71 ++++++---------- .../jooby/output/ByteBufferOutputFactory.java | 33 +++----- .../ForwardingBufferedOutputFactory.java | 66 +++++++++++++++ .../jooby/output/ForwardingOutputFactory.java | 71 ---------------- jooby/src/test/java/io/jooby/Issue3607.java | 9 +- .../test/java/io/jooby/ServerOptionsTest.java | 4 +- .../java/io/jooby/ServerSentMessageTest.java | 9 +- .../test/java/io/jooby/buffer/Issue3434.java | 5 +- .../io/jooby/output/BufferedOutputTest.java | 2 +- .../jooby/avaje/jsonb/AvajeJsonbModule.java | 4 +- .../avaje/jsonb/BufferedJsonOutput.java | 6 +- .../avaje/jsonb/AvajeJsonbEncoderBench.java | 11 +-- .../freemarker/FreemarkerTemplateEngine.java | 4 +- .../main/java/io/jooby/gson/GsonModule.java | 4 +- .../test/java/io/jooby/gson/Issue3434.java | 5 +- .../handlebars/HandlebarsTemplateEngine.java | 4 +- .../java/io/jooby/jackson/JacksonModule.java | 4 +- .../java/io/jooby/jackson/JacksonBench.java | 11 +-- .../jooby/jackson/JacksonJsonModuleTest.java | 7 +- .../jooby/internal/jetty/JettyCallbacks.java | 6 +- .../io/jooby/internal/jetty/JettyContext.java | 4 +- .../io/jooby/internal/jetty/JettySender.java | 4 +- .../jooby/internal/jetty/JettyWebSocket.java | 6 +- .../jetty/WebSocketOutputCallback.java | 4 +- .../main/java/io/jooby/jetty/JettyServer.java | 17 +++- .../jstachio/JStachioMessageEncoder.java | 8 +- .../internal/jte/BufferedTemplateOutput.java | 6 +- .../jooby/internal/jte/JteModelEncoder.java | 4 +- .../java/io/jooby/jte/JteTemplateEngine.java | 4 +- .../jte/BufferedTemplateOutputTest.java | 9 +- .../src/test/java/io/jooby/jte/Issue3599.java | 12 +-- .../src/test/java/io/jooby/jte/Issue3602.java | 8 +- .../src/test/kotlin/io/jooby/kt/Idioms.kt | 1 - .../internal/netty/NettyBufferedOutput.java | 14 ++-- .../internal/netty/NettyByteBufOutput.java | 6 +- .../io/jooby/internal/netty/NettyContext.java | 4 +- .../internal/netty/NettyOutputFactory.java | 37 ++++---- .../io/jooby/internal/netty/NettySender.java | 4 +- .../jooby/internal/netty/NettyWebSocket.java | 6 +- .../internal/netty/NettyWrappedOutput.java | 14 ++-- .../main/java/io/jooby/netty/NettyServer.java | 28 ++++--- .../io/jooby/pebble/PebbleTemplateEngine.java | 4 +- .../io/jooby/rocker/BufferedRockerOutput.java | 12 +-- .../io/jooby/rocker/RockerMessageEncoder.java | 4 +- .../main/java/io/jooby/test/MockContext.java | 13 +-- .../java/io/jooby/test/MockWebSocket.java | 6 +- .../thymeleaf/ThymeleafTemplateEngine.java | 4 +- .../internal/undertow/UndertowContext.java | 4 +- .../undertow/UndertowOutputCallback.java | 4 +- .../internal/undertow/UndertowSender.java | 4 +- .../UndertowServerSentConnection.java | 12 ++- .../internal/undertow/UndertowWebSocket.java | 10 +-- .../io/jooby/undertow/UndertowServer.java | 14 +++- .../java/io/jooby/yasson/YassonModule.java | 4 +- .../io/jooby/yasson/YassonModuleTest.java | 5 +- .../test/java/io/jooby/i2613/Issue2613.java | 4 +- .../test/java/io/jooby/i2806/Issue2806.java | 3 +- .../java/io/jooby/junit/ServerTestRunner.java | 5 +- .../test/java/io/jooby/test/FeaturedTest.java | 3 +- 83 files changed, 513 insertions(+), 438 deletions(-) create mode 100644 jooby/src/main/java/io/jooby/output/BufferOptions.java rename jooby/src/main/java/io/jooby/output/{Output.java => BufferedOutput.java} (87%) rename jooby/src/main/java/io/jooby/output/{OutputFactory.java => BufferedOutputFactory.java} (54%) create mode 100644 jooby/src/main/java/io/jooby/output/ForwardingBufferedOutputFactory.java delete mode 100644 jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java index 1b2b2c18be..623cdb1f94 100644 --- a/jooby/src/main/java/io/jooby/Context.java +++ b/jooby/src/main/java/io/jooby/Context.java @@ -34,8 +34,8 @@ 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.output.BufferedOutput; +import io.jooby.output.BufferedOutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -145,7 +145,7 @@ private static Selector single() { */ Router getRouter(); - OutputFactory getOutputFactory(); + BufferedOutputFactory getOutputFactory(); /** * Forward executing to another route. We use the given path to find a matching route. @@ -1312,7 +1312,7 @@ Context responseWriter( * @param output Output. * @return This context. */ - Context send(@NonNull Output output); + Context send(@NonNull BufferedOutput output); /** * Send response data. diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 0365d4adf8..d103744bdc 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -35,7 +35,7 @@ import io.jooby.internal.MissingValue; import io.jooby.internal.SingleValue; import io.jooby.internal.UrlParser; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferedOutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -654,7 +654,7 @@ default Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { } @Override - default OutputFactory getOutputFactory() { + default BufferedOutputFactory getOutputFactory() { return getRouter().getOutputFactory(); } } diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index 2e3d874e20..2227f2c7a5 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -24,8 +24,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.exception.RegistryException; -import io.jooby.output.Output; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferedOutput; +import io.jooby.output.BufferedOutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -671,7 +671,7 @@ public Router getRouter() { } @Override - public OutputFactory getOutputFactory() { + public BufferedOutputFactory getOutputFactory() { return ctx.getOutputFactory(); } @@ -1229,7 +1229,7 @@ public Context send(@NonNull ByteBuffer data) { } @Override - public Context send(@NonNull Output output) { + public Context send(@NonNull BufferedOutput output) { ctx.send(output); return this; } diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 5551c5dfae..9ea5fc9e21 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -51,7 +51,7 @@ import io.jooby.internal.MutedServer; import io.jooby.internal.RegistryRef; import io.jooby.internal.RouterImpl; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferedOutputFactory; import io.jooby.problem.ProblemDetailsHandler; import io.jooby.value.ValueFactory; import jakarta.inject.Provider; @@ -920,12 +920,12 @@ public Jooby setValueFactory(@NonNull ValueFactory valueFactory) { } @NonNull @Override - public OutputFactory getOutputFactory() { + public BufferedOutputFactory getOutputFactory() { return router.getOutputFactory(); } @NonNull @Override - public Jooby setOutputFactory(@NonNull OutputFactory outputFactory) { + public Jooby setOutputFactory(@NonNull BufferedOutputFactory outputFactory) { router.setOutputFactory(outputFactory); return this; } diff --git a/jooby/src/main/java/io/jooby/MessageEncoder.java b/jooby/src/main/java/io/jooby/MessageEncoder.java index b79ef124dd..116e65570b 100644 --- a/jooby/src/main/java/io/jooby/MessageEncoder.java +++ b/jooby/src/main/java/io/jooby/MessageEncoder.java @@ -8,7 +8,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.exception.NotAcceptableException; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; /** * Render a route output as a byte array. @@ -36,5 +36,5 @@ public interface MessageEncoder { * @return Value as a byte array or null if given object isn't supported it. * @throws Exception If something goes wrong. */ - @Nullable Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception; + @Nullable BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) throws Exception; } diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 92c60a2158..57e94230e8 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -36,7 +36,7 @@ import io.jooby.exception.MissingValueException; import io.jooby.handler.AssetHandler; import io.jooby.handler.AssetSource; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferedOutputFactory; import io.jooby.value.ValueFactory; import jakarta.inject.Provider; @@ -536,9 +536,9 @@ default Object execute(@NonNull Context context) { */ @NonNull Router setDefaultWorker(@NonNull Executor worker); - @NonNull OutputFactory getOutputFactory(); + @NonNull BufferedOutputFactory getOutputFactory(); - @NonNull Router setOutputFactory(@NonNull OutputFactory outputFactory); + @NonNull Router setOutputFactory(@NonNull BufferedOutputFactory outputFactory); /** * Attach a filter to the route pipeline. diff --git a/jooby/src/main/java/io/jooby/Sender.java b/jooby/src/main/java/io/jooby/Sender.java index a5dae5fb97..97333d6565 100644 --- a/jooby/src/main/java/io/jooby/Sender.java +++ b/jooby/src/main/java/io/jooby/Sender.java @@ -10,7 +10,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; /** * Non-blocking sender. Reactive responses use this class to send partial data in a non-blocking @@ -94,7 +94,7 @@ interface Callback { */ Sender write(@NonNull byte[] data, @NonNull Callback callback); - Sender write(@NonNull Output output, @NonNull Callback callback); + Sender write(@NonNull BufferedOutput output, @NonNull Callback callback); /** Close the sender. */ void close(); diff --git a/jooby/src/main/java/io/jooby/ServerOptions.java b/jooby/src/main/java/io/jooby/ServerOptions.java index 30ab5d4f60..df261479be 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.BufferOptions; /** * 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,9 +71,6 @@ 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; @@ -100,6 +98,8 @@ public class ServerOptions { /** Name of server: Jetty, Netty or Undertow. */ private String server; + private BufferOptions buffer = BufferOptions.defaults(); + /** * Maximum request size in bytes. Request exceeding this value results in {@link * io.jooby.StatusCode#REQUEST_ENTITY_TOO_LARGE} response. Default is 10mb. @@ -143,8 +143,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.buffer.size")) { + options.buffer.setSize(conf.getInt("server.buffer.size")); + } + if (conf.hasPath("server.buffer.useDirectBuffers")) { + options.buffer.setDirectBuffers(conf.getBoolean("server.buffer.useDirectBuffers")); } if (conf.hasPath("server.defaultHeaders")) { options.setDefaultHeaders(conf.getBoolean("server.defaultHeaders")); @@ -191,7 +194,7 @@ public String toString() { buff.append(", ioThreads: ").append(Optional.ofNullable(ioThreads).orElse(IO_THREADS)); } buff.append(", workerThreads: ").append(getWorkerThreads()); - buff.append(", bufferSize: ").append(bufferSize); + buff.append(", buffer: ").append(getBuffer()); buff.append(", maxRequestSize: ").append(maxRequestSize); buff.append(", httpsOnly: ").append(httpsOnly); if (compressionLevel != null) { @@ -405,24 +408,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 BufferOptions getBuffer() { + return buffer; } - /** - * Set buffer size. - * - * @param bufferSize Buffer size. - * @return This options. - */ - public @NonNull ServerOptions setBufferSize(int bufferSize) { - this.bufferSize = bufferSize; + public ServerOptions setBuffer(@NonNull BufferOptions buffer) { + this.buffer = buffer; return this; } diff --git a/jooby/src/main/java/io/jooby/ServerSentMessage.java b/jooby/src/main/java/io/jooby/ServerSentMessage.java index 8f36d26ab7..797b7f7540 100644 --- a/jooby/src/main/java/io/jooby/ServerSentMessage.java +++ b/jooby/src/main/java/io/jooby/ServerSentMessage.java @@ -132,7 +132,7 @@ public ServerSentMessage(@NonNull Object data) { * @param ctx Web context. To encode complex objects. * @return Encoded data. */ - public @NonNull Output encode(@NonNull Context ctx) { + public @NonNull BufferedOutput encode(@NonNull Context ctx) { try { var route = ctx.getRoute(); var encoder = route.getEncoder(); diff --git a/jooby/src/main/java/io/jooby/TemplateEngine.java b/jooby/src/main/java/io/jooby/TemplateEngine.java index ab4dd426c8..b46347d7e2 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.output.Output; +import io.jooby.output.BufferedOutput; /** * 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. */ - Output render(Context ctx, ModelAndView modelAndView) throws Exception; + BufferedOutput render(Context ctx, ModelAndView modelAndView) throws Exception; @Override - default Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + default BufferedOutput 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/WebSocket.java b/jooby/src/main/java/io/jooby/WebSocket.java index 85a79c7ec0..960c05edce 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.output.Output; +import io.jooby.output.BufferedOutput; /** * Websocket. Usage: @@ -241,11 +241,11 @@ interface WriteCallback { @NonNull WebSocket send(@NonNull ByteBuffer message, @NonNull WriteCallback callback); - default @NonNull WebSocket send(@NonNull Output message) { + default @NonNull WebSocket send(@NonNull BufferedOutput message) { return send(message, WriteCallback.NOOP); } - @NonNull WebSocket send(@NonNull Output message, @NonNull WriteCallback callback); + @NonNull WebSocket send(@NonNull BufferedOutput message, @NonNull WriteCallback callback); /** * Send a binary message to client. @@ -293,11 +293,11 @@ interface WriteCallback { @NonNull WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback callback); - default @NonNull WebSocket sendBinary(@NonNull Output message) { + default @NonNull WebSocket sendBinary(@NonNull BufferedOutput message) { return sendBinary(message, WriteCallback.NOOP); } - @NonNull WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback); + @NonNull WebSocket sendBinary(@NonNull BufferedOutput message, @NonNull WriteCallback callback); /** * Encode a value and send a text message to client. diff --git a/jooby/src/main/java/io/jooby/internal/HeadContext.java b/jooby/src/main/java/io/jooby/internal/HeadContext.java index c4dc99aec9..50d87ebe63 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.output.Output; +import io.jooby.output.BufferedOutput; public class HeadContext extends ForwardingContext { /** @@ -66,7 +66,7 @@ public Context send(@NonNull ByteBuffer data) { } @NonNull @Override - public Context send(@NonNull Output output) { + public Context send(@NonNull BufferedOutput output) { ctx.setResponseLength(output.size()); checkSizeHeaders(); ctx.send(StatusCode.OK); @@ -186,7 +186,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull Output output, @NonNull Callback callback) { + public Sender write(@NonNull BufferedOutput output, @NonNull Callback callback) { return this; } diff --git a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java index 5fc8de2670..6f36ef8866 100644 --- a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java +++ b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java @@ -20,7 +20,7 @@ import io.jooby.ModelAndView; import io.jooby.StatusCode; import io.jooby.TemplateEngine; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; public class HttpMessageEncoder implements MessageEncoder { @@ -42,7 +42,7 @@ public HttpMessageEncoder add(MediaType type, MessageEncoder encoder) { } @Override - public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) throws Exception { if (value instanceof ModelAndView modelAndView) { for (var engine : templateEngineList) { if (engine.supports(modelAndView)) { diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 7d819569de..452b3f3f41 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -44,7 +44,7 @@ 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.output.BufferedOutputFactory; import io.jooby.problem.ProblemDetailsHandler; import io.jooby.value.ValueFactory; import jakarta.inject.Provider; @@ -171,7 +171,7 @@ public Stack executor(Executor executor) { private ValueFactory valueFactory = new ValueFactory(); - private OutputFactory outputFactory = OutputFactory.create(false); + private BufferedOutputFactory outputFactory = BufferedOutputFactory.create(); public RouterImpl() { stack.addLast(new Stack(chi, null)); @@ -457,12 +457,12 @@ public Router setValueFactory(@NonNull ValueFactory valueFactory) { } @NonNull @Override - public OutputFactory getOutputFactory() { + public BufferedOutputFactory getOutputFactory() { return outputFactory; } @NonNull @Override - public Router setOutputFactory(@NonNull OutputFactory outputFactory) { + public Router setOutputFactory(@NonNull BufferedOutputFactory outputFactory) { this.outputFactory = outputFactory; return this; } diff --git a/jooby/src/main/java/io/jooby/internal/WebSocketSender.java b/jooby/src/main/java/io/jooby/internal/WebSocketSender.java index f2c98791b0..9ec214e382 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.output.Output; +import io.jooby.output.BufferedOutput; public class WebSocketSender extends ForwardingContext implements DefaultContext { @@ -69,7 +69,7 @@ public Context send(@NonNull ByteBuffer data) { } @Override - public Context send(@NonNull Output output) { + public Context send(@NonNull BufferedOutput output) { if (binary) { ws.sendBinary(output, callback); } else { 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 6867ec8e1f..e1b934468f 100644 --- a/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java +++ b/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java @@ -14,7 +14,7 @@ import io.jooby.Route; import io.jooby.Sender; import io.jooby.Server; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; public class ChunkedSubscriber implements Flow.Subscriber { @@ -116,7 +116,7 @@ public void onComplete() { sender().close(); } - private static Output prepend(Context ctx, Output data, byte c) { + private static BufferedOutput prepend(Context ctx, BufferedOutput data, byte c) { var buffer = ctx.getOutputFactory().newCompositeOutput(); buffer.write(c); data.transferTo(buffer::write); diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java b/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java index 0da4015f8c..8377e1b6f4 100644 --- a/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java @@ -11,9 +11,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; -public class ByteArrayWrappedOutput implements Output { +public class ByteArrayWrappedOutput implements BufferedOutput { private final byte[] buffer; @@ -22,22 +22,22 @@ public ByteArrayWrappedOutput(byte[] source) { } @Override - public Output write(byte b) { + public BufferedOutput write(byte b) { throw new UnsupportedOperationException(); } @Override - public Output write(byte[] source) { + public BufferedOutput write(byte[] source) { throw new UnsupportedOperationException(); } @Override - public Output write(byte[] source, int offset, int length) { + public BufferedOutput write(byte[] source, int offset, int length) { throw new UnsupportedOperationException(); } @Override - public Output clear() { + public BufferedOutput clear() { return this; } diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java b/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java index 41c05bb482..b3612d4904 100644 --- a/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java @@ -13,9 +13,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; -public class ByteBufferOutput implements Output { +public class ByteBufferOutput implements BufferedOutput { private static final int MAX_CAPACITY = Integer.MAX_VALUE; private static final int CAPACITY_THRESHOLD = 1024 * 1024 * 4; @@ -70,7 +70,7 @@ public String asString(@NonNull Charset charset) { } @Override - public Output write(byte b) { + public BufferedOutput write(byte b) { ensureWritable(1); this.buffer.put(this.writePosition, b); this.writePosition += 1; @@ -78,12 +78,12 @@ public Output write(byte b) { } @Override - public Output write(byte[] source) { + public BufferedOutput write(byte[] source) { return write(source, 0, source.length); } @Override - public Output write(byte[] source, int offset, int length) { + public BufferedOutput write(byte[] source, int offset, int length) { ensureWritable(length); var tmp = this.buffer.duplicate(); @@ -96,7 +96,7 @@ public Output write(byte[] source, int offset, int length) { } @Override - public Output write(@NonNull ByteBuffer source) { + public BufferedOutput write(@NonNull ByteBuffer source) { ensureWritable(source.remaining()); var length = source.remaining(); var tmp = this.buffer.duplicate(); @@ -108,7 +108,7 @@ public Output write(@NonNull ByteBuffer source) { } @Override - public Output clear() { + public BufferedOutput clear() { this.buffer.clear(); return this; } diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java b/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java index 8c64efe090..5c9b2c2c0c 100644 --- a/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java @@ -11,9 +11,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; -public class ByteBufferWrappedOutput implements Output { +public class ByteBufferWrappedOutput implements BufferedOutput { private final ByteBuffer buffer; @@ -22,22 +22,22 @@ public ByteBufferWrappedOutput(ByteBuffer buffer) { } @Override - public Output write(byte b) { + public BufferedOutput write(byte b) { throw new UnsupportedOperationException(); } @Override - public Output write(byte[] source) { + public BufferedOutput write(byte[] source) { throw new UnsupportedOperationException(); } @Override - public Output write(byte[] source, int offset, int length) { + public BufferedOutput write(byte[] source, int offset, int length) { throw new UnsupportedOperationException(); } @Override - public Output clear() { + public BufferedOutput clear() { buffer.clear(); return this; } diff --git a/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java b/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java index e9d53689ab..ffc30dc66e 100644 --- a/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java @@ -13,12 +13,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; -public class CompsiteByteBufferOutput implements Output { +public class CompsiteByteBufferOutput implements BufferedOutput { private final List chunks = new ArrayList<>(); private int size = 0; - private int bufferSizeHint = BUFFER_SIZE; @Override public int size() { @@ -26,25 +25,25 @@ public int size() { } @Override - public Output write(byte b) { + public BufferedOutput write(byte b) { addChunk(ByteBuffer.wrap(new byte[] {b})); return this; } @Override - public Output write(byte[] source) { + public BufferedOutput write(byte[] source) { addChunk(ByteBuffer.wrap(source)); return this; } @Override - public Output write(byte[] source, int offset, int length) { + public BufferedOutput write(byte[] source, int offset, int length) { addChunk(ByteBuffer.wrap(source, offset, length)); return this; } @Override - public Output clear() { + public BufferedOutput clear() { chunks.forEach(ByteBuffer::clear); chunks.clear(); return this; @@ -89,8 +88,5 @@ private void addChunk(ByteBuffer chunk) { chunks.add(chunk); int length = chunk.remaining(); size += length; - if (bufferSizeHint < length) { - bufferSizeHint = length; - } } } diff --git a/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java index bae5482d19..8c6b6bf67b 100644 --- a/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java +++ b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java @@ -9,20 +9,20 @@ import java.io.OutputStream; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; /** - * An {@link OutputStream} that writes to a {@link io.jooby.output.Output}. + * An {@link OutputStream} that writes to a {@link BufferedOutput}. * - * @see io.jooby.output.Output#asOutputStream() + * @see BufferedOutput#asOutputStream() */ public class OutputOutputStream extends OutputStream { - private final Output output; + private final BufferedOutput output; private boolean closed; - public OutputOutputStream(@NonNull Output output) { + public OutputOutputStream(@NonNull BufferedOutput output) { this.output = output; } diff --git a/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java b/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java index f49779657d..c3028b119f 100644 --- a/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java +++ b/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java @@ -11,14 +11,14 @@ import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; public class OutputWriter extends Writer { - private final Output output; + private final BufferedOutput output; private final Charset charset; private boolean closed; - public OutputWriter(@NonNull Output output, @NonNull Charset charset) { + public OutputWriter(@NonNull BufferedOutput output, @NonNull Charset charset) { this.output = output; this.charset = charset; } diff --git a/jooby/src/main/java/io/jooby/output/BufferOptions.java b/jooby/src/main/java/io/jooby/output/BufferOptions.java new file mode 100644 index 0000000000..4fe607a9ff --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/BufferOptions.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 BufferOptions { + private int size; + + private boolean directBuffers; + + public BufferOptions() { + 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 BufferOptions small() { + return new BufferOptions().setDirectBuffers(false).setSize(512); + } + + public static BufferOptions defaults() { + return new BufferOptions(); + } + + /** + * 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 BufferOptions setSize(int size) { + this.size = size; + return this; + } + + public boolean isDirectBuffers() { + return directBuffers; + } + + public BufferOptions setDirectBuffers(boolean directBuffers) { + this.directBuffers = directBuffers; + return this; + } + + @Override + public String toString() { + return "{size: " + size + ", direct: " + directBuffers + '}'; + } +} diff --git a/jooby/src/main/java/io/jooby/output/Output.java b/jooby/src/main/java/io/jooby/output/BufferedOutput.java similarity index 87% rename from jooby/src/main/java/io/jooby/output/Output.java rename to jooby/src/main/java/io/jooby/output/BufferedOutput.java index d9490cb530..261cfee3f5 100644 --- a/jooby/src/main/java/io/jooby/output/Output.java +++ b/jooby/src/main/java/io/jooby/output/BufferedOutput.java @@ -24,16 +24,13 @@ * buffers. * *

There are two implementations of output one is backed by a {@link ByteBuffer} and the other is - * a view of multiple {@link ByteBuffer} byffers. See {@link OutputFactory#newBufferedOutput()} and - * {@link OutputFactory#newCompositeOutput()} + * a view of multiple {@link ByteBuffer} byffers. See {@link + * BufferedOutputFactory#newBufferedOutput()} and {@link BufferedOutputFactory#newCompositeOutput()} * * @author edgar * @since 4.0.0 */ -public interface Output { - /** Default buffer size: 4k. */ - int BUFFER_SIZE = 4096; - +public interface BufferedOutput { /** * This output as an output stream. Changes made to the output stream are reflected in this * output. @@ -111,7 +108,7 @@ default Iterator iterator() { * @param b the byte to be written * @return this output */ - Output write(byte b); + BufferedOutput write(byte b); /** * Write the given source into this buffer, starting at the current writing position of this @@ -120,7 +117,7 @@ default Iterator iterator() { * @param source the bytes to be written into this buffer * @return this output */ - Output write(byte[] source); + BufferedOutput write(byte[] source); /** * Write at most {@code length} bytes of the given source into this buffer, starting at the @@ -131,7 +128,7 @@ default Iterator iterator() { * @param length the maximum number of bytes to be written from {@code source} * @return this output */ - Output write(byte[] source, int offset, int length); + BufferedOutput write(byte[] source, int offset, int length); /** * Write the given {@code String} using {@code UTF-8}, starting at the current writing position. @@ -139,7 +136,7 @@ default Iterator iterator() { * @param source the char sequence to write into this buffer * @return this output */ - default Output write(@NonNull String source) { + default BufferedOutput write(@NonNull String source) { return write(source, StandardCharsets.UTF_8); } @@ -151,7 +148,7 @@ default Output write(@NonNull String source) { * @param charset the charset to encode the char sequence with * @return this output */ - default Output write(@NonNull String source, @NonNull Charset charset) { + default BufferedOutput write(@NonNull String source, @NonNull Charset charset) { if (!source.isEmpty()) { return write(source.getBytes(charset)); } @@ -165,7 +162,7 @@ default Output write(@NonNull String source, @NonNull Charset charset) { * @param source the bytes to be written into this buffer * @return this output */ - default Output write(@NonNull ByteBuffer source) { + default BufferedOutput write(@NonNull ByteBuffer source) { if (source.hasArray()) { return write(source.array(), source.arrayOffset() + source.position(), source.remaining()); } else { @@ -175,7 +172,7 @@ default Output write(@NonNull ByteBuffer source) { } } - default Output write(@NonNull CharBuffer source, @NonNull Charset charset) { + default BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { if (!source.isEmpty()) { return write(charset.encode(source)); } @@ -184,5 +181,5 @@ default Output write(@NonNull CharBuffer source, @NonNull Charset charset) { void send(io.jooby.Context ctx); - Output clear(); + BufferedOutput clear(); } diff --git a/jooby/src/main/java/io/jooby/output/OutputFactory.java b/jooby/src/main/java/io/jooby/output/BufferedOutputFactory.java similarity index 54% rename from jooby/src/main/java/io/jooby/output/OutputFactory.java rename to jooby/src/main/java/io/jooby/output/BufferedOutputFactory.java index 6e4ddeea50..73efbeb60a 100644 --- a/jooby/src/main/java/io/jooby/output/OutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/BufferedOutputFactory.java @@ -14,12 +14,12 @@ import edu.umd.cs.findbugs.annotations.NonNull; /** - * Factory class for buffered {@link Output}. + * Factory class for buffered {@link BufferedOutput}. * * @author edgar * @since 4.0.0 */ -public interface OutputFactory { +public interface BufferedOutputFactory { /** * Thread local for output buffer. Please note only store calls to {@link #newBufferedOutput()}, @@ -28,22 +28,23 @@ public interface OutputFactory { * @param factory Factory. * @return Thread local factory. */ - static OutputFactory threadLocal(OutputFactory factory) { - return new ForwardingOutputFactory(factory) { - private final ThreadLocal threadLocal = withInitial(factory::newBufferedOutput); + static BufferedOutputFactory threadLocal(BufferedOutputFactory factory) { + return new ForwardingBufferedOutputFactory(factory) { + private final ThreadLocal threadLocal = + withInitial(factory::newBufferedOutput); @Override - public Output newBufferedOutput(boolean direct, int size) { + public BufferedOutput newBufferedOutput(boolean direct, int size) { return threadLocal.get().clear(); } @Override - public Output newBufferedOutput(int size) { + public BufferedOutput newBufferedOutput(int size) { return threadLocal.get().clear(); } @Override - public Output newBufferedOutput() { + public BufferedOutput newBufferedOutput() { return threadLocal.get().clear(); } }; @@ -54,8 +55,8 @@ public Output newBufferedOutput() { * * @return Default output factory. */ - static OutputFactory create(boolean direct, int bufferSize) { - return new ByteBufferOutputFactory(direct, bufferSize); + static BufferedOutputFactory create(BufferOptions options) { + return new ByteBufferOutputFactory(options); } /** @@ -63,31 +64,13 @@ static OutputFactory create(boolean direct, int bufferSize) { * * @return Default output factory. */ - static OutputFactory create(boolean direct) { - return new ByteBufferOutputFactory(direct, Output.BUFFER_SIZE); + static BufferedOutputFactory create() { + return create(new BufferOptions()); } - /** - * 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 - */ - boolean isDirect(); + BufferOptions getOptions(); - /** - * Buffer of a default initial capacity. Default capacity is 1024 bytes. - * - * @return buffer of a default initial capacity. - */ - int getInitialBufferSize(); - - /** - * Set default buffer initial capacity. - * - * @param initialBufferSize Default initial buffer capacity. - * @return This buffer factory. - */ - OutputFactory setInitialBufferSize(int initialBufferSize); + BufferedOutputFactory setOptions(BufferOptions options); /** * Creates a new byte buffered output. @@ -96,20 +79,20 @@ static OutputFactory create(boolean direct) { * @param size Output size. * @return A byte buffered output. */ - Output newBufferedOutput(boolean direct, int size); + BufferedOutput newBufferedOutput(boolean direct, int size); /** - * Creates a new byte buffered output with an initial size of {@link Output#BUFFER_SIZE}. + * Creates a new byte buffered output. * * @param size Output size. * @return A byte buffered output. */ - default Output newBufferedOutput(int size) { - return newBufferedOutput(isDirect(), size); + default BufferedOutput newBufferedOutput(int size) { + return newBufferedOutput(getOptions().isDirectBuffers(), size); } - default Output newBufferedOutput() { - return newBufferedOutput(isDirect(), Output.BUFFER_SIZE); + default BufferedOutput newBufferedOutput() { + return newBufferedOutput(getOptions().isDirectBuffers(), getOptions().getSize()); } /** @@ -118,7 +101,7 @@ default Output newBufferedOutput() { * * @return A new composite buffer. */ - Output newCompositeOutput(); + BufferedOutput newCompositeOutput(); /** * Readonly buffer created from string utf-8 bytes. @@ -126,7 +109,7 @@ default Output newBufferedOutput() { * @param value String. * @return Readonly buffer. */ - default Output wrap(String value) { + default BufferedOutput wrap(String value) { return wrap(value, StandardCharsets.UTF_8); } @@ -137,7 +120,7 @@ default Output wrap(String value) { * @param charset Charset to use. * @return Readonly buffer. */ - default Output wrap(@NonNull String value, @NonNull Charset charset) { + default BufferedOutput wrap(@NonNull String value, @NonNull Charset charset) { return wrap(value.getBytes(charset)); } @@ -147,7 +130,7 @@ default Output wrap(@NonNull String value, @NonNull Charset charset) { * @param buffer Input buffer. * @return Readonly buffer. */ - Output wrap(@NonNull ByteBuffer buffer); + BufferedOutput wrap(@NonNull ByteBuffer buffer); /** * Readonly buffer created from byte array. @@ -155,7 +138,7 @@ default Output wrap(@NonNull String value, @NonNull Charset charset) { * @param bytes Byte array. * @return Readonly buffer. */ - Output wrap(@NonNull byte[] bytes); + BufferedOutput wrap(@NonNull byte[] bytes); /** * Readonly buffer created from byte array. @@ -165,5 +148,5 @@ default Output wrap(@NonNull String value, @NonNull Charset charset) { * @param length Length. * @return Readonly buffer. */ - Output wrap(@NonNull byte[] bytes, int offset, int length); + BufferedOutput wrap(@NonNull byte[] bytes, int offset, int length); } diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java b/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java index 977056189c..77a6ea00f4 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java @@ -19,53 +19,46 @@ * @author edgar * @since 4.0.0 */ -public class ByteBufferOutputFactory implements OutputFactory { - private int initialBufferSize; - private final boolean direct; +public class ByteBufferOutputFactory implements BufferedOutputFactory { + private BufferOptions options; - public ByteBufferOutputFactory(boolean direct, int initialBufferSize) { - this.initialBufferSize = initialBufferSize; - this.direct = direct; + public ByteBufferOutputFactory(BufferOptions options) { + this.options = options; } @Override - public int getInitialBufferSize() { - return initialBufferSize; + public BufferOptions getOptions() { + return options; } @Override - public OutputFactory setInitialBufferSize(int initialBufferSize) { - this.initialBufferSize = initialBufferSize; + public BufferedOutputFactory setOptions(BufferOptions options) { + this.options = options; return this; } @Override - public boolean isDirect() { - return direct; - } - - @Override - public Output newBufferedOutput(boolean direct, int size) { + public BufferedOutput newBufferedOutput(boolean direct, int size) { return new ByteBufferOutput(direct, size); } @Override - public Output newCompositeOutput() { + public BufferedOutput newCompositeOutput() { return new CompsiteByteBufferOutput(); } @Override - public Output wrap(@NonNull ByteBuffer buffer) { + public BufferedOutput wrap(@NonNull ByteBuffer buffer) { return new ByteBufferWrappedOutput(buffer); } @Override - public Output wrap(@NonNull byte[] bytes) { + public BufferedOutput wrap(@NonNull byte[] bytes) { return new ByteArrayWrappedOutput(bytes); } @Override - public Output wrap(@NonNull byte[] bytes, int offset, int length) { + public BufferedOutput wrap(@NonNull byte[] bytes, int offset, int length) { return new ByteBufferWrappedOutput(ByteBuffer.wrap(bytes, offset, length)); } } diff --git a/jooby/src/main/java/io/jooby/output/ForwardingBufferedOutputFactory.java b/jooby/src/main/java/io/jooby/output/ForwardingBufferedOutputFactory.java new file mode 100644 index 0000000000..b931b99e90 --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/ForwardingBufferedOutputFactory.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 ForwardingBufferedOutputFactory implements BufferedOutputFactory { + + protected final BufferedOutputFactory delegate; + + public ForwardingBufferedOutputFactory(@NonNull BufferedOutputFactory delegate) { + this.delegate = delegate; + } + + @Override + public BufferOptions getOptions() { + return delegate.getOptions(); + } + + @Override + public BufferedOutputFactory setOptions(BufferOptions options) { + delegate.setOptions(options); + return this; + } + + @Override + public BufferedOutput newBufferedOutput(int size) { + return delegate.newBufferedOutput(size); + } + + @Override + public BufferedOutput newBufferedOutput(boolean direct, int size) { + return delegate.newBufferedOutput(direct, size); + } + + @Override + public BufferedOutput newCompositeOutput() { + return delegate.newCompositeOutput(); + } + + @Override + public BufferedOutput wrap(@NonNull ByteBuffer buffer) { + return delegate.wrap(buffer); + } + + @Override + public BufferedOutput wrap(@NonNull byte[] bytes) { + return delegate.wrap(bytes); + } + + @Override + public BufferedOutput wrap(@NonNull byte[] bytes, int offset, int length) { + return delegate.wrap(bytes, offset, length); + } +} diff --git a/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java b/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java deleted file mode 100644 index fedf0ef74a..0000000000 --- a/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 int getInitialBufferSize() { - return delegate.getInitialBufferSize(); - } - - @Override - public OutputFactory setInitialBufferSize(int initialBufferSize) { - delegate.setInitialBufferSize(initialBufferSize); - return this; - } - - @Override - public boolean isDirect() { - return delegate.isDirect(); - } - - @Override - public Output newBufferedOutput(int size) { - return delegate.newBufferedOutput(size); - } - - @Override - public Output newBufferedOutput(boolean direct, int size) { - return delegate.newBufferedOutput(direct, size); - } - - @Override - public Output newCompositeOutput() { - return delegate.newCompositeOutput(); - } - - @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/test/java/io/jooby/Issue3607.java b/jooby/src/test/java/io/jooby/Issue3607.java index e95efaccd1..da72fe4cbc 100644 --- a/jooby/src/test/java/io/jooby/Issue3607.java +++ b/jooby/src/test/java/io/jooby/Issue3607.java @@ -9,14 +9,15 @@ import org.junit.jupiter.api.Test; -import io.jooby.output.Output; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferOptions; +import io.jooby.output.BufferedOutput; +import io.jooby.output.BufferedOutputFactory; public class Issue3607 { private static class TemplateEngineImpl implements TemplateEngine { @Override - public Output render(Context ctx, ModelAndView modelAndView) throws Exception { + public BufferedOutput render(Context ctx, ModelAndView modelAndView) throws Exception { // do nothing return ctx.getOutputFactory().wrap(new byte[0]); } @@ -25,7 +26,7 @@ public Output render(Context ctx, ModelAndView modelAndView) throws Exception @Test public void shouldNotGenerateEmptyFlashMap() throws Exception { var ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(false)); + when(ctx.getOutputFactory()).thenReturn(BufferedOutputFactory.create(BufferOptions.small())); var templateEngine = new TemplateEngineImpl(); templateEngine.encode(ctx, ModelAndView.map("index.html")); 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 b71f81c329..2760591021 100644 --- a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java +++ b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java @@ -13,7 +13,8 @@ import org.junit.jupiter.api.Test; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferOptions; +import io.jooby.output.BufferedOutputFactory; public class ServerSentMessageTest { @@ -22,7 +23,7 @@ public void shouldFormatMessage() throws Exception { var data = "some"; var ctx = mock(Context.class); - var bufferFactory = OutputFactory.create(false); + var bufferFactory = BufferedOutputFactory.create(BufferOptions.small()); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) @@ -42,7 +43,7 @@ public void shouldFormatMultiLineMessage() throws Exception { var data = "line 1\n line ,a .. 2\nline ...abc 3"; var ctx = mock(Context.class); - var bufferFactory = OutputFactory.create(false); + var bufferFactory = BufferedOutputFactory.create(BufferOptions.small()); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) @@ -64,7 +65,7 @@ public void shouldFormatMessageEndingWithNL() throws Exception { var data = "line 1\n"; var ctx = mock(Context.class); - var bufferFactory = OutputFactory.create(false); + var bufferFactory = BufferedOutputFactory.create(BufferOptions.small()); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) diff --git a/jooby/src/test/java/io/jooby/buffer/Issue3434.java b/jooby/src/test/java/io/jooby/buffer/Issue3434.java index 4839fdcd58..03292093e8 100644 --- a/jooby/src/test/java/io/jooby/buffer/Issue3434.java +++ b/jooby/src/test/java/io/jooby/buffer/Issue3434.java @@ -15,10 +15,11 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferOptions; +import io.jooby.output.BufferedOutputFactory; public class Issue3434 { - OutputFactory factory = OutputFactory.create(false); + BufferedOutputFactory factory = BufferedOutputFactory.create(BufferOptions.small()); @Test void shouldWriteCharBufferOnBufferWriter() throws IOException { diff --git a/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java b/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java index a6e6247679..134c06c3d5 100644 --- a/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java +++ b/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java @@ -75,7 +75,7 @@ public void bufferedOutput() { new ByteBufferOutput(false, 255)); } - private void output(SneakyThrows.Consumer consumer, Output... buffers) { + private void output(SneakyThrows.Consumer consumer, BufferedOutput... buffers) { Stream.of(buffers).forEach(consumer); } 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 4c21bdc16e..ee414047dd 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 @@ -19,7 +19,7 @@ import io.jooby.MessageEncoder; import io.jooby.ServiceRegistry; import io.jooby.internal.avaje.jsonb.BufferedJsonOutput; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; /** * JSON module using Avaje-JsonB: message; - private OutputFactory factory; - private ThreadLocal cache = + private BufferedOutputFactory factory; + private ThreadLocal cache = ThreadLocal.withInitial( () -> { return factory.newBufferedOutput(1024); @@ -37,7 +38,7 @@ public class AvajeJsonbEncoderBench { public void setup() { message = Map.of("id", 98, "value", "Hello World"); jsonb = Jsonb.builder().build(); - factory = OutputFactory.create(false); + factory = BufferedOutputFactory.create(BufferOptions.small()); } @Benchmark 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 d8340e80d1..e0742b22ce 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.output.Output; +import io.jooby.output.BufferedOutput; class FreemarkerTemplateEngine implements TemplateEngine { @@ -33,7 +33,7 @@ public List extensions() { } @Override - public Output render(Context ctx, ModelAndView modelAndView) throws Exception { + public BufferedOutput render(Context ctx, ModelAndView modelAndView) throws Exception { var buffer = ctx.getOutputFactory().newBufferedOutput(); var template = freemarker.getTemplate(modelAndView.getView()); var writer = buffer.asWriter(); 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 bb4072ecb2..15b83084d9 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.output.Output; +import io.jooby.output.BufferedOutput; /** * JSON module using Gson: https://github.com/google/gson. @@ -107,7 +107,7 @@ public Object decode(@NonNull Context ctx, @NonNull Type type) throws Exception } @NonNull @Override - public Output encode(@NonNull Context ctx, @NonNull Object value) { + public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) { var buffer = ctx.getOutputFactory().newBufferedOutput(); ctx.setDefaultResponseType(MediaType.json); gson.toJson(value, buffer.asWriter()); 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 ac19294b97..25fad306df 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.output.OutputFactory; +import io.jooby.output.BufferOptions; +import io.jooby.output.BufferedOutputFactory; public class Issue3434 { String text = @@ -139,7 +140,7 @@ public class Issue3434 { @Test void shouldEncodeUsingBufferWriter() { var gson = new GsonBuilder().create(); - var factory = OutputFactory.create(false); + var factory = BufferedOutputFactory.create(BufferOptions.small()); var ctx = mock(Context.class); when(ctx.getOutputFactory()).thenReturn(factory); 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 90eec6c874..e7bcc1be5b 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.output.Output; +import io.jooby.output.BufferedOutput; public class HandlebarsTemplateEngine implements TemplateEngine { @@ -35,7 +35,7 @@ public List extensions() { } @Override - public Output render(Context ctx, ModelAndView modelAndView) throws Exception { + public BufferedOutput render(Context ctx, ModelAndView modelAndView) throws Exception { var template = handlebars.compile(modelAndView.getView()); var engineModel = com.github.jknack.handlebars.Context.newBuilder(modelAndView.getModel()) 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 ff50761dbc..ea801ee940 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.output.Output; +import io.jooby.output.BufferedOutput; /** * JSON module using Jackson: https://jooby.io/modules/jackson. @@ -153,7 +153,7 @@ public void install(@NonNull Jooby application) { } @Override - public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) throws Exception { var factory = ctx.getOutputFactory(); ctx.setDefaultResponseType(mediaType); // let jackson uses his own cache, so just wrap the bytes 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 index 07bb9e732c..992d0a4bcb 100644 --- a/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java +++ b/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java @@ -13,8 +13,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.jooby.output.Output; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferOptions; +import io.jooby.output.BufferedOutput; +import io.jooby.output.BufferedOutputFactory; @Fork(5) @Warmup(iterations = 5, time = 1) @@ -26,15 +27,15 @@ public class JacksonBench { private ObjectMapper mapper; private Map message; - private OutputFactory factory; - private ThreadLocal cache = + private BufferedOutputFactory factory; + private ThreadLocal cache = ThreadLocal.withInitial(() -> factory.newBufferedOutput(1024)); @Setup public void setup() { message = Map.of("id", 98, "value", "Hello World"); mapper = new ObjectMapper(); - factory = OutputFactory.create(false); + factory = BufferedOutputFactory.create(BufferOptions.small()); } @Benchmark 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 21af89c76e..a9dae085b2 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,14 +21,15 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferOptions; +import io.jooby.output.BufferedOutputFactory; public class JacksonJsonModuleTest { @Test public void renderJson() throws Exception { Context ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(false)); + when(ctx.getOutputFactory()).thenReturn(BufferedOutputFactory.create(BufferOptions.small())); JacksonModule jackson = new JacksonModule(new ObjectMapper()); @@ -57,7 +58,7 @@ public void parseJson() throws Exception { @Test public void renderXml() throws Exception { Context ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(false)); + when(ctx.getOutputFactory()).thenReturn(BufferedOutputFactory.create(BufferOptions.small())); JacksonModule jackson = new JacksonModule(new XmlMapper()); 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 432fe708de..0d4005cb3f 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 @@ -11,7 +11,7 @@ import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.Callback; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; public class JettyCallbacks { public static class ByteBufferArrayCallback implements Callback { @@ -52,7 +52,7 @@ public static class OutputCallback implements Callback { private final Iterator it; private boolean closeOnLast; - public OutputCallback(Response response, Callback cb, Output buffer) { + public OutputCallback(Response response, Callback cb, BufferedOutput buffer) { this.response = response; this.cb = cb; this.it = buffer.iterator(); @@ -87,7 +87,7 @@ public void failed(Throwable x) { } } - public static OutputCallback fromOutput(Response response, Callback cb, Output output) { + public static OutputCallback fromOutput(Response response, Callback cb, BufferedOutput output) { return new OutputCallback(response, cb, output); } 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 ffd8bf08f7..3d8ae36727 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 @@ -70,7 +70,7 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.WebSocket; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.jooby.value.Value; public class JettyContext implements DefaultContext, Callback { @@ -518,7 +518,7 @@ public Context send(@NonNull String data, @NonNull Charset charset) { } @NonNull @Override - public Context send(@NonNull Output output) { + public Context send(@NonNull BufferedOutput output) { output.send(this); return this; } 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 c8f235041f..e97f86f3bc 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 @@ -13,7 +13,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Sender; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; public class JettySender implements Sender { private final JettyContext ctx; @@ -31,7 +31,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull Output output, @NonNull Callback callback) { + public Sender write(@NonNull BufferedOutput 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/JettyWebSocket.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyWebSocket.java index d6ba8d3e25..24891fc65f 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.output.Output; +import io.jooby.output.BufferedOutput; public class JettyWebSocket implements Session.Listener, WebSocketConfigurer, WebSocket { @@ -287,14 +287,14 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback } @NonNull @Override - public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { + public WebSocket send(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { return sendMessage( (remote, writeCallback) -> remote.sendText(message.asString(UTF_8), writeCallback), new WriteCallbackAdaptor(this, callback)); } @NonNull @Override - public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { + public WebSocket sendBinary(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { return sendMessage( (remote, writeCallback) -> new WebSocketOutputCallback(writeCallback, message, remote::sendBinary).send(), diff --git a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketOutputCallback.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketOutputCallback.java index 1f11df0989..97b744dd45 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketOutputCallback.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketOutputCallback.java @@ -11,7 +11,7 @@ import org.eclipse.jetty.websocket.api.Callback; import io.jooby.SneakyThrows.Consumer2; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; public class WebSocketOutputCallback implements Callback { @@ -20,7 +20,7 @@ public class WebSocketOutputCallback implements Callback { private Consumer2 sender; public WebSocketOutputCallback( - Callback cb, Output buffer, Consumer2 sender) { + Callback cb, BufferedOutput buffer, Consumer2 sender) { this.cb = cb; this.it = buffer.iterator(); this.sender = sender; 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 5dd5a03169..aa2555f963 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 @@ -54,10 +54,19 @@ public class JettyServer extends io.jooby.Server.Base { private Consumer httpConfigurer; + 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 @@ -68,7 +77,7 @@ public JettyServer setOptions(@NonNull ServerOptions options) { @Override protected ServerOptions defaultOptions() { - return new ServerOptions().setServer("jetty").setWorkerThreads(THREADS); + return new ServerOptions().setServer(getName()).setWorkerThreads(THREADS); } @NonNull @Override @@ -117,8 +126,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.getBuffer().getSize()); + httpConf.setOutputAggregationSize(options.getBuffer().getSize()); httpConf.setSendXPoweredBy(false); httpConf.setSendDateHeader(options.getDefaultHeaders()); httpConf.setSendServerVersion(false); @@ -268,7 +277,7 @@ private List> createHandler( new JettyHandler( invocationType, applications.get(0), - options.getBufferSize(), + options.getBuffer().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 2249d8deac..8b715e1cf6 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 @@ -11,11 +11,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.MessageEncoder; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; 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, @@ -25,7 +25,7 @@ public JStachioMessageEncoder( } @Override - public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) throws Exception { if (supportsType(value.getClass())) { return render(ctx, value); } @@ -33,7 +33,7 @@ public Output encode(@NonNull Context ctx, @NonNull Object value) throws Excepti } @Override - Output extractOutput(Context ctx, ByteBufferEncodedOutput stream) throws IOException { + BufferedOutput extractOutput(Context ctx, ByteBufferEncodedOutput stream) throws IOException { return ctx.getOutputFactory().wrap(stream.asByteBuffer()); } } diff --git a/modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.java b/modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.java index 54f0713bd8..6129c812dc 100644 --- a/modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.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.output.Output; +import io.jooby.output.BufferedOutput; public class BufferedTemplateOutput implements TemplateOutput { - private final Output buffer; + private final BufferedOutput buffer; private final Charset charset; - public BufferedTemplateOutput(Output 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 e36ae3f54c..05ded3648d 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,11 +11,11 @@ import edu.umd.cs.findbugs.annotations.Nullable; import gg.jte.models.runtime.JteModel; import io.jooby.Context; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; public class JteModelEncoder implements io.jooby.MessageEncoder { @Nullable @Override - public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) throws Exception { if (value instanceof JteModel jte) { var buffer = ctx.getOutputFactory().newBufferedOutput(); jte.render(new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8)); 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 36b81d2790..66d7dc0f93 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 @@ -15,7 +15,7 @@ import io.jooby.MapModelAndView; import io.jooby.ModelAndView; import io.jooby.internal.jte.BufferedTemplateOutput; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; class JteTemplateEngine implements io.jooby.TemplateEngine { private final TemplateEngine jte; @@ -32,7 +32,7 @@ public List extensions() { } @Override - public Output render(Context ctx, ModelAndView modelAndView) { + public BufferedOutput render(Context ctx, ModelAndView modelAndView) { var buffer = ctx.getOutputFactory().newBufferedOutput(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); var attributes = ctx.getAttributes(); 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 index 04a78bce7a..57f1d27948 100644 --- 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 @@ -11,13 +11,14 @@ import org.junit.jupiter.api.Test; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferOptions; +import io.jooby.output.BufferedOutputFactory; public class BufferedTemplateOutputTest { @Test public void checkWriteContent() { - var factory = OutputFactory.create(false); + var factory = BufferedOutputFactory.create(BufferOptions.small()); var buffer = factory.newBufferedOutput(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeContent("Hello"); @@ -26,7 +27,7 @@ public void checkWriteContent() { @Test public void checkWriteContentSubstring() { - var factory = OutputFactory.create(false); + var factory = BufferedOutputFactory.create(BufferOptions.small()); var buffer = factory.newBufferedOutput(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeContent(" Hello World! ", 1, " Hello World! ".length() - 2); @@ -35,7 +36,7 @@ public void checkWriteContentSubstring() { @Test public void checkWriteBinaryContent() { - var factory = OutputFactory.create(false); + var factory = BufferedOutputFactory.create(BufferOptions.small()); var buffer = factory.newBufferedOutput(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeBinaryContent("Hello".getBytes(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 7a15dffb36..f54ac20f16 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,15 +19,15 @@ import io.jooby.Context; import io.jooby.MapModelAndView; import io.jooby.ModelAndView; -import io.jooby.output.Output; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferedOutput; +import io.jooby.output.BufferedOutputFactory; public class Issue3599 { @Test public void shouldNotCallObjectMethodOnMapModels() { - var bufferFactory = mock(OutputFactory.class); - var buffer = mock(Output.class); + var bufferFactory = mock(BufferedOutputFactory.class); + var buffer = mock(BufferedOutput.class); when(bufferFactory.newBufferedOutput()).thenReturn(buffer); var attributes = Map.of("foo", 1); @@ -53,8 +53,8 @@ public void shouldNotCallObjectMethodOnMapModels() { @Test public void shouldCallObjectMethodOnObjectModels() { - var bufferFactory = mock(OutputFactory.class); - var buffer = mock(Output.class); + var bufferFactory = mock(BufferedOutputFactory.class); + var buffer = mock(BufferedOutput.class); when(bufferFactory.newBufferedOutput()).thenReturn(buffer); var attributes = Map.of("foo", 1); 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 35e3d319ed..84075fd362 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 @@ -16,15 +16,15 @@ import gg.jte.models.runtime.JteModel; import io.jooby.Context; import io.jooby.internal.jte.JteModelEncoder; -import io.jooby.output.Output; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferedOutput; +import io.jooby.output.BufferedOutputFactory; public class Issue3602 { @Test public void shouldRenderJteModel() throws Exception { - var bufferFactory = mock(OutputFactory.class); - var buffer = mock(Output.class); + var bufferFactory = mock(BufferedOutputFactory.class); + var buffer = mock(BufferedOutput.class); when(bufferFactory.newBufferedOutput()).thenReturn(buffer); var attributes = Map.of("foo", 1); 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..10acb7cb0c 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 @@ -37,7 +37,6 @@ class Idioms : /** Options: */ serverOptions { - bufferSize = 8194 ioThreads = 8 compressionLevel = 6 defaultHeaders = false diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java index 3580cd4968..177226d57b 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java @@ -9,7 +9,7 @@ import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.netty.buffer.ByteBuf; public class NettyBufferedOutput implements NettyByteBufOutput { @@ -25,37 +25,37 @@ protected NettyBufferedOutput(ByteBuf buffer) { } @Override - @NonNull public Output write(byte b) { + @NonNull public BufferedOutput write(byte b) { buffer.writeByte(b); return this; } @Override - @NonNull public Output write(byte[] source) { + @NonNull public BufferedOutput write(byte[] source) { buffer.writeBytes(source); return this; } @Override - @NonNull public Output write(byte[] source, int offset, int length) { + @NonNull public BufferedOutput write(byte[] source, int offset, int length) { this.buffer.writeBytes(source, offset, length); return this; } @Override - @NonNull public Output write(@NonNull String source, @NonNull Charset charset) { + @NonNull public BufferedOutput write(@NonNull String source, @NonNull Charset charset) { this.buffer.writeBytes(source.getBytes(charset)); return this; } @Override - public Output write(@NonNull CharBuffer source, @NonNull Charset charset) { + public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { this.buffer.writeBytes(charset.encode(source)); return this; } @Override - @NonNull public Output clear() { + @NonNull public BufferedOutput clear() { this.buffer.clear(); return this; } 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 index 32ab202bcd..5955cc7d1d 100644 --- 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 @@ -11,11 +11,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -public interface NettyByteBufOutput extends Output { +public interface NettyByteBufOutput extends BufferedOutput { @NonNull ByteBuf byteBuf(); @Override @@ -47,7 +47,7 @@ default void send(Context ctx) { } } - static ByteBuf byteBuf(Output output) { + static ByteBuf byteBuf(BufferedOutput output) { if (output instanceof NettyByteBufOutput netty) { return netty.byteBuf(); } else { 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 8f9db1fb98..a2634ce423 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 @@ -67,7 +67,7 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.WebSocket; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.jooby.value.Value; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -594,7 +594,7 @@ public final Context send(ByteBuffer data) { } @Override - @NonNull public Context send(@NonNull Output output) { + @NonNull public Context send(@NonNull BufferedOutput output) { output.send(this); return this; } 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 index b91787e4d5..bdc7a35492 100644 --- 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 @@ -9,13 +9,14 @@ import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.output.Output; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferOptions; +import io.jooby.output.BufferedOutput; +import io.jooby.output.BufferedOutputFactory; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.util.ResourceLeakDetector; -public class NettyOutputFactory implements OutputFactory { +public class NettyOutputFactory implements BufferedOutputFactory { private static final String LEAK_DETECTION = "io.netty.leakDetection.level"; static { @@ -27,10 +28,11 @@ public class NettyOutputFactory implements OutputFactory { } private final ByteBufAllocator allocator; - private int initialBufferSize = Output.BUFFER_SIZE; + private BufferOptions options; - public NettyOutputFactory(ByteBufAllocator allocator) { + public NettyOutputFactory(ByteBufAllocator allocator, BufferOptions options) { this.allocator = allocator; + this.options = options; } public ByteBufAllocator getAllocator() { @@ -38,49 +40,44 @@ public ByteBufAllocator getAllocator() { } @Override - public boolean isDirect() { - return allocator.isDirectBufferPooled(); + public BufferOptions getOptions() { + return options; } @Override - public int getInitialBufferSize() { - return initialBufferSize; - } - - @Override - public @NonNull OutputFactory setInitialBufferSize(int initialBufferSize) { - this.initialBufferSize = initialBufferSize; + public BufferedOutputFactory setOptions(BufferOptions options) { + this.options = options; return this; } @Override - public @NonNull Output newBufferedOutput(boolean direct, int size) { + public @NonNull BufferedOutput newBufferedOutput(boolean direct, int size) { return new NettyBufferedOutput( direct ? this.allocator.directBuffer(size) : this.allocator.heapBuffer(size)); } @Override - @NonNull public Output wrap(@NonNull ByteBuffer buffer) { + @NonNull public BufferedOutput wrap(@NonNull ByteBuffer buffer) { return new NettyWrappedOutput(Unpooled.wrappedBuffer(buffer)); } @Override - public Output wrap(@NonNull String value, @NonNull Charset charset) { + public BufferedOutput wrap(@NonNull String value, @NonNull Charset charset) { return new NettyWrappedOutput(Unpooled.wrappedBuffer(value.getBytes(charset))); } @Override - @NonNull public Output wrap(@NonNull byte[] bytes) { + @NonNull public BufferedOutput wrap(@NonNull byte[] bytes) { return new NettyWrappedOutput(Unpooled.wrappedBuffer(bytes)); } @Override - @NonNull public Output wrap(@NonNull byte[] bytes, int offset, int length) { + @NonNull public BufferedOutput wrap(@NonNull byte[] bytes, int offset, int length) { return new NettyWrappedOutput(Unpooled.wrappedBuffer(bytes, offset, length)); } @Override - @NonNull public Output newCompositeOutput() { + @NonNull public BufferedOutput newCompositeOutput() { return new NettyBufferedOutput(allocator.compositeBuffer(48)); } } 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 512832b133..eddba1ecac 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 @@ -9,7 +9,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Sender; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; @@ -35,7 +35,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull Output output, @NonNull Callback callback) { + public Sender write(@NonNull BufferedOutput output, @NonNull Callback callback) { context .writeAndFlush(new DefaultHttpContent(byteBuf(output))) .addListener(newChannelFutureListener(ctx, callback)); 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 c0519d6258..35127fb784 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 @@ -28,7 +28,7 @@ import io.jooby.WebSocketCloseStatus; import io.jooby.WebSocketConfigurer; import io.jooby.WebSocketMessage; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; @@ -140,12 +140,12 @@ public WebSocket sendBinary(@NonNull byte[] message, @NonNull WriteCallback call } @NonNull @Override - public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { + public WebSocket send(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { return sendMessage(byteBuf(message), false, callback); } @NonNull @Override - public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { + public WebSocket sendBinary(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { return sendMessage(byteBuf(message), true, callback); } 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 index e0857c41ae..f9e6b18411 100644 --- 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 @@ -9,7 +9,7 @@ import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.netty.buffer.ByteBuf; public class NettyWrappedOutput implements NettyByteBufOutput { @@ -25,32 +25,32 @@ protected NettyWrappedOutput(ByteBuf buffer) { } @Override - @NonNull public Output write(byte b) { + @NonNull public BufferedOutput write(byte b) { throw new UnsupportedOperationException(); } @Override - @NonNull public Output write(byte[] source) { + @NonNull public BufferedOutput write(byte[] source) { throw new UnsupportedOperationException(); } @Override - @NonNull public Output write(byte[] source, int offset, int length) { + @NonNull public BufferedOutput write(byte[] source, int offset, int length) { throw new UnsupportedOperationException(); } @Override - @NonNull public Output write(@NonNull String source, @NonNull Charset charset) { + @NonNull public BufferedOutput write(@NonNull String source, @NonNull Charset charset) { throw new UnsupportedOperationException(); } @Override - public Output write(@NonNull CharBuffer source, @NonNull Charset charset) { + public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { throw new UnsupportedOperationException(); } @Override - @NonNull public Output clear() { + @NonNull public BufferedOutput clear() { return this; } } 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 c636c37903..9cab0d07e5 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 @@ -54,6 +54,16 @@ public class NettyServer extends Server.Base { private List applications; + /** + * Creates a server. + * + * @param worker Thread-pool to use. + */ + public NettyServer(@NonNull ServerOptions options, @NonNull ExecutorService worker) { + super.setOptions(options); + this.worker = worker; + } + /** * Creates a server. * @@ -64,17 +74,15 @@ public NettyServer(@NonNull ExecutorService worker) { } /** Creates a server. */ - public NettyServer() {} - - @Override - public @NonNull NettyServer setOptions(@NonNull ServerOptions options) { + public NettyServer(@NonNull ServerOptions options) { super.setOptions(options); - return this; } + public NettyServer() {} + @Override protected ServerOptions defaultOptions() { - return new ServerOptions().setServer("netty"); + return new ServerOptions().setServer(getName()); } @NonNull @Override @@ -88,13 +96,12 @@ public Server start(@NonNull Jooby... application) { var options = getOptions(); 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")); } // Make sure context use same buffer factory - var outputFactory = new NettyOutputFactory(ByteBufAllocator.DEFAULT); + var outputFactory = new NettyOutputFactory(ByteBufAllocator.DEFAULT, options.getBuffer()); applications.forEach(app -> app.setOutputFactory(outputFactory)); addShutdownHook(); @@ -172,12 +179,11 @@ private ClientAuth toClientAuth(SslOptions.ClientAuth clientAuth) { private NettyPipeline newPipeline( ServerOptions options, SslContext sslContext, NettyDateService dateService, boolean http2) { - var bufferSize = options.getBufferSize(); var decoderConfig = new HttpDecoderConfig() .setMaxInitialLineLength(_4KB) .setMaxHeaderSize(_8KB) - .setMaxChunkSize(bufferSize) + .setMaxChunkSize(options.getBuffer().getSize()) .setHeadersFactory(NettyContext.HEADERS) .setTrailersFactory(NettyContext.HEADERS); return new NettyPipeline( @@ -186,7 +192,7 @@ private NettyPipeline newPipeline( decoderConfig, applications, options.getMaxRequestSize(), - bufferSize, + options.getBuffer().getSize(), options.getDefaultHeaders(), http2, options.isExpectContinue() == Boolean.TRUE, 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 48f2b55a9a..9cba1c3fe6 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.output.Output; +import io.jooby.output.BufferedOutput; import io.pebbletemplates.pebble.PebbleEngine; class PebbleTemplateEngine implements TemplateEngine { @@ -34,7 +34,7 @@ public List extensions() { } @Override - public Output render(Context ctx, ModelAndView modelAndView) throws Exception { + public BufferedOutput render(Context ctx, ModelAndView modelAndView) throws Exception { if (modelAndView instanceof MapModelAndView mapModelAndView) { var buffer = ctx.getOutputFactory().newBufferedOutput(); var template = engine.getTemplate(modelAndView.getView()); diff --git a/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java b/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java index fee3bd3e01..2c78e154ee 100644 --- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java +++ b/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java @@ -10,8 +10,8 @@ import com.fizzed.rocker.ContentType; import com.fizzed.rocker.RockerOutput; import com.fizzed.rocker.RockerOutputFactory; -import io.jooby.output.Output; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferedOutput; +import io.jooby.output.BufferedOutputFactory; /** * Rocker output that uses a byte array to render the output. @@ -27,9 +27,9 @@ public class BufferedRockerOutput implements RockerOutput private final ContentType contentType; /** The buffer where data is stored. */ - protected Output output; + protected BufferedOutput output; - BufferedRockerOutput(Charset charset, ContentType contentType, Output output) { + BufferedRockerOutput(Charset charset, ContentType contentType, BufferedOutput output) { this.charset = charset; this.contentType = contentType; this.output = output; @@ -67,12 +67,12 @@ public int getByteLength() { * * @return Byte buffer. */ - public Output asOutput() { + public BufferedOutput asOutput() { return output; } static RockerOutputFactory factory( - Charset charset, OutputFactory factory, int bufferSize) { + Charset charset, BufferedOutputFactory factory, int bufferSize) { return (contentType, charsetName) -> new BufferedRockerOutput(charset, contentType, factory.newCompositeOutput()); } 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 841ebd49a2..b003e8c3b6 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,7 +11,7 @@ import io.jooby.Context; import io.jooby.MediaType; import io.jooby.MessageEncoder; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; class RockerMessageEncoder implements MessageEncoder { private final RockerOutputFactory factory; @@ -21,7 +21,7 @@ class RockerMessageEncoder implements MessageEncoder { } @Override - public Output encode(@NonNull Context ctx, @NonNull Object value) { + public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) { if (value instanceof RockerModel template) { var output = template.render(factory); ctx.setResponseLength(output.getByteLength()); 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 f36886051a..b857d27dd0 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 @@ -54,8 +54,9 @@ import io.jooby.StatusCode; import io.jooby.WebSocket; import io.jooby.exception.TypeMismatchException; -import io.jooby.output.Output; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferOptions; +import io.jooby.output.BufferedOutput; +import io.jooby.output.BufferedOutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -118,7 +119,7 @@ public class MockContext implements DefaultContext { private int port = -1; - private OutputFactory outputFactory = OutputFactory.create(false); + private BufferedOutputFactory outputFactory = BufferedOutputFactory.create(BufferOptions.small()); @Override public String getMethod() { @@ -137,7 +138,7 @@ public int getPort() { } @Override - public OutputFactory getOutputFactory() { + public BufferedOutputFactory getOutputFactory() { return outputFactory; } @@ -570,7 +571,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull Output output, @NonNull Callback callback) { + public Sender write(@NonNull BufferedOutput output, @NonNull Callback callback) { response.setResult(output); callback.onComplete(MockContext.this, null); return this; @@ -669,7 +670,7 @@ public MockContext send(@NonNull ByteBuffer data) { } @Override - public Context send(@NonNull Output output) { + public Context send(@NonNull BufferedOutput output) { responseStarted = true; this.response.setResult(output).setContentLength(output.size()); listeners.run(this); 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 1445614eac..1d97d23034 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.output.Output; +import io.jooby.output.BufferedOutput; /** * 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 Output message, @NonNull WriteCallback callback) { + public WebSocket send(@NonNull BufferedOutput 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 Output message, @NonNull WriteCallback callback) { + public WebSocket sendBinary(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { return sendObject(message, callback); } 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 30ac63bcb8..ff51625bea 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,7 +16,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.MapModelAndView; import io.jooby.ModelAndView; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; public class ThymeleafTemplateEngine implements io.jooby.TemplateEngine { private final TemplateEngine templateEngine; @@ -33,7 +33,7 @@ public List extensions() { } @Override - public @NonNull Output render(io.jooby.Context ctx, ModelAndView modelAndView) { + public @NonNull BufferedOutput render(io.jooby.Context ctx, ModelAndView modelAndView) { if (modelAndView instanceof MapModelAndView mapModelAndView) { Map model = new HashMap<>(ctx.getAttributes()); model.putAll(mapModelAndView.getModel()); 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 a5da9314e6..cff902c183 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 @@ -57,7 +57,7 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.WebSocket; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.jooby.value.Value; import io.undertow.Handlers; import io.undertow.io.IoCallback; @@ -499,7 +499,7 @@ public Context send(@NonNull ByteBuffer data) { } @NonNull @Override - public Context send(@NonNull Output output) { + public Context send(@NonNull BufferedOutput output) { output.send(this); return this; } 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 index 0a2222f6cc..6aad99ed07 100644 --- 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 @@ -9,7 +9,7 @@ import java.nio.ByteBuffer; import java.util.Iterator; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.undertow.io.IoCallback; import io.undertow.io.Sender; import io.undertow.server.HttpServerExchange; @@ -19,7 +19,7 @@ public class UndertowOutputCallback implements IoCallback { private Iterator iterator; private IoCallback callback; - public UndertowOutputCallback(Output buffer, IoCallback callback) { + public UndertowOutputCallback(BufferedOutput buffer, IoCallback callback) { this.iterator = buffer.iterator(); this.callback = callback; } 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 a3844cfc6f..bf098429a9 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.output.Output; +import io.jooby.output.BufferedOutput; import io.undertow.io.IoCallback; import io.undertow.server.HttpServerExchange; @@ -30,7 +30,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull Output output, @NonNull Callback callback) { + public Sender write(@NonNull BufferedOutput 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 0ba3230b7b..4b68895e48 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 @@ -25,7 +25,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.ServerSentMessage; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; @@ -169,7 +169,7 @@ private void fillBuffer() { * * @param dest Destination buffer. */ - private void transferTo(@NonNull Output source, @NonNull ByteBuffer dest) { + private void transferTo(@NonNull BufferedOutput source, @NonNull ByteBuffer dest) { transferTo(source, 0, dest, dest.position(), source.size()); } @@ -184,7 +184,11 @@ private void transferTo(@NonNull Output source, @NonNull ByteBuffer dest) { * @param length the amount of data to copy */ private void transferTo( - @NonNull Output source, int srcPos, @NonNull ByteBuffer dest, int destPos, int length) { + @NonNull BufferedOutput source, + int srcPos, + @NonNull ByteBuffer dest, + int destPos, + int length) { dest = dest.duplicate().clear(); dest.put(destPos, source.asByteBuffer(), srcPos, length); } @@ -273,7 +277,7 @@ private static class SSEData { final ServerSentMessage message; final UndertowServerSentConnection.EventCallback callback; private int endBufferPosition = -1; - private Output leftOverData; + private BufferedOutput 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 5400c68a72..141fddb62b 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 @@ -33,7 +33,7 @@ import io.jooby.WebSocketCloseStatus; import io.jooby.WebSocketConfigurer; import io.jooby.WebSocketMessage; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.undertow.websockets.core.AbstractReceiveListener; import io.undertow.websockets.core.BufferedBinaryMessage; import io.undertow.websockets.core.BufferedTextMessage; @@ -99,7 +99,7 @@ public WebSocketOutputCallback( WebSocketChannel channel, WriteCallback callback, boolean binary, - Output buffer) { + BufferedOutput buffer) { this.ws = ws; this.channel = channel; this.binary = binary; @@ -223,12 +223,12 @@ public WebSocket sendBinary(@NonNull String message, @NonNull WriteCallback call } @NonNull @Override - public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { + public WebSocket sendBinary(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { return sendMessage(message, true, callback); } @NonNull @Override - public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { + public WebSocket send(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { return sendMessage(message, false, callback); } @@ -237,7 +237,7 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback return sendMessage(message, true, callback); } - private WebSocket sendMessage(Output buffer, boolean binary, WriteCallback callback) { + private WebSocket sendMessage(BufferedOutput buffer, boolean binary, WriteCallback callback) { if (isOpen()) { try { new WebSocketOutputCallback(this, channel, callback, binary, buffer).send(); 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 78f47062e9..20b0d0cd7f 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 @@ -48,10 +48,17 @@ public class UndertowServer extends Server.Base { private static final int _10 = 10; private Undertow server; + private List applications; private XnioWorker worker; + public UndertowServer(@NonNull ServerOptions options) { + setOptions(options); + } + + public UndertowServer() {} + @NonNull @Override public UndertowServer setOptions(@NonNull ServerOptions options) { // default io threads @@ -61,7 +68,7 @@ public UndertowServer setOptions(@NonNull ServerOptions options) { @Override protected ServerOptions defaultOptions() { - return new ServerOptions().setIoThreads(ServerOptions.IO_THREADS).setServer("utow"); + return new ServerOptions().setIoThreads(ServerOptions.IO_THREADS).setServer(getName()); } @NonNull @Override @@ -81,7 +88,7 @@ public String getName() { HttpHandler handler = new UndertowHandler( this.applications, - options.getBufferSize(), + getOptions().getBuffer().getSize(), options.getMaxRequestSize(), options.getDefaultHeaders()); @@ -115,7 +122,8 @@ public String getName() { Undertow.Builder builder = Undertow.builder() - .setBufferSize(options.getBufferSize()) + .setBufferSize(options.getBuffer().getSize()) + .setDirectBuffers(options.getBuffer().isDirectBuffers()) /* Socket : */ .setSocketOption(Options.BACKLOG, BACKLOG) /* Server: */ 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 30af5a7500..0513ad6391 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 @@ -20,7 +20,7 @@ import io.jooby.MessageDecoder; import io.jooby.MessageEncoder; import io.jooby.ServiceRegistry; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; @@ -101,7 +101,7 @@ public Object decode(@NonNull final Context ctx, @NonNull final Type type) throw } @Nullable @Override - public Output encode(@NonNull final Context ctx, @NonNull final Object value) { + public BufferedOutput encode(@NonNull final Context ctx, @NonNull final Object value) { ctx.setDefaultResponseType(MediaType.json); var factory = ctx.getOutputFactory(); var output = factory.newBufferedOutput(); 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 95444ea4b1..75199ce5b8 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.output.OutputFactory; +import io.jooby.output.BufferOptions; +import io.jooby.output.BufferedOutputFactory; public class YassonModuleTest { @@ -39,7 +40,7 @@ public void render() { user.age = Integer.MAX_VALUE; Context ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(false)); + when(ctx.getOutputFactory()).thenReturn(BufferedOutputFactory.create(BufferOptions.small())); var buffer = YassonModule.encode(ctx, user); assertEquals( "{\"age\":2147483647,\"id\":-1,\"name\":\"Lorem €@!?\"}", diff --git a/tests/src/test/java/io/jooby/i2613/Issue2613.java b/tests/src/test/java/io/jooby/i2613/Issue2613.java index ddd458e005..d5ec553ae6 100644 --- a/tests/src/test/java/io/jooby/i2613/Issue2613.java +++ b/tests/src/test/java/io/jooby/i2613/Issue2613.java @@ -17,7 +17,7 @@ import io.jooby.jackson.JacksonModule; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; public class Issue2613 { @@ -30,7 +30,7 @@ public String html() { public static class ThemeResultEncoder implements MessageEncoder { @Override - public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) throws Exception { if (value instanceof ThemeResult) { ctx.setDefaultResponseType(MediaType.html); return ctx.getOutputFactory() diff --git a/tests/src/test/java/io/jooby/i2806/Issue2806.java b/tests/src/test/java/io/jooby/i2806/Issue2806.java index 3b5ff321e5..8351cf0c20 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.BufferOptions; import okhttp3.MediaType; import okhttp3.RequestBody; @@ -30,7 +31,7 @@ public void renderShouldWorkFromErrorHandlerWhenLargeRequestAreSent(ServerTestRu app -> { app.setServerOptions( new ServerOptions() - .setBufferSize(ServerOptions._16KB / 2) + .setBuffer(new BufferOptions().setSize(ServerOptions._16KB / 2)) .setMaxRequestSize(ServerOptions._16KB)); app.install(new JacksonModule()); diff --git a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index 1b6d85d82e..7623578e76 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -30,7 +30,8 @@ import io.jooby.StatusCode; import io.jooby.internal.MutedServer; import io.jooby.netty.NettyServer; -import io.jooby.output.OutputFactory; +import io.jooby.output.BufferOptions; +import io.jooby.output.BufferedOutputFactory; import io.jooby.test.WebClient; public class ServerTestRunner { @@ -92,7 +93,7 @@ public void ready(SneakyThrows.Consumer2 onReady) { System.setProperty("___server_name__", server.getName()); var app = provider.get(); if (!(server instanceof NettyServer)) { - app.setOutputFactory(OutputFactory.create(false)); + app.setOutputFactory(BufferedOutputFactory.create(BufferOptions.defaults())); } Optional.ofNullable(executionMode).ifPresent(app::setExecutionMode); // Reduce log from maven build: diff --git a/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index dbeb49cb16..e9136be61c 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -86,6 +86,7 @@ import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; import io.jooby.netty.NettyServer; +import io.jooby.output.BufferOptions; import io.jooby.rxjava3.Reactivex; import io.jooby.undertow.UndertowServer; import io.reactivex.rxjava3.core.Single; @@ -1536,7 +1537,7 @@ public void maxRequestSize(ServerTestRunner runner) { app -> { app.setServerOptions( new ServerOptions() - .setBufferSize(ServerOptions._16KB / 2) + .setBuffer(new BufferOptions().setSize(ServerOptions._16KB / 2)) .setMaxRequestSize(ServerOptions._16KB)); app.post("/request-size", ctx -> ctx.body().value("")); From 0032049ad3b55eb1cd49b12cdee979aa3e3fd35b Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 28 Jun 2025 20:32:16 -0300 Subject: [PATCH 23/60] netty: fix outputstream with new buffer size --- .../io/jooby/internal/netty/NettyOutputStream.java | 11 +++-------- .../test/java/io/jooby/internal/netty/Issue3554.java | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) 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/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); From 49fc58f9f995a3e89ede3b18a142e21102b07e33 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 29 Jun 2025 11:12:08 -0300 Subject: [PATCH 24/60] router: remove setServerOptions from router fix #3703 --- .../main/java/io/jooby/DefaultContext.java | 4 +- jooby/src/main/java/io/jooby/Jooby.java | 56 ++++--------------- jooby/src/main/java/io/jooby/Router.java | 15 ++--- .../java/io/jooby/handler/SSLHandler.java | 8 ++- .../java/io/jooby/internal/RouterImpl.java | 5 +- .../main/java/io/jooby/jetty/JettyServer.java | 4 ++ .../src/main/kotlin/io/jooby/kt/Kooby.kt | 9 --- .../src/test/kotlin/io/jooby/kt/Idioms.kt | 14 ----- .../io/jooby/internal/netty/NettyContext.java | 21 +------ .../main/java/io/jooby/netty/NettyServer.java | 6 +- .../java/io/jooby/test/JoobyExtension.java | 10 ++-- .../io/jooby/undertow/UndertowServer.java | 5 ++ .../test/java/io/jooby/i2363/Issue2363.java | 3 +- .../test/java/io/jooby/i2399/Issue2399.java | 6 +- .../test/java/io/jooby/i2806/Issue2806.java | 9 ++- .../test/java/io/jooby/i3554/Issue3554.java | 4 +- .../java/io/jooby/junit/ServerTestRunner.java | 10 +++- .../test/java/io/jooby/test/FeaturedTest.java | 11 ++-- .../test/java/io/jooby/test/Http2Test.java | 6 +- .../test/java/io/jooby/test/HttpsTest.java | 32 +++++------ .../test/java/io/jooby/test/Issue1656.java | 3 +- .../test/java/io/jooby/test/Issue2372.java | 3 +- tests/src/test/kotlin/NoPckg.kt | 5 +- 23 files changed, 93 insertions(+), 156 deletions(-) diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index d103744bdc..67557c1e02 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -341,13 +341,13 @@ default String getHostAndPort() { @Override default String getServerHost() { - var host = getRouter().getServerOptions().getHost(); + var host = require(ServerOptions.class).getHost(); return host.equals("0.0.0.0") ? "localhost" : host; } @Override default int getServerPort() { - var 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()) diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 9ea5fc9e21..fed9dce416 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -111,8 +111,6 @@ public class Jooby implements Router, Registry { private RegistryRef registry = new RegistryRef(); - private ServerOptions serverOptions; - private List startupSummary; private EnvironmentOptions environmentOptions; @@ -144,30 +142,6 @@ public Jooby() { } } - /** - * 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() { return router.getRouterOptions(); @@ -473,6 +447,11 @@ public String getContextPath() { return router; } + @Nullable @Override + public ServerOptions getServerOptions() { + return router.getServerOptions(); + } + @Override public boolean isTrustProxy() { return router.isTrustProxy(); @@ -990,14 +969,6 @@ public Jooby setStartupSummary(List startupSummary) { 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); @@ -1079,8 +1050,6 @@ 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)) { @@ -1326,14 +1295,12 @@ public static void runApp( 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( + options -> { + options.setServer(server.getName()); + server.setOptions(options); + }); apps.add(app); } targetServer.start(apps.toArray(new Jooby[0])); @@ -1535,7 +1502,6 @@ private MvcFactory mvcReflectionFallback(Class source, ClassLoader classLoader) * @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/Router.java b/jooby/src/main/java/io/jooby/Router.java index 57e94230e8..0a0269ffe4 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -182,6 +182,14 @@ default Object execute(@NonNull Context context) { */ @NonNull ServiceRegistry getServices(); + /** + * Server options. They might be null during application initialization. Once deployed, they are + * never null (at runtime). + * + * @return Server options or null. + */ + @Nullable ServerOptions getServerOptions(); + /** * Set application context path. Context path is the base path for all routes. Default is: / * . @@ -956,13 +964,6 @@ default Object execute(@NonNull Context context) { @NonNull Router setValueFactory(@NonNull ValueFactory valueFactory); - /** - * Available server options. - * - * @return Server options. - */ - @NonNull ServerOptions getServerOptions(); - /** * Ensure path start with a /(leading slash). * 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/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 452b3f3f41..ec52bcecad 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -39,6 +39,7 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.*; import io.jooby.exception.RegistryException; import io.jooby.exception.StatusCodeException; @@ -812,9 +813,9 @@ public Router setFlashCookie(@NonNull Cookie flashCookie) { return this; } - @NonNull @Override + @Nullable @Override public ServerOptions getServerOptions() { - throw new UnsupportedOperationException(); + return services.getOrNull(ServerOptions.class); } @NonNull @Override 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 aa2555f963..9ddcd223a0 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 @@ -107,6 +107,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) { 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 f2b397f1e7..a88e22b1fe 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 @@ -22,7 +22,6 @@ import io.jooby.RouteSet import io.jooby.Router import io.jooby.RouterOption import io.jooby.Server -import io.jooby.ServerOptions import io.jooby.ServiceRegistry import io.jooby.handler.Cors import io.jooby.value.Value @@ -303,14 +302,6 @@ open class Kooby() : Jooby() { return super.sse(pattern) { sse -> handler(ServerSentHandler(sse.context, sse)) } } - @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) 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 10acb7cb0c..e38f882794 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 @@ -9,7 +9,6 @@ import io.jooby.Jooby import io.jooby.RouterOption.IGNORE_CASE import io.jooby.RouterOption.IGNORE_TRAILING_SLASH import io.jooby.ServiceKey -import io.jooby.SslOptions import java.nio.file.Paths import java.time.Duration import kotlinx.coroutines.delay @@ -35,19 +34,6 @@ class Idioms : services.getOrNull(Jooby::class) - /** Options: */ - serverOptions { - 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) setHiddenMethod { ctx -> ctx.header("").toOptional() } 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 a2634ce423..c6ea0649ba 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 @@ -47,26 +47,9 @@ 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.WebSocket; import io.jooby.output.BufferedOutput; import io.jooby.value.Value; import io.netty.buffer.ByteBuf; @@ -946,6 +929,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/netty/NettyServer.java b/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java index 9cab0d07e5..2c4d35d786 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 @@ -102,7 +102,11 @@ public Server start(@NonNull Jooby... application) { } // Make sure context use same buffer factory var outputFactory = new NettyOutputFactory(ByteBufAllocator.DEFAULT, options.getBuffer()); - applications.forEach(app -> app.setOutputFactory(outputFactory)); + for (var app : applications) { + app.setOutputFactory(outputFactory); + app.getServices().put(ServerOptions.class, options); + app.getServices().put(Server.class, this); + } addShutdownHook(); 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..f0e9680c44 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; /** @@ -85,10 +84,11 @@ private Jooby startApp(ExtensionContext context, JoobyTest metadata) throws Exce app = fromFactoryMethod(context, metadata, factoryMethod); } var server = Server.loadServer(); - ServerOptions serverOptions = app.getServerOptions(); - if (serverOptions == null) { - serverOptions = server.getOptions(); - } + var serverOptions = server.getOptions(); + // ServerOptions serverOptions = app.getServerOptions(); + // if (serverOptions == null) { + // serverOptions = server.getOptions(); + // } serverOptions.setPort(port(metadata.port(), DEFAULT_PORT)); server.setOptions(serverOptions); server.start(app); 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 20b0d0cd7f..ab3689d651 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 @@ -83,6 +83,11 @@ 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 = 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/i2806/Issue2806.java b/tests/src/test/java/io/jooby/i2806/Issue2806.java index 8351cf0c20..9aadc59758 100644 --- a/tests/src/test/java/io/jooby/i2806/Issue2806.java +++ b/tests/src/test/java/io/jooby/i2806/Issue2806.java @@ -27,13 +27,12 @@ public void renderShouldWorkFromErrorHandlerWhenLargeRequestAreSent(ServerTestRu Arrays.fill(chars, 'S'); String _19kb = new String(chars); runner + .options( + new ServerOptions() + .setBuffer(new BufferOptions().setSize(ServerOptions._16KB / 2)) + .setMaxRequestSize(ServerOptions._16KB)) .define( app -> { - app.setServerOptions( - new ServerOptions() - .setBuffer(new BufferOptions().setSize(ServerOptions._16KB / 2)) - .setMaxRequestSize(ServerOptions._16KB)); - app.install(new JacksonModule()); app.error( 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/junit/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index 7623578e76..2243986643 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -47,6 +47,7 @@ public class ServerTestRunner { private final ExecutionMode executionMode; private final Method testMethod; + private ServerOptions serverOptions; private Supplier provider; @@ -66,6 +67,9 @@ public ServerTestRunner define(Consumer consumer) { use( () -> { Jooby app = new Jooby(); + if (serverOptions != null) { + app.getServices().put(ServerOptions.class, serverOptions); + } app.setExecutionMode(executionMode); consumer.accept(app); return app; @@ -73,6 +77,11 @@ public ServerTestRunner define(Consumer consumer) { return this; } + public ServerTestRunner options(ServerOptions options) { + this.serverOptions = options; + return this; + } + public ServerTestRunner use(Supplier provider) { this.provider = provider; return this; @@ -106,7 +115,6 @@ public void ready(SneakyThrows.Consumer2 onReady) { .orElseGet(ServerTestRunner::buildErrorHandler)); } - ServerOptions serverOptions = app.getServerOptions(); if (serverOptions != null) { server.setOptions(serverOptions); } diff --git a/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index e9136be61c..f355c3d2c9 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -390,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( @@ -1533,12 +1532,12 @@ public void routerCaseSensitive(ServerTestRunner runner) { @ServerTest public void maxRequestSize(ServerTestRunner runner) { runner + .options( + new ServerOptions() + .setBuffer(new BufferOptions().setSize(ServerOptions._16KB / 2)) + .setMaxRequestSize(ServerOptions._16KB)) .define( app -> { - app.setServerOptions( - new ServerOptions() - .setBuffer(new BufferOptions().setSize(ServerOptions._16KB / 2)) - .setMaxRequestSize(ServerOptions._16KB)); app.post("/request-size", ctx -> ctx.body().value("")); app.get("/request-size", ctx -> ctx.body().value("")); 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..6bda11db21 100644 --- a/tests/src/test/java/io/jooby/test/HttpsTest.java +++ b/tests/src/test/java/io/jooby/test/HttpsTest.java @@ -18,9 +18,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 +31,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 +54,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 +69,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 +93,12 @@ public void httpsX509(ServerTestRunner runner) { @ServerTest public void forceSSL(ServerTestRunner runner) { runner + .options(new ServerOptions().setSecurePort(8443)) .define( app -> { app.setTrustProxy(true); app.setContextPath("/secure"); - app.setServerOptions(new ServerOptions().setSecurePort(8443)); - app.before(new SSLHandler()); app.get("/{path}", ctx -> ctx.getRequestPath()); @@ -129,12 +128,11 @@ 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.before(new SSLHandler()); app.get("/{path}", ctx -> ctx.getRequestPath()); @@ -163,10 +161,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 +191,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 +223,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 +237,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 +256,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/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/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/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" } } } From d8c9c3e4b2ae139fd7d97719a04267002bf30715 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 29 Jun 2025 11:46:18 -0300 Subject: [PATCH 25/60] remove: Jooby.start() fix #3704 --- jooby/src/main/java/io/jooby/Jooby.java | 52 ++++++------------- .../java/io/jooby/i3500/WidgetService.java | 4 -- 2 files changed, 16 insertions(+), 40 deletions(-) diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index fed9dce416..2c4adddf7c 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -954,40 +954,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 { - 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. @@ -1019,7 +986,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()); @@ -1303,7 +1270,20 @@ public static void runApp( }); apps.add(app); } - targetServer.start(apps.toArray(new Jooby[0])); + try { + targetServer.start(apps.toArray(new Jooby[0])); + } catch (Throwable startupError) { + apps.forEach(app -> app.stopped.set(true)); + try { + server.stop(); + } catch (Throwable ignored) { + // no need to log here + } + // rethrow + throw startupError instanceof StartupException + ? (StartupException) startupError + : new StartupException("Startup resulted in exception", startupError); + } } /** 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 { From 07939920d6ef71885b179dc4cb99ef10814be111 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 29 Jun 2025 15:23:33 -0300 Subject: [PATCH 26/60] remove: MvcFactory fix #3705 --- jooby/src/main/java/io/jooby/Jooby.java | 70 ------------------- jooby/src/main/java/io/jooby/MvcFactory.java | 32 --------- jooby/src/main/java/io/jooby/Router.java | 34 --------- .../java/io/jooby/internal/RouterImpl.java | 15 ---- jooby/src/main/java/module-info.java | 1 - .../src/main/kotlin/io/jooby/kt/Kooby.kt | 14 ---- modules/jooby-openapi/pom.xml | 6 ++ .../internal/openapi/AnnotationParser.java | 32 ++++++++- .../test/java/examples/ControllerExample.java | 15 ++-- .../src/test/java/examples/FormMvcApp.java | 4 +- .../src/test/java/examples/MvcApp.java | 4 +- .../test/java/examples/MvcAppWithRoutes.java | 4 +- .../src/test/java/examples/MvcDaggerApp.java | 4 +- .../test/java/examples/MvcInstanceApp.java | 4 +- .../src/test/java/examples/MvcRequireApp.java | 4 +- .../src/test/java/examples/OpenApiApp.java | 4 +- .../jooby/openapi/MvcExtensionGenerator.java | 42 +++++++++++ .../src/test/java/issues/App1359.java | 5 +- .../src/test/java/issues/App1586.java | 4 +- .../src/test/java/issues/App1586b.java | 4 +- .../src/test/java/issues/App1586c.java | 4 +- .../src/test/java/issues/i1573/App1573.java | 4 +- .../src/test/java/issues/i1580/App1580.java | 4 +- .../src/test/java/issues/i1581/App1581.java | 4 +- .../java/issues/i1596/ClassLevelTagApp.java | 4 +- .../src/test/java/issues/i1601/App1601b.java | 4 +- .../src/test/java/issues/i1768/App1768.java | 4 +- .../src/test/java/issues/i1794/App1794.java | 4 +- .../src/test/java/issues/i1795/App1795.java | 4 +- .../src/test/java/issues/i1805/App1805.java | 4 +- .../src/test/java/issues/i1855/App1855.java | 4 +- .../src/test/java/issues/i1934/App1934.java | 4 +- .../src/test/java/issues/i2403/App2403.java | 4 +- .../src/test/java/issues/i2505/App2505.java | 4 +- .../src/test/java/issues/i2542/App2542.java | 4 +- .../src/test/java/issues/i2594/App2594.java | 4 +- .../issues/i2594/ControllersAppV12594.java | 4 +- .../issues/i2594/ControllersAppV22594.java | 4 +- .../java/issues/i3059/IndirectRunner.java | 4 +- .../src/test/java/issues/i3397/App3397.java | 4 +- .../src/test/java/issues/i3412/App3412.java | 3 +- .../src/test/java/issues/i3461/App3461.java | 4 +- .../src/test/java/issues/i3575/App3575.java | 4 +- .../src/test/java/issues/i3652/App3652.java | 4 +- .../src/test/java/issues/i3654/App3654.java | 4 +- .../src/test/kotlin/kt/KtMvcApp.kt | 8 +-- .../src/test/kotlin/kt/KtMvcAppWithRoutes.kt | 8 +-- .../src/test/kotlin/kt/KtMvcInstanceApp.kt | 3 +- .../src/test/kotlin/kt/KtMvcObjectApp.kt | 3 +- .../src/test/kotlin/kt/i2121/App2121.kt | 3 +- .../src/test/kotlin/kt/i3217/App3217.kt | 3 +- .../test/kotlin/kt/issues/i2004/App2004.kt | 3 +- 52 files changed, 207 insertions(+), 225 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/MvcFactory.java create mode 100644 modules/jooby-openapi/src/test/java/io/jooby/openapi/MvcExtensionGenerator.java diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 2c4adddf7c..492c577fd3 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,8 +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; @@ -54,7 +51,6 @@ import io.jooby.output.BufferedOutputFactory; import io.jooby.problem.ProblemDetailsHandler; import io.jooby.value.ValueFactory; -import jakarta.inject.Provider; /** * Welcome to Jooby! @@ -520,49 +516,6 @@ public Jooby mvc(@NonNull MvcExtension router) { } } - @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); @@ -1452,29 +1405,6 @@ 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. * 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/Router.java b/jooby/src/main/java/io/jooby/Router.java index 0a0269ffe4..f631821216 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -38,7 +38,6 @@ import io.jooby.handler.AssetSource; import io.jooby.output.BufferedOutputFactory; import io.jooby.value.ValueFactory; -import jakarta.inject.Provider; /** * Routing DSL functions. @@ -425,39 +424,6 @@ default Object execute(@NonNull Context context) { */ @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. * diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index ec52bcecad..581355a833 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -333,21 +333,6 @@ 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); diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index d8e9bd0f34..87b3707006 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -16,7 +16,6 @@ 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; 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 a88e22b1fe..e4e2a75b15 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 @@ -143,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() } } diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 02705e5490..5f8e7cd9f9 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -127,6 +127,12 @@ kotlinx-coroutines-core test + + net.bytebuddy + byte-buddy + 1.17.5 + 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..6a3cfae604 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,7 +185,7 @@ 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; @@ -220,8 +220,36 @@ public static List parse( 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/test/java/examples/ControllerExample.java b/modules/jooby-openapi/src/test/java/examples/ControllerExample.java index e14fe5768f..660ac36167 100644 --- a/modules/jooby-openapi/src/test/java/examples/ControllerExample.java +++ b/modules/jooby-openapi/src/test/java/examples/ControllerExample.java @@ -8,12 +8,13 @@ import java.util.List; import java.util.Optional; +import org.jetbrains.annotations.NotNull; + import io.jooby.Context; +import io.jooby.Jooby; +import io.jooby.MvcExtension; 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 { @@ -63,3 +64,9 @@ public ABean save(ABean bean) { return bean; } } + +@Generated(ControllerExample.class) +class COntrollerExample_ implements MvcExtension { + @Override + public void install(@NotNull Jooby application) throws Exception {} +} 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..8e6df2e7da --- /dev/null +++ b/modules/jooby-openapi/src/test/java/io/jooby/openapi/MvcExtensionGenerator.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.openapi; + +import examples.ControllerExample; +import io.jooby.MvcExtension; +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 MvcExtension toMvcExtension(Object controller) { + return toMvcExtension(controller.getClass()); + } + + public static MvcExtension toMvcExtension(Class controller) { + try { + return new ByteBuddy() + .subclass(MvcExtension.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); + } + } + + public static void main(String[] args) { + var instance = toMvcExtension(ControllerExample.class); + System.out.println(instance); + } +} 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/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/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)) }) From ded9a21e2b7be72a85178e4eb844c0565d726bfa Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 30 Jun 2025 07:15:23 -0300 Subject: [PATCH 27/60] remove: MvcFactory - update and code cleanup for generated controller - add new constructor which fit better for beanScope - ref #3705 --- .../java/io/jooby/internal/apt/MvcRoute.java | 2 +- .../java/io/jooby/internal/apt/MvcRouter.java | 21 +++---- .../io/jooby/internal/apt/Source.java | 14 +++-- .../resources/io/jooby/internal/apt/Source.kt | 15 ++++- .../src/test/java/tests/i3460/Issue3460.java | 2 +- .../src/test/java/tests/i3567/Issue3567.java | 2 +- .../src/test/java/tests/i3705/C3705.java | 8 +++ .../src/test/java/tests/i3705/C3705_.java | 37 ++++++++++++ .../internal/openapi/AnnotationParser.java | 10 +++- .../src/test/kotlin/kt/i3705/C3705.kt | 47 +++++++++++++++ .../src/test/kotlin/kt/i3705/Issue3705.kt | 59 +++++++++++++++++++ 11 files changed, 193 insertions(+), 24 deletions(-) create mode 100644 modules/jooby-apt/src/test/java/tests/i3705/C3705.java create mode 100644 modules/jooby-apt/src/test/java/tests/i3705/C3705_.java create mode 100644 modules/jooby-openapi/src/test/kotlin/kt/i3705/C3705.kt create mode 100644 modules/jooby-openapi/src/test/kotlin/kt/i3705/Issue3705.kt diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java index 8df7d431b8..1d158c8822 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java @@ -158,7 +158,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/resources/io/jooby/internal/apt/Source.java b/modules/jooby-apt/src/main/resources/io/jooby/internal/apt/Source.java index 8beea1cc6c..328df0b0ea 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 @@ -2,17 +2,21 @@ ${imports} @io.jooby.annotation.Generated(${className}.class) public class ${generatedClassName} implements io.jooby.MvcExtension { - protected final java.util.function.Function factory; + 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..4b77f3cca9 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.MvcExtension { + 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/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/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/i3705/C3705.java b/modules/jooby-apt/src/test/java/tests/i3705/C3705.java new file mode 100644 index 0000000000..bfe0cac50a --- /dev/null +++ b/modules/jooby-apt/src/test/java/tests/i3705/C3705.java @@ -0,0 +1,8 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package tests.i3705; + +public class C3705 {} diff --git a/modules/jooby-apt/src/test/java/tests/i3705/C3705_.java b/modules/jooby-apt/src/test/java/tests/i3705/C3705_.java new file mode 100644 index 0000000000..4ebd2ea94c --- /dev/null +++ b/modules/jooby-apt/src/test/java/tests/i3705/C3705_.java @@ -0,0 +1,37 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package tests.i3705; + +@io.jooby.annotation.Generated(C3705.class) +public class C3705_ implements io.jooby.MvcExtension { + protected java.util.function.Function factory; + + public C3705_() { + this(new C3705()); + } + + public C3705_(C3705 instance) { + setup(ctx -> instance); + } + + public C3705_(Class type) { + setup(ctx -> ctx.require(type)); + } + + public C3705_(java.util.function.Supplier provider) { + setup(ctx -> provider.get()); + } + + public C3705_(java.util.function.Function, C3705> factory) { + setup(ctx -> factory.apply(C3705.class)); + } + + private void setup(java.util.function.Function factory) { + this.factory = factory; + } + + public void install(io.jooby.Jooby app) throws Exception {} +} 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 6a3cfae604..fbb769ceb4 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 @@ -187,10 +187,13 @@ public static List parse( return parse(ctx, prefix, type); } 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 = @@ -204,6 +207,7 @@ public static List parse( .orElse(type); return parse(ctx, prefix, controllerType); } else if (methodInsnNode.getOpcode() == Opcodes.INVOKEINTERFACE) { + // TODO: almost sure this is dead code AbstractInsnNode methodPrev = methodInsnNode.getPrevious(); if (methodPrev instanceof VarInsnNode) { // mvc(daggerApp.myController()); 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..4eca27a667 --- /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.Jooby +import io.jooby.MvcExtension +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_ : MvcExtension { + + 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(), + ) + } +} From 0e01cf482a7a743d349574b567f1b1cb33b5df8a Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 30 Jun 2025 10:07:05 -0300 Subject: [PATCH 28/60] remove: MvcFactory - now remove MvcExtension - ref #3705 --- jooby/src/main/java/io/jooby/Jooby.java | 2 +- .../src/main/java/io/jooby/MvcExtension.java | 14 ------- jooby/src/main/java/io/jooby/Router.java | 4 +- .../java/io/jooby/internal/RouterImpl.java | 2 +- .../io/jooby/internal/apt/Source.java | 2 +- .../resources/io/jooby/internal/apt/Source.kt | 2 +- .../java/io/jooby/apt/ProcessorRunner.java | 2 +- .../src/test/java/tests/i3705/C3705.java | 8 ---- .../src/test/java/tests/i3705/C3705_.java | 37 ------------------- .../jooby/internal/openapi/TypeFactory.java | 2 +- .../test/java/examples/ControllerExample.java | 10 ----- .../jooby/openapi/MvcExtensionGenerator.java | 14 ++----- .../src/test/java/issues/i2968/C2968_.java | 4 +- .../src/test/kotlin/kt/i3705/C3705.kt | 4 +- 14 files changed, 16 insertions(+), 91 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/MvcExtension.java delete mode 100644 modules/jooby-apt/src/test/java/tests/i3705/C3705.java delete mode 100644 modules/jooby-apt/src/test/java/tests/i3705/C3705_.java diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 492c577fd3..ee1abb0863 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -507,7 +507,7 @@ public Jooby mount(@NonNull Router router) { } @NonNull @Override - public Jooby mvc(@NonNull MvcExtension router) { + public Jooby mvc(@NonNull Extension router) { try { router.install(this); return this; 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/Router.java b/jooby/src/main/java/io/jooby/Router.java index f631821216..40fd691f2d 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -417,12 +417,12 @@ default Object execute(@NonNull Context context) { */ /** - * Import all route method from the given controller class. + * Import all routes from the given controller class. * * @param router Router extension. * @return This router. */ - @NonNull Router mvc(@NonNull MvcExtension router); + @NonNull Router mvc(@NonNull Extension router); /** * Add a websocket handler. diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 581355a833..873389ee8b 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -329,7 +329,7 @@ public Router mount(@NonNull Router router) { } @NonNull @Override - public Router mvc(@NonNull MvcExtension router) { + public Router mvc(@NonNull Extension router) { throw new UnsupportedOperationException(); } 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 328df0b0ea..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,7 +1,7 @@ package ${packageName}; ${imports} @io.jooby.annotation.Generated(${className}.class) -public class ${generatedClassName} implements io.jooby.MvcExtension { +public class ${generatedClassName} implements io.jooby.Extension { protected java.util.function.Function factory; ${constructors} public ${generatedClassName}(${className} instance) { 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 4b77f3cca9..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,7 +1,7 @@ package ${packageName} ${imports} @io.jooby.annotation.Generated(${className}::class) -open class ${generatedClassName} : io.jooby.MvcExtension { +open class ${generatedClassName} : io.jooby.Extension { private lateinit var factory: java.util.function.Function ${constructors} 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/tests/i3705/C3705.java b/modules/jooby-apt/src/test/java/tests/i3705/C3705.java deleted file mode 100644 index bfe0cac50a..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i3705/C3705.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.i3705; - -public class C3705 {} diff --git a/modules/jooby-apt/src/test/java/tests/i3705/C3705_.java b/modules/jooby-apt/src/test/java/tests/i3705/C3705_.java deleted file mode 100644 index 4ebd2ea94c..0000000000 --- a/modules/jooby-apt/src/test/java/tests/i3705/C3705_.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 tests.i3705; - -@io.jooby.annotation.Generated(C3705.class) -public class C3705_ implements io.jooby.MvcExtension { - protected java.util.function.Function factory; - - public C3705_() { - this(new C3705()); - } - - public C3705_(C3705 instance) { - setup(ctx -> instance); - } - - public C3705_(Class type) { - setup(ctx -> ctx.require(type)); - } - - public C3705_(java.util.function.Supplier provider) { - setup(ctx -> provider.get()); - } - - public C3705_(java.util.function.Function, C3705> factory) { - setup(ctx -> factory.apply(C3705.class)); - } - - private void setup(java.util.function.Function factory) { - this.factory = factory; - } - - public void install(io.jooby.Jooby app) throws Exception {} -} 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 660ac36167..dad18c2d7d 100644 --- a/modules/jooby-openapi/src/test/java/examples/ControllerExample.java +++ b/modules/jooby-openapi/src/test/java/examples/ControllerExample.java @@ -8,11 +8,7 @@ import java.util.List; import java.util.Optional; -import org.jetbrains.annotations.NotNull; - import io.jooby.Context; -import io.jooby.Jooby; -import io.jooby.MvcExtension; import io.jooby.Session; import io.jooby.annotation.*; @@ -64,9 +60,3 @@ public ABean save(ABean bean) { return bean; } } - -@Generated(ControllerExample.class) -class COntrollerExample_ implements MvcExtension { - @Override - public void install(@NotNull Jooby application) throws Exception {} -} 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 index 8e6df2e7da..c8828a53c9 100644 --- a/modules/jooby-openapi/src/test/java/io/jooby/openapi/MvcExtensionGenerator.java +++ b/modules/jooby-openapi/src/test/java/io/jooby/openapi/MvcExtensionGenerator.java @@ -5,8 +5,7 @@ */ package io.jooby.openapi; -import examples.ControllerExample; -import io.jooby.MvcExtension; +import io.jooby.Extension; import io.jooby.annotation.Generated; import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.annotation.AnnotationDescription; @@ -14,14 +13,14 @@ public class MvcExtensionGenerator { - public static MvcExtension toMvcExtension(Object controller) { + public static Extension toMvcExtension(Object controller) { return toMvcExtension(controller.getClass()); } - public static MvcExtension toMvcExtension(Class controller) { + public static Extension toMvcExtension(Class controller) { try { return new ByteBuddy() - .subclass(MvcExtension.class) + .subclass(Extension.class) .annotateType( AnnotationDescription.Builder.ofType(Generated.class) .define("value", controller) @@ -34,9 +33,4 @@ public static MvcExtension toMvcExtension(Class controller) { throw new RuntimeException(e); } } - - public static void main(String[] args) { - var instance = toMvcExtension(ControllerExample.class); - System.out.println(instance); - } } 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/kotlin/kt/i3705/C3705.kt b/modules/jooby-openapi/src/test/kotlin/kt/i3705/C3705.kt index 4eca27a667..76f8a98ac8 100644 --- a/modules/jooby-openapi/src/test/kotlin/kt/i3705/C3705.kt +++ b/modules/jooby-openapi/src/test/kotlin/kt/i3705/C3705.kt @@ -5,8 +5,8 @@ */ package kt.i3705 +import io.jooby.Extension import io.jooby.Jooby -import io.jooby.MvcExtension import io.jooby.SneakyThrows import io.jooby.annotation.GET import io.jooby.annotation.Path @@ -27,7 +27,7 @@ class BeanScope { } @io.jooby.annotation.Generated(C3705::class) -class C3705_ : MvcExtension { +class C3705_ : Extension { constructor() {} From 647520ad4ea054d9774f798cb372b0e60eeae447 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 30 Jun 2025 10:44:07 -0300 Subject: [PATCH 29/60] context: move default method to defaultcontext --- jooby/src/main/java/io/jooby/Context.java | 22 ++------ .../main/java/io/jooby/DefaultContext.java | 56 +++++++++++++++---- .../main/java/io/jooby/ForwardingContext.java | 14 ++++- jooby/src/main/java/io/jooby/Jooby.java | 2 +- .../main/java/io/jooby/MessageEncoder.java | 2 +- jooby/src/main/java/io/jooby/Router.java | 2 +- jooby/src/main/java/io/jooby/Sender.java | 2 +- .../src/main/java/io/jooby/ServerOptions.java | 2 +- .../main/java/io/jooby/ServerSentMessage.java | 2 +- .../main/java/io/jooby/TemplateEngine.java | 2 +- jooby/src/main/java/io/jooby/WebSocket.java | 2 +- .../{output => buffer}/BufferOptions.java | 2 +- .../{output => buffer}/BufferedOutput.java | 2 +- .../BufferedOutputFactory.java | 2 +- .../ByteBufferOutputFactory.java | 2 +- .../ForwardingBufferedOutputFactory.java | 2 +- .../{output => buffer}/package-info.java | 2 +- .../java/io/jooby/internal/HeadContext.java | 2 +- .../io/jooby/internal/HttpMessageEncoder.java | 2 +- .../java/io/jooby/internal/RouterImpl.java | 2 +- .../io/jooby/internal/WebSocketSender.java | 2 +- .../internal/handler/ChunkedSubscriber.java | 2 +- .../output/ByteArrayWrappedOutput.java | 2 +- .../internal/output/ByteBufferOutput.java | 2 +- .../output/ByteBufferWrappedOutput.java | 2 +- .../output/CompsiteByteBufferOutput.java | 2 +- .../internal/output/OutputOutputStream.java | 2 +- .../jooby/internal/output/OutputWriter.java | 2 +- jooby/src/main/java/module-info.java | 2 +- jooby/src/test/java/io/jooby/Issue3607.java | 6 +- .../java/io/jooby/ServerSentMessageTest.java | 4 +- .../BufferedOutputTest.java | 2 +- .../test/java/io/jooby/buffer/Issue3434.java | 2 - .../jooby/avaje/jsonb/AvajeJsonbModule.java | 2 +- .../avaje/jsonb/BufferedJsonOutput.java | 2 +- .../avaje/jsonb/AvajeJsonbEncoderBench.java | 6 +- .../freemarker/FreemarkerTemplateEngine.java | 2 +- .../main/java/io/jooby/gson/GsonModule.java | 2 +- .../test/java/io/jooby/gson/Issue3434.java | 4 +- .../handlebars/HandlebarsTemplateEngine.java | 2 +- .../java/io/jooby/jackson/JacksonModule.java | 2 +- .../java/io/jooby/jackson/JacksonBench.java | 6 +- .../jooby/jackson/JacksonJsonModuleTest.java | 4 +- .../jooby/internal/jetty/JettyCallbacks.java | 2 +- .../io/jooby/internal/jetty/JettyContext.java | 2 +- .../io/jooby/internal/jetty/JettySender.java | 2 +- .../jooby/internal/jetty/JettyWebSocket.java | 2 +- .../jetty/WebSocketOutputCallback.java | 2 +- .../jstachio/JStachioMessageEncoder.java | 2 +- .../internal/jte/BufferedTemplateOutput.java | 2 +- .../jooby/internal/jte/JteModelEncoder.java | 2 +- .../java/io/jooby/jte/JteTemplateEngine.java | 2 +- .../jte/BufferedTemplateOutputTest.java | 4 +- .../src/test/java/io/jooby/jte/Issue3599.java | 4 +- .../src/test/java/io/jooby/jte/Issue3602.java | 4 +- .../internal/netty/NettyBufferedOutput.java | 2 +- .../internal/netty/NettyByteBufOutput.java | 2 +- .../io/jooby/internal/netty/NettyContext.java | 2 +- .../internal/netty/NettyOutputFactory.java | 6 +- .../io/jooby/internal/netty/NettySender.java | 2 +- .../jooby/internal/netty/NettyWebSocket.java | 2 +- .../internal/netty/NettyWrappedOutput.java | 2 +- .../io/jooby/pebble/PebbleTemplateEngine.java | 2 +- .../io/jooby/rocker/BufferedRockerOutput.java | 4 +- .../io/jooby/rocker/RockerMessageEncoder.java | 2 +- .../main/java/io/jooby/test/MockContext.java | 6 +- .../java/io/jooby/test/MockWebSocket.java | 2 +- .../thymeleaf/ThymeleafTemplateEngine.java | 2 +- .../internal/undertow/UndertowContext.java | 2 +- .../undertow/UndertowOutputCallback.java | 2 +- .../internal/undertow/UndertowSender.java | 2 +- .../UndertowServerSentConnection.java | 2 +- .../internal/undertow/UndertowWebSocket.java | 2 +- .../java/io/jooby/yasson/YassonModule.java | 2 +- .../io/jooby/yasson/YassonModuleTest.java | 4 +- .../test/java/io/jooby/i2613/Issue2613.java | 2 +- .../test/java/io/jooby/i2806/Issue2806.java | 2 +- .../java/io/jooby/junit/ServerTestRunner.java | 4 +- .../test/java/io/jooby/test/FeaturedTest.java | 2 +- 79 files changed, 156 insertions(+), 126 deletions(-) rename jooby/src/main/java/io/jooby/{output => buffer}/BufferOptions.java (98%) rename jooby/src/main/java/io/jooby/{output => buffer}/BufferedOutput.java (99%) rename jooby/src/main/java/io/jooby/{output => buffer}/BufferedOutputFactory.java (99%) rename jooby/src/main/java/io/jooby/{output => buffer}/ByteBufferOutputFactory.java (98%) rename jooby/src/main/java/io/jooby/{output => buffer}/ForwardingBufferedOutputFactory.java (98%) rename jooby/src/main/java/io/jooby/{output => buffer}/package-info.java (72%) rename jooby/src/test/java/io/jooby/{output => buffer}/BufferedOutputTest.java (99%) diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java index 623cdb1f94..06f8bf8b29 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,12 +29,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import io.jooby.buffer.BufferedOutput; +import io.jooby.buffer.BufferedOutputFactory; import io.jooby.internal.LocaleUtils; -import io.jooby.internal.ParamLookupImpl; import io.jooby.internal.ReadOnlyContext; import io.jooby.internal.WebSocketSender; -import io.jooby.output.BufferedOutput; -import io.jooby.output.BufferedOutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -822,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 @@ -849,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 diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 67557c1e02..ae8b351283 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -20,22 +20,15 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Instant; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import org.slf4j.Logger; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import io.jooby.buffer.BufferedOutputFactory; 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.output.BufferedOutputFactory; +import io.jooby.internal.*; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -189,6 +182,49 @@ default Value cookie(@NonNull String name) { 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 default Value path(@NonNull String name) { String value = pathMap().get(name); diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index 2227f2c7a5..d7d583d528 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -23,9 +23,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import io.jooby.buffer.BufferedOutput; +import io.jooby.buffer.BufferedOutputFactory; import io.jooby.exception.RegistryException; -import io.jooby.output.BufferedOutput; -import io.jooby.output.BufferedOutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -747,6 +747,16 @@ public Context setRequestPath(@NonNull String path) { return this; } + @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); diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index ee1abb0863..251cf4afc7 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -42,13 +42,13 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import io.jooby.buffer.BufferedOutputFactory; 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.BufferedOutputFactory; import io.jooby.problem.ProblemDetailsHandler; import io.jooby.value.ValueFactory; diff --git a/jooby/src/main/java/io/jooby/MessageEncoder.java b/jooby/src/main/java/io/jooby/MessageEncoder.java index 116e65570b..6d73f8fc2c 100644 --- a/jooby/src/main/java/io/jooby/MessageEncoder.java +++ b/jooby/src/main/java/io/jooby/MessageEncoder.java @@ -7,8 +7,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import io.jooby.buffer.BufferedOutput; import io.jooby.exception.NotAcceptableException; -import io.jooby.output.BufferedOutput; /** * Render a route output as a byte array. diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 40fd691f2d..efa02c5686 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -33,10 +33,10 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import io.jooby.buffer.BufferedOutputFactory; import io.jooby.exception.MissingValueException; import io.jooby.handler.AssetHandler; import io.jooby.handler.AssetSource; -import io.jooby.output.BufferedOutputFactory; import io.jooby.value.ValueFactory; /** diff --git a/jooby/src/main/java/io/jooby/Sender.java b/jooby/src/main/java/io/jooby/Sender.java index 97333d6565..5c0bbee15c 100644 --- a/jooby/src/main/java/io/jooby/Sender.java +++ b/jooby/src/main/java/io/jooby/Sender.java @@ -10,7 +10,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; /** * Non-blocking sender. Reactive responses use this class to send partial data in a non-blocking diff --git a/jooby/src/main/java/io/jooby/ServerOptions.java b/jooby/src/main/java/io/jooby/ServerOptions.java index df261479be..cf7601a768 100644 --- a/jooby/src/main/java/io/jooby/ServerOptions.java +++ b/jooby/src/main/java/io/jooby/ServerOptions.java @@ -25,8 +25,8 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import io.jooby.buffer.BufferOptions; import io.jooby.internal.SslContextProvider; -import io.jooby.output.BufferOptions; /** * Available server options. To load server options from configuration files, just do: diff --git a/jooby/src/main/java/io/jooby/ServerSentMessage.java b/jooby/src/main/java/io/jooby/ServerSentMessage.java index 797b7f7540..6bfacd039d 100644 --- a/jooby/src/main/java/io/jooby/ServerSentMessage.java +++ b/jooby/src/main/java/io/jooby/ServerSentMessage.java @@ -14,7 +14,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.output.*; +import io.jooby.buffer.*; /** * Server-Sent message. diff --git a/jooby/src/main/java/io/jooby/TemplateEngine.java b/jooby/src/main/java/io/jooby/TemplateEngine.java index b46347d7e2..fb21089ddc 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.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; /** * Template engine renderer. This class renderer instances of {@link ModelAndView} objects. Template diff --git a/jooby/src/main/java/io/jooby/WebSocket.java b/jooby/src/main/java/io/jooby/WebSocket.java index 960c05edce..83f93ee4e4 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.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; /** * Websocket. Usage: diff --git a/jooby/src/main/java/io/jooby/output/BufferOptions.java b/jooby/src/main/java/io/jooby/buffer/BufferOptions.java similarity index 98% rename from jooby/src/main/java/io/jooby/output/BufferOptions.java rename to jooby/src/main/java/io/jooby/buffer/BufferOptions.java index 4fe607a9ff..475435d6a4 100644 --- a/jooby/src/main/java/io/jooby/output/BufferOptions.java +++ b/jooby/src/main/java/io/jooby/buffer/BufferOptions.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.output; +package io.jooby.buffer; public class BufferOptions { private int size; diff --git a/jooby/src/main/java/io/jooby/output/BufferedOutput.java b/jooby/src/main/java/io/jooby/buffer/BufferedOutput.java similarity index 99% rename from jooby/src/main/java/io/jooby/output/BufferedOutput.java rename to jooby/src/main/java/io/jooby/buffer/BufferedOutput.java index 261cfee3f5..f7dce0c8a6 100644 --- a/jooby/src/main/java/io/jooby/output/BufferedOutput.java +++ b/jooby/src/main/java/io/jooby/buffer/BufferedOutput.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.output; +package io.jooby.buffer; import java.io.OutputStream; import java.io.Writer; diff --git a/jooby/src/main/java/io/jooby/output/BufferedOutputFactory.java b/jooby/src/main/java/io/jooby/buffer/BufferedOutputFactory.java similarity index 99% rename from jooby/src/main/java/io/jooby/output/BufferedOutputFactory.java rename to jooby/src/main/java/io/jooby/buffer/BufferedOutputFactory.java index 73efbeb60a..2cd3900c28 100644 --- a/jooby/src/main/java/io/jooby/output/BufferedOutputFactory.java +++ b/jooby/src/main/java/io/jooby/buffer/BufferedOutputFactory.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.output; +package io.jooby.buffer; import static java.lang.ThreadLocal.withInitial; diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java b/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java similarity index 98% rename from jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java rename to jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java index 77a6ea00f4..0bf1ce14f9 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java +++ b/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.output; +package io.jooby.buffer; import java.nio.ByteBuffer; diff --git a/jooby/src/main/java/io/jooby/output/ForwardingBufferedOutputFactory.java b/jooby/src/main/java/io/jooby/buffer/ForwardingBufferedOutputFactory.java similarity index 98% rename from jooby/src/main/java/io/jooby/output/ForwardingBufferedOutputFactory.java rename to jooby/src/main/java/io/jooby/buffer/ForwardingBufferedOutputFactory.java index b931b99e90..f4cfe4355e 100644 --- a/jooby/src/main/java/io/jooby/output/ForwardingBufferedOutputFactory.java +++ b/jooby/src/main/java/io/jooby/buffer/ForwardingBufferedOutputFactory.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.output; +package io.jooby.buffer; import java.nio.ByteBuffer; diff --git a/jooby/src/main/java/io/jooby/output/package-info.java b/jooby/src/main/java/io/jooby/buffer/package-info.java similarity index 72% rename from jooby/src/main/java/io/jooby/output/package-info.java rename to jooby/src/main/java/io/jooby/buffer/package-info.java index 6dbbe0ce94..80750baffa 100644 --- a/jooby/src/main/java/io/jooby/output/package-info.java +++ b/jooby/src/main/java/io/jooby/buffer/package-info.java @@ -1,2 +1,2 @@ @edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault -package io.jooby.output; +package io.jooby.buffer; diff --git a/jooby/src/main/java/io/jooby/internal/HeadContext.java b/jooby/src/main/java/io/jooby/internal/HeadContext.java index 50d87ebe63..b1c314d90d 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.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; public class HeadContext extends ForwardingContext { /** diff --git a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java index 6f36ef8866..db3ed366e2 100644 --- a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java +++ b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java @@ -20,7 +20,7 @@ import io.jooby.ModelAndView; import io.jooby.StatusCode; import io.jooby.TemplateEngine; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; public class HttpMessageEncoder implements MessageEncoder { diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 873389ee8b..94499b219b 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -41,11 +41,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.*; +import io.jooby.buffer.BufferedOutputFactory; 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.BufferedOutputFactory; import io.jooby.problem.ProblemDetailsHandler; import io.jooby.value.ValueFactory; import jakarta.inject.Provider; diff --git a/jooby/src/main/java/io/jooby/internal/WebSocketSender.java b/jooby/src/main/java/io/jooby/internal/WebSocketSender.java index 9ec214e382..2825ee1422 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.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; public class WebSocketSender extends ForwardingContext implements DefaultContext { 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 e1b934468f..1dfefde148 100644 --- a/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java +++ b/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java @@ -14,7 +14,7 @@ import io.jooby.Route; import io.jooby.Sender; import io.jooby.Server; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; public class ChunkedSubscriber implements Flow.Subscriber { diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java b/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java index 8377e1b6f4..81815ef31b 100644 --- a/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java @@ -11,7 +11,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; public class ByteArrayWrappedOutput implements BufferedOutput { diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java b/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java index b3612d4904..1d26996f91 100644 --- a/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java @@ -13,7 +13,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; public class ByteBufferOutput implements BufferedOutput { private static final int MAX_CAPACITY = Integer.MAX_VALUE; diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java b/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java index 5c9b2c2c0c..d31e79290a 100644 --- a/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java @@ -11,7 +11,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; public class ByteBufferWrappedOutput implements BufferedOutput { diff --git a/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java b/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java index ffc30dc66e..c9a861391a 100644 --- a/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java @@ -13,7 +13,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; public class CompsiteByteBufferOutput implements BufferedOutput { private final List chunks = new ArrayList<>(); diff --git a/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java index 8c6b6bf67b..209899e735 100644 --- a/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java +++ b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java @@ -9,7 +9,7 @@ import java.io.OutputStream; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; /** * An {@link OutputStream} that writes to a {@link BufferedOutput}. diff --git a/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java b/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java index c3028b119f..93b295fb71 100644 --- a/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java +++ b/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java @@ -11,7 +11,7 @@ import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; public class OutputWriter extends Writer { private final BufferedOutput output; diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index 87b3707006..00f982f9a6 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -13,7 +13,7 @@ exports io.jooby.validation; exports io.jooby.problem; exports io.jooby.value; - exports io.jooby.output; + exports io.jooby.buffer; exports io.jooby.internal.output; uses io.jooby.Server; diff --git a/jooby/src/test/java/io/jooby/Issue3607.java b/jooby/src/test/java/io/jooby/Issue3607.java index da72fe4cbc..bfa6c34299 100644 --- a/jooby/src/test/java/io/jooby/Issue3607.java +++ b/jooby/src/test/java/io/jooby/Issue3607.java @@ -9,9 +9,9 @@ import org.junit.jupiter.api.Test; -import io.jooby.output.BufferOptions; -import io.jooby.output.BufferedOutput; -import io.jooby.output.BufferedOutputFactory; +import io.jooby.buffer.BufferOptions; +import io.jooby.buffer.BufferedOutput; +import io.jooby.buffer.BufferedOutputFactory; public class Issue3607 { diff --git a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java index 2760591021..37be3e996c 100644 --- a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java +++ b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java @@ -13,8 +13,8 @@ import org.junit.jupiter.api.Test; -import io.jooby.output.BufferOptions; -import io.jooby.output.BufferedOutputFactory; +import io.jooby.buffer.BufferOptions; +import io.jooby.buffer.BufferedOutputFactory; public class ServerSentMessageTest { diff --git a/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java b/jooby/src/test/java/io/jooby/buffer/BufferedOutputTest.java similarity index 99% rename from jooby/src/test/java/io/jooby/output/BufferedOutputTest.java rename to jooby/src/test/java/io/jooby/buffer/BufferedOutputTest.java index 134c06c3d5..0e32523a20 100644 --- a/jooby/src/test/java/io/jooby/output/BufferedOutputTest.java +++ b/jooby/src/test/java/io/jooby/buffer/BufferedOutputTest.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.output; +package io.jooby.buffer; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/jooby/src/test/java/io/jooby/buffer/Issue3434.java b/jooby/src/test/java/io/jooby/buffer/Issue3434.java index 03292093e8..dcd7dfa04b 100644 --- a/jooby/src/test/java/io/jooby/buffer/Issue3434.java +++ b/jooby/src/test/java/io/jooby/buffer/Issue3434.java @@ -15,8 +15,6 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; -import io.jooby.output.BufferOptions; -import io.jooby.output.BufferedOutputFactory; public class Issue3434 { BufferedOutputFactory factory = BufferedOutputFactory.create(BufferOptions.small()); 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 ee414047dd..ced1898843 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.BufferedOutput; import io.jooby.internal.avaje.jsonb.BufferedJsonOutput; -import io.jooby.output.BufferedOutput; /** * JSON module using Avaje-JsonB: factory; 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 b857d27dd0..6ba1c65b80 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 @@ -53,10 +53,10 @@ import io.jooby.Session; import io.jooby.StatusCode; import io.jooby.WebSocket; +import io.jooby.buffer.BufferOptions; +import io.jooby.buffer.BufferedOutput; +import io.jooby.buffer.BufferedOutputFactory; import io.jooby.exception.TypeMismatchException; -import io.jooby.output.BufferOptions; -import io.jooby.output.BufferedOutput; -import io.jooby.output.BufferedOutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; 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 1d97d23034..4a68821715 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.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; /** * Mock implementation of {@link WebSocket} for unit testing purpose. 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 ff51625bea..59e23a92da 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,7 +16,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.MapModelAndView; import io.jooby.ModelAndView; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; public class ThymeleafTemplateEngine implements io.jooby.TemplateEngine { private final TemplateEngine templateEngine; 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 cff902c183..a4d23f30bc 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 @@ -57,7 +57,7 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.WebSocket; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; import io.jooby.value.Value; import io.undertow.Handlers; import io.undertow.io.IoCallback; 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 index 6aad99ed07..b027c6fdb5 100644 --- 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 @@ -9,7 +9,7 @@ import java.nio.ByteBuffer; import java.util.Iterator; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; import io.undertow.io.IoCallback; import io.undertow.io.Sender; import io.undertow.server.HttpServerExchange; 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 bf098429a9..ea28366918 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.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; import io.undertow.io.IoCallback; import io.undertow.server.HttpServerExchange; 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 4b68895e48..7b79daee9e 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 @@ -25,7 +25,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.ServerSentMessage; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; 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 141fddb62b..30d02393cc 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 @@ -33,7 +33,7 @@ import io.jooby.WebSocketCloseStatus; import io.jooby.WebSocketConfigurer; import io.jooby.WebSocketMessage; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; import io.undertow.websockets.core.AbstractReceiveListener; import io.undertow.websockets.core.BufferedBinaryMessage; import io.undertow.websockets.core.BufferedTextMessage; 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 0513ad6391..b038043ee8 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 @@ -20,7 +20,7 @@ import io.jooby.MessageDecoder; import io.jooby.MessageEncoder; import io.jooby.ServiceRegistry; -import io.jooby.output.BufferedOutput; +import io.jooby.buffer.BufferedOutput; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; 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 75199ce5b8..40983950b2 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,8 +19,8 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.output.BufferOptions; -import io.jooby.output.BufferedOutputFactory; +import io.jooby.buffer.BufferOptions; +import io.jooby.buffer.BufferedOutputFactory; public class YassonModuleTest { diff --git a/tests/src/test/java/io/jooby/i2613/Issue2613.java b/tests/src/test/java/io/jooby/i2613/Issue2613.java index d5ec553ae6..ee5a23fe88 100644 --- a/tests/src/test/java/io/jooby/i2613/Issue2613.java +++ b/tests/src/test/java/io/jooby/i2613/Issue2613.java @@ -14,10 +14,10 @@ import io.jooby.Context; import io.jooby.MediaType; import io.jooby.MessageEncoder; +import io.jooby.buffer.BufferedOutput; import io.jooby.jackson.JacksonModule; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; -import io.jooby.output.BufferedOutput; public class Issue2613 { diff --git a/tests/src/test/java/io/jooby/i2806/Issue2806.java b/tests/src/test/java/io/jooby/i2806/Issue2806.java index 9aadc59758..20740f09df 100644 --- a/tests/src/test/java/io/jooby/i2806/Issue2806.java +++ b/tests/src/test/java/io/jooby/i2806/Issue2806.java @@ -12,10 +12,10 @@ import com.google.common.collect.ImmutableMap; import io.jooby.ServerOptions; +import io.jooby.buffer.BufferOptions; import io.jooby.jackson.JacksonModule; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; -import io.jooby.output.BufferOptions; import okhttp3.MediaType; import okhttp3.RequestBody; diff --git a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index 2243986643..716744ba19 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -28,10 +28,10 @@ import io.jooby.SneakyThrows; import io.jooby.StartupSummary; import io.jooby.StatusCode; +import io.jooby.buffer.BufferOptions; +import io.jooby.buffer.BufferedOutputFactory; import io.jooby.internal.MutedServer; import io.jooby.netty.NettyServer; -import io.jooby.output.BufferOptions; -import io.jooby.output.BufferedOutputFactory; import io.jooby.test.WebClient; public class ServerTestRunner { diff --git a/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index f355c3d2c9..38a5aa15eb 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -72,6 +72,7 @@ import io.jooby.ServiceKey; import io.jooby.ServiceRegistry; import io.jooby.StatusCode; +import io.jooby.buffer.BufferOptions; import io.jooby.handlebars.HandlebarsModule; import io.jooby.handler.AccessLogHandler; import io.jooby.handler.AssetHandler; @@ -86,7 +87,6 @@ import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; import io.jooby.netty.NettyServer; -import io.jooby.output.BufferOptions; import io.jooby.rxjava3.Reactivex; import io.jooby.undertow.UndertowServer; import io.reactivex.rxjava3.core.Single; From b882feb7ac7cf161965ca42fea0740401353bce8 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 30 Jun 2025 15:24:57 -0300 Subject: [PATCH 30/60] session: require cookie session name - remove the default jooby.sid cookie name, users must configured session store as well as cookie name - the default session store will generate an usage exception at runtime - fix #3639 --- docs/asciidoc/modules/redis.adoc | 13 +- docs/asciidoc/session.adoc | 25 ++-- jooby/src/main/java/io/jooby/Cookie.java | 11 ++ .../src/main/java/io/jooby/SessionStore.java | 117 +++++++----------- .../src/main/java/io/jooby/SessionToken.java | 13 +- jooby/src/main/java/io/jooby/Usage.java | 12 +- .../java/io/jooby/annotation/BindParam.java | 2 +- .../io/jooby/internal/MemorySessionStore.java | 11 +- .../jooby/internal/MultipleSessionToken.java | 7 +- .../java/io/jooby/internal/RouterImpl.java | 2 +- .../jooby/caffeine/CaffeineSessionStore.java | 12 +- .../java/io/jooby/jwt/JwtSessionStore.java | 19 +-- .../main/java/io/jooby/redis/RedisModule.java | 13 +- .../io/jooby/redis/RedisSessionStore.java | 38 +++--- .../src/test/java/io/jooby/i1570/App1570.java | 3 +- .../java/io/jooby/junit/ServerTestRunner.java | 16 +-- .../test/java/io/jooby/test/Issue1737.java | 2 +- .../test/java/io/jooby/test/SessionTest.java | 11 +- 18 files changed, 150 insertions(+), 177 deletions(-) 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/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/SessionStore.java b/jooby/src/main/java/io/jooby/SessionStore.java index c363b088ec..2cca94d8b3 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; @@ -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/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/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/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/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/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 94499b219b..870b6ed120 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -152,7 +152,7 @@ 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); 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-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-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/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/junit/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index 716744ba19..110750db88 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -17,17 +17,7 @@ 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.*; import io.jooby.buffer.BufferOptions; import io.jooby.buffer.BufferedOutputFactory; import io.jooby.internal.MutedServer; @@ -70,6 +60,10 @@ public ServerTestRunner define(Consumer consumer) { if (serverOptions != null) { app.getServices().put(ServerOptions.class, serverOptions); } + if (app.getSessionStore() == SessionStore.UNSUPPORTED) { + // set default session + app.setSessionStore(SessionStore.memory(Cookie.session("jooby.sid"))); + } app.setExecutionMode(executionMode); consumer.accept(app); return app; 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/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 -> { From bb8326691320e26b9fbccc062f30572b9d62513f Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 30 Jun 2025 16:57:21 -0300 Subject: [PATCH 31/60] jooby-apt: should not generate META-INF/services by default fix #3542 --- .../src/main/java/io/jooby/apt/JoobyProcessor.java | 10 +--------- .../main/java/io/jooby/internal/apt/MvcContext.java | 7 ------- 2 files changed, 1 insertion(+), 16 deletions(-) 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..94079aa506 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 @@ -34,7 +34,6 @@ HANDLER, DEBUG, INCREMENTAL, - SERVICES, MVC_METHOD, RETURN_TYPE, ROUTER_PREFIX, @@ -51,7 +50,6 @@ public interface Options { 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 +99,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 +323,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..fd40d32c4b 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 @@ -29,7 +29,6 @@ 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; @@ -46,13 +45,11 @@ public MvcContext( 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.routerPrefix = Options.string(processingEnvironment, Options.ROUTER_PREFIX, ""); this.routerSuffix = Options.string(processingEnvironment, Options.ROUTER_SUFFIX, "_"); computeResultTypes(processingEnvironment, handler::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( @@ -243,10 +240,6 @@ public Set superTypes(Element owner) { return result; } - public boolean generateServices() { - return services; - } - public boolean generateMvcMethod() { return mvcMethod; } From 71305e105f94fc8686cc8252da1ec59961243eb4 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 30 Jun 2025 17:01:15 -0300 Subject: [PATCH 32/60] jooby-apt: turn off mvcMethod generator by default fix #3535 --- .../src/main/java/io/jooby/internal/apt/MvcContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fd40d32c4b..53b48c2f93 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 @@ -44,7 +44,7 @@ public MvcContext( 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.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); From 5ff899affc869b82a8af08e1aabe838974318018 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 30 Jun 2025 17:26:57 -0300 Subject: [PATCH 33/60] router: clean up - remove Route.Decorator --- jooby/src/main/java/io/jooby/Body.java | 4 ++-- jooby/src/main/java/io/jooby/Jooby.java | 11 +++++------ jooby/src/main/java/io/jooby/Route.java | 6 ------ jooby/src/main/java/io/jooby/RouteSet.java | 15 ++++----------- jooby/src/main/java/io/jooby/Router.java | 12 ------------ 5 files changed, 11 insertions(+), 37 deletions(-) diff --git a/jooby/src/main/java/io/jooby/Body.java b/jooby/src/main/java/io/jooby/Body.java index 2cd7103886..48128b4415 100644 --- a/jooby/src/main/java/io/jooby/Body.java +++ b/jooby/src/main/java/io/jooby/Body.java @@ -91,9 +91,9 @@ default Iterator iterator() { long getSize(); /** - * Body as readable channel. + * Body as a readable channel. * - * @return Body as readable channel. + * @return Body as a readable channel. */ ReadableByteChannel channel(); diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 251cf4afc7..a3eb4d9f6c 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -309,9 +309,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 RouteSet install(@NonNull SneakyThrows.Supplier factory) { return install("/", factory); } @@ -343,13 +343,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 RouteSet 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; } diff --git a/jooby/src/main/java/io/jooby/Route.java b/jooby/src/main/java/io/jooby/Route.java index 68d89988bc..8900215d42 100644 --- a/jooby/src/main/java/io/jooby/Route.java +++ b/jooby/src/main/java/io/jooby/Route.java @@ -106,12 +106,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. * diff --git a/jooby/src/main/java/io/jooby/RouteSet.java b/jooby/src/main/java/io/jooby/RouteSet.java index 2197c04636..a85b556113 100644 --- a/jooby/src/main/java/io/jooby/RouteSet.java +++ b/jooby/src/main/java/io/jooby/RouteSet.java @@ -5,14 +5,9 @@ */ 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.*; import java.util.concurrent.Executor; import edu.umd.cs.findbugs.annotations.NonNull; @@ -29,7 +24,7 @@ public class RouteSet { private List routes; - private List tags = EMPTY_LIST; + private List tags; private String summary; @@ -154,7 +149,7 @@ public class RouteSet { * @return Route tags. */ public @NonNull List getTags() { - return tags; + return tags == null ? List.of() : tags; } /** @@ -164,9 +159,7 @@ public class RouteSet { * @return This route. */ public @NonNull RouteSet setTags(@NonNull List tags) { - if (this.tags == EMPTY_LIST) { - this.tags = new ArrayList<>(); - } + this.tags = tags; routes.forEach(it -> tags.forEach(it::addTag)); return this; } diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index efa02c5686..c7ad6e8b17 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -522,18 +522,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. * From dadaca430b6d943716585d84822c5444043e49be Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 30 Jun 2025 17:40:00 -0300 Subject: [PATCH 34/60] router: remove deprecated code - returnType --- jooby/src/main/java/io/jooby/Jooby.java | 8 ++--- jooby/src/main/java/io/jooby/Route.java | 34 +++--------------- jooby/src/main/java/io/jooby/RouteSet.java | 2 +- jooby/src/main/java/io/jooby/Router.java | 5 +-- .../java/io/jooby/apt/JoobyProcessor.java | 2 -- .../io/jooby/internal/apt/MvcContext.java | 6 ---- .../java/io/jooby/internal/apt/MvcRoute.java | 4 --- .../src/test/java/source/Routes.java | 7 ---- .../src/test/java/tests/Issue1525.java | 4 +-- .../src/test/java/tests/Issue1527.java | 11 +++--- .../test/java/tests/ModuleCompilerTest.java | 35 +++++++------------ .../src/test/java/tests/i1814/C1814.java | 4 --- .../src/main/kotlin/io/jooby/kt/Kooby.kt | 2 +- .../src/test/kotlin/io/jooby/kt/Idioms.kt | 2 +- .../main/java/io/jooby/test/MockRouter.java | 2 +- .../test/java/examples/InstanceRouter.java | 2 +- .../test/java/io/jooby/problem/data/App.java | 2 +- .../test/java/io/jooby/test/Issue1599.java | 4 +-- .../io/jooby/test/RouteAttributeTest.java | 6 ++-- 19 files changed, 42 insertions(+), 100 deletions(-) diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index a3eb4d9f6c..67742fcc90 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -728,14 +728,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 diff --git a/jooby/src/main/java/io/jooby/Route.java b/jooby/src/main/java/io/jooby/Route.java index 8900215d42..21da0793e9 100644 --- a/jooby/src/main/java/io/jooby/Route.java +++ b/jooby/src/main/java/io/jooby/Route.java @@ -10,7 +10,6 @@ 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; @@ -443,8 +442,6 @@ public MethodHandle toMethodHandle() { private MessageEncoder encoder; - private Type returnType; - private Object handle; private List produces = EMPTY_LIST; @@ -693,30 +690,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. @@ -806,7 +779,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); } @@ -828,7 +802,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); } @@ -1072,7 +1046,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; diff --git a/jooby/src/main/java/io/jooby/RouteSet.java b/jooby/src/main/java/io/jooby/RouteSet.java index a85b556113..129fb8f49d 100644 --- a/jooby/src/main/java/io/jooby/RouteSet.java +++ b/jooby/src/main/java/io/jooby/RouteSet.java @@ -121,7 +121,7 @@ public class RouteSet { * @param value attribute value * @return This route. */ - public @NonNull RouteSet attribute(@NonNull String name, @NonNull Object value) { + public @NonNull RouteSet setAttribute(@NonNull String name, @NonNull Object value) { routes.forEach(it -> it.getAttributes().putIfAbsent(name, value)); return this; } diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index c7ad6e8b17..905342e458 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -151,7 +151,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 +167,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; } 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 94079aa506..f2117826f4 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 @@ -35,7 +35,6 @@ DEBUG, INCREMENTAL, MVC_METHOD, - RETURN_TYPE, ROUTER_PREFIX, ROUTER_SUFFIX, SKIP_ATTRIBUTE_ANNOTATIONS @@ -48,7 +47,6 @@ public interface Options { 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 SKIP_ATTRIBUTE_ANNOTATIONS = "jooby.skipAttributeAnnotations"; 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 53b48c2f93..5b7f9b1853 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 @@ -33,7 +33,6 @@ private record ResultType(String type, String handler, boolean nonBlocking) {} 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<>(); @@ -43,7 +42,6 @@ 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, false); this.routerPrefix = Options.string(processingEnvironment, Options.ROUTER_PREFIX, ""); this.routerSuffix = Options.string(processingEnvironment, Options.ROUTER_SUFFIX, "_"); @@ -244,10 +242,6 @@ public boolean generateMvcMethod() { return mvcMethod; } - public boolean generateReturnType() { - return returnType; - } - public boolean isIncremental() { return incremental; } diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java index 1d158c8822..975a746d46 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java @@ -145,10 +145,6 @@ public List 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 */ 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/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/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-kotlin/src/main/kotlin/io/jooby/kt/Kooby.kt b/modules/jooby-kotlin/src/main/kotlin/io/jooby/kt/Kooby.kt index e4e2a75b15..abfd36a4a7 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 @@ -263,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 } 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 e38f882794..d8ead09ec5 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 @@ -68,7 +68,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-test/src/main/java/io/jooby/test/MockRouter.java b/modules/jooby-test/src/main/java/io/jooby/test/MockRouter.java index 6d7352d3a9..893fd6a050 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)); } 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/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/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/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 -> { From 66e14eb7240f3192ad88323e9c462f281a1e9b0a Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 30 Jun 2025 18:04:52 -0300 Subject: [PATCH 35/60] router: improve routeSet API - remove mvc() from router - make all multiple import of routes a routeSet (unify API) --- jooby/src/main/java/io/jooby/Jooby.java | 30 +++++++++------- jooby/src/main/java/io/jooby/RouteSet.java | 17 +++++++++- jooby/src/main/java/io/jooby/Router.java | 28 ++++++--------- .../java/io/jooby/internal/RouterImpl.java | 34 +++++++------------ .../src/test/java/tests/i3490/Box3490.java | 16 --------- .../src/test/java/tests/i3490/C3490.java | 17 ---------- .../src/test/java/tests/i3490/Issue3490.java | 31 ----------------- 7 files changed, 55 insertions(+), 118 deletions(-) delete mode 100644 modules/jooby-apt/src/test/java/tests/i3490/Box3490.java delete mode 100644 modules/jooby-apt/src/test/java/tests/i3490/C3490.java delete mode 100644 modules/jooby-apt/src/test/java/tests/i3490/Issue3490.java diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 67742fcc90..03333604dc 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -469,9 +469,8 @@ public Jooby setTrustProxy(boolean trustProxy) { } @NonNull @Override - public Router domain(@NonNull String domain, @NonNull Router subrouter) { - this.router.domain(domain, subrouter); - return this; + public RouteSet domain(@NonNull String domain, @NonNull Router subrouter) { + return this.router.domain(domain, subrouter); } @NonNull @Override @@ -485,31 +484,36 @@ public RouteSet mount(@NonNull Predicate predicate, @NonNull Runnable b } @NonNull @Override - public Jooby mount(@NonNull Predicate predicate, @NonNull Router subrouter) { - this.router.mount(predicate, subrouter); - return this; + public RouteSet 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 RouteSet 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 RouteSet mount(@NonNull Router router) { return mount("/", router); } - @NonNull @Override - public Jooby mvc(@NonNull Extension router) { + /** + * Add controller routes. + * + * @param router Mvc extension. + * @return Route set. + */ + @NonNull public RouteSet mvc(@NonNull Extension router) { try { + int start = this.router.getRoutes().size(); router.install(this); - return this; + return new RouteSet(this.router.getRoutes().subList(start, this.router.getRoutes().size())); } catch (Exception cause) { throw SneakyThrows.propagate(cause); } diff --git a/jooby/src/main/java/io/jooby/RouteSet.java b/jooby/src/main/java/io/jooby/RouteSet.java index 129fb8f49d..7a596c52fe 100644 --- a/jooby/src/main/java/io/jooby/RouteSet.java +++ b/jooby/src/main/java/io/jooby/RouteSet.java @@ -9,6 +9,8 @@ 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; @@ -20,7 +22,7 @@ * @author edgar * @since 2.7.3 */ -public class RouteSet { +public class RouteSet implements Iterable { private List routes; @@ -30,6 +32,10 @@ public class RouteSet { private String description; + public RouteSet(List routes) { + this.routes = routes; + } + /** * Sub-routes. Always empty except when used it from {@link Router#path(String, Runnable)} or * {@link Router#routes(Runnable)}. @@ -233,4 +239,13 @@ public class RouteSet { public @NonNull RouteSet 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/Router.java b/jooby/src/main/java/io/jooby/Router.java index 905342e458..756efe78e0 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -312,9 +312,9 @@ default Object execute(@NonNull Context context) { * * @param domain Predicate * @param subrouter Subrouter. - * @return This router. + * @return Created routes. */ - @NonNull Router domain(@NonNull String domain, @NonNull Router subrouter); + @NonNull RouteSet domain(@NonNull String domain, @NonNull Router subrouter); /** * Enabled routes for specific domain. Domain matching is done using the host header. @@ -334,7 +334,7 @@ default Object execute(@NonNull Context context) { * * @param domain Predicate * @param body Route action. - * @return This router. + * @return Created routes. */ @NonNull RouteSet domain(@NonNull String domain, @NonNull Runnable body); @@ -360,9 +360,9 @@ default Object execute(@NonNull Context context) { * * @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 RouteSet mount(@NonNull Predicate predicate, @NonNull Router router); /** * Import routes from given action. Predicate works like a filter and only when predicate pass the @@ -387,7 +387,7 @@ default Object execute(@NonNull Context context) { * * @param predicate Context predicate. * @param body Route action. - * @return This router. + * @return Created routes. */ @NonNull RouteSet mount(@NonNull Predicate predicate, @NonNull Runnable body); @@ -398,9 +398,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 RouteSet mount(@NonNull String path, @NonNull Router router); /** * Import all routes from the given router. @@ -408,23 +408,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 RouteSet mount(@NonNull Router router); /* *********************************************************************************************** * Mvc * *********************************************************************************************** */ - /** - * Import all routes from the given controller class. - * - * @param router Router extension. - * @return This router. - */ - @NonNull Router mvc(@NonNull Extension router); - /** * Add a websocket handler. * diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 870b6ed120..cb3e2ce1cf 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -136,7 +136,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; @@ -265,19 +265,17 @@ public RouteSet domain(@NonNull String domain, @NonNull Runnable body) { } @NonNull @Override - public Router domain(@NonNull String domain, @NonNull Router subrouter) { + public RouteSet 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(); 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 RouteSet(this.routes.subList(start, this.routes.size())); } public Router install( @@ -297,11 +295,11 @@ public Router install( } @NonNull @Override - public Router mount(@NonNull Predicate predicate, @NonNull Router subrouter) { - /** Override services: */ + public RouteSet mount(@NonNull Predicate predicate, @NonNull Router subrouter) { + /* Override services: */ overrideAll(this, subrouter); - /** Routes: */ - mount( + /* Routes: */ + return mount( predicate, () -> { for (Route route : subrouter.getRoutes()) { @@ -309,30 +307,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 RouteSet 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 RouteSet(this.routes.subList(start, this.routes.size())); } @NonNull @Override - public Router mount(@NonNull Router router) { + public RouteSet mount(@NonNull Router router) { return mount("/", router); } - @NonNull @Override - public Router mvc(@NonNull Extension router) { - throw new UnsupportedOperationException(); - } - @NonNull @Override public Router encoder(@NonNull MessageEncoder encoder) { this.encoder.add(MediaType.all, encoder); @@ -413,11 +406,9 @@ public RouteSet routes(@NonNull Runnable action) { @Override @NonNull public RouteSet path(@NonNull String pattern, @NonNull Runnable action) { - RouteSet routeSet = new RouteSet(); int start = this.routes.size(); newStack(chi, pattern, action); - routeSet.setRoutes(this.routes.subList(start, this.routes.size())); - return routeSet; + return new RouteSet(this.routes.subList(start, this.routes.size())); } @NonNull @Override @@ -898,7 +889,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()); 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())")); - }); - } -} From eee79370b0ca0dfb4474162178e905d09caa8691 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 30 Jun 2025 18:12:28 -0300 Subject: [PATCH 36/60] open api: remove dead code - ref #3705 --- .../io/jooby/internal/openapi/AnnotationParser.java | 12 ------------ 1 file changed, 12 deletions(-) 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 fbb769ceb4..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 @@ -206,18 +206,6 @@ public static List parse( .findFirst() .orElse(type); return parse(ctx, prefix, controllerType); - } else if (methodInsnNode.getOpcode() == Opcodes.INVOKEINTERFACE) { - // TODO: almost sure this is dead code - 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)) From 9af5e367f12632b3a51b799ae591912b7594497c Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 30 Jun 2025 18:49:15 -0300 Subject: [PATCH 37/60] route: remove handle field - more code cleanup --- jooby/src/main/java/io/jooby/Route.java | 33 +----------------- jooby/src/main/java/io/jooby/WebSocket.java | 4 +++ .../java/io/jooby/internal/RouterImpl.java | 7 ++-- .../internal/handler/WebSocketHandler.java | 7 ++-- .../kotlin/io/jooby/kt/CoroutineRouter.kt | 34 +++++++++---------- .../src/main/kotlin/io/jooby/kt/Kooby.kt | 2 +- .../main/java/io/jooby/test/MockRouter.java | 6 ++-- 7 files changed, 32 insertions(+), 61 deletions(-) diff --git a/jooby/src/main/java/io/jooby/Route.java b/jooby/src/main/java/io/jooby/Route.java index 21da0793e9..294efdc0a3 100644 --- a/jooby/src/main/java/io/jooby/Route.java +++ b/jooby/src/main/java/io/jooby/Route.java @@ -442,8 +442,6 @@ public MethodHandle toMethodHandle() { private MessageEncoder encoder; - private Object handle; - private List produces = EMPTY_LIST; private List consumes = EMPTY_LIST; @@ -477,7 +475,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; } /** @@ -561,17 +558,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. * @@ -612,17 +598,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. * @@ -1112,17 +1087,11 @@ 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); } return pipeline; } - - private Route.Handler computeHeadPipeline() { - Route.Handler pipeline = filter == null ? handler : filter.then(handler); - - return pipeline; - } } diff --git a/jooby/src/main/java/io/jooby/WebSocket.java b/jooby/src/main/java/io/jooby/WebSocket.java index 83f93ee4e4..32a1e7ec6c 100644 --- a/jooby/src/main/java/io/jooby/WebSocket.java +++ b/jooby/src/main/java/io/jooby/WebSocket.java @@ -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 { /** diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index cb3e2ce1cf..dfb1197436 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -446,14 +446,12 @@ public Router setOutputFactory(@NonNull BufferedOutputFactory 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 @@ -889,7 +887,6 @@ private void copy(Route src, Route it) { it.setFilter(filter); it.setAfter(after); it.setEncoder(src.getEncoder()); - 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/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/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 abfd36a4a7..5ba3542eab 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 @@ -275,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 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 893fd6a050..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 @@ -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(); From 635688b448cc6db1e7ecb6e765091410a00e995f Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Tue, 1 Jul 2025 12:27:25 -0300 Subject: [PATCH 38/60] router: make router option a class - remove enum --- jooby/src/main/java/io/jooby/Context.java | 4 +- jooby/src/main/java/io/jooby/Jooby.java | 5 +- jooby/src/main/java/io/jooby/Route.java | 19 +- jooby/src/main/java/io/jooby/Router.java | 5 +- .../src/main/java/io/jooby/RouterOption.java | 40 ---- .../src/main/java/io/jooby/RouterOptions.java | 181 ++++++++++++++++++ .../java/io/jooby/internal/RouterImpl.java | 20 +- .../io/jooby/internal/jetty/JettyContext.java | 3 +- .../src/main/kotlin/io/jooby/kt/Kooby.kt | 6 +- .../src/test/kotlin/io/jooby/kt/Idioms.kt | 5 +- .../io/jooby/internal/netty/NettyContext.java | 4 +- .../src/test/java/io/jooby/test/TestApp.java | 4 +- .../internal/undertow/UndertowContext.java | 3 +- .../test/java/io/jooby/test/FeaturedTest.java | 6 +- .../test/java/io/jooby/test/Issue1409.java | 4 +- 15 files changed, 225 insertions(+), 84 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/RouterOption.java create mode 100644 jooby/src/main/java/io/jooby/RouterOptions.java diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java index 06f8bf8b29..c3e11ba9ef 100644 --- a/jooby/src/main/java/io/jooby/Context.java +++ b/jooby/src/main/java/io/jooby/Context.java @@ -1390,10 +1390,10 @@ Context responseWriter( /** * 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(); diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 03333604dc..61d68102d5 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -139,12 +139,11 @@ public Jooby() { } @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; } diff --git a/jooby/src/main/java/io/jooby/Route.java b/jooby/src/main/java/io/jooby/Route.java index 294efdc0a3..686c659d17 100644 --- a/jooby/src/main/java/io/jooby/Route.java +++ b/jooby/src/main/java/io/jooby/Route.java @@ -8,7 +8,6 @@ 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.util.ArrayList; import java.util.Arrays; @@ -404,17 +403,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. */ diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 756efe78e0..261f1223a4 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; @@ -845,7 +844,7 @@ default Object execute(@NonNull Context context) { * * @return Router options. */ - @NonNull Set getRouterOptions(); + @NonNull RouterOptions getRouterOptions(); /** * Set router options. @@ -853,7 +852,7 @@ 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. 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: - * - *

- * - * @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..300026e57e --- /dev/null +++ b/jooby/src/main/java/io/jooby/RouterOptions.java @@ -0,0 +1,181 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby; + +/** + * 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; + + /** + * 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); + } +} diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index dfb1197436..535cbbe693 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,7 +22,6 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.function.BiConsumer; @@ -160,7 +158,7 @@ public Stack executor(Executor executor) { private ContextInitializer postDispatchInitializer; - private Set routerOptions = EnumSet.of(RouterOption.RESET_HEADERS_ON_ERROR); + private RouterOptions routerOptions = RouterOptions.defaults(); private boolean trustProxy; @@ -199,19 +197,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); @@ -499,7 +497,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(); } @@ -604,13 +602,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); } 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 072500a7b7..5821866f25 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 @@ -61,7 +61,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; @@ -589,7 +588,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-kotlin/src/main/kotlin/io/jooby/kt/Kooby.kt b/modules/jooby-kotlin/src/main/kotlin/io/jooby/kt/Kooby.kt index 5ba3542eab..be1a03f53e 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 @@ -20,7 +20,7 @@ import io.jooby.Registry import io.jooby.Route import io.jooby.RouteSet import io.jooby.Router -import io.jooby.RouterOption +import io.jooby.RouterOptions import io.jooby.Server import io.jooby.ServiceRegistry import io.jooby.handler.Cors @@ -289,8 +289,8 @@ open class Kooby() : Jooby() { } @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 d8ead09ec5..cf47cc70fd 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,8 +6,7 @@ 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 java.nio.file.Paths import java.time.Duration @@ -34,7 +33,7 @@ class Idioms : services.getOrNull(Jooby::class) - routerOptions(IGNORE_CASE, IGNORE_TRAILING_SLASH) + routerOptions(RouterOptions().ignoreCase(true).ignoreTrailingSlash(true)) setHiddenMethod { ctx -> ctx.header("").toOptional() } 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 3accb63dff..31723e31b1 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 @@ -728,8 +728,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 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-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java index a4d23f30bc..0980adcc29 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,7 +49,6 @@ 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; @@ -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/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index 38a5aa15eb..518fd406ca 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -66,7 +66,7 @@ import io.jooby.Jooby; 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; @@ -1457,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); @@ -1590,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"); 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( From 02a9b6ee359f0d97d3f2d5ab7cae07838eb41a6e Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 2 Jul 2025 19:12:34 -0300 Subject: [PATCH 39/60] refactor: move RouteSet as inner class of Route and rename to Set, so Route.Set --- jooby/src/main/java/io/jooby/Jooby.java | 27 +- jooby/src/main/java/io/jooby/Route.java | 252 +++++++++++++++++- jooby/src/main/java/io/jooby/RouteSet.java | 251 ----------------- jooby/src/main/java/io/jooby/Router.java | 16 +- .../java/io/jooby/internal/RouterImpl.java | 22 +- .../src/main/kotlin/io/jooby/kt/Kooby.kt | 6 +- .../jooby/internal/openapi/RouteParser.java | 18 +- 7 files changed, 280 insertions(+), 312 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/RouteSet.java diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 61d68102d5..edc69f69e1 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -25,7 +25,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Properties; -import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; @@ -310,7 +309,7 @@ public String getContextPath() { * @param factory Application factory. * @return Created routes. */ - @NonNull public RouteSet install(@NonNull SneakyThrows.Supplier factory) { + @NonNull public Route.Set install(@NonNull SneakyThrows.Supplier factory) { return install("/", factory); } @@ -344,7 +343,7 @@ public String getContextPath() { * @param factory Application factory. * @return Created routes. */ - @NonNull public RouteSet install(@NonNull String path, @NonNull SneakyThrows.Supplier factory) { + @NonNull public Route.Set install(@NonNull String path, @NonNull SneakyThrows.Supplier factory) { try { owner = this; return path(path, factory::get); @@ -468,27 +467,27 @@ public Jooby setTrustProxy(boolean trustProxy) { } @NonNull @Override - public RouteSet domain(@NonNull String domain, @NonNull Router subrouter) { + public Route.Set domain(@NonNull String domain, @NonNull Router subrouter) { return this.router.domain(domain, subrouter); } @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 RouteSet mount(@NonNull Predicate predicate, @NonNull Router subrouter) { + public Route.Set mount(@NonNull Predicate predicate, @NonNull Router subrouter) { return this.router.mount(predicate, subrouter); } @NonNull @Override - public RouteSet mount(@NonNull String path, @NonNull Router 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; @@ -498,7 +497,7 @@ public RouteSet mount(@NonNull String path, @NonNull Router router) { } @NonNull @Override - public RouteSet mount(@NonNull Router router) { + public Route.Set mount(@NonNull Router router) { return mount("/", router); } @@ -508,11 +507,11 @@ public RouteSet mount(@NonNull Router router) { * @param router Mvc extension. * @return Route set. */ - @NonNull public RouteSet mvc(@NonNull Extension router) { + @NonNull public Route.Set mvc(@NonNull Extension router) { try { int start = this.router.getRoutes().size(); router.install(this); - return new RouteSet(this.router.getRoutes().subList(start, this.router.getRoutes().size())); + return new Route.Set(this.router.getRoutes().subList(start, this.router.getRoutes().size())); } catch (Exception cause) { throw SneakyThrows.propagate(cause); } @@ -618,12 +617,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); } @@ -1302,7 +1301,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, diff --git a/jooby/src/main/java/io/jooby/Route.java b/jooby/src/main/java/io/jooby/Route.java index 686c659d17..c6ccda68d9 100644 --- a/jooby/src/main/java/io/jooby/Route.java +++ b/jooby/src/main/java/io/jooby/Route.java @@ -5,21 +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.reflect.Method; -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; @@ -455,7 +450,7 @@ public MethodHandle toMethodHandle() { private Map attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - private Set supportedMethod; + private java.util.Set supportedMethod; private String executorKey; @@ -1101,4 +1096,239 @@ private Route.Handler computePipeline() { } return pipeline; } + + /** + * 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 { + + 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 7a596c52fe..0000000000 --- a/jooby/src/main/java/io/jooby/RouteSet.java +++ /dev/null @@ -1,251 +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.Optional.ofNullable; - -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; - -/** - * 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 implements Iterable { - - private List routes; - - private List tags; - - private String summary; - - private String description; - - public RouteSet(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 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 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 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 == null ? List.of() : 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) { - 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 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); - } - - 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/Router.java b/jooby/src/main/java/io/jooby/Router.java index 261f1223a4..16a28a98e6 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -313,7 +313,7 @@ default Object execute(@NonNull Context context) { * @param subrouter Subrouter. * @return Created routes. */ - @NonNull RouteSet 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. @@ -335,7 +335,7 @@ default Object execute(@NonNull Context context) { * @param body Route action. * @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 @@ -361,7 +361,7 @@ default Object execute(@NonNull Context context) { * @param router Router to import. * @return Created routes. */ - @NonNull RouteSet 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 @@ -388,7 +388,7 @@ default Object execute(@NonNull Context context) { * @param body Route action. * @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. @@ -399,7 +399,7 @@ default Object execute(@NonNull Context context) { * @param router Router to import. * @return Created routes. */ - @NonNull RouteSet mount(@NonNull String path, @NonNull Router router); + @NonNull Route.Set mount(@NonNull String path, @NonNull Router router); /** * Import all routes from the given router. @@ -409,7 +409,7 @@ default Object execute(@NonNull Context context) { * @param router Router to import. * @return Created routes. */ - @NonNull RouteSet mount(@NonNull Router router); + @NonNull Route.Set mount(@NonNull Router router); /* *********************************************************************************************** * Mvc @@ -556,7 +556,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 @@ -566,7 +566,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. diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 535cbbe693..6236162de1 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -258,22 +258,22 @@ public Router setTrustProxy(boolean trustProxy) { } @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 RouteSet 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) { + 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); - return new RouteSet(this.routes.subList(start, this.routes.size())); + return new Route.Set(this.routes.subList(start, this.routes.size())); } public Router install( @@ -293,7 +293,7 @@ public Router install( } @NonNull @Override - public RouteSet mount(@NonNull Predicate predicate, @NonNull Router subrouter) { + public Route.Set mount(@NonNull Predicate predicate, @NonNull Router subrouter) { /* Override services: */ overrideAll(this, subrouter); /* Routes: */ @@ -308,7 +308,7 @@ public RouteSet mount(@NonNull Predicate predicate, @NonNull Router sub } @NonNull @Override - public RouteSet 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); @@ -316,11 +316,11 @@ public RouteSet mount(@NonNull String path, @NonNull Router router) { mergeErrorHandler(router); /** Routes: */ copyRoutes(path, router); - return new RouteSet(this.routes.subList(start, this.routes.size())); + return new Route.Set(this.routes.subList(start, this.routes.size())); } @NonNull @Override - public RouteSet mount(@NonNull Router router) { + public Route.Set mount(@NonNull Router router) { return mount("/", router); } @@ -398,15 +398,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) { + @NonNull public Route.Set path(@NonNull String pattern, @NonNull Runnable action) { int start = this.routes.size(); newStack(chi, pattern, action); - return new RouteSet(this.routes.subList(start, this.routes.size())); + return new Route.Set(this.routes.subList(start, this.routes.size())); } @NonNull @Override 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 be1a03f53e..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,7 +18,7 @@ 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.RouterOptions import io.jooby.Server @@ -162,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) } 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)); From 161f1478aeb81b3ac14fc6d4701a914c2e7f2680 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 2 Jul 2025 19:22:19 -0300 Subject: [PATCH 40/60] refactor: Context: replace call to getRouter.getValueFactory by getValueFactory --- jooby/src/main/java/io/jooby/internal/SessionImpl.java | 6 ++---- .../main/java/io/jooby/internal/jetty/JettyContext.java | 9 ++++----- .../main/java/io/jooby/internal/netty/NettyContext.java | 9 ++++----- .../java/io/jooby/internal/undertow/UndertowContext.java | 9 ++++----- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/jooby/src/main/java/io/jooby/internal/SessionImpl.java b/jooby/src/main/java/io/jooby/internal/SessionImpl.java index 146fe85c10..6d76f9026e 100644 --- a/jooby/src/main/java/io/jooby/internal/SessionImpl.java +++ b/jooby/src/main/java/io/jooby/internal/SessionImpl.java @@ -77,7 +77,7 @@ public Session setId(@Nullable String id) { @Override public @NonNull Value get(@NonNull String name) { - return Value.create(ctx.getRouter().getValueFactory(), name, attributes.get(name)); + return Value.create(ctx.getValueFactory(), name, attributes.get(name)); } @Override @@ -91,9 +91,7 @@ public Session setId(@Nullable String id) { public @NonNull Value remove(@NonNull String name) { String value = attributes.remove(name); updateState(); - return value == null - ? Value.missing(name) - : Value.value(ctx.getRouter().getValueFactory(), name, value); + return value == null ? Value.missing(name) : Value.value(ctx.getValueFactory(), name, value); } @Override 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 5821866f25..3707651f16 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 @@ -202,7 +202,7 @@ public Context setPathMap(Map pathMap) { @NonNull @Override public QueryString query() { if (query == null) { - query = QueryString.create(getRouter().getValueFactory(), request.getHttpURI().getQuery()); + query = QueryString.create(getValueFactory(), request.getHttpURI().getQuery()); } return query; } @@ -210,7 +210,7 @@ public QueryString query() { @NonNull @Override public Formdata form() { if (formdata == null) { - formdata = Formdata.create(getRouter().getValueFactory()); + formdata = Formdata.create(getValueFactory()); formParam(request, formdata); @@ -251,8 +251,7 @@ public Formdata form() { @NonNull @Override public Value header(@NonNull String name) { - return Value.create( - getRouter().getValueFactory(), name, request.getHeaders().getValuesList(name)); + return Value.create(getValueFactory(), name, request.getHeaders().getValuesList(name)); } @NonNull @Override @@ -262,7 +261,7 @@ public Value header() { for (HttpField header : request.getHeaders()) { headerMap.put(header.getName(), header.getValueList()); } - headers = Value.headers(getRouter().getValueFactory(), headerMap); + headers = Value.headers(getValueFactory(), headerMap); } return headers; } 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 31723e31b1..96eec56123 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 @@ -222,8 +222,7 @@ public QueryString query() { if (query == null) { String uri = req.uri(); int q = uri.indexOf('?'); - query = - QueryString.create(getRouter().getValueFactory(), q >= 0 ? uri.substring(q + 1) : null); + query = QueryString.create(getValueFactory(), q >= 0 ? uri.substring(q + 1) : null); } return query; } @@ -231,7 +230,7 @@ public QueryString query() { @NonNull @Override public Formdata form() { if (formdata == null) { - formdata = Formdata.create(getRouter().getValueFactory()); + formdata = Formdata.create(getValueFactory()); decodeForm(formdata); } return formdata; @@ -239,7 +238,7 @@ public Formdata form() { @NonNull @Override public Value header(@NonNull String name) { - return Value.create(getRouter().getValueFactory(), name, req.headers().getAll(name)); + return Value.create(getValueFactory(), name, req.headers().getAll(name)); } @NonNull @Override @@ -330,7 +329,7 @@ public Value header() { for (String name : names) { headerMap.put(name, headers.getAll(name)); } - this.headers = Value.headers(getRouter().getValueFactory(), headerMap); + this.headers = Value.headers(getValueFactory(), headerMap); } return headers; } 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 0980adcc29..340a79d5eb 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 @@ -255,8 +255,7 @@ public Context setPort(int port) { @NonNull @Override public Value header(@NonNull String name) { - return Value.create( - getRouter().getValueFactory(), name, exchange.getRequestHeaders().get(name)); + return Value.create(getValueFactory(), name, exchange.getRequestHeaders().get(name)); } @NonNull @Override @@ -269,7 +268,7 @@ public Value header() { HeaderValues values = map.get(name); headerMap.put(name.toString(), values); } - headers = Value.headers(getRouter().getValueFactory(), headerMap); + headers = Value.headers(getValueFactory(), headerMap); } return headers; } @@ -277,7 +276,7 @@ public Value header() { @NonNull @Override public QueryString query() { if (query == null) { - query = QueryString.create(getRouter().getValueFactory(), exchange.getQueryString()); + query = QueryString.create(getValueFactory(), exchange.getQueryString()); } return query; } @@ -285,7 +284,7 @@ public QueryString query() { @NonNull @Override public Formdata form() { if (formdata == null) { - formdata = Formdata.create(getRouter().getValueFactory()); + formdata = Formdata.create(getValueFactory()); formData(formdata, exchange.getAttachment(FORM_DATA)); } return formdata; From 91f121f56556f996ca1e8dbb65f2f00120e96d7d Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 2 Jul 2025 20:10:13 -0300 Subject: [PATCH 41/60] refactor: code cleanup --- jooby/src/main/java/io/jooby/SessionStore.java | 2 +- .../java/io/jooby/internal/RouterImpl.java | 18 +++++++++--------- .../java/io/jooby/internal/SessionImpl.java | 18 ++++++++++++------ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/jooby/src/main/java/io/jooby/SessionStore.java b/jooby/src/main/java/io/jooby/SessionStore.java index 2cca94d8b3..8c19350f46 100644 --- a/jooby/src/main/java/io/jooby/SessionStore.java +++ b/jooby/src/main/java/io/jooby/SessionStore.java @@ -167,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())); } diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 6236162de1..1015a4b0ce 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -467,9 +467,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 = @@ -481,15 +481,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); } @@ -507,10 +509,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); diff --git a/jooby/src/main/java/io/jooby/internal/SessionImpl.java b/jooby/src/main/java/io/jooby/internal/SessionImpl.java index 6d76f9026e..ccec6d784d 100644 --- a/jooby/src/main/java/io/jooby/internal/SessionImpl.java +++ b/jooby/src/main/java/io/jooby/internal/SessionImpl.java @@ -81,17 +81,23 @@ public Session setId(@Nullable String id) { } @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 Value remove(@NonNull String name) { - String value = attributes.remove(name); + var value = get(name); + attributes.remove(name); updateState(); - return value == null ? Value.missing(name) : Value.value(ctx.getValueFactory(), name, value); + return value; } @Override @@ -105,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; } @@ -121,7 +127,7 @@ public Session setCreationTime(Instant creationTime) { return this; } - @Override + @NonNull @Override public Session clear() { attributes.clear(); updateState(); @@ -135,7 +141,7 @@ public void destroy() { store(ctx).deleteSession(ctx, this); } - @Override + @NonNull @Override public Session renewId() { store(ctx).renewSessionId(ctx, this); updateState(); From b934718ce9c67820100d14e7a1fc8a3e4d037f84 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 3 Jul 2025 07:31:18 -0300 Subject: [PATCH 42/60] refactor: rename buffer interal package --- .../java/io/jooby/annotation/ResultType.java | 2 +- .../java/io/jooby/buffer/BufferedOutput.java | 4 +- .../jooby/buffer/ByteBufferOutputFactory.java | 8 +- .../ByteArrayWrappedOutput.java | 2 +- .../{output => buffer}/ByteBufferOutput.java | 2 +- .../ByteBufferWrappedOutput.java | 2 +- .../CompsiteByteBufferOutput.java | 2 +- .../OutputOutputStream.java | 2 +- .../{output => buffer}/OutputWriter.java | 2 +- .../{output => buffer}/package-info.java | 2 +- jooby/src/main/java/module-info.java | 2 +- .../io/jooby/buffer/BufferedOutputTest.java | 6 +- modules/jooby-graphiql/pom.xml | 23 - modules/jooby-redoc/build.xml | 3 +- modules/jooby-redoc/package-lock.json | 1099 ----------------- modules/jooby-redoc/package.json | 9 - modules/jooby-redoc/pom.xml | 50 - 17 files changed, 20 insertions(+), 1200 deletions(-) rename jooby/src/main/java/io/jooby/internal/{output => buffer}/ByteArrayWrappedOutput.java (97%) rename jooby/src/main/java/io/jooby/internal/{output => buffer}/ByteBufferOutput.java (99%) rename jooby/src/main/java/io/jooby/internal/{output => buffer}/ByteBufferWrappedOutput.java (97%) rename jooby/src/main/java/io/jooby/internal/{output => buffer}/CompsiteByteBufferOutput.java (98%) rename jooby/src/main/java/io/jooby/internal/{output => buffer}/OutputOutputStream.java (97%) rename jooby/src/main/java/io/jooby/internal/{output => buffer}/OutputWriter.java (98%) rename jooby/src/main/java/io/jooby/internal/{output => buffer}/package-info.java (75%) delete mode 100644 modules/jooby-redoc/package-lock.json delete mode 100644 modules/jooby-redoc/package.json diff --git a/jooby/src/main/java/io/jooby/annotation/ResultType.java b/jooby/src/main/java/io/jooby/annotation/ResultType.java index f0f4c73527..d46cdb9726 100644 --- a/jooby/src/main/java/io/jooby/annotation/ResultType.java +++ b/jooby/src/main/java/io/jooby/annotation/ResultType.java @@ -12,7 +12,7 @@ /** * 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 + * use a custom handler. This annotation is only for source code generator process so only applies * for MVC routes. Example: * *

{@code
diff --git a/jooby/src/main/java/io/jooby/buffer/BufferedOutput.java b/jooby/src/main/java/io/jooby/buffer/BufferedOutput.java
index f7dce0c8a6..1a48df46cd 100644
--- a/jooby/src/main/java/io/jooby/buffer/BufferedOutput.java
+++ b/jooby/src/main/java/io/jooby/buffer/BufferedOutput.java
@@ -16,8 +16,8 @@
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 import io.jooby.SneakyThrows;
-import io.jooby.internal.output.OutputOutputStream;
-import io.jooby.internal.output.OutputWriter;
+import io.jooby.internal.buffer.OutputOutputStream;
+import io.jooby.internal.buffer.OutputWriter;
 
 /**
  * Buffered output used to support multiple implementations like byte array, byte buffer, netty
diff --git a/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java b/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java
index 0bf1ce14f9..d74389fe1c 100644
--- a/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java
+++ b/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java
@@ -8,10 +8,10 @@
 import java.nio.ByteBuffer;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
-import io.jooby.internal.output.ByteArrayWrappedOutput;
-import io.jooby.internal.output.ByteBufferOutput;
-import io.jooby.internal.output.ByteBufferWrappedOutput;
-import io.jooby.internal.output.CompsiteByteBufferOutput;
+import io.jooby.internal.buffer.ByteArrayWrappedOutput;
+import io.jooby.internal.buffer.ByteBufferOutput;
+import io.jooby.internal.buffer.ByteBufferWrappedOutput;
+import io.jooby.internal.buffer.CompsiteByteBufferOutput;
 
 /**
  * An output factory backed by {@link ByteBuffer}.
diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java b/jooby/src/main/java/io/jooby/internal/buffer/ByteArrayWrappedOutput.java
similarity index 97%
rename from jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java
rename to jooby/src/main/java/io/jooby/internal/buffer/ByteArrayWrappedOutput.java
index 81815ef31b..0f9d9dfbd1 100644
--- a/jooby/src/main/java/io/jooby/internal/output/ByteArrayWrappedOutput.java
+++ b/jooby/src/main/java/io/jooby/internal/buffer/ByteArrayWrappedOutput.java
@@ -3,7 +3,7 @@
  * Apache License Version 2.0 https://jooby.io/LICENSE.txt
  * Copyright 2014 Edgar Espina
  */
-package io.jooby.internal.output;
+package io.jooby.internal.buffer;
 
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java b/jooby/src/main/java/io/jooby/internal/buffer/ByteBufferOutput.java
similarity index 99%
rename from jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java
rename to jooby/src/main/java/io/jooby/internal/buffer/ByteBufferOutput.java
index 1d26996f91..996b0cea8d 100644
--- a/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutput.java
+++ b/jooby/src/main/java/io/jooby/internal/buffer/ByteBufferOutput.java
@@ -3,7 +3,7 @@
  * Apache License Version 2.0 https://jooby.io/LICENSE.txt
  * Copyright 2014 Edgar Espina
  */
-package io.jooby.internal.output;
+package io.jooby.internal.buffer;
 
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java b/jooby/src/main/java/io/jooby/internal/buffer/ByteBufferWrappedOutput.java
similarity index 97%
rename from jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java
rename to jooby/src/main/java/io/jooby/internal/buffer/ByteBufferWrappedOutput.java
index d31e79290a..1da5e56e99 100644
--- a/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java
+++ b/jooby/src/main/java/io/jooby/internal/buffer/ByteBufferWrappedOutput.java
@@ -3,7 +3,7 @@
  * Apache License Version 2.0 https://jooby.io/LICENSE.txt
  * Copyright 2014 Edgar Espina
  */
-package io.jooby.internal.output;
+package io.jooby.internal.buffer;
 
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
diff --git a/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java b/jooby/src/main/java/io/jooby/internal/buffer/CompsiteByteBufferOutput.java
similarity index 98%
rename from jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java
rename to jooby/src/main/java/io/jooby/internal/buffer/CompsiteByteBufferOutput.java
index c9a861391a..b1b3f86fe1 100644
--- a/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java
+++ b/jooby/src/main/java/io/jooby/internal/buffer/CompsiteByteBufferOutput.java
@@ -3,7 +3,7 @@
  * Apache License Version 2.0 https://jooby.io/LICENSE.txt
  * Copyright 2014 Edgar Espina
  */
-package io.jooby.internal.output;
+package io.jooby.internal.buffer;
 
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
diff --git a/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java b/jooby/src/main/java/io/jooby/internal/buffer/OutputOutputStream.java
similarity index 97%
rename from jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java
rename to jooby/src/main/java/io/jooby/internal/buffer/OutputOutputStream.java
index 209899e735..6095626617 100644
--- a/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java
+++ b/jooby/src/main/java/io/jooby/internal/buffer/OutputOutputStream.java
@@ -3,7 +3,7 @@
  * Apache License Version 2.0 https://jooby.io/LICENSE.txt
  * Copyright 2014 Edgar Espina
  */
-package io.jooby.internal.output;
+package io.jooby.internal.buffer;
 
 import java.io.IOException;
 import java.io.OutputStream;
diff --git a/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java b/jooby/src/main/java/io/jooby/internal/buffer/OutputWriter.java
similarity index 98%
rename from jooby/src/main/java/io/jooby/internal/output/OutputWriter.java
rename to jooby/src/main/java/io/jooby/internal/buffer/OutputWriter.java
index 93b295fb71..9961d128c3 100644
--- a/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java
+++ b/jooby/src/main/java/io/jooby/internal/buffer/OutputWriter.java
@@ -3,7 +3,7 @@
  * Apache License Version 2.0 https://jooby.io/LICENSE.txt
  * Copyright 2014 Edgar Espina
  */
-package io.jooby.internal.output;
+package io.jooby.internal.buffer;
 
 import java.io.IOException;
 import java.io.Writer;
diff --git a/jooby/src/main/java/io/jooby/internal/output/package-info.java b/jooby/src/main/java/io/jooby/internal/buffer/package-info.java
similarity index 75%
rename from jooby/src/main/java/io/jooby/internal/output/package-info.java
rename to jooby/src/main/java/io/jooby/internal/buffer/package-info.java
index 881ac8de5c..bd31b7a64b 100644
--- a/jooby/src/main/java/io/jooby/internal/output/package-info.java
+++ b/jooby/src/main/java/io/jooby/internal/buffer/package-info.java
@@ -1,4 +1,4 @@
 @ReturnValuesAreNonnullByDefault
-package io.jooby.internal.output;
+package io.jooby.internal.buffer;
 
 import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;
diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java
index 00f982f9a6..f105686dc6 100644
--- a/jooby/src/main/java/module-info.java
+++ b/jooby/src/main/java/module-info.java
@@ -14,7 +14,7 @@
   exports io.jooby.problem;
   exports io.jooby.value;
   exports io.jooby.buffer;
-  exports io.jooby.internal.output;
+  exports io.jooby.internal.buffer;
 
   uses io.jooby.Server;
   uses io.jooby.SslProvider;
diff --git a/jooby/src/test/java/io/jooby/buffer/BufferedOutputTest.java b/jooby/src/test/java/io/jooby/buffer/BufferedOutputTest.java
index 0e32523a20..566f96dd26 100644
--- a/jooby/src/test/java/io/jooby/buffer/BufferedOutputTest.java
+++ b/jooby/src/test/java/io/jooby/buffer/BufferedOutputTest.java
@@ -15,9 +15,9 @@
 import org.junit.jupiter.api.Test;
 
 import io.jooby.SneakyThrows;
-import io.jooby.internal.output.ByteBufferOutput;
-import io.jooby.internal.output.ByteBufferWrappedOutput;
-import io.jooby.internal.output.CompsiteByteBufferOutput;
+import io.jooby.internal.buffer.ByteBufferOutput;
+import io.jooby.internal.buffer.ByteBufferWrappedOutput;
+import io.jooby.internal.buffer.CompsiteByteBufferOutput;
 
 public class BufferedOutputTest {
 
diff --git a/modules/jooby-graphiql/pom.xml b/modules/jooby-graphiql/pom.xml
index 64632dbf04..fab6336c37 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-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 046586628e..22652c735c 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

From 30d5ad1efeadc4325688c43979f43b1414e74730 Mon Sep 17 00:00:00 2001
From: Edgar Espina 
Date: Thu, 3 Jul 2025 11:04:18 -0300
Subject: [PATCH 43/60] upgrade: hibernate-orm 7.0 and hibernate-validator 9.0

fix #3713
fix #3714
---
 jooby/src/main/java/io/jooby/Server.java  | 10 ++++++----
 modules/jooby-hibernate-validator/pom.xml | 10 ++--------
 modules/jooby-undertow/pom.xml            |  7 ++++++-
 pom.xml                                   |  2 +-
 4 files changed, 15 insertions(+), 14 deletions(-)

diff --git a/jooby/src/main/java/io/jooby/Server.java b/jooby/src/main/java/io/jooby/Server.java
index d233ceda6f..5accca37a3 100644
--- a/jooby/src/main/java/io/jooby/Server.java
+++ b/jooby/src/main/java/io/jooby/Server.java
@@ -110,10 +110,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();
+          }
         }
       }
     }
diff --git a/modules/jooby-hibernate-validator/pom.xml b/modules/jooby-hibernate-validator/pom.xml
index 9c79c96a8c..3410b1f597 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-undertow/pom.xml b/modules/jooby-undertow/pom.xml
index 882e7b7429..daafb14348 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/pom.xml b/pom.xml
index be0774e306..debf48e30c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -72,7 +72,7 @@
     
     6.3.0
     1.2
-    6.6.19.Final
+    7.0.4.Final
     15.11.0
     3.49.5
     11.8.2

From 5c1377bab555f67ff2b338789ace2584a97a2595 Mon Sep 17 00:00:00 2001
From: Edgar Espina 
Date: Thu, 3 Jul 2025 11:18:12 -0300
Subject: [PATCH 44/60] refactor: remove deprecated code

---
 .../validator/HibernateValidatorModule.java   | 15 ------
 .../io/jooby/jstachio/JStachioModule.java     | 13 -----
 .../io/jooby/rocker/BufferedRockerOutput.java |  2 +-
 .../java/io/jooby/rocker/RockerModule.java    | 48 +------------------
 4 files changed, 3 insertions(+), 75 deletions(-)

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-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-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java b/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java
index 61e933a0e2..a026dd1d38 100644
--- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java
+++ b/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java
@@ -72,7 +72,7 @@ public BufferedOutput asOutput() {
   }
 
   static RockerOutputFactory factory(
-      Charset charset, BufferedOutputFactory factory, int bufferSize) {
+      Charset charset, BufferedOutputFactory factory) {
     return (contentType, charsetName) ->
         new BufferedRockerOutput(charset, contentType, factory.newCompositeOutput());
   }
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 7dd3a36934..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, BufferedRockerOutput.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 BufferedRockerOutput}.
-   *
-   * @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 = BufferedRockerOutput.factory(charset, application.getOutputFactory(), bufferSize);
+    var factory = BufferedRockerOutput.factory(charset, application.getOutputFactory());
     runtime.setReloading(reloading);
     // renderer
     application.encoder(new RockerMessageEncoder(factory));

From 993782b0980e59ac6fd963031702dfd6624d3746 Mon Sep 17 00:00:00 2001
From: Edgar Espina 
Date: Thu, 10 Jul 2025 14:13:43 -0300
Subject: [PATCH 45/60] remove: ResultType annotation

- make reactive support static
- fix #3717
---
 .../main/java/io/jooby/ReactiveSupport.java   |   5 -
 .../java/io/jooby/annotation/ResultType.java  |  76 -----------
 .../java/io/jooby/apt/JoobyProcessor.java     |   2 -
 .../io/jooby/internal/apt/MvcContext.java     | 119 ++++--------------
 .../io/jooby/internal/apt/ReactiveType.java   |  59 +++++++++
 .../src/test/java/tests/i3422/C3422.java      |  16 ---
 .../src/test/java/tests/i3422/Issue3422.java  |  26 ----
 .../test/java/tests/i3422/ReactiveType.java   |   8 --
 .../tests/i3422/ReactiveTypeGenerator.java    |  17 ---
 .../src/main/java/io/jooby/mutiny/Mutiny.java |   5 -
 .../main/java/io/jooby/reactor/Reactor.java   |  38 +++---
 .../main/java/io/jooby/rxjava3/Reactivex.java |   5 -
 12 files changed, 98 insertions(+), 278 deletions(-)
 delete mode 100644 jooby/src/main/java/io/jooby/annotation/ResultType.java
 create mode 100644 modules/jooby-apt/src/main/java/io/jooby/internal/apt/ReactiveType.java
 delete mode 100644 modules/jooby-apt/src/test/java/tests/i3422/C3422.java
 delete mode 100644 modules/jooby-apt/src/test/java/tests/i3422/Issue3422.java
 delete mode 100644 modules/jooby-apt/src/test/java/tests/i3422/ReactiveType.java
 delete mode 100644 modules/jooby-apt/src/test/java/tests/i3422/ReactiveTypeGenerator.java

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/annotation/ResultType.java b/jooby/src/main/java/io/jooby/annotation/ResultType.java
deleted file mode 100644
index d46cdb9726..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 is 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/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java b/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java index f2117826f4..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,7 +31,6 @@ import io.jooby.internal.apt.*; @SupportedOptions({ - HANDLER, DEBUG, INCREMENTAL, MVC_METHOD, @@ -42,7 +41,6 @@ @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"; 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 5b7f9b1853..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,8 +22,6 @@ * 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; @@ -34,7 +30,7 @@ private record ResultType(String type, String handler, boolean nonBlocking) {} private final BiConsumer output; private final List routers = new ArrayList<>(); private final boolean mvcMethod; - private final Map handler = new HashMap<>(); + private final Map reactiveTypeMap = new HashMap<>(); public MvcContext( ProcessingEnvironment processingEnvironment, BiConsumer output) { @@ -45,99 +41,28 @@ public MvcContext( 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"); } - 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)); + }); } }); } @@ -199,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())) { @@ -281,7 +206,7 @@ public void generateStaticImports(MvcRouter mvcRouter, BiConsumer 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/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-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-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-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 = From ced3b2aa40f4cb27bc95dd68129252ba29231a31 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Thu, 10 Jul 2025 19:09:31 -0300 Subject: [PATCH 46/60] jetty: remove usage of deprecated method --- .../io/jooby/internal/jetty/JettyContext.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) 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 3707651f16..1844d12630 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; @@ -231,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))); From 2e4e19314b851374e53128873a4cf44c3c1b777b Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 13 Jul 2025 13:19:35 -0300 Subject: [PATCH 47/60] netty: get back custom headers implementation --- .../jooby/internal/netty/HeadersMultiMap.java | 642 ++++++++++++++++++ .../io/jooby/internal/netty/NettyContext.java | 3 +- .../jooby/internal/netty/NettyPipeline.java | 2 +- .../internal/netty/NettyResponseEncoder.java | 26 + 4 files changed, 670 insertions(+), 3 deletions(-) create mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/HeadersMultiMap.java create mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.java 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 new file mode 100644 index 0000000000..3308db9fbe --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/HeadersMultiMap.java @@ -0,0 +1,642 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.netty; + +import static io.netty.handler.codec.http.HttpConstants.*; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +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; + +/** + * A case-insensitive implementation that extends Netty {@link HttpHeaders} for convenience. + * + * @author
Julien Viet + */ +public final class HeadersMultiMap extends HttpHeaders { + + /** + * Convert the {@code value} to a non null {@code CharSequence} + * + * @param value the value + * @return the char sequence + */ + private static CharSequence toValidCharSequence(Object value) { + if (value instanceof CharSequence) { + return (CharSequence) value; + } else { + // Throws NPE + return value.toString(); + } + } + + public static final boolean DISABLE_HEADER_VALIDATION = + Boolean.parseBoolean(System.getProperty("io.netty.disableHttpHeadersValidation", "false")); + static final BiConsumer HTTP_VALIDATOR; + + static { + if (DISABLE_HEADER_VALIDATION) { + HTTP_VALIDATOR = null; + } else { + var nameValidator = DefaultHttpHeadersFactory.headersFactory().getNameValidator(); + var valueValidator = DefaultHttpHeadersFactory.headersFactory().getValueValidator(); + HTTP_VALIDATOR = + (name, value) -> { + nameValidator.validateName(name); + valueValidator.validate(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(); + } + + private final BiConsumer validator; + private final HeadersMultiMap.MapEntry[] entries = new HeadersMultiMap.MapEntry[16]; + private final HeadersMultiMap.MapEntry head = new HeadersMultiMap.MapEntry(); + + public HeadersMultiMap() { + this(HTTP_VALIDATOR); + } + + public HeadersMultiMap(BiConsumer validator) { + this.validator = validator; + head.before = head.after = head; + } + + public HeadersMultiMap add(CharSequence name, CharSequence value) { + Objects.requireNonNull(value); + int h = AsciiString.hashCode(name); + int i = h & 0x0000000F; + add0(h, i, name, value); + return this; + } + + @Override + public HeadersMultiMap add(CharSequence name, Object value) { + return add(name, toValidCharSequence(value)); + } + + @Override + public HttpHeaders add(String name, Object value) { + return add((CharSequence) name, toValidCharSequence(value)); + } + + public HeadersMultiMap add(String name, String strVal) { + return add((CharSequence) name, strVal); + } + + @Override + public HeadersMultiMap add(CharSequence name, Iterable values) { + int h = AsciiString.hashCode(name); + int i = h & 0x0000000F; + for (Object vstr : values) { + add0(h, i, name, toValidCharSequence(vstr)); + } + return this; + } + + @Override + public HeadersMultiMap add(String name, Iterable values) { + return add((CharSequence) name, values); + } + + @Override + public HeadersMultiMap remove(CharSequence name) { + Objects.requireNonNull(name, "name"); + int h = AsciiString.hashCode(name); + int i = h & 0x0000000F; + remove0(h, i, name); + return this; + } + + @Override + public HeadersMultiMap remove(final String name) { + return remove((CharSequence) name); + } + + public HeadersMultiMap set(CharSequence name, CharSequence value) { + return set0(name, value); + } + + public HeadersMultiMap set(String name, String value) { + return set0(name, value); + } + + @Override + public HeadersMultiMap set(String name, Object value) { + return set0(name, toValidCharSequence(value)); + } + + @Override + public HeadersMultiMap set(CharSequence name, Object value) { + return set(name, toValidCharSequence(value)); + } + + @Override + public HeadersMultiMap set(CharSequence name, Iterable values) { + Objects.requireNonNull(values, "values"); + + int h = AsciiString.hashCode(name); + int i = h & 0x0000000F; + + remove0(h, i, name); + for (Object v : values) { + if (v == null) { + break; + } + add0(h, i, name, toValidCharSequence(v)); + } + + return this; + } + + @Override + public HeadersMultiMap set(String name, Iterable values) { + return set((CharSequence) name, values); + } + + @Override + public boolean containsValue(CharSequence name, CharSequence value, boolean ignoreCase) { + return containsInternal(name, value, false, ignoreCase); + } + + @Override + public boolean contains(CharSequence name, CharSequence value, boolean ignoreCase) { + return containsInternal(name, value, true, ignoreCase); + } + + private boolean containsInternal( + CharSequence name, CharSequence value, boolean equals, boolean ignoreCase) { + int h = AsciiString.hashCode(name); + int i = h & 0x0000000F; + HeadersMultiMap.MapEntry e = entries[i]; + while (e != null) { + CharSequence key = e.key; + if (e.hash == h && (name == key || AsciiString.contentEqualsIgnoreCase(name, key))) { + CharSequence other = e.getValue(); + if (equals) { + if ((ignoreCase && AsciiString.contentEqualsIgnoreCase(value, other)) + || (!ignoreCase && AsciiString.contentEquals(value, other))) { + return true; + } + } else { + int prev = 0; + while (true) { + final int idx = AsciiString.indexOf(other, ',', prev); + int to; + if (idx == -1) { + to = other.length(); + } else { + to = idx; + } + while (to > prev && other.charAt(to - 1) == ' ') { + to--; + } + int from = prev; + while (from < to && other.charAt(from) == ' ') { + from++; + } + int len = to - from; + if (len > 0 && AsciiString.regionMatches(other, ignoreCase, from, value, 0, len)) { + return true; + } else if (idx == -1) { + break; + } + prev = idx + 1; + } + } + } + e = e.next; + } + return false; + } + + @Override + public boolean contains(String name, String value, boolean ignoreCase) { + return contains((CharSequence) name, value, ignoreCase); + } + + @Override + public boolean contains(CharSequence name) { + return get0(name) != null; + } + + @Override + public boolean contains(String name) { + return contains((CharSequence) name); + } + + @Override + public String get(CharSequence name) { + Objects.requireNonNull(name, "name"); + CharSequence ret = get0(name); + return ret != null ? ret.toString() : null; + } + + @Override + public String get(String name) { + return get((CharSequence) name); + } + + @Override + public List getAll(CharSequence name) { + Objects.requireNonNull(name, "name"); + LinkedList values = null; + int h = AsciiString.hashCode(name); + int i = h & 0x0000000F; + HeadersMultiMap.MapEntry e = entries[i]; + while (e != null) { + CharSequence key = e.key; + if (e.hash == h && (name == key || AsciiString.contentEqualsIgnoreCase(name, key))) { + if (values == null) { + values = new LinkedList<>(); + } + values.addFirst(e.getValue().toString()); + } + e = e.next; + } + return values == null ? Collections.emptyList() : Collections.unmodifiableList(values); + } + + @Override + public List> entries() { + if (isEmpty()) { + return Collections.emptyList(); + } + List> entries = new ArrayList<>(this.entries.length); + forEach((Consumer>) entries::add); + return entries; + } + + @Override + public List getAll(String name) { + return getAll((CharSequence) name); + } + + @Override + public void forEach(Consumer> action) { + HeadersMultiMap.MapEntry e = head.after; + while (e != head) { + action.accept(e.stringEntry()); + e = e.after; + } + } + + public void forEach(BiConsumer action) { + HeadersMultiMap.MapEntry e = head.after; + while (e != head) { + action.accept(e.getKey().toString(), e.getValue().toString()); + e = e.after; + } + } + + @Override + public Iterator> iterator() { + return new Iterator>() { + MapEntry curr = head; + + @Override + public boolean hasNext() { + return curr.after != head; + } + + @Override + public Map.Entry next() { + MapEntry next = curr.after; + if (next == head) { + throw new NoSuchElementException(); + } + curr = next; + return new Map.Entry() { + @Override + public String getKey() { + return next.key.toString(); + } + + @Override + public String getValue() { + return next.value.toString(); + } + + @Override + public String setValue(String value) { + return next.setValue(value).toString(); + } + + @Override + public String toString() { + return getKey() + "=" + getValue(); + } + }; + } + }; + } + + @Override + public boolean isEmpty() { + return head == head.after; + } + + @Override + public Set names() { + Set names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + HeadersMultiMap.MapEntry e = head.after; + while (e != head) { + names.add(e.getKey().toString()); + e = e.after; + } + return names; + } + + @Override + public HeadersMultiMap clear() { + Arrays.fill(entries, null); + head.before = head.after = head; + return this; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : this) { + sb.append(entry).append('\n'); + } + return sb.toString(); + } + + @Override + public Integer getInt(CharSequence name) { + return getValue(name, Integer::parseInt, null); + } + + private T getValue(CharSequence name, Function mapper, T defaultValue) { + var value = get(name); + return value == null ? defaultValue : mapper.apply(value); + } + + @Override + public int getInt(CharSequence name, int defaultValue) { + return getValue(name, Integer::parseInt, defaultValue); + } + + @Override + public Short getShort(CharSequence name) { + return getValue(name, Short::parseShort, null); + } + + @Override + public short getShort(CharSequence name, short defaultValue) { + return getValue(name, Short::parseShort, defaultValue); + } + + @Override + public Long getTimeMillis(CharSequence name) { + return getValue(name, dateToMillis(), null); + } + + @Override + public long getTimeMillis(CharSequence name, long defaultValue) { + return getValue(name, dateToMillis(), defaultValue); + } + + private static Function dateToMillis() { + return value -> { + var date = DateFormatter.parseHttpDate(value); + return date == null ? null : date.getTime(); + }; + } + + @Override + public Iterator> iteratorCharSequence() { + return new Iterator>() { + HeadersMultiMap.MapEntry current = head.after; + + @Override + public boolean hasNext() { + return current != head; + } + + @Override + public Map.Entry next() { + Map.Entry next = current; + current = current.after; + return next; + } + }; + } + + @Override + public HttpHeaders addInt(CharSequence name, int value) { + add(name, Integer.toString(value)); + return this; + } + + @Override + public HttpHeaders addShort(CharSequence name, short value) { + add(name, Short.toString(value)); + return this; + } + + @Override + public HttpHeaders setInt(CharSequence name, int value) { + return set(name, Integer.toString(value)); + } + + @Override + public HttpHeaders setShort(CharSequence name, short value) { + return set(name, Short.toString(value)); + } + + public void encode(ByteBuf buf) { + HeadersMultiMap.MapEntry current = head.after; + while (current != head) { + encoderHeader(current.key, current.value, buf); + current = current.after; + } + } + + private static final int COLON_AND_SPACE_SHORT = (COLON << 8) | SP; + static final int CRLF_SHORT = (CR << 8) | LF; + + static void encoderHeader(CharSequence name, CharSequence value, ByteBuf buf) { + final int nameLen = name.length(); + final int valueLen = value.length(); + final int entryLen = nameLen + valueLen + 4; + buf.ensureWritable(entryLen); + int offset = buf.writerIndex(); + writeAscii(buf, offset, name); + offset += nameLen; + ByteBufUtil.setShortBE(buf, offset, COLON_AND_SPACE_SHORT); + offset += 2; + writeAscii(buf, offset, value); + offset += valueLen; + ByteBufUtil.setShortBE(buf, offset, CRLF_SHORT); + offset += 2; + buf.writerIndex(offset); + } + + private static void writeAscii(ByteBuf buf, int offset, CharSequence value) { + if (value instanceof AsciiString) { + ByteBufUtil.copy((AsciiString) value, 0, buf, offset, value.length()); + } else { + buf.setCharSequence(offset, value, CharsetUtil.US_ASCII); + } + } + + private final class MapEntry implements Map.Entry { + final int hash; + final CharSequence key; + CharSequence value; + HeadersMultiMap.MapEntry next; + HeadersMultiMap.MapEntry before, after; + + MapEntry() { + this.hash = -1; + this.key = null; + this.value = null; + } + + MapEntry(int hash, CharSequence key, CharSequence value) { + this.hash = hash; + this.key = key; + this.value = value; + } + + void remove() { + before.after = after; + after.before = before; + after = null; + before = null; + } + + void addBefore(HeadersMultiMap.MapEntry e) { + after = e; + before = e.before; + before.after = this; + after.before = this; + } + + @Override + public CharSequence getKey() { + return key; + } + + @Override + public CharSequence getValue() { + return value; + } + + @Override + public CharSequence setValue(CharSequence value) { + Objects.requireNonNull(value, "value"); + if (validator != null) { + validator.accept("", value); + } + CharSequence oldValue = this.value; + this.value = value; + return oldValue; + } + + @Override + public String toString() { + return getKey() + "=" + getValue(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private Map.Entry stringEntry() { + if (key instanceof String && value instanceof String) { + return (Map.Entry) this; + } else { + return new AbstractMap.SimpleEntry<>(key.toString(), value.toString()); + } + } + } + + private void remove0(int h, int i, CharSequence name) { + HeadersMultiMap.MapEntry e = entries[i]; + MapEntry prev = null; + while (e != null) { + MapEntry next = e.next; + CharSequence key = e.key; + if (e.hash == h && (name == key || AsciiString.contentEqualsIgnoreCase(name, key))) { + if (prev == null) { + entries[i] = next; + } else { + prev.next = next; + } + e.remove(); + } else { + prev = e; + } + e = next; + } + } + + private void add0(int h, int i, final CharSequence name, final CharSequence value) { + if (validator != null) { + validator.accept(name, value); + } + // Update the hash table. + HeadersMultiMap.MapEntry e = entries[i]; + HeadersMultiMap.MapEntry newEntry; + entries[i] = newEntry = new HeadersMultiMap.MapEntry(h, name, value); + newEntry.next = e; + + // Update the linked list. + newEntry.addBefore(head); + } + + private HeadersMultiMap set0(final CharSequence name, final CharSequence strVal) { + int h = AsciiString.hashCode(name); + int i = h & 0x0000000F; + remove0(h, i, name); + if (strVal != null) { + add0(h, i, name, strVal); + } + return this; + } + + private CharSequence get0(CharSequence name) { + int h = AsciiString.hashCode(name); + int i = h & 0x0000000F; + HeadersMultiMap.MapEntry e = entries[i]; + CharSequence value = null; + while (e != null) { + CharSequence key = e.key; + if (e.hash == h && (name == key || AsciiString.contentEqualsIgnoreCase(name, key))) { + value = e.getValue(); + } + e = e.next; + } + return value; + } +} 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 96eec56123..8829936503 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 @@ -75,8 +75,7 @@ import io.netty.util.IllegalReferenceCountException; public class NettyContext implements DefaultContext, ChannelFutureListener { - public static HttpHeadersFactory HEADERS = - DefaultHttpHeadersFactory.headersFactory().withValidation(false); + public static HttpHeadersFactory HEADERS = HeadersMultiMap.httpHeadersFactory(); private static final HttpHeaders NO_TRAILING = EmptyHttpHeaders.INSTANCE; private static final String STREAM_ID = "x-http2-stream-id"; diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java index a1441cbfc9..44b12e46cc 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java @@ -119,7 +119,7 @@ private void http11Upgrade( private void http11(ChannelPipeline p) { p.addLast("decoder", new NettyRequestDecoder(decoderConfig)); - p.addLast("encoder", new HttpResponseEncoder()); + p.addLast("encoder", new NettyResponseEncoder()); additionalHandlers(p); p.addLast("handler", createHandler()); } 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 new file mode 100644 index 0000000000..1b97bfe9fb --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyResponseEncoder.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 io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponseEncoder; + +public class NettyResponseEncoder extends HttpResponseEncoder { + @Override + protected void encodeHeaders(HttpHeaders headers, ByteBuf buf) { + if (headers instanceof HeadersMultiMap headersMultiMap) { + headersMultiMap.encode(buf); + } else { + super.encodeHeaders(headers, buf); + } + } + + @Override + public boolean acceptOutboundMessage(Object msg) throws Exception { + return super.acceptOutboundMessage(msg); + } +} From 76d391fd67aacc250c1da1e6c9cf4c7b1b5d89a0 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 14 Jul 2025 07:34:52 -0300 Subject: [PATCH 48/60] output api: make sure outputfactory is accessible while creating the application --- jooby/src/main/java/io/jooby/Jooby.java | 29 +++++--- jooby/src/main/java/io/jooby/Router.java | 2 - jooby/src/main/java/io/jooby/Server.java | 3 + .../jooby/buffer/ByteBufferOutputFactory.java | 3 +- .../java/io/jooby/internal/MutedServer.java | 6 ++ .../java/io/jooby/internal/RouterImpl.java | 6 +- .../buffer/ByteArrayWrappedOutput.java | 73 ------------------- jooby/src/test/java/io/jooby/Issue3653.java | 7 ++ .../kotlin/io/jooby/kt/KotlinIdiomTest.kt | 21 ------ ...y.java => NettyBufferedOutputFactory.java} | 12 +-- ...t.java => NettyBufferedWrappedOutput.java} | 6 +- 11 files changed, 46 insertions(+), 122 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/internal/buffer/ByteArrayWrappedOutput.java delete mode 100644 modules/jooby-kotlin/src/test/kotlin/io/jooby/kt/KotlinIdiomTest.kt rename modules/jooby-netty/src/main/java/io/jooby/internal/netty/{NettyOutputFactory.java => NettyBufferedOutputFactory.java} (81%) rename modules/jooby-netty/src/main/java/io/jooby/internal/netty/{NettyWrappedOutput.java => NettyBufferedWrappedOutput.java} (87%) diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index edc69f69e1..b81ef942d7 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -87,6 +87,7 @@ public class Jooby implements Router, Registry { private static Jooby owner; private static ExecutionMode BOOT_EXECUTION_MODE = ExecutionMode.DEFAULT; + private static Server BOOT_SERVER; private RouterImpl router; @@ -132,6 +133,14 @@ public Jooby() { startingCallbacks = new ArrayList<>(); readyCallbacks = new ArrayList<>(); lateExtensions = new ArrayList<>(); + server = BOOT_SERVER; + if (server != null) { + router.setOutputFactory(server.getOutputFactory()); + } else { + // NOTE: fallback to default, this is required for direct instance creation of class + // app bootstrap always ensures server instance. + router.setOutputFactory(BufferedOutputFactory.create()); + } } else { copyState(owner, this); } @@ -857,12 +866,6 @@ public BufferedOutputFactory getOutputFactory() { return router.getOutputFactory(); } - @NonNull @Override - public Jooby setOutputFactory(@NonNull BufferedOutputFactory outputFactory) { - router.setOutputFactory(outputFactory); - return this; - } - @NonNull @Override public Jooby setHiddenMethod(@NonNull Function> provider) { router.setHiddenMethod(provider); @@ -1210,8 +1213,7 @@ public static void runApp( var apps = new ArrayList(); var targetServer = server.getLoggerOff().isEmpty() ? server : MutedServer.mute(server); 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 @@ -1248,7 +1250,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 = @@ -1261,7 +1265,8 @@ public static Jooby createApp( Jooby app; try { - BOOT_EXECUTION_MODE = executionMode; + Jooby.BOOT_SERVER = server; + Jooby.BOOT_EXECUTION_MODE = executionMode; app = provider.get(); } catch (Throwable t) { LoggerFactory.getLogger(Jooby.class) @@ -1271,7 +1276,8 @@ public static Jooby createApp( ? (StartupException) t : new StartupException("Application initialization resulted in exception", t); } finally { - BOOT_EXECUTION_MODE = ExecutionMode.DEFAULT; + Jooby.BOOT_EXECUTION_MODE = executionMode; + Jooby.BOOT_SERVER = null; } return app; @@ -1426,5 +1432,6 @@ private static void copyState(Jooby source, Jooby dest) { dest.readyCallbacks = source.readyCallbacks; dest.startingCallbacks = source.startingCallbacks; dest.stopCallbacks = source.stopCallbacks; + dest.server = source.server; } } diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 16a28a98e6..2da49917b5 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -504,8 +504,6 @@ default Object execute(@NonNull Context context) { @NonNull BufferedOutputFactory getOutputFactory(); - @NonNull Router setOutputFactory(@NonNull BufferedOutputFactory outputFactory); - /** * Attach a filter to the route pipeline. * diff --git a/jooby/src/main/java/io/jooby/Server.java b/jooby/src/main/java/io/jooby/Server.java index 5accca37a3..2bb5685cba 100644 --- a/jooby/src/main/java/io/jooby/Server.java +++ b/jooby/src/main/java/io/jooby/Server.java @@ -28,6 +28,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import io.jooby.buffer.BufferedOutputFactory; import io.jooby.exception.StartupException; import io.jooby.internal.MutedServer; @@ -158,6 +159,8 @@ public Server setOptions(@NonNull ServerOptions options) { protected abstract ServerOptions defaultOptions(); } + @NonNull BufferedOutputFactory getOutputFactory(); + /** * Set server options. This method should be called once, calling this method multiple times will * print a warning. diff --git a/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java b/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java index d74389fe1c..0f7d5b64d7 100644 --- a/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java +++ b/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java @@ -8,7 +8,6 @@ import java.nio.ByteBuffer; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.internal.buffer.ByteArrayWrappedOutput; import io.jooby.internal.buffer.ByteBufferOutput; import io.jooby.internal.buffer.ByteBufferWrappedOutput; import io.jooby.internal.buffer.CompsiteByteBufferOutput; @@ -54,7 +53,7 @@ public BufferedOutput wrap(@NonNull ByteBuffer buffer) { @Override public BufferedOutput wrap(@NonNull byte[] bytes) { - return new ByteArrayWrappedOutput(bytes); + return new ByteBufferWrappedOutput(ByteBuffer.wrap(bytes)); } @Override diff --git a/jooby/src/main/java/io/jooby/internal/MutedServer.java b/jooby/src/main/java/io/jooby/internal/MutedServer.java index af5f64be35..8c477e8aa3 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.buffer.BufferedOutputFactory; 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 BufferedOutputFactory getOutputFactory() { + return delegate.getOutputFactory(); + } + /** * Muted a server when need it. * diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 1015a4b0ce..6432d11ab6 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -170,7 +170,7 @@ public Stack executor(Executor executor) { private ValueFactory valueFactory = new ValueFactory(); - private BufferedOutputFactory outputFactory = BufferedOutputFactory.create(); + private BufferedOutputFactory outputFactory; public RouterImpl() { stack.addLast(new Stack(chi, null)); @@ -436,10 +436,8 @@ public BufferedOutputFactory getOutputFactory() { return outputFactory; } - @NonNull @Override - public Router setOutputFactory(@NonNull BufferedOutputFactory outputFactory) { + public void setOutputFactory(@NonNull BufferedOutputFactory outputFactory) { this.outputFactory = outputFactory; - return this; } @NonNull @Override diff --git a/jooby/src/main/java/io/jooby/internal/buffer/ByteArrayWrappedOutput.java b/jooby/src/main/java/io/jooby/internal/buffer/ByteArrayWrappedOutput.java deleted file mode 100644 index 0f9d9dfbd1..0000000000 --- a/jooby/src/main/java/io/jooby/internal/buffer/ByteArrayWrappedOutput.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.buffer; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; - -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Context; -import io.jooby.SneakyThrows; -import io.jooby.buffer.BufferedOutput; - -public class ByteArrayWrappedOutput implements BufferedOutput { - - private final byte[] buffer; - - public ByteArrayWrappedOutput(byte[] source) { - this.buffer = source; - } - - @Override - public BufferedOutput write(byte b) { - throw new UnsupportedOperationException(); - } - - @Override - public BufferedOutput write(byte[] source) { - throw new UnsupportedOperationException(); - } - - @Override - public BufferedOutput write(byte[] source, int offset, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public BufferedOutput clear() { - return this; - } - - @Override - public int size() { - return buffer.length; - } - - @Override - public void transferTo(@NonNull SneakyThrows.Consumer consumer) { - consumer.accept(ByteBuffer.wrap(buffer)); - } - - @Override - public ByteBuffer asByteBuffer() { - return ByteBuffer.wrap(buffer); - } - - @Override - public String asString(@NonNull Charset charset) { - return charset.decode(asByteBuffer()).toString(); - } - - @Override - public String toString() { - return "size=" + size(); - } - - @Override - public void send(Context ctx) { - ctx.send(buffer); - } -} diff --git a/jooby/src/test/java/io/jooby/Issue3653.java b/jooby/src/test/java/io/jooby/Issue3653.java index b7054a0bb2..cfc7999b51 100644 --- a/jooby/src/test/java/io/jooby/Issue3653.java +++ b/jooby/src/test/java/io/jooby/Issue3653.java @@ -13,12 +13,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.jooby.buffer.BufferedOutputFactory; + public class Issue3653 { private static final ServerOptions defaultOptions = new ServerOptions(); private static class TestServer extends Server.Base { + @NotNull @Override + public BufferedOutputFactory getOutputFactory() { + return null; + } + @Override protected ServerOptions defaultOptions() { return defaultOptions; 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-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutputFactory.java similarity index 81% rename from modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.java rename to modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutputFactory.java index 414e4a9905..7d3a8e0827 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutputFactory.java @@ -16,7 +16,7 @@ import io.netty.buffer.Unpooled; import io.netty.util.ResourceLeakDetector; -public class NettyOutputFactory implements BufferedOutputFactory { +public class NettyBufferedOutputFactory implements BufferedOutputFactory { private static final String LEAK_DETECTION = "io.netty.leakDetection.level"; static { @@ -30,7 +30,7 @@ public class NettyOutputFactory implements BufferedOutputFactory { private final ByteBufAllocator allocator; private BufferOptions options; - public NettyOutputFactory(ByteBufAllocator allocator, BufferOptions options) { + public NettyBufferedOutputFactory(ByteBufAllocator allocator, BufferOptions options) { this.allocator = allocator; this.options = options; } @@ -58,22 +58,22 @@ public BufferedOutputFactory setOptions(BufferOptions options) { @Override @NonNull public BufferedOutput wrap(@NonNull ByteBuffer buffer) { - return new NettyWrappedOutput(Unpooled.wrappedBuffer(buffer)); + return new NettyByteBufferWrappedOutput(buffer); } @Override public BufferedOutput wrap(@NonNull String value, @NonNull Charset charset) { - return new NettyWrappedOutput(Unpooled.wrappedBuffer(value.getBytes(charset))); + return new NettyBufferedOutput(Unpooled.wrappedBuffer(value.getBytes(charset))); } @Override @NonNull public BufferedOutput wrap(@NonNull byte[] bytes) { - return new NettyWrappedOutput(Unpooled.wrappedBuffer(bytes)); + return new NettyByteArrayWrappedOutput(bytes, 0, bytes.length); } @Override @NonNull public BufferedOutput wrap(@NonNull byte[] bytes, int offset, int length) { - return new NettyWrappedOutput(Unpooled.wrappedBuffer(bytes, offset, length)); + return new NettyByteArrayWrappedOutput(bytes, 0, bytes.length); } @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/NettyBufferedWrappedOutput.java similarity index 87% rename from modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWrappedOutput.java rename to modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedWrappedOutput.java index c8de316c5e..c0841f4771 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWrappedOutput.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedWrappedOutput.java @@ -12,16 +12,16 @@ import io.jooby.buffer.BufferedOutput; import io.netty.buffer.ByteBuf; -public class NettyWrappedOutput implements NettyByteBufOutput { +public class NettyBufferedWrappedOutput implements NettyByteBufOutput { private final ByteBuf buffer; - protected NettyWrappedOutput(ByteBuf buffer) { + protected NettyBufferedWrappedOutput(ByteBuf buffer) { this.buffer = buffer; } @NonNull public ByteBuf byteBuf() { - return this.buffer; + return this.buffer.duplicate(); } @Override From 1761a34289647daae224286de2433cc76856f967 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 14 Jul 2025 07:35:34 -0300 Subject: [PATCH 49/60] netty: improve performance by caching the content-length header on bytes wrapped responses --- .../main/java/io/jooby/jetty/JettyServer.java | 10 +++ .../src/test/kotlin/io/jooby/kt/Idioms.kt | 4 +- .../jooby/internal/netty/HeadersMultiMap.java | 4 +- .../netty/NettyByteArrayWrappedOutput.java | 74 +++++++++++++++++++ .../internal/netty/NettyByteBufOutput.java | 3 +- .../netty/NettyByteBufferWrappedOutput.java | 71 ++++++++++++++++++ .../io/jooby/internal/netty/NettyContext.java | 8 +- .../internal/netty/NettyResponseEncoder.java | 5 -- .../main/java/io/jooby/netty/NettyServer.java | 14 +++- .../java/io/jooby/test/JoobyExtension.java | 14 ++-- .../io/jooby/undertow/UndertowServer.java | 11 +++ .../java/io/jooby/junit/ServerProvider.java | 20 +++-- .../java/io/jooby/junit/ServerTestRunner.java | 25 ++++--- 13 files changed, 222 insertions(+), 41 deletions(-) create mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteArrayWrappedOutput.java create mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufferWrappedOutput.java 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 9ddcd223a0..fe36a50ad9 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 @@ -31,6 +31,7 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.*; +import io.jooby.buffer.BufferedOutputFactory; import io.jooby.internal.jetty.JettyHandler; import io.jooby.internal.jetty.JettyHttpExpectAndContinueHandler; import io.jooby.internal.jetty.PrefixHandler; @@ -45,6 +46,7 @@ public class JettyServer extends io.jooby.Server.Base { private static final int THREADS = 200; + private BufferedOutputFactory outputFactory; private Server server; @@ -69,6 +71,14 @@ public JettyServer(@NonNull ServerOptions options) { public JettyServer() {} + @NonNull @Override + public BufferedOutputFactory getOutputFactory() { + if (outputFactory == null) { + this.outputFactory = BufferedOutputFactory.create(getOptions().getBuffer()); + } + return outputFactory; + } + @NonNull @Override public JettyServer setOptions(@NonNull ServerOptions options) { super.setOptions(options.setWorkerThreads(options.getWorkerThreads(THREADS))); 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 cf47cc70fd..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 @@ -13,8 +13,8 @@ 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({ 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..180dd025d0 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 @@ -505,8 +505,8 @@ 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 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/NettyByteArrayWrappedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteArrayWrappedOutput.java new file mode 100644 index 0000000000..5ad02dd355 --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteArrayWrappedOutput.java @@ -0,0 +1,74 @@ +/* + * 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.Context; +import io.jooby.buffer.BufferedOutput; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.AsciiString; + +public class NettyByteArrayWrappedOutput implements NettyByteBufOutput { + + private final byte[] buffer; + private final int offset; + private final int length; + private final AsciiString contentLength; + + protected NettyByteArrayWrappedOutput(byte[] buffer, int offset, int length) { + this.buffer = buffer; + this.offset = offset; + this.length = length; + this.contentLength = AsciiString.of(Integer.toString(length - offset)); + } + + @NonNull public ByteBuf byteBuf() { + return Unpooled.wrappedBuffer(buffer, offset, length); + } + + @Override + @NonNull public BufferedOutput write(byte b) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public BufferedOutput write(byte[] source) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public BufferedOutput write(byte[] source, int offset, int length) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public BufferedOutput write(@NonNull String source, @NonNull Charset charset) { + throw new UnsupportedOperationException(); + } + + @Override + public void send(Context ctx) { + if (ctx instanceof NettyContext netty) { + netty.send(Unpooled.wrappedBuffer(buffer, offset, length), contentLength); + } else { + ctx.send(asByteBuffer()); + } + } + + @Override + public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public BufferedOutput clear() { + return this; + } +} 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 index b3f04681bb..fe310e6280 100644 --- 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 @@ -41,7 +41,8 @@ default int size() { @Override default void send(Context ctx) { if (ctx instanceof NettyContext netty) { - netty.send(byteBuf()); + var buf = byteBuf(); + netty.send(buf, Integer.toString(buf.readableBytes())); } else { ctx.send(asByteBuffer()); } diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufferWrappedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufferWrappedOutput.java new file mode 100644 index 0000000000..2a79c03469 --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufferWrappedOutput.java @@ -0,0 +1,71 @@ +/* + * 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.CharBuffer; +import java.nio.charset.Charset; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.buffer.BufferedOutput; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.AsciiString; + +public class NettyByteBufferWrappedOutput implements NettyByteBufOutput { + + private final ByteBuffer buffer; + private final AsciiString contentLength; + + protected NettyByteBufferWrappedOutput(ByteBuffer buffer) { + this.buffer = buffer; + this.contentLength = AsciiString.of(Integer.toString(buffer.remaining())); + } + + @NonNull public ByteBuf byteBuf() { + return Unpooled.wrappedBuffer(buffer); + } + + @Override + @NonNull public BufferedOutput write(byte b) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public BufferedOutput write(byte[] source) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public BufferedOutput write(byte[] source, int offset, int length) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public BufferedOutput write(@NonNull String source, @NonNull Charset charset) { + throw new UnsupportedOperationException(); + } + + @Override + public void send(Context ctx) { + if (ctx instanceof NettyContext netty) { + netty.send(Unpooled.wrappedBuffer(buffer), contentLength); + } else { + ctx.send(asByteBuffer()); + } + } + + @Override + public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { + throw new UnsupportedOperationException(); + } + + @Override + @NonNull public BufferedOutput clear() { + return this; + } +} 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 8829936503..e87ba55064 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 @@ -580,10 +580,14 @@ public final Context send(ByteBuffer data) { 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())); + setHeaders.set(CONTENT_LENGTH, contentLength); var response = new DefaultFullHttpResponse(HTTP_1_1, status, data, setHeaders, NO_TRAILING); if (ctx.channel().eventLoop().inEventLoop()) { needsFlush = true; 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 1b97bfe9fb..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 @@ -18,9 +18,4 @@ protected void encodeHeaders(HttpHeaders headers, ByteBuf buf) { super.encodeHeaders(headers, buf); } } - - @Override - public boolean acceptOutboundMessage(Object msg) throws Exception { - return super.acceptOutboundMessage(msg); - } } 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 2c4d35d786..65539f56d5 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 @@ -22,6 +22,7 @@ import io.jooby.ServerOptions; import io.jooby.SneakyThrows; import io.jooby.SslOptions; +import io.jooby.buffer.BufferedOutputFactory; import io.jooby.internal.netty.*; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBufAllocator; @@ -53,6 +54,7 @@ public class NettyServer extends Server.Base { private ExecutorService worker; private List applications; + private NettyBufferedOutputFactory outputFactory; /** * Creates a server. @@ -80,6 +82,15 @@ public NettyServer(@NonNull ServerOptions options) { public NettyServer() {} + @NonNull @Override + public BufferedOutputFactory getOutputFactory() { + if (outputFactory == null) { + outputFactory = + new NettyBufferedOutputFactory(ByteBufAllocator.DEFAULT, getOptions().getBuffer()); + } + return outputFactory; + } + @Override protected ServerOptions defaultOptions() { return new ServerOptions().setServer(getName()); @@ -101,9 +112,7 @@ public Server start(@NonNull Jooby... application) { worker = newFixedThreadPool(options.getWorkerThreads(), new DefaultThreadFactory("worker")); } // Make sure context use same buffer factory - var outputFactory = new NettyOutputFactory(ByteBufAllocator.DEFAULT, options.getBuffer()); for (var app : applications) { - app.setOutputFactory(outputFactory); app.getServices().put(ServerOptions.class, options); app.getServices().put(Server.class, this); } @@ -124,6 +133,7 @@ public Server start(@NonNull Jooby... application) { this.dateLoop = Executors.newSingleThreadScheduledExecutor(); var dateService = new NettyDateService(dateLoop); + var outputFactory = (NettyBufferedOutputFactory) getOutputFactory(); var allocator = outputFactory.getAllocator(); var http2 = options.isHttp2() == Boolean.TRUE; /* Bootstrap: */ 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 f0e9680c44..715fe5274f 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 @@ -69,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 { @@ -83,14 +87,6 @@ private Jooby startApp(ExtensionContext context, JoobyTest metadata) throws Exce } else { app = fromFactoryMethod(context, metadata, factoryMethod); } - var server = Server.loadServer(); - var serverOptions = server.getOptions(); - // 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); 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 ab3689d651..62b608f344 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 @@ -21,6 +21,7 @@ import io.jooby.ServerOptions; import io.jooby.SneakyThrows; import io.jooby.SslOptions; +import io.jooby.buffer.BufferedOutputFactory; import io.jooby.exception.StartupException; import io.jooby.internal.undertow.UndertowHandler; import io.jooby.internal.undertow.UndertowWebSocket; @@ -53,6 +54,8 @@ public class UndertowServer extends Server.Base { private XnioWorker worker; + private BufferedOutputFactory outputFactory; + public UndertowServer(@NonNull ServerOptions options) { setOptions(options); } @@ -66,6 +69,14 @@ public UndertowServer setOptions(@NonNull ServerOptions options) { return this; } + @NonNull @Override + public BufferedOutputFactory getOutputFactory() { + if (outputFactory == null) { + outputFactory = BufferedOutputFactory.create(getOptions().getBuffer()); + } + return outputFactory; + } + @Override protected ServerOptions defaultOptions() { return new ServerOptions().setIoThreads(ServerOptions.IO_THREADS).setServer(getName()); 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 110750db88..70aa2c679e 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -18,10 +18,7 @@ import org.junit.jupiter.api.condition.DisabledOnOs; import io.jooby.*; -import io.jooby.buffer.BufferOptions; -import io.jooby.buffer.BufferedOutputFactory; import io.jooby.internal.MutedServer; -import io.jooby.netty.NettyServer; import io.jooby.test.WebClient; public class ServerTestRunner { @@ -89,15 +86,13 @@ public void ready(SneakyThrows.Consumer2 onReady) { if (disabled()) { return; } - Server server = this.server.get(); + Server server = this.server.get(serverOptions); String applogger = null; try { + setBootServer(server); System.setProperty("___app_name__", testName); System.setProperty("___server_name__", server.getName()); var app = provider.get(); - if (!(server instanceof NettyServer)) { - app.setOutputFactory(BufferedOutputFactory.create(BufferOptions.defaults())); - } Optional.ofNullable(executionMode).ifPresent(app::setExecutionMode); // Reduce log from maven build: var mavenBuild = System.getProperty("surefire.real.class.path", "").length() > 0; @@ -109,9 +104,6 @@ public void ready(SneakyThrows.Consumer2 onReady) { .orElseGet(ServerTestRunner::buildErrorHandler)); } - 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)); @@ -141,12 +133,25 @@ 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(); } + setBootServer(null); + } + } + + private static void setBootServer(Server server) { + try { + var bootServer = Jooby.class.getDeclaredField("BOOT_SERVER"); + bootServer.setAccessible(true); + bootServer.set(null, server); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw SneakyThrows.propagate(e); } } From 15ad691cc3928ddff5894e81717b626ae044f27f Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Fri, 18 Jul 2025 11:03:07 -0300 Subject: [PATCH 50/60] server: remove server from application - we just need the buffered factory --- jooby/src/main/java/io/jooby/Jooby.java | 23 +++++++------------ .../java/io/jooby/test/JoobyExtension.java | 3 +-- .../java/io/jooby/junit/ServerTestRunner.java | 13 ++++++----- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 7d0dd871f9..0df43de1c5 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -87,7 +87,7 @@ public class Jooby implements Router, Registry { private static Jooby owner; private static ExecutionMode BOOT_EXECUTION_MODE = ExecutionMode.DEFAULT; - private static Server BOOT_SERVER; + private static BufferedOutputFactory BUFFER_FACTORY; private RouterImpl router; @@ -121,8 +121,6 @@ public class Jooby implements Router, Registry { private String version; - private Server server; - /** Creates a new Jooby instance. */ public Jooby() { if (owner == null) { @@ -133,14 +131,10 @@ public Jooby() { startingCallbacks = new ArrayList<>(); readyCallbacks = new ArrayList<>(); lateExtensions = new ArrayList<>(); - server = BOOT_SERVER; - if (server != null) { - router.setOutputFactory(server.getOutputFactory()); - } else { - // NOTE: fallback to default, this is required for direct instance creation of class - // app bootstrap always ensures server instance. - router.setOutputFactory(BufferedOutputFactory.create()); - } + // NOTE: fallback to default, this is required for direct instance creation of class + // app bootstrap always ensures server instance. + router.setOutputFactory( + Optional.ofNullable(BUFFER_FACTORY).orElseGet(BufferedOutputFactory::create)); } else { copyState(owner, this); } @@ -1311,12 +1305,12 @@ public static Jooby createApp( Jooby app; try { - Jooby.BOOT_SERVER = server; + Jooby.BUFFER_FACTORY = server.getOutputFactory(); Jooby.BOOT_EXECUTION_MODE = executionMode; app = provider.get(); } finally { Jooby.BOOT_EXECUTION_MODE = executionMode; - Jooby.BOOT_SERVER = null; + Jooby.BUFFER_FACTORY = null; } return app; @@ -1452,7 +1446,7 @@ private void joobyRunHook(ClassLoader loader, Server server) { } /** - * 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. @@ -1471,6 +1465,5 @@ private static void copyState(Jooby source, Jooby dest) { dest.readyCallbacks = source.readyCallbacks; dest.startingCallbacks = source.startingCallbacks; dest.stopCallbacks = source.stopCallbacks; - dest.server = source.server; } } 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 715fe5274f..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 @@ -232,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/tests/src/test/java/io/jooby/junit/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index 70aa2c679e..f6e60d00ee 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.condition.DisabledOnOs; import io.jooby.*; +import io.jooby.buffer.BufferedOutputFactory; import io.jooby.internal.MutedServer; import io.jooby.test.WebClient; @@ -89,7 +90,7 @@ public void ready(SneakyThrows.Consumer2 onReady) { Server server = this.server.get(serverOptions); String applogger = null; try { - setBootServer(server); + setBufferFactory(server.getOutputFactory()); System.setProperty("___app_name__", testName); System.setProperty("___server_name__", server.getName()); var app = provider.get(); @@ -141,15 +142,15 @@ public void ready(SneakyThrows.Consumer2 onReady) { } else { MutedServer.mute(server).stop(); } - setBootServer(null); + setBufferFactory(null); } } - private static void setBootServer(Server server) { + private static void setBufferFactory(BufferedOutputFactory bufferedOutputFactory) { try { - var bootServer = Jooby.class.getDeclaredField("BOOT_SERVER"); - bootServer.setAccessible(true); - bootServer.set(null, server); + var field = Jooby.class.getDeclaredField("BUFFER_FACTORY"); + field.setAccessible(true); + field.set(null, bufferedOutputFactory); } catch (NoSuchFieldException | IllegalAccessException e) { throw SneakyThrows.propagate(e); } From 667155882057d18ae86a84e271b8b980d7899527 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 19 Jul 2025 13:13:06 -0300 Subject: [PATCH 51/60] buffer: bug fixing --- jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java | 3 +-- .../io/jooby/internal/netty/NettyBufferedOutputFactory.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java index db3ed366e2..7de1e6fab0 100644 --- a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java +++ b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java @@ -92,8 +92,7 @@ public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) throws return outputFactory.wrap(bytes); } if (value instanceof ByteBuffer buffer) { - ctx.send(buffer); - return null; + return outputFactory.wrap(buffer); } if (encoders != null) { // Content negotiation, find best: diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutputFactory.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutputFactory.java index 7d3a8e0827..b6f66000ea 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutputFactory.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutputFactory.java @@ -73,7 +73,7 @@ public BufferedOutput wrap(@NonNull String value, @NonNull Charset charset) { @Override @NonNull public BufferedOutput wrap(@NonNull byte[] bytes, int offset, int length) { - return new NettyByteArrayWrappedOutput(bytes, 0, bytes.length); + return new NettyByteArrayWrappedOutput(bytes, offset, length); } @Override From 97ad1efdc1dc50d95fa9b1ca1901b2fae3504217 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 20 Jul 2025 10:56:01 -0300 Subject: [PATCH 52/60] performance: attempt to improve performance - reset ioThreads to server defaults - bug fixing on netty buffered output - reduce call to netty headers set(String, String) --- .../src/main/java/io/jooby/ServerOptions.java | 6 ++- .../src/main/java/io/jooby/internal/Chi.java | 15 ------ .../test/java/io/jooby/internal/ChiTest.java | 3 ++ .../main/java/io/jooby/jetty/JettyServer.java | 8 ++++ .../jooby/internal/netty/HeadersMultiMap.java | 15 ------ .../io/jooby/internal/netty/NettyContext.java | 9 ++-- .../internal/netty/NettyHeadersFactory.java | 23 +++++++++ .../main/java/io/jooby/netty/NettyServer.java | 5 +- .../io/jooby/undertow/UndertowServer.java | 5 ++ tests/src/test/java/examples/Performance.java | 46 ++++++++++++++++++ .../jooby/internal/ChiStaticPathsBench.java | 48 +++++++++++++++++++ 11 files changed, 144 insertions(+), 39 deletions(-) create mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHeadersFactory.java create mode 100644 tests/src/test/java/examples/Performance.java create mode 100644 tests/src/test/java/io/jooby/internal/ChiStaticPathsBench.java diff --git a/jooby/src/main/java/io/jooby/ServerOptions.java b/jooby/src/main/java/io/jooby/ServerOptions.java index dd98e8dec6..6c594a8837 100644 --- a/jooby/src/main/java/io/jooby/ServerOptions.java +++ b/jooby/src/main/java/io/jooby/ServerOptions.java @@ -72,7 +72,11 @@ public class ServerOptions { private static final String LOCAL_HOST = "0.0.0.0"; /** 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( + "jooby.server.ioThreads", + Integer.toString(Runtime.getRuntime().availableProcessors()))); /** * Number of worker (a.k.a application) threads. It is the number of processors multiply by diff --git a/jooby/src/main/java/io/jooby/internal/Chi.java b/jooby/src/main/java/io/jooby/internal/Chi.java index 14af60b3aa..017eff0a5e 100644 --- a/jooby/src/main/java/io/jooby/internal/Chi.java +++ b/jooby/src/main/java/io/jooby/internal/Chi.java @@ -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; @@ -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 { diff --git a/jooby/src/test/java/io/jooby/internal/ChiTest.java b/jooby/src/test/java/io/jooby/internal/ChiTest.java index b1b598bd8d..cd3ae45f34 100644 --- a/jooby/src/test/java/io/jooby/internal/ChiTest.java +++ b/jooby/src/test/java/io/jooby/internal/ChiTest.java @@ -42,6 +42,9 @@ public void staticMap6() { 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 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 bd733f8cbd..2e872cb9fb 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; @@ -47,6 +48,13 @@ public class JettyServer extends io.jooby.Server.Base { private static final int THREADS = 200; + + static { + int cpus = ProcessorUtils.availableProcessors(); + var ioThreads = Math.max(1, Math.min(cpus / 2, THREADS / 16)); + System.setProperty("jooby.server.ioThreads", ioThreads + ""); + } + private BufferedOutputFactory outputFactory; private Server server; 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 180dd025d0..7e5c4d8735 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(); 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 e87ba55064..e4f0594fab 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,6 +5,7 @@ */ package io.jooby.internal.netty; +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; @@ -31,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; @@ -75,12 +75,11 @@ import io.netty.util.IllegalReferenceCountException; public class NettyContext implements DefaultContext, ChannelFutureListener { - public static HttpHeadersFactory HEADERS = HeadersMultiMap.httpHeadersFactory(); private static final HttpHeaders NO_TRAILING = EmptyHttpHeaders.INSTANCE; private static final String STREAM_ID = "x-http2-stream-id"; private String streamId; - HttpHeaders setHeaders = HEADERS.newHeaders(); + HeadersMultiMap setHeaders = HEADERS.newHeaders(); private int bufferSize; InterfaceHttpPostRequestDecoder decoder; DefaultHttpDataFactory httpDataFactory; @@ -286,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); } @@ -420,13 +419,11 @@ public Context upgrade(@NonNull ServerSentEmitter.Handler handler) { responseStarted = true; 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; } 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/netty/NettyServer.java b/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java index a958816d57..f5527811a7 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; @@ -199,8 +200,8 @@ private NettyPipeline newPipeline( .setMaxInitialLineLength(_4KB) .setMaxHeaderSize(options.getMaxHeaderSize()) .setMaxChunkSize(options.getBuffer().getSize()) - .setHeadersFactory(NettyContext.HEADERS) - .setTrailersFactory(NettyContext.HEADERS); + .setHeadersFactory(HEADERS) + .setTrailersFactory(HEADERS); return new NettyPipeline( sslContext, dateService, 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 32ab58004a..685f0d316f 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 @@ -42,6 +42,11 @@ */ public class UndertowServer extends Server.Base { + static { + System.setProperty( + "jooby.server.ioThreads", (Runtime.getRuntime().availableProcessors() * 2) + ""); + } + private static final int BACKLOG = 8192; private static final int _100 = 100; diff --git a/tests/src/test/java/examples/Performance.java b/tests/src/test/java/examples/Performance.java new file mode 100644 index 0000000000..3f26c3dcfb --- /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 java.nio.charset.StandardCharsets; + +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!"; + + private static final byte[] MESSAGE_BYTES = MESSAGE.getBytes(StandardCharsets.UTF_8); + + { + var message = getOutputFactory().wrap(MESSAGE_BYTES); + 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("/fortuxnes", 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 NettyServer(), EVENT_LOOP, Performance::new); + } +} diff --git a/tests/src/test/java/io/jooby/internal/ChiStaticPathsBench.java b/tests/src/test/java/io/jooby/internal/ChiStaticPathsBench.java new file mode 100644 index 0000000000..05dacab34b --- /dev/null +++ b/tests/src/test/java/io/jooby/internal/ChiStaticPathsBench.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.internal; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; + +import io.jooby.MessageEncoder; +import io.jooby.Route; + +@Fork(5) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 10, time = 1) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Benchmark) +public class ChiStaticPathsBench { + + private Chi router; + + @Setup + public void setup() { + this.router = new Chi(); + router.insert(route("GET", "/plaintext", stringHandler("plaintext"))); + router.insert(route("GET", "/json", stringHandler("json"))); + router.insert(route("GET", "/fortune", stringHandler("fortune"))); + router.insert(route("GET", "/db", stringHandler("db"))); + router.insert(route("GET", "/updates", stringHandler("updates"))); + router.insert(route("GET", "/queries", stringHandler("queries"))); + } + + @Benchmark + public void plaintext() { + router.find("GET", "/plaintext").matches(); + } + + private Route.Handler stringHandler(String foo) { + return ctx -> foo; + } + + private Route route(String method, String pattern, Route.Handler handler) { + return new Route(method, pattern, handler).setEncoder(MessageEncoder.TO_STRING); + } +} From 2f3965159cad4a4519e5850f8ca5821f18fd40a3 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 20 Jul 2025 11:42:03 -0300 Subject: [PATCH 53/60] output: remove buffer name from output classes --- jooby/src/main/java/io/jooby/Context.java | 8 +- .../main/java/io/jooby/DefaultContext.java | 4 +- .../main/java/io/jooby/ForwardingContext.java | 8 +- jooby/src/main/java/io/jooby/Jooby.java | 29 +++---- .../main/java/io/jooby/MessageEncoder.java | 4 +- jooby/src/main/java/io/jooby/Router.java | 4 +- jooby/src/main/java/io/jooby/Sender.java | 4 +- jooby/src/main/java/io/jooby/Server.java | 4 +- .../src/main/java/io/jooby/ServerOptions.java | 8 +- .../main/java/io/jooby/ServerSentMessage.java | 6 +- .../main/java/io/jooby/TemplateEngine.java | 6 +- jooby/src/main/java/io/jooby/WebSocket.java | 10 +-- .../ForwardingBufferedOutputFactory.java | 66 --------------- .../java/io/jooby/internal/HeadContext.java | 6 +- .../io/jooby/internal/HttpMessageEncoder.java | 4 +- .../java/io/jooby/internal/MutedServer.java | 4 +- .../java/io/jooby/internal/RouterImpl.java | 8 +- .../io/jooby/internal/WebSocketSender.java | 4 +- .../internal/handler/ChunkedSubscriber.java | 4 +- .../ByteBufferWrappedOutput.java | 14 ++-- .../CompsiteByteBufferOutput.java | 14 ++-- .../OutputOutputStream.java | 12 +-- .../{buffer => output}/OutputWriter.java | 8 +- .../{buffer => output}/package-info.java | 2 +- .../buffer => output}/ByteBufferOutput.java | 15 ++-- .../ByteBufferOutputFactory.java | 27 +++--- .../jooby/output/ForwardingOutputFactory.java | 66 +++++++++++++++ .../Output.java} | 28 +++---- .../OutputFactory.java} | 55 ++++++------ .../OutputOptions.java} | 18 ++-- .../{buffer => output}/package-info.java | 2 +- jooby/src/main/java/module-info.java | 4 +- jooby/src/test/java/io/jooby/Issue3607.java | 10 +-- jooby/src/test/java/io/jooby/Issue3653.java | 4 +- .../java/io/jooby/ServerSentMessageTest.java | 10 +-- .../test/java/io/jooby/buffer/Issue3434.java | 6 +- ...ufferedOutputTest.java => OutputTest.java} | 11 +-- .../jooby/avaje/jsonb/AvajeJsonbModule.java | 6 +- .../avaje/jsonb/BufferedJsonOutput.java | 6 +- .../avaje/jsonb/AvajeJsonbEncoderBench.java | 16 ++-- .../freemarker/FreemarkerTemplateEngine.java | 6 +- .../main/java/io/jooby/gson/GsonModule.java | 6 +- .../test/java/io/jooby/gson/Issue3434.java | 6 +- .../handlebars/HandlebarsTemplateEngine.java | 6 +- .../java/io/jooby/jackson/JacksonModule.java | 4 +- .../java/io/jooby/jackson/JacksonBench.java | 13 ++- .../jooby/jackson/JacksonJsonModuleTest.java | 8 +- .../jooby/internal/jetty/JettyCallbacks.java | 6 +- .../io/jooby/internal/jetty/JettyContext.java | 4 +- .../io/jooby/internal/jetty/JettySender.java | 4 +- .../jooby/internal/jetty/JettyWebSocket.java | 6 +- .../jetty/WebSocketOutputCallback.java | 4 +- .../main/java/io/jooby/jetty/JettyServer.java | 8 +- .../jstachio/JStachioMessageEncoder.java | 8 +- .../internal/jte/BufferedTemplateOutput.java | 6 +- .../jooby/internal/jte/JteModelEncoder.java | 6 +- .../java/io/jooby/jte/JteTemplateEngine.java | 6 +- .../jte/BufferedTemplateOutputTest.java | 16 ++-- .../src/test/java/io/jooby/jte/Issue3599.java | 16 ++-- .../src/test/java/io/jooby/jte/Issue3602.java | 10 +-- .../netty/NettyBufferedOutputFactory.java | 83 ------------------ .../netty/NettyBufferedWrappedOutput.java | 56 ------------- .../netty/NettyByteArrayWrappedOutput.java | 74 ---------------- .../io/jooby/internal/netty/NettyContext.java | 4 +- ...BufOutput.java => NettyOutputByteBuf.java} | 8 +- ...redOutput.java => NettyOutputDefault.java} | 18 ++-- .../internal/netty/NettyOutputFactory.java | 84 +++++++++++++++++++ ...ppedOutput.java => NettyOutputStatic.java} | 32 ++++--- .../io/jooby/internal/netty/NettySender.java | 6 +- .../netty/NettyServerSentEmitter.java | 2 +- .../jooby/internal/netty/NettyWebSocket.java | 8 +- .../main/java/io/jooby/netty/NettyServer.java | 11 ++- .../io/jooby/pebble/PebbleTemplateEngine.java | 6 +- .../io/jooby/rocker/BufferedRockerOutput.java | 13 ++- .../io/jooby/rocker/RockerMessageEncoder.java | 4 +- .../main/java/io/jooby/test/MockContext.java | 14 ++-- .../java/io/jooby/test/MockWebSocket.java | 6 +- .../thymeleaf/ThymeleafTemplateEngine.java | 6 +- .../internal/undertow/UndertowContext.java | 4 +- .../undertow/UndertowOutputCallback.java | 4 +- .../internal/undertow/UndertowSender.java | 4 +- .../UndertowServerSentConnection.java | 12 +-- .../internal/undertow/UndertowWebSocket.java | 10 +-- .../io/jooby/undertow/UndertowServer.java | 8 +- .../java/io/jooby/yasson/YassonModule.java | 6 +- .../io/jooby/yasson/YassonModuleTest.java | 6 +- .../test/java/io/jooby/i2613/Issue2613.java | 4 +- .../test/java/io/jooby/i2806/Issue2806.java | 4 +- .../jooby/internal/ChiStaticPathsBench.java | 48 ----------- .../java/io/jooby/junit/ServerTestRunner.java | 24 ++++-- .../test/java/io/jooby/test/FeaturedTest.java | 4 +- 91 files changed, 533 insertions(+), 714 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/buffer/ForwardingBufferedOutputFactory.java rename jooby/src/main/java/io/jooby/internal/{buffer => output}/ByteBufferWrappedOutput.java (78%) rename jooby/src/main/java/io/jooby/internal/{buffer => output}/CompsiteByteBufferOutput.java (83%) rename jooby/src/main/java/io/jooby/internal/{buffer => output}/OutputOutputStream.java (76%) rename jooby/src/main/java/io/jooby/internal/{buffer => output}/OutputWriter.java (88%) rename jooby/src/main/java/io/jooby/internal/{buffer => output}/package-info.java (75%) rename jooby/src/main/java/io/jooby/{internal/buffer => output}/ByteBufferOutput.java (93%) rename jooby/src/main/java/io/jooby/{buffer => output}/ByteBufferOutputFactory.java (51%) create mode 100644 jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java rename jooby/src/main/java/io/jooby/{buffer/BufferedOutput.java => output/Output.java} (85%) rename jooby/src/main/java/io/jooby/{buffer/BufferedOutputFactory.java => output/OutputFactory.java} (59%) rename jooby/src/main/java/io/jooby/{buffer/BufferOptions.java => output/OutputOptions.java} (81%) rename jooby/src/main/java/io/jooby/{buffer => output}/package-info.java (72%) rename jooby/src/test/java/io/jooby/buffer/{BufferedOutputTest.java => OutputTest.java} (92%) delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutputFactory.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedWrappedOutput.java delete mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteArrayWrappedOutput.java rename modules/jooby-netty/src/main/java/io/jooby/internal/netty/{NettyByteBufOutput.java => NettyOutputByteBuf.java} (85%) rename modules/jooby-netty/src/main/java/io/jooby/internal/netty/{NettyBufferedOutput.java => NettyOutputDefault.java} (61%) create mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.java rename modules/jooby-netty/src/main/java/io/jooby/internal/netty/{NettyByteBufferWrappedOutput.java => NettyOutputStatic.java} (51%) delete mode 100644 tests/src/test/java/io/jooby/internal/ChiStaticPathsBench.java diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java index c3e11ba9ef..10d4e79d56 100644 --- a/jooby/src/main/java/io/jooby/Context.java +++ b/jooby/src/main/java/io/jooby/Context.java @@ -29,11 +29,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.BufferedOutput; -import io.jooby.buffer.BufferedOutputFactory; import io.jooby.internal.LocaleUtils; 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; @@ -143,7 +143,7 @@ private static Selector single() { */ Router getRouter(); - BufferedOutputFactory getOutputFactory(); + OutputFactory getOutputFactory(); /** * Forward executing to another route. We use the given path to find a matching route. @@ -1298,7 +1298,7 @@ Context responseWriter( * @param output Output. * @return This context. */ - Context send(@NonNull BufferedOutput output); + Context send(@NonNull Output output); /** * Send response data. diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index ae8b351283..9668f3a74d 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -26,9 +26,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.BufferedOutputFactory; import io.jooby.exception.RegistryException; import io.jooby.internal.*; +import io.jooby.output.OutputFactory; import io.jooby.value.Value; import io.jooby.value.ValueFactory; @@ -690,7 +690,7 @@ default Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { } @Override - default BufferedOutputFactory getOutputFactory() { + default OutputFactory getOutputFactory() { return getRouter().getOutputFactory(); } } diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index d7d583d528..9453d8eb45 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -23,9 +23,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.BufferedOutput; -import io.jooby.buffer.BufferedOutputFactory; 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; @@ -671,7 +671,7 @@ public Router getRouter() { } @Override - public BufferedOutputFactory getOutputFactory() { + public OutputFactory getOutputFactory() { return ctx.getOutputFactory(); } @@ -1239,7 +1239,7 @@ public Context send(@NonNull ByteBuffer data) { } @Override - public Context send(@NonNull BufferedOutput output) { + public Context send(@NonNull Output output) { ctx.send(output); return this; } diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 0df43de1c5..f60c3b3d85 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -41,13 +41,13 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.BufferedOutputFactory; 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 io.jooby.value.ValueFactory; @@ -80,6 +80,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); @@ -87,11 +88,11 @@ public class Jooby implements Router, Registry { private static Jooby owner; private static ExecutionMode BOOT_EXECUTION_MODE = ExecutionMode.DEFAULT; - private static BufferedOutputFactory BUFFER_FACTORY; + private static OutputFactory OUTPUT_FACTORY; private RouterImpl router; - private ExecutionMode mode = BOOT_EXECUTION_MODE; + private ExecutionMode mode; private Path tmpdir; @@ -125,6 +126,7 @@ public class Jooby implements Router, Registry { public Jooby() { if (owner == null) { ClassLoader classLoader = getClass().getClassLoader(); + mode = BOOT_EXECUTION_MODE; environmentOptions = new EnvironmentOptions().setClassLoader(classLoader); router = new RouterImpl(); stopCallbacks = new LinkedList<>(); @@ -133,8 +135,7 @@ public Jooby() { 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(BUFFER_FACTORY).orElseGet(BufferedOutputFactory::create)); + router.setOutputFactory(Optional.ofNullable(OUTPUT_FACTORY).orElseGet(OutputFactory::create)); } else { copyState(owner, this); } @@ -845,7 +846,7 @@ public Jooby setValueFactory(@NonNull ValueFactory valueFactory) { } @NonNull @Override - public BufferedOutputFactory getOutputFactory() { + public OutputFactory getOutputFactory() { return router.getOutputFactory(); } @@ -903,9 +904,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; @@ -1253,18 +1252,14 @@ 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(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 */ - ServerOptions.from(app.getConfig()) - .ifPresent( - options -> { - options.setServer(server.getName()); - server.setOptions(options); - }); + ServerOptions.from(app.getConfig()).ifPresent(server::setOptions); apps.add(app); } @@ -1305,12 +1300,12 @@ public static Jooby createApp( Jooby app; try { - Jooby.BUFFER_FACTORY = server.getOutputFactory(); + Jooby.OUTPUT_FACTORY = server.getOutputFactory(); Jooby.BOOT_EXECUTION_MODE = executionMode; app = provider.get(); } finally { Jooby.BOOT_EXECUTION_MODE = executionMode; - Jooby.BUFFER_FACTORY = null; + Jooby.OUTPUT_FACTORY = null; } return app; diff --git a/jooby/src/main/java/io/jooby/MessageEncoder.java b/jooby/src/main/java/io/jooby/MessageEncoder.java index bf9ab90110..eef31ecef7 100644 --- a/jooby/src/main/java/io/jooby/MessageEncoder.java +++ b/jooby/src/main/java/io/jooby/MessageEncoder.java @@ -7,8 +7,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.BufferedOutput; import io.jooby.exception.NotAcceptableException; +import io.jooby.output.Output; /** * Render a route output as a byte array. @@ -35,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 BufferedOutput 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/Router.java b/jooby/src/main/java/io/jooby/Router.java index 2da49917b5..35964fb3d8 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -32,10 +32,10 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.BufferedOutputFactory; import io.jooby.exception.MissingValueException; import io.jooby.handler.AssetHandler; import io.jooby.handler.AssetSource; +import io.jooby.output.OutputFactory; import io.jooby.value.ValueFactory; /** @@ -502,7 +502,7 @@ default Object execute(@NonNull Context context) { */ @NonNull Router setDefaultWorker(@NonNull Executor worker); - @NonNull BufferedOutputFactory getOutputFactory(); + @NonNull OutputFactory getOutputFactory(); /** * Attach a filter to the route pipeline. diff --git a/jooby/src/main/java/io/jooby/Sender.java b/jooby/src/main/java/io/jooby/Sender.java index 5c0bbee15c..a5dae5fb97 100644 --- a/jooby/src/main/java/io/jooby/Sender.java +++ b/jooby/src/main/java/io/jooby/Sender.java @@ -10,7 +10,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; /** * Non-blocking sender. Reactive responses use this class to send partial data in a non-blocking @@ -94,7 +94,7 @@ interface Callback { */ Sender write(@NonNull byte[] data, @NonNull Callback callback); - Sender write(@NonNull BufferedOutput output, @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 2bb5685cba..e2b6e3d1d8 100644 --- a/jooby/src/main/java/io/jooby/Server.java +++ b/jooby/src/main/java/io/jooby/Server.java @@ -28,9 +28,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.BufferedOutputFactory; 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 @@ -159,7 +159,7 @@ public Server setOptions(@NonNull ServerOptions options) { protected abstract ServerOptions defaultOptions(); } - @NonNull BufferedOutputFactory getOutputFactory(); + @NonNull OutputFactory getOutputFactory(); /** * Set server options. This method should be called once, calling this method multiple times will diff --git a/jooby/src/main/java/io/jooby/ServerOptions.java b/jooby/src/main/java/io/jooby/ServerOptions.java index 6c594a8837..5cc2ba0608 100644 --- a/jooby/src/main/java/io/jooby/ServerOptions.java +++ b/jooby/src/main/java/io/jooby/ServerOptions.java @@ -25,8 +25,8 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.BufferOptions; import io.jooby.internal.SslContextProvider; +import io.jooby.output.OutputOptions; /** * Available server options. To load server options from configuration files, just do: @@ -102,7 +102,7 @@ public class ServerOptions { /** Name of server: Jetty, Netty or Undertow. */ private String server; - private BufferOptions buffer = BufferOptions.defaults(); + private OutputOptions buffer = OutputOptions.defaults(); /** * Maximum request size in bytes. Request exceeding this value results in {@link @@ -415,11 +415,11 @@ public boolean getDefaultHeaders() { return this; } - public BufferOptions getBuffer() { + public OutputOptions getBuffer() { return buffer; } - public ServerOptions setBuffer(@NonNull BufferOptions buffer) { + public ServerOptions setBuffer(@NonNull OutputOptions buffer) { this.buffer = buffer; return this; } diff --git a/jooby/src/main/java/io/jooby/ServerSentMessage.java b/jooby/src/main/java/io/jooby/ServerSentMessage.java index 6bfacd039d..14ca5119ca 100644 --- a/jooby/src/main/java/io/jooby/ServerSentMessage.java +++ b/jooby/src/main/java/io/jooby/ServerSentMessage.java @@ -14,7 +14,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import io.jooby.buffer.*; +import io.jooby.output.Output; /** * Server-Sent message. @@ -132,12 +132,12 @@ public ServerSentMessage(@NonNull Object data) { * @param ctx Web context. To encode complex objects. * @return Encoded data. */ - public @NonNull BufferedOutput encode(@NonNull Context ctx) { + public @NonNull Output encode(@NonNull Context ctx) { try { var route = ctx.getRoute(); var encoder = route.getEncoder(); var bufferFactory = ctx.getOutputFactory(); - var buffer = bufferFactory.newBufferedOutput(); + var buffer = bufferFactory.newOutput(); if (id != null) { buffer.write(ID); diff --git a/jooby/src/main/java/io/jooby/TemplateEngine.java b/jooby/src/main/java/io/jooby/TemplateEngine.java index fb21089ddc..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.BufferedOutput; +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. */ - BufferedOutput render(Context ctx, ModelAndView modelAndView) throws Exception; + Output render(Context ctx, ModelAndView modelAndView) throws Exception; @Override - default BufferedOutput 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/WebSocket.java b/jooby/src/main/java/io/jooby/WebSocket.java index 32a1e7ec6c..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.BufferedOutput; +import io.jooby.output.Output; /** * Websocket. Usage: @@ -245,11 +245,11 @@ interface WriteCallback { @NonNull WebSocket send(@NonNull ByteBuffer message, @NonNull WriteCallback callback); - default @NonNull WebSocket send(@NonNull BufferedOutput message) { + default @NonNull WebSocket send(@NonNull Output message) { return send(message, WriteCallback.NOOP); } - @NonNull WebSocket send(@NonNull BufferedOutput message, @NonNull WriteCallback callback); + @NonNull WebSocket send(@NonNull Output message, @NonNull WriteCallback callback); /** * Send a binary message to client. @@ -297,11 +297,11 @@ interface WriteCallback { @NonNull WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback callback); - default @NonNull WebSocket sendBinary(@NonNull BufferedOutput message) { + default @NonNull WebSocket sendBinary(@NonNull Output message) { return sendBinary(message, WriteCallback.NOOP); } - @NonNull WebSocket sendBinary(@NonNull BufferedOutput 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/buffer/ForwardingBufferedOutputFactory.java b/jooby/src/main/java/io/jooby/buffer/ForwardingBufferedOutputFactory.java deleted file mode 100644 index f4cfe4355e..0000000000 --- a/jooby/src/main/java/io/jooby/buffer/ForwardingBufferedOutputFactory.java +++ /dev/null @@ -1,66 +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 edu.umd.cs.findbugs.annotations.NonNull; - -/** - * Delegate/forwarding class for output factory. - * - * @author edgar - * @since 4.0.0 - */ -public abstract class ForwardingBufferedOutputFactory implements BufferedOutputFactory { - - protected final BufferedOutputFactory delegate; - - public ForwardingBufferedOutputFactory(@NonNull BufferedOutputFactory delegate) { - this.delegate = delegate; - } - - @Override - public BufferOptions getOptions() { - return delegate.getOptions(); - } - - @Override - public BufferedOutputFactory setOptions(BufferOptions options) { - delegate.setOptions(options); - return this; - } - - @Override - public BufferedOutput newBufferedOutput(int size) { - return delegate.newBufferedOutput(size); - } - - @Override - public BufferedOutput newBufferedOutput(boolean direct, int size) { - return delegate.newBufferedOutput(direct, size); - } - - @Override - public BufferedOutput newCompositeOutput() { - return delegate.newCompositeOutput(); - } - - @Override - public BufferedOutput wrap(@NonNull ByteBuffer buffer) { - return delegate.wrap(buffer); - } - - @Override - public BufferedOutput wrap(@NonNull byte[] bytes) { - return delegate.wrap(bytes); - } - - @Override - public BufferedOutput wrap(@NonNull byte[] bytes, int offset, int length) { - return delegate.wrap(bytes, offset, length); - } -} diff --git a/jooby/src/main/java/io/jooby/internal/HeadContext.java b/jooby/src/main/java/io/jooby/internal/HeadContext.java index b1c314d90d..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.BufferedOutput; +import io.jooby.output.Output; public class HeadContext extends ForwardingContext { /** @@ -66,7 +66,7 @@ public Context send(@NonNull ByteBuffer data) { } @NonNull @Override - public Context send(@NonNull BufferedOutput output) { + public Context send(@NonNull Output output) { ctx.setResponseLength(output.size()); checkSizeHeaders(); ctx.send(StatusCode.OK); @@ -186,7 +186,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull BufferedOutput output, @NonNull Callback callback) { + public Sender write(@NonNull Output output, @NonNull Callback callback) { return this; } diff --git a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java index 7de1e6fab0..4650a6d2f6 100644 --- a/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java +++ b/jooby/src/main/java/io/jooby/internal/HttpMessageEncoder.java @@ -20,7 +20,7 @@ import io.jooby.ModelAndView; import io.jooby.StatusCode; import io.jooby.TemplateEngine; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; public class HttpMessageEncoder implements MessageEncoder { @@ -42,7 +42,7 @@ public HttpMessageEncoder add(MediaType type, MessageEncoder encoder) { } @Override - public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { if (value instanceof ModelAndView modelAndView) { for (var engine : templateEngineList) { if (engine.supports(modelAndView)) { diff --git a/jooby/src/main/java/io/jooby/internal/MutedServer.java b/jooby/src/main/java/io/jooby/internal/MutedServer.java index 8c477e8aa3..804ae04855 100644 --- a/jooby/src/main/java/io/jooby/internal/MutedServer.java +++ b/jooby/src/main/java/io/jooby/internal/MutedServer.java @@ -16,7 +16,7 @@ import io.jooby.LoggingService; import io.jooby.Server; import io.jooby.ServerOptions; -import io.jooby.buffer.BufferedOutputFactory; +import io.jooby.output.OutputFactory; public class MutedServer implements Server { private Server delegate; @@ -32,7 +32,7 @@ private MutedServer(Server server, LoggingService loggingService, List m } @NonNull @Override - public BufferedOutputFactory getOutputFactory() { + public OutputFactory getOutputFactory() { return delegate.getOutputFactory(); } diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 6432d11ab6..98c0793544 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -39,11 +39,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.*; -import io.jooby.buffer.BufferedOutputFactory; 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 io.jooby.value.ValueFactory; import jakarta.inject.Provider; @@ -170,7 +170,7 @@ public Stack executor(Executor executor) { private ValueFactory valueFactory = new ValueFactory(); - private BufferedOutputFactory outputFactory; + private OutputFactory outputFactory; public RouterImpl() { stack.addLast(new Stack(chi, null)); @@ -432,11 +432,11 @@ public Router setValueFactory(@NonNull ValueFactory valueFactory) { } @NonNull @Override - public BufferedOutputFactory getOutputFactory() { + public OutputFactory getOutputFactory() { return outputFactory; } - public void setOutputFactory(@NonNull BufferedOutputFactory outputFactory) { + public void setOutputFactory(@NonNull OutputFactory outputFactory) { this.outputFactory = outputFactory; } diff --git a/jooby/src/main/java/io/jooby/internal/WebSocketSender.java b/jooby/src/main/java/io/jooby/internal/WebSocketSender.java index 2825ee1422..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.BufferedOutput; +import io.jooby.output.Output; public class WebSocketSender extends ForwardingContext implements DefaultContext { @@ -69,7 +69,7 @@ public Context send(@NonNull ByteBuffer data) { } @Override - public Context send(@NonNull BufferedOutput output) { + public Context send(@NonNull Output output) { if (binary) { ws.sendBinary(output, callback); } else { 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 1dfefde148..6867ec8e1f 100644 --- a/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java +++ b/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java @@ -14,7 +14,7 @@ import io.jooby.Route; import io.jooby.Sender; import io.jooby.Server; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; public class ChunkedSubscriber implements Flow.Subscriber { @@ -116,7 +116,7 @@ public void onComplete() { sender().close(); } - private static BufferedOutput prepend(Context ctx, BufferedOutput data, byte c) { + private static Output prepend(Context ctx, Output data, byte c) { var buffer = ctx.getOutputFactory().newCompositeOutput(); buffer.write(c); data.transferTo(buffer::write); diff --git a/jooby/src/main/java/io/jooby/internal/buffer/ByteBufferWrappedOutput.java b/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java similarity index 78% rename from jooby/src/main/java/io/jooby/internal/buffer/ByteBufferWrappedOutput.java rename to jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java index 1da5e56e99..8c64efe090 100644 --- a/jooby/src/main/java/io/jooby/internal/buffer/ByteBufferWrappedOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.internal.buffer; +package io.jooby.internal.output; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -11,9 +11,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; -public class ByteBufferWrappedOutput implements BufferedOutput { +public class ByteBufferWrappedOutput implements Output { private final ByteBuffer buffer; @@ -22,22 +22,22 @@ public ByteBufferWrappedOutput(ByteBuffer buffer) { } @Override - public BufferedOutput write(byte b) { + public Output write(byte b) { throw new UnsupportedOperationException(); } @Override - public BufferedOutput write(byte[] source) { + public Output write(byte[] source) { throw new UnsupportedOperationException(); } @Override - public BufferedOutput write(byte[] source, int offset, int length) { + public Output write(byte[] source, int offset, int length) { throw new UnsupportedOperationException(); } @Override - public BufferedOutput clear() { + public Output clear() { buffer.clear(); return this; } diff --git a/jooby/src/main/java/io/jooby/internal/buffer/CompsiteByteBufferOutput.java b/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java similarity index 83% rename from jooby/src/main/java/io/jooby/internal/buffer/CompsiteByteBufferOutput.java rename to jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java index b1b3f86fe1..4562504d3e 100644 --- a/jooby/src/main/java/io/jooby/internal/buffer/CompsiteByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.internal.buffer; +package io.jooby.internal.output; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -13,9 +13,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; -public class CompsiteByteBufferOutput implements BufferedOutput { +public class CompsiteByteBufferOutput implements Output { private final List chunks = new ArrayList<>(); private int size = 0; @@ -25,25 +25,25 @@ public int size() { } @Override - public BufferedOutput write(byte b) { + public Output write(byte b) { addChunk(ByteBuffer.wrap(new byte[] {b})); return this; } @Override - public BufferedOutput write(byte[] source) { + public Output write(byte[] source) { addChunk(ByteBuffer.wrap(source)); return this; } @Override - public BufferedOutput write(byte[] source, int offset, int length) { + public Output write(byte[] source, int offset, int length) { addChunk(ByteBuffer.wrap(source, offset, length)); return this; } @Override - public BufferedOutput clear() { + public Output clear() { chunks.forEach(ByteBuffer::clear); chunks.clear(); return this; diff --git a/jooby/src/main/java/io/jooby/internal/buffer/OutputOutputStream.java b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java similarity index 76% rename from jooby/src/main/java/io/jooby/internal/buffer/OutputOutputStream.java rename to jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java index 6095626617..eb38732151 100644 --- a/jooby/src/main/java/io/jooby/internal/buffer/OutputOutputStream.java +++ b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java @@ -3,26 +3,26 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.internal.buffer; +package io.jooby.internal.output; import java.io.IOException; import java.io.OutputStream; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; /** - * An {@link OutputStream} that writes to a {@link BufferedOutput}. + * An {@link OutputStream} that writes to a {@link Output}. * - * @see BufferedOutput#asOutputStream() + * @see Output#asOutputStream() */ public class OutputOutputStream extends OutputStream { - private final BufferedOutput output; + private final Output output; private boolean closed; - public OutputOutputStream(@NonNull BufferedOutput output) { + public OutputOutputStream(@NonNull Output output) { this.output = output; } diff --git a/jooby/src/main/java/io/jooby/internal/buffer/OutputWriter.java b/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java similarity index 88% rename from jooby/src/main/java/io/jooby/internal/buffer/OutputWriter.java rename to jooby/src/main/java/io/jooby/internal/output/OutputWriter.java index 9961d128c3..f49779657d 100644 --- a/jooby/src/main/java/io/jooby/internal/buffer/OutputWriter.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.internal.buffer; +package io.jooby.internal.output; import java.io.IOException; import java.io.Writer; @@ -11,14 +11,14 @@ import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; public class OutputWriter extends Writer { - private final BufferedOutput output; + private final Output output; private final Charset charset; private boolean closed; - public OutputWriter(@NonNull BufferedOutput output, @NonNull Charset charset) { + public OutputWriter(@NonNull Output output, @NonNull Charset charset) { this.output = output; this.charset = charset; } diff --git a/jooby/src/main/java/io/jooby/internal/buffer/package-info.java b/jooby/src/main/java/io/jooby/internal/output/package-info.java similarity index 75% rename from jooby/src/main/java/io/jooby/internal/buffer/package-info.java rename to jooby/src/main/java/io/jooby/internal/output/package-info.java index bd31b7a64b..881ac8de5c 100644 --- a/jooby/src/main/java/io/jooby/internal/buffer/package-info.java +++ b/jooby/src/main/java/io/jooby/internal/output/package-info.java @@ -1,4 +1,4 @@ @ReturnValuesAreNonnullByDefault -package io.jooby.internal.buffer; +package io.jooby.internal.output; import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault; diff --git a/jooby/src/main/java/io/jooby/internal/buffer/ByteBufferOutput.java b/jooby/src/main/java/io/jooby/output/ByteBufferOutput.java similarity index 93% rename from jooby/src/main/java/io/jooby/internal/buffer/ByteBufferOutput.java rename to jooby/src/main/java/io/jooby/output/ByteBufferOutput.java index 996b0cea8d..d0acea8f09 100644 --- a/jooby/src/main/java/io/jooby/internal/buffer/ByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/output/ByteBufferOutput.java @@ -3,7 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.internal.buffer; +package io.jooby.output; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -13,9 +13,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.buffer.BufferedOutput; -public class ByteBufferOutput implements BufferedOutput { +public class ByteBufferOutput implements Output { private static final int MAX_CAPACITY = Integer.MAX_VALUE; private static final int CAPACITY_THRESHOLD = 1024 * 1024 * 4; @@ -70,7 +69,7 @@ public String asString(@NonNull Charset charset) { } @Override - public BufferedOutput write(byte b) { + public Output write(byte b) { ensureWritable(1); this.buffer.put(this.writePosition, b); this.writePosition += 1; @@ -78,12 +77,12 @@ public BufferedOutput write(byte b) { } @Override - public BufferedOutput write(byte[] source) { + public Output write(byte[] source) { return write(source, 0, source.length); } @Override - public BufferedOutput write(byte[] source, int offset, int length) { + public Output write(byte[] source, int offset, int length) { ensureWritable(length); var tmp = this.buffer.duplicate(); @@ -96,7 +95,7 @@ public BufferedOutput write(byte[] source, int offset, int length) { } @Override - public BufferedOutput write(@NonNull ByteBuffer source) { + public Output write(@NonNull ByteBuffer source) { ensureWritable(source.remaining()); var length = source.remaining(); var tmp = this.buffer.duplicate(); @@ -108,7 +107,7 @@ public BufferedOutput write(@NonNull ByteBuffer source) { } @Override - public BufferedOutput clear() { + public Output clear() { this.buffer.clear(); return this; } diff --git a/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java b/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java similarity index 51% rename from jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java rename to jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java index 0f7d5b64d7..e557ff7fe1 100644 --- a/jooby/src/main/java/io/jooby/buffer/ByteBufferOutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java @@ -3,14 +3,13 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.buffer; +package io.jooby.output; import java.nio.ByteBuffer; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.internal.buffer.ByteBufferOutput; -import io.jooby.internal.buffer.ByteBufferWrappedOutput; -import io.jooby.internal.buffer.CompsiteByteBufferOutput; +import io.jooby.internal.output.ByteBufferWrappedOutput; +import io.jooby.internal.output.CompsiteByteBufferOutput; /** * An output factory backed by {@link ByteBuffer}. @@ -18,46 +17,46 @@ * @author edgar * @since 4.0.0 */ -public class ByteBufferOutputFactory implements BufferedOutputFactory { - private BufferOptions options; +public class ByteBufferOutputFactory implements OutputFactory { + private OutputOptions options; - public ByteBufferOutputFactory(BufferOptions options) { + public ByteBufferOutputFactory(OutputOptions options) { this.options = options; } @Override - public BufferOptions getOptions() { + public OutputOptions getOptions() { return options; } @Override - public BufferedOutputFactory setOptions(BufferOptions options) { + public OutputFactory setOptions(OutputOptions options) { this.options = options; return this; } @Override - public BufferedOutput newBufferedOutput(boolean direct, int size) { + public Output newOutput(boolean direct, int size) { return new ByteBufferOutput(direct, size); } @Override - public BufferedOutput newCompositeOutput() { + public Output newCompositeOutput() { return new CompsiteByteBufferOutput(); } @Override - public BufferedOutput wrap(@NonNull ByteBuffer buffer) { + public Output wrap(@NonNull ByteBuffer buffer) { return new ByteBufferWrappedOutput(buffer); } @Override - public BufferedOutput wrap(@NonNull byte[] bytes) { + public Output wrap(@NonNull byte[] bytes) { return new ByteBufferWrappedOutput(ByteBuffer.wrap(bytes)); } @Override - public BufferedOutput wrap(@NonNull byte[] bytes, int offset, int length) { + public Output wrap(@NonNull byte[] bytes, int offset, int length) { return new ByteBufferWrappedOutput(ByteBuffer.wrap(bytes, offset, length)); } } 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..f79e38f39a --- /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(OutputOptions options) { + delegate.setOptions(options); + return this; + } + + @Override + public Output newOutput(int size) { + return delegate.newOutput(size); + } + + @Override + public Output newOutput(boolean direct, int size) { + return delegate.newOutput(direct, size); + } + + @Override + public Output newCompositeOutput() { + return delegate.newCompositeOutput(); + } + + @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/buffer/BufferedOutput.java b/jooby/src/main/java/io/jooby/output/Output.java similarity index 85% rename from jooby/src/main/java/io/jooby/buffer/BufferedOutput.java rename to jooby/src/main/java/io/jooby/output/Output.java index 1a48df46cd..57dd405ad4 100644 --- a/jooby/src/main/java/io/jooby/buffer/BufferedOutput.java +++ b/jooby/src/main/java/io/jooby/output/Output.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 java.io.OutputStream; import java.io.Writer; @@ -16,21 +16,21 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.SneakyThrows; -import io.jooby.internal.buffer.OutputOutputStream; -import io.jooby.internal.buffer.OutputWriter; +import io.jooby.internal.output.OutputOutputStream; +import io.jooby.internal.output.OutputWriter; /** * Buffered output used to support multiple implementations like byte array, byte buffer, netty * buffers. * *

There are two implementations of output one is backed by a {@link ByteBuffer} and the other is - * a view of multiple {@link ByteBuffer} byffers. See {@link - * BufferedOutputFactory#newBufferedOutput()} and {@link BufferedOutputFactory#newCompositeOutput()} + * a view of multiple {@link ByteBuffer} byffers. See {@link OutputFactory#newOutput()} and {@link + * OutputFactory#newCompositeOutput()} * * @author edgar * @since 4.0.0 */ -public interface BufferedOutput { +public interface Output { /** * This output as an output stream. Changes made to the output stream are reflected in this * output. @@ -108,7 +108,7 @@ default Iterator iterator() { * @param b the byte to be written * @return this output */ - BufferedOutput write(byte b); + Output write(byte b); /** * Write the given source into this buffer, starting at the current writing position of this @@ -117,7 +117,7 @@ default Iterator iterator() { * @param source the bytes to be written into this buffer * @return this output */ - BufferedOutput write(byte[] source); + Output write(byte[] source); /** * Write at most {@code length} bytes of the given source into this buffer, starting at the @@ -128,7 +128,7 @@ default Iterator iterator() { * @param length the maximum number of bytes to be written from {@code source} * @return this output */ - BufferedOutput write(byte[] source, int offset, int length); + Output write(byte[] source, int offset, int length); /** * Write the given {@code String} using {@code UTF-8}, starting at the current writing position. @@ -136,7 +136,7 @@ default Iterator iterator() { * @param source the char sequence to write into this buffer * @return this output */ - default BufferedOutput write(@NonNull String source) { + default Output write(@NonNull String source) { return write(source, StandardCharsets.UTF_8); } @@ -148,7 +148,7 @@ default BufferedOutput write(@NonNull String source) { * @param charset the charset to encode the char sequence with * @return this output */ - default BufferedOutput write(@NonNull String source, @NonNull Charset charset) { + default Output write(@NonNull String source, @NonNull Charset charset) { if (!source.isEmpty()) { return write(source.getBytes(charset)); } @@ -162,7 +162,7 @@ default BufferedOutput write(@NonNull String source, @NonNull Charset charset) { * @param source the bytes to be written into this buffer * @return this output */ - default BufferedOutput write(@NonNull ByteBuffer source) { + default Output write(@NonNull ByteBuffer source) { if (source.hasArray()) { return write(source.array(), source.arrayOffset() + source.position(), source.remaining()); } else { @@ -172,7 +172,7 @@ default BufferedOutput write(@NonNull ByteBuffer source) { } } - default BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { + default Output write(@NonNull CharBuffer source, @NonNull Charset charset) { if (!source.isEmpty()) { return write(charset.encode(source)); } @@ -181,5 +181,5 @@ default BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charse void send(io.jooby.Context ctx); - BufferedOutput clear(); + Output clear(); } diff --git a/jooby/src/main/java/io/jooby/buffer/BufferedOutputFactory.java b/jooby/src/main/java/io/jooby/output/OutputFactory.java similarity index 59% rename from jooby/src/main/java/io/jooby/buffer/BufferedOutputFactory.java rename to jooby/src/main/java/io/jooby/output/OutputFactory.java index 2cd3900c28..a3b9072b8b 100644 --- a/jooby/src/main/java/io/jooby/buffer/BufferedOutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/OutputFactory.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 java.lang.ThreadLocal.withInitial; @@ -14,37 +14,36 @@ import edu.umd.cs.findbugs.annotations.NonNull; /** - * Factory class for buffered {@link BufferedOutput}. + * Factory class for {@link Output}. * * @author edgar * @since 4.0.0 */ -public interface BufferedOutputFactory { +public interface OutputFactory { /** - * Thread local for output buffer. Please note only store calls to {@link #newBufferedOutput()}, - * {@link #newCompositeOutput()} are not saved into thread local. + * Thread local for output buffer. Please note only store calls to {@link #newOutput()}, {@link + * #newCompositeOutput()} are not saved into thread local. * * @param factory Factory. * @return Thread local factory. */ - static BufferedOutputFactory threadLocal(BufferedOutputFactory factory) { - return new ForwardingBufferedOutputFactory(factory) { - private final ThreadLocal threadLocal = - withInitial(factory::newBufferedOutput); + static OutputFactory threadLocal(OutputFactory factory) { + return new ForwardingOutputFactory(factory) { + private final ThreadLocal threadLocal = withInitial(factory::newOutput); @Override - public BufferedOutput newBufferedOutput(boolean direct, int size) { + public Output newOutput(boolean direct, int size) { return threadLocal.get().clear(); } @Override - public BufferedOutput newBufferedOutput(int size) { + public Output newOutput(int size) { return threadLocal.get().clear(); } @Override - public BufferedOutput newBufferedOutput() { + public Output newOutput() { return threadLocal.get().clear(); } }; @@ -55,7 +54,7 @@ public BufferedOutput newBufferedOutput() { * * @return Default output factory. */ - static BufferedOutputFactory create(BufferOptions options) { + static OutputFactory create(OutputOptions options) { return new ByteBufferOutputFactory(options); } @@ -64,13 +63,13 @@ static BufferedOutputFactory create(BufferOptions options) { * * @return Default output factory. */ - static BufferedOutputFactory create() { - return create(new BufferOptions()); + static OutputFactory create() { + return create(new OutputOptions()); } - BufferOptions getOptions(); + OutputOptions getOptions(); - BufferedOutputFactory setOptions(BufferOptions options); + OutputFactory setOptions(OutputOptions options); /** * Creates a new byte buffered output. @@ -79,7 +78,7 @@ static BufferedOutputFactory create() { * @param size Output size. * @return A byte buffered output. */ - BufferedOutput newBufferedOutput(boolean direct, int size); + Output newOutput(boolean direct, int size); /** * Creates a new byte buffered output. @@ -87,12 +86,12 @@ static BufferedOutputFactory create() { * @param size Output size. * @return A byte buffered output. */ - default BufferedOutput newBufferedOutput(int size) { - return newBufferedOutput(getOptions().isDirectBuffers(), size); + default Output newOutput(int size) { + return newOutput(getOptions().isDirectBuffers(), size); } - default BufferedOutput newBufferedOutput() { - return newBufferedOutput(getOptions().isDirectBuffers(), getOptions().getSize()); + default Output newOutput() { + return newOutput(getOptions().isDirectBuffers(), getOptions().getSize()); } /** @@ -101,7 +100,7 @@ default BufferedOutput newBufferedOutput() { * * @return A new composite buffer. */ - BufferedOutput newCompositeOutput(); + Output newCompositeOutput(); /** * Readonly buffer created from string utf-8 bytes. @@ -109,7 +108,7 @@ default BufferedOutput newBufferedOutput() { * @param value String. * @return Readonly buffer. */ - default BufferedOutput wrap(String value) { + default Output wrap(String value) { return wrap(value, StandardCharsets.UTF_8); } @@ -120,7 +119,7 @@ default BufferedOutput wrap(String value) { * @param charset Charset to use. * @return Readonly buffer. */ - default BufferedOutput wrap(@NonNull String value, @NonNull Charset charset) { + default Output wrap(@NonNull String value, @NonNull Charset charset) { return wrap(value.getBytes(charset)); } @@ -130,7 +129,7 @@ default BufferedOutput wrap(@NonNull String value, @NonNull Charset charset) { * @param buffer Input buffer. * @return Readonly buffer. */ - BufferedOutput wrap(@NonNull ByteBuffer buffer); + Output wrap(@NonNull ByteBuffer buffer); /** * Readonly buffer created from byte array. @@ -138,7 +137,7 @@ default BufferedOutput wrap(@NonNull String value, @NonNull Charset charset) { * @param bytes Byte array. * @return Readonly buffer. */ - BufferedOutput wrap(@NonNull byte[] bytes); + Output wrap(@NonNull byte[] bytes); /** * Readonly buffer created from byte array. @@ -148,5 +147,5 @@ default BufferedOutput wrap(@NonNull String value, @NonNull Charset charset) { * @param length Length. * @return Readonly buffer. */ - BufferedOutput wrap(@NonNull byte[] bytes, int offset, int length); + Output wrap(@NonNull byte[] bytes, int offset, int length); } diff --git a/jooby/src/main/java/io/jooby/buffer/BufferOptions.java b/jooby/src/main/java/io/jooby/output/OutputOptions.java similarity index 81% rename from jooby/src/main/java/io/jooby/buffer/BufferOptions.java rename to jooby/src/main/java/io/jooby/output/OutputOptions.java index 475435d6a4..b7d0b506c1 100644 --- a/jooby/src/main/java/io/jooby/buffer/BufferOptions.java +++ b/jooby/src/main/java/io/jooby/output/OutputOptions.java @@ -3,14 +3,14 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.buffer; +package io.jooby.output; -public class BufferOptions { +public class OutputOptions { private int size; private boolean directBuffers; - public BufferOptions() { + public OutputOptions() { long maxMemory = Runtime.getRuntime().maxMemory(); // smaller than 64mb of ram we use 512b buffers if (maxMemory < 64 * 1024 * 1024) { @@ -40,12 +40,12 @@ public BufferOptions() { * * @return Heap buffer of 512kb initial size. */ - public static BufferOptions small() { - return new BufferOptions().setDirectBuffers(false).setSize(512); + public static OutputOptions small() { + return new OutputOptions().setDirectBuffers(false).setSize(512); } - public static BufferOptions defaults() { - return new BufferOptions(); + public static OutputOptions defaults() { + return new OutputOptions(); } /** @@ -63,7 +63,7 @@ public int getSize() { * @param size The default initial buffer size. * @return This options. */ - public BufferOptions setSize(int size) { + public OutputOptions setSize(int size) { this.size = size; return this; } @@ -72,7 +72,7 @@ public boolean isDirectBuffers() { return directBuffers; } - public BufferOptions setDirectBuffers(boolean directBuffers) { + public OutputOptions setDirectBuffers(boolean directBuffers) { this.directBuffers = directBuffers; return this; } diff --git a/jooby/src/main/java/io/jooby/buffer/package-info.java b/jooby/src/main/java/io/jooby/output/package-info.java similarity index 72% rename from jooby/src/main/java/io/jooby/buffer/package-info.java rename to jooby/src/main/java/io/jooby/output/package-info.java index 80750baffa..6dbbe0ce94 100644 --- a/jooby/src/main/java/io/jooby/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.buffer; +package io.jooby.output; diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index f105686dc6..87b3707006 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -13,8 +13,8 @@ exports io.jooby.validation; exports io.jooby.problem; exports io.jooby.value; - exports io.jooby.buffer; - exports io.jooby.internal.buffer; + exports io.jooby.output; + exports io.jooby.internal.output; uses io.jooby.Server; uses io.jooby.SslProvider; diff --git a/jooby/src/test/java/io/jooby/Issue3607.java b/jooby/src/test/java/io/jooby/Issue3607.java index bfa6c34299..3b2d539eb9 100644 --- a/jooby/src/test/java/io/jooby/Issue3607.java +++ b/jooby/src/test/java/io/jooby/Issue3607.java @@ -9,15 +9,15 @@ import org.junit.jupiter.api.Test; -import io.jooby.buffer.BufferOptions; -import io.jooby.buffer.BufferedOutput; -import io.jooby.buffer.BufferedOutputFactory; +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 BufferedOutput render(Context ctx, ModelAndView modelAndView) throws Exception { + public Output render(Context ctx, ModelAndView modelAndView) throws Exception { // do nothing return ctx.getOutputFactory().wrap(new byte[0]); } @@ -26,7 +26,7 @@ public BufferedOutput render(Context ctx, ModelAndView modelAndView) throws E @Test public void shouldNotGenerateEmptyFlashMap() throws Exception { var ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(BufferedOutputFactory.create(BufferOptions.small())); + 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 cfc7999b51..4cc3af9bcc 100644 --- a/jooby/src/test/java/io/jooby/Issue3653.java +++ b/jooby/src/test/java/io/jooby/Issue3653.java @@ -13,7 +13,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.jooby.buffer.BufferedOutputFactory; +import io.jooby.output.OutputFactory; public class Issue3653 { @@ -22,7 +22,7 @@ public class Issue3653 { private static class TestServer extends Server.Base { @NotNull @Override - public BufferedOutputFactory getOutputFactory() { + public OutputFactory getOutputFactory() { return null; } diff --git a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java index 37be3e996c..b705bd5865 100644 --- a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java +++ b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java @@ -13,8 +13,8 @@ import org.junit.jupiter.api.Test; -import io.jooby.buffer.BufferOptions; -import io.jooby.buffer.BufferedOutputFactory; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; public class ServerSentMessageTest { @@ -23,7 +23,7 @@ public void shouldFormatMessage() throws Exception { var data = "some"; var ctx = mock(Context.class); - var bufferFactory = BufferedOutputFactory.create(BufferOptions.small()); + var bufferFactory = OutputFactory.create(OutputOptions.small()); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) @@ -43,7 +43,7 @@ public void shouldFormatMultiLineMessage() throws Exception { var data = "line 1\n line ,a .. 2\nline ...abc 3"; var ctx = mock(Context.class); - var bufferFactory = BufferedOutputFactory.create(BufferOptions.small()); + var bufferFactory = OutputFactory.create(OutputOptions.small()); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) @@ -65,7 +65,7 @@ public void shouldFormatMessageEndingWithNL() throws Exception { var data = "line 1\n"; var ctx = mock(Context.class); - var bufferFactory = BufferedOutputFactory.create(BufferOptions.small()); + var bufferFactory = OutputFactory.create(OutputOptions.small()); when(ctx.getOutputFactory()).thenReturn(bufferFactory); var encoder = mock(MessageEncoder.class); when(encoder.encode(ctx, data)) diff --git a/jooby/src/test/java/io/jooby/buffer/Issue3434.java b/jooby/src/test/java/io/jooby/buffer/Issue3434.java index dcd7dfa04b..713e73be49 100644 --- a/jooby/src/test/java/io/jooby/buffer/Issue3434.java +++ b/jooby/src/test/java/io/jooby/buffer/Issue3434.java @@ -15,9 +15,11 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; public class Issue3434 { - BufferedOutputFactory factory = BufferedOutputFactory.create(BufferOptions.small()); + OutputFactory factory = OutputFactory.create(OutputOptions.small()); @Test void shouldWriteCharBufferOnBufferWriter() throws IOException { @@ -142,7 +144,7 @@ void shouldWriteCharBufferOnBufferWriter() throws IOException { private String writeCharSequence(Charset charset, SneakyThrows.Consumer writer) throws IOException { - var buffer = factory.newBufferedOutput(); + var buffer = factory.newOutput(); try (var out = buffer.asWriter(charset)) { writer.accept(out); return buffer.asString(charset); diff --git a/jooby/src/test/java/io/jooby/buffer/BufferedOutputTest.java b/jooby/src/test/java/io/jooby/buffer/OutputTest.java similarity index 92% rename from jooby/src/test/java/io/jooby/buffer/BufferedOutputTest.java rename to jooby/src/test/java/io/jooby/buffer/OutputTest.java index 566f96dd26..ca6f7dd7d1 100644 --- a/jooby/src/test/java/io/jooby/buffer/BufferedOutputTest.java +++ b/jooby/src/test/java/io/jooby/buffer/OutputTest.java @@ -15,11 +15,12 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; -import io.jooby.internal.buffer.ByteBufferOutput; -import io.jooby.internal.buffer.ByteBufferWrappedOutput; -import io.jooby.internal.buffer.CompsiteByteBufferOutput; +import io.jooby.internal.output.ByteBufferWrappedOutput; +import io.jooby.internal.output.CompsiteByteBufferOutput; +import io.jooby.output.ByteBufferOutput; +import io.jooby.output.Output; -public class BufferedOutputTest { +public class OutputTest { @Test public void bufferedOutput() { @@ -75,7 +76,7 @@ public void bufferedOutput() { new ByteBufferOutput(false, 255)); } - private void output(SneakyThrows.Consumer consumer, BufferedOutput... buffers) { + private void output(SneakyThrows.Consumer consumer, Output... buffers) { Stream.of(buffers).forEach(consumer); } 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 ced1898843..6ee5961284 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.BufferedOutput; import io.jooby.internal.avaje.jsonb.BufferedJsonOutput; +import io.jooby.output.Output; /** * JSON module using Avaje-JsonB: message; - private BufferedOutputFactory factory; - private ThreadLocal cache = + private OutputFactory factory; + private ThreadLocal cache = ThreadLocal.withInitial( () -> { - return factory.newBufferedOutput(1024); + return factory.newOutput(1024); }); @Setup public void setup() { message = Map.of("id", 98, "value", "Hello World"); jsonb = Jsonb.builder().build(); - factory = BufferedOutputFactory.create(BufferOptions.small()); + factory = OutputFactory.create(OutputOptions.small()); } @Benchmark @@ -54,7 +54,7 @@ public void witCachedBufferedOutput() { @Benchmark public void witBufferedOutput() { - var buffer = factory.newBufferedOutput(1024); + var buffer = factory.newOutput(1024); jsonb.toJson(message, jsonb.writer(new BufferedJsonOutput(buffer))); } } 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 e7b737ce81..9719be94c5 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.BufferedOutput; +import io.jooby.output.Output; class FreemarkerTemplateEngine implements TemplateEngine { @@ -33,8 +33,8 @@ public List extensions() { } @Override - public BufferedOutput render(Context ctx, ModelAndView modelAndView) throws Exception { - var buffer = ctx.getOutputFactory().newBufferedOutput(); + public Output render(Context ctx, ModelAndView modelAndView) throws Exception { + var buffer = ctx.getOutputFactory().newOutput(); var template = freemarker.getTemplate(modelAndView.getView()); var writer = buffer.asWriter(); var wrapper = freemarker.getObjectWrapper(); 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 05206fb285..c4f7ef3621 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.BufferedOutput; +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 BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) { - var buffer = ctx.getOutputFactory().newBufferedOutput(); + public Output encode(@NonNull Context ctx, @NonNull Object value) { + var buffer = ctx.getOutputFactory().newOutput(); 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 412db36454..e20a1aa62e 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,8 +15,8 @@ import com.google.gson.GsonBuilder; import io.jooby.Context; -import io.jooby.buffer.BufferOptions; -import io.jooby.buffer.BufferedOutputFactory; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; public class Issue3434 { String text = @@ -140,7 +140,7 @@ public class Issue3434 { @Test void shouldEncodeUsingBufferWriter() { var gson = new GsonBuilder().create(); - var factory = BufferedOutputFactory.create(BufferOptions.small()); + var factory = OutputFactory.create(OutputOptions.small()); var ctx = mock(Context.class); when(ctx.getOutputFactory()).thenReturn(factory); 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 926f03f11d..6285b79c0c 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.BufferedOutput; +import io.jooby.output.Output; public class HandlebarsTemplateEngine implements TemplateEngine { @@ -35,14 +35,14 @@ public List extensions() { } @Override - public BufferedOutput 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.getOutputFactory().newBufferedOutput(); + var buffer = ctx.getOutputFactory().newOutput(); template.apply(engineModel, buffer.asWriter()); return buffer; } 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 3895501b84..ff50761dbc 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.BufferedOutput; +import io.jooby.output.Output; /** * JSON module using Jackson: https://jooby.io/modules/jackson. @@ -153,7 +153,7 @@ public void install(@NonNull Jooby application) { } @Override - public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { var factory = ctx.getOutputFactory(); ctx.setDefaultResponseType(mediaType); // let jackson uses his own cache, so just wrap the bytes 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 index 25dc084c65..1baf3982bb 100644 --- a/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java +++ b/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java @@ -13,9 +13,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.jooby.buffer.BufferOptions; -import io.jooby.buffer.BufferedOutput; -import io.jooby.buffer.BufferedOutputFactory; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; @Fork(5) @Warmup(iterations = 5, time = 1) @@ -27,15 +27,14 @@ public class JacksonBench { private ObjectMapper mapper; private Map message; - private BufferedOutputFactory factory; - private ThreadLocal cache = - ThreadLocal.withInitial(() -> factory.newBufferedOutput(1024)); + private OutputFactory factory; + private ThreadLocal cache = ThreadLocal.withInitial(() -> factory.newOutput(1024)); @Setup public void setup() { message = Map.of("id", 98, "value", "Hello World"); mapper = new ObjectMapper(); - factory = BufferedOutputFactory.create(BufferOptions.small()); + factory = OutputFactory.create(OutputOptions.small()); } @Benchmark 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 0f1e0ee643..65e571199a 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,15 +21,15 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.buffer.BufferOptions; -import io.jooby.buffer.BufferedOutputFactory; +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.getOutputFactory()).thenReturn(BufferedOutputFactory.create(BufferOptions.small())); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small())); JacksonModule jackson = new JacksonModule(new ObjectMapper()); @@ -58,7 +58,7 @@ public void parseJson() throws Exception { @Test public void renderXml() throws Exception { Context ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(BufferedOutputFactory.create(BufferOptions.small())); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small())); JacksonModule jackson = new JacksonModule(new XmlMapper()); 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 abb3009c58..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 @@ -11,7 +11,7 @@ import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.Callback; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; public class JettyCallbacks { public static class ByteBufferArrayCallback implements Callback { @@ -52,7 +52,7 @@ public static class OutputCallback implements Callback { private final Iterator it; private boolean closeOnLast; - public OutputCallback(Response response, Callback cb, BufferedOutput buffer) { + public OutputCallback(Response response, Callback cb, Output buffer) { this.response = response; this.cb = cb; this.it = buffer.iterator(); @@ -87,7 +87,7 @@ public void failed(Throwable x) { } } - public static OutputCallback fromOutput(Response response, Callback cb, BufferedOutput output) { + public static OutputCallback fromOutput(Response response, Callback cb, Output output) { return new OutputCallback(response, cb, output); } 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 1844d12630..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 @@ -71,7 +71,7 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.WebSocket; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import io.jooby.value.Value; public class JettyContext implements DefaultContext, Callback { @@ -536,7 +536,7 @@ public Context send(@NonNull String data, @NonNull Charset charset) { } @NonNull @Override - public Context send(@NonNull BufferedOutput output) { + public Context send(@NonNull Output output) { output.send(this); return this; } 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 47c7a65ff2..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 @@ -13,7 +13,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Sender; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; public class JettySender implements Sender { private final JettyContext ctx; @@ -31,7 +31,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull BufferedOutput output, @NonNull Callback callback) { + 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/JettyWebSocket.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyWebSocket.java index dd960886aa..d6ba8d3e25 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.BufferedOutput; +import io.jooby.output.Output; public class JettyWebSocket implements Session.Listener, WebSocketConfigurer, WebSocket { @@ -287,14 +287,14 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback } @NonNull @Override - public WebSocket send(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { + public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage( (remote, writeCallback) -> remote.sendText(message.asString(UTF_8), writeCallback), new WriteCallbackAdaptor(this, callback)); } @NonNull @Override - public WebSocket sendBinary(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { + public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage( (remote, writeCallback) -> new WebSocketOutputCallback(writeCallback, message, remote::sendBinary).send(), diff --git a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketOutputCallback.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketOutputCallback.java index 80703c1969..1f11df0989 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketOutputCallback.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/WebSocketOutputCallback.java @@ -11,7 +11,7 @@ import org.eclipse.jetty.websocket.api.Callback; import io.jooby.SneakyThrows.Consumer2; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; public class WebSocketOutputCallback implements Callback { @@ -20,7 +20,7 @@ public class WebSocketOutputCallback implements Callback { private Consumer2 sender; public WebSocketOutputCallback( - Callback cb, BufferedOutput buffer, Consumer2 sender) { + Callback cb, Output buffer, Consumer2 sender) { this.cb = cb; this.it = buffer.iterator(); this.sender = sender; 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 2e872cb9fb..f2ed292917 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 @@ -32,12 +32,12 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.*; -import io.jooby.buffer.BufferedOutputFactory; import io.jooby.exception.StartupException; import io.jooby.internal.jetty.JettyHandler; 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. @@ -55,7 +55,7 @@ public class JettyServer extends io.jooby.Server.Base { System.setProperty("jooby.server.ioThreads", ioThreads + ""); } - private BufferedOutputFactory outputFactory; + private OutputFactory outputFactory; private Server server; @@ -81,9 +81,9 @@ public JettyServer(@NonNull ServerOptions options) { public JettyServer() {} @NonNull @Override - public BufferedOutputFactory getOutputFactory() { + public OutputFactory getOutputFactory() { if (outputFactory == null) { - this.outputFactory = BufferedOutputFactory.create(getOptions().getBuffer()); + this.outputFactory = OutputFactory.create(getOptions().getBuffer()); } return outputFactory; } 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 89dff936fe..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 @@ -11,11 +11,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.MessageEncoder; -import io.jooby.buffer.BufferedOutput; +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, @@ -25,7 +25,7 @@ public JStachioMessageEncoder( } @Override - public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) throws Exception { + public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { if (supportsType(value.getClass())) { return render(ctx, value); } @@ -33,7 +33,7 @@ public BufferedOutput encode(@NonNull Context ctx, @NonNull Object value) throws } @Override - BufferedOutput extractOutput(Context ctx, ByteBufferEncodedOutput stream) throws IOException { + Output extractOutput(Context ctx, ByteBufferEncodedOutput stream) throws IOException { return ctx.getOutputFactory().wrap(stream.asByteBuffer()); } } diff --git a/modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.java b/modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.java index 02b4eaa450..54f0713bd8 100644 --- a/modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.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.BufferedOutput; +import io.jooby.output.Output; public class BufferedTemplateOutput implements TemplateOutput { - private final BufferedOutput buffer; + private final Output buffer; private final Charset charset; - public BufferedTemplateOutput(BufferedOutput buffer, Charset charset) { + public BufferedTemplateOutput(Output 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 592a8419e4..d85fc35a5e 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,13 +11,13 @@ import edu.umd.cs.findbugs.annotations.Nullable; import gg.jte.models.runtime.JteModel; import io.jooby.Context; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; public class JteModelEncoder implements io.jooby.MessageEncoder { @Nullable @Override - public BufferedOutput 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.getOutputFactory().newBufferedOutput(); + var buffer = ctx.getOutputFactory().newOutput(); jte.render(new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8)); return buffer; } 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 7821dc71e3..fb11f0ee0d 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.BufferedOutput; import io.jooby.internal.jte.BufferedTemplateOutput; +import io.jooby.output.Output; class JteTemplateEngine implements io.jooby.TemplateEngine { private final TemplateEngine jte; @@ -32,8 +32,8 @@ public List extensions() { } @Override - public BufferedOutput render(Context ctx, ModelAndView modelAndView) { - var buffer = ctx.getOutputFactory().newBufferedOutput(); + public Output render(Context ctx, ModelAndView modelAndView) { + var buffer = ctx.getOutputFactory().newOutput(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); var attributes = ctx.getAttributes(); if (modelAndView instanceof MapModelAndView mapModelAndView) { 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 index f2700eff30..007e7114f1 100644 --- 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 @@ -11,15 +11,15 @@ import org.junit.jupiter.api.Test; -import io.jooby.buffer.BufferOptions; -import io.jooby.buffer.BufferedOutputFactory; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; public class BufferedTemplateOutputTest { @Test public void checkWriteContent() { - var factory = BufferedOutputFactory.create(BufferOptions.small()); - var buffer = factory.newBufferedOutput(); + var factory = OutputFactory.create(OutputOptions.small()); + var buffer = factory.newOutput(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeContent("Hello"); assertEquals("Hello", buffer.asString(StandardCharsets.UTF_8)); @@ -27,8 +27,8 @@ public void checkWriteContent() { @Test public void checkWriteContentSubstring() { - var factory = BufferedOutputFactory.create(BufferOptions.small()); - var buffer = factory.newBufferedOutput(); + var factory = OutputFactory.create(OutputOptions.small()); + var buffer = factory.newOutput(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeContent(" Hello World! ", 1, " Hello World! ".length() - 2); assertEquals("Hello World", buffer.asString(StandardCharsets.UTF_8)); @@ -36,8 +36,8 @@ public void checkWriteContentSubstring() { @Test public void checkWriteBinaryContent() { - var factory = BufferedOutputFactory.create(BufferOptions.small()); - var buffer = factory.newBufferedOutput(); + var factory = OutputFactory.create(OutputOptions.small()); + var buffer = factory.newOutput(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeBinaryContent("Hello".getBytes(StandardCharsets.UTF_8)); assertEquals("Hello", buffer.asString(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 456fd4c21b..9f2fe97fbc 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,16 +19,16 @@ import io.jooby.Context; import io.jooby.MapModelAndView; import io.jooby.ModelAndView; -import io.jooby.buffer.BufferedOutput; -import io.jooby.buffer.BufferedOutputFactory; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; public class Issue3599 { @Test public void shouldNotCallObjectMethodOnMapModels() { - var bufferFactory = mock(BufferedOutputFactory.class); - var buffer = mock(BufferedOutput.class); - when(bufferFactory.newBufferedOutput()).thenReturn(buffer); + var bufferFactory = mock(OutputFactory.class); + var buffer = mock(Output.class); + when(bufferFactory.newOutput()).thenReturn(buffer); var attributes = Map.of("foo", 1); var mapModel = new HashMap(); @@ -53,9 +53,9 @@ public void shouldNotCallObjectMethodOnMapModels() { @Test public void shouldCallObjectMethodOnObjectModels() { - var bufferFactory = mock(BufferedOutputFactory.class); - var buffer = mock(BufferedOutput.class); - when(bufferFactory.newBufferedOutput()).thenReturn(buffer); + var bufferFactory = mock(OutputFactory.class); + var buffer = mock(Output.class); + when(bufferFactory.newOutput()).thenReturn(buffer); var attributes = Map.of("foo", 1); var model = new Issue3599(); 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 5fd48fd2cf..43afa02914 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,17 +15,17 @@ import gg.jte.TemplateOutput; import gg.jte.models.runtime.JteModel; import io.jooby.Context; -import io.jooby.buffer.BufferedOutput; -import io.jooby.buffer.BufferedOutputFactory; import io.jooby.internal.jte.JteModelEncoder; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; public class Issue3602 { @Test public void shouldRenderJteModel() throws Exception { - var bufferFactory = mock(BufferedOutputFactory.class); - var buffer = mock(BufferedOutput.class); - when(bufferFactory.newBufferedOutput()).thenReturn(buffer); + var bufferFactory = mock(OutputFactory.class); + var buffer = mock(Output.class); + when(bufferFactory.newOutput()).thenReturn(buffer); var attributes = Map.of("foo", 1); var ctx = mock(Context.class); diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutputFactory.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutputFactory.java deleted file mode 100644 index b6f66000ea..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutputFactory.java +++ /dev/null @@ -1,83 +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 java.nio.ByteBuffer; -import java.nio.charset.Charset; - -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.buffer.BufferOptions; -import io.jooby.buffer.BufferedOutput; -import io.jooby.buffer.BufferedOutputFactory; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.netty.util.ResourceLeakDetector; - -public class NettyBufferedOutputFactory implements BufferedOutputFactory { - 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 allocator; - private BufferOptions options; - - public NettyBufferedOutputFactory(ByteBufAllocator allocator, BufferOptions options) { - this.allocator = allocator; - this.options = options; - } - - public ByteBufAllocator getAllocator() { - return allocator; - } - - @Override - public BufferOptions getOptions() { - return options; - } - - @Override - public BufferedOutputFactory setOptions(BufferOptions options) { - this.options = options; - return this; - } - - @Override - public @NonNull BufferedOutput newBufferedOutput(boolean direct, int size) { - return new NettyBufferedOutput( - direct ? this.allocator.directBuffer(size) : this.allocator.heapBuffer(size)); - } - - @Override - @NonNull public BufferedOutput wrap(@NonNull ByteBuffer buffer) { - return new NettyByteBufferWrappedOutput(buffer); - } - - @Override - public BufferedOutput wrap(@NonNull String value, @NonNull Charset charset) { - return new NettyBufferedOutput(Unpooled.wrappedBuffer(value.getBytes(charset))); - } - - @Override - @NonNull public BufferedOutput wrap(@NonNull byte[] bytes) { - return new NettyByteArrayWrappedOutput(bytes, 0, bytes.length); - } - - @Override - @NonNull public BufferedOutput wrap(@NonNull byte[] bytes, int offset, int length) { - return new NettyByteArrayWrappedOutput(bytes, offset, length); - } - - @Override - @NonNull public BufferedOutput newCompositeOutput() { - return new NettyBufferedOutput(allocator.compositeBuffer(48)); - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedWrappedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedWrappedOutput.java deleted file mode 100644 index c0841f4771..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedWrappedOutput.java +++ /dev/null @@ -1,56 +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 java.nio.CharBuffer; -import java.nio.charset.Charset; - -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.buffer.BufferedOutput; -import io.netty.buffer.ByteBuf; - -public class NettyBufferedWrappedOutput implements NettyByteBufOutput { - - private final ByteBuf buffer; - - protected NettyBufferedWrappedOutput(ByteBuf buffer) { - this.buffer = buffer; - } - - @NonNull public ByteBuf byteBuf() { - return this.buffer.duplicate(); - } - - @Override - @NonNull public BufferedOutput write(byte b) { - throw new UnsupportedOperationException(); - } - - @Override - @NonNull public BufferedOutput write(byte[] source) { - throw new UnsupportedOperationException(); - } - - @Override - @NonNull public BufferedOutput write(byte[] source, int offset, int length) { - throw new UnsupportedOperationException(); - } - - @Override - @NonNull public BufferedOutput write(@NonNull String source, @NonNull Charset charset) { - throw new UnsupportedOperationException(); - } - - @Override - public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { - throw new UnsupportedOperationException(); - } - - @Override - @NonNull public BufferedOutput clear() { - return this; - } -} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteArrayWrappedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteArrayWrappedOutput.java deleted file mode 100644 index 5ad02dd355..0000000000 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteArrayWrappedOutput.java +++ /dev/null @@ -1,74 +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 java.nio.CharBuffer; -import java.nio.charset.Charset; - -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.Context; -import io.jooby.buffer.BufferedOutput; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.util.AsciiString; - -public class NettyByteArrayWrappedOutput implements NettyByteBufOutput { - - private final byte[] buffer; - private final int offset; - private final int length; - private final AsciiString contentLength; - - protected NettyByteArrayWrappedOutput(byte[] buffer, int offset, int length) { - this.buffer = buffer; - this.offset = offset; - this.length = length; - this.contentLength = AsciiString.of(Integer.toString(length - offset)); - } - - @NonNull public ByteBuf byteBuf() { - return Unpooled.wrappedBuffer(buffer, offset, length); - } - - @Override - @NonNull public BufferedOutput write(byte b) { - throw new UnsupportedOperationException(); - } - - @Override - @NonNull public BufferedOutput write(byte[] source) { - throw new UnsupportedOperationException(); - } - - @Override - @NonNull public BufferedOutput write(byte[] source, int offset, int length) { - throw new UnsupportedOperationException(); - } - - @Override - @NonNull public BufferedOutput write(@NonNull String source, @NonNull Charset charset) { - throw new UnsupportedOperationException(); - } - - @Override - public void send(Context ctx) { - if (ctx instanceof NettyContext netty) { - netty.send(Unpooled.wrappedBuffer(buffer, offset, length), contentLength); - } else { - ctx.send(asByteBuffer()); - } - } - - @Override - public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { - throw new UnsupportedOperationException(); - } - - @Override - @NonNull public BufferedOutput clear() { - return this; - } -} 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 e4f0594fab..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 @@ -50,7 +50,7 @@ import io.jooby.*; import io.jooby.Cookie; import io.jooby.FileUpload; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import io.jooby.value.Value; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -572,7 +572,7 @@ public final Context send(ByteBuffer data) { } @Override - @NonNull public Context send(@NonNull BufferedOutput output) { + @NonNull public Context send(@NonNull Output output) { output.send(this); return this; } 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/NettyOutputByteBuf.java similarity index 85% rename from modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufOutput.java rename to modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputByteBuf.java index fe310e6280..8bc2e835a4 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufOutput.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputByteBuf.java @@ -11,11 +11,11 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.SneakyThrows; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -public interface NettyByteBufOutput extends BufferedOutput { +public interface NettyOutputByteBuf extends Output { @NonNull ByteBuf byteBuf(); @Override @@ -48,8 +48,8 @@ default void send(Context ctx) { } } - static ByteBuf byteBuf(BufferedOutput output) { - if (output instanceof NettyByteBufOutput netty) { + static ByteBuf byteBuf(Output output) { + if (output instanceof NettyOutputByteBuf netty) { return netty.byteBuf(); } else { return Unpooled.wrappedBuffer(output.asByteBuffer()); diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputDefault.java similarity index 61% rename from modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java rename to modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputDefault.java index 7e87b2ab5d..55f3ca0f1c 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyBufferedOutput.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputDefault.java @@ -9,14 +9,14 @@ import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import io.netty.buffer.ByteBuf; -public class NettyBufferedOutput implements NettyByteBufOutput { +public class NettyOutputDefault implements NettyOutputByteBuf { private final ByteBuf buffer; - protected NettyBufferedOutput(ByteBuf buffer) { + protected NettyOutputDefault(ByteBuf buffer) { this.buffer = buffer; } @@ -25,37 +25,37 @@ protected NettyBufferedOutput(ByteBuf buffer) { } @Override - @NonNull public BufferedOutput write(byte b) { + @NonNull public Output write(byte b) { buffer.writeByte(b); return this; } @Override - @NonNull public BufferedOutput write(byte[] source) { + @NonNull public Output write(byte[] source) { buffer.writeBytes(source); return this; } @Override - @NonNull public BufferedOutput write(byte[] source, int offset, int length) { + @NonNull public Output 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) { + @NonNull public Output write(@NonNull String source, @NonNull Charset charset) { this.buffer.writeBytes(source.getBytes(charset)); return this; } @Override - public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { + public Output write(@NonNull CharBuffer source, @NonNull Charset charset) { this.buffer.writeBytes(charset.encode(source)); return this; } @Override - @NonNull public BufferedOutput clear() { + @NonNull public Output clear() { this.buffer.clear(); return this; } 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..43a8645d86 --- /dev/null +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.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.internal.netty; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import edu.umd.cs.findbugs.annotations.NonNull; +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 final ByteBufAllocator allocator; + private OutputOptions options; + + public NettyOutputFactory(ByteBufAllocator allocator, OutputOptions options) { + this.allocator = allocator; + this.options = options; + } + + public ByteBufAllocator getAllocator() { + return allocator; + } + + @Override + public OutputOptions getOptions() { + return options; + } + + @Override + public OutputFactory setOptions(OutputOptions options) { + this.options = options; + return this; + } + + @Override + public @NonNull Output newOutput(boolean direct, int size) { + return new NettyOutputDefault( + 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 + public Output wrap(@NonNull String value, @NonNull Charset charset) { + return new NettyOutputDefault(Unpooled.wrappedBuffer(value.getBytes(charset))); + } + + @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 Output newCompositeOutput() { + return new NettyOutputDefault(allocator.compositeBuffer(48)); + } +} diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufferWrappedOutput.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputStatic.java similarity index 51% rename from modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufferWrappedOutput.java rename to modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputStatic.java index 2a79c03469..769ace53e9 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufferWrappedOutput.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputStatic.java @@ -5,67 +5,65 @@ */ package io.jooby.internal.netty; -import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; +import java.util.function.Supplier; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; import io.netty.util.AsciiString; -public class NettyByteBufferWrappedOutput implements NettyByteBufOutput { - - private final ByteBuffer buffer; +public class NettyOutputStatic implements NettyOutputByteBuf { + private final Supplier provider; private final AsciiString contentLength; - protected NettyByteBufferWrappedOutput(ByteBuffer buffer) { - this.buffer = buffer; - this.contentLength = AsciiString.of(Integer.toString(buffer.remaining())); + protected NettyOutputStatic(int length, Supplier provider) { + this.provider = provider; + this.contentLength = AsciiString.of(Integer.toString(length)); } @NonNull public ByteBuf byteBuf() { - return Unpooled.wrappedBuffer(buffer); + return provider.get(); } @Override - @NonNull public BufferedOutput write(byte b) { + @NonNull public Output write(byte b) { throw new UnsupportedOperationException(); } @Override - @NonNull public BufferedOutput write(byte[] source) { + @NonNull public Output write(byte[] source) { throw new UnsupportedOperationException(); } @Override - @NonNull public BufferedOutput write(byte[] source, int offset, int length) { + @NonNull public Output write(byte[] source, int offset, int length) { throw new UnsupportedOperationException(); } @Override - @NonNull public BufferedOutput write(@NonNull String source, @NonNull Charset charset) { + @NonNull public Output write(@NonNull String source, @NonNull Charset charset) { throw new UnsupportedOperationException(); } @Override public void send(Context ctx) { if (ctx instanceof NettyContext netty) { - netty.send(Unpooled.wrappedBuffer(buffer), contentLength); + netty.send(provider.get(), contentLength); } else { ctx.send(asByteBuffer()); } } @Override - public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { + public Output write(@NonNull CharBuffer source, @NonNull Charset charset) { throw new UnsupportedOperationException(); } @Override - @NonNull public BufferedOutput clear() { + @NonNull public Output clear() { return this; } } 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 caba8e036b..8a9f9f4029 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,11 +5,11 @@ */ package io.jooby.internal.netty; -import static io.jooby.internal.netty.NettyByteBufOutput.byteBuf; +import static io.jooby.internal.netty.NettyOutputByteBuf.byteBuf; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Sender; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; @@ -35,7 +35,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull BufferedOutput output, @NonNull Callback callback) { + public Sender write(@NonNull Output output, @NonNull Callback callback) { context .writeAndFlush(new DefaultHttpContent(byteBuf(output))) .addListener(newChannelFutureListener(ctx, callback)); 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 bbc10f9ab0..a8e50f1697 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,7 +5,7 @@ */ package io.jooby.internal.netty; -import static io.jooby.internal.netty.NettyByteBufOutput.byteBuf; +import static io.jooby.internal.netty.NettyOutputByteBuf.byteBuf; import java.util.UUID; import java.util.concurrent.TimeUnit; 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 6806b17969..05f9f8e27f 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,7 +5,7 @@ */ package io.jooby.internal.netty; -import static io.jooby.internal.netty.NettyByteBufOutput.byteBuf; +import static io.jooby.internal.netty.NettyOutputByteBuf.byteBuf; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -28,7 +28,7 @@ import io.jooby.WebSocketCloseStatus; import io.jooby.WebSocketConfigurer; import io.jooby.WebSocketMessage; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; @@ -140,12 +140,12 @@ public WebSocket sendBinary(@NonNull byte[] message, @NonNull WriteCallback call } @NonNull @Override - public WebSocket send(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { + public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage(byteBuf(message), false, callback); } @NonNull @Override - public WebSocket sendBinary(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { + public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage(byteBuf(message), true, callback); } 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 f5527811a7..72386c9663 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 @@ -22,9 +22,9 @@ import io.jooby.ServerOptions; import io.jooby.SneakyThrows; import io.jooby.SslOptions; -import io.jooby.buffer.BufferedOutputFactory; import io.jooby.exception.StartupException; import io.jooby.internal.netty.*; +import io.jooby.output.OutputFactory; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelOption; @@ -55,7 +55,7 @@ public class NettyServer extends Server.Base { private ExecutorService worker; private List applications; - private NettyBufferedOutputFactory outputFactory; + private NettyOutputFactory outputFactory; /** * Creates a server. @@ -84,10 +84,9 @@ public NettyServer(@NonNull ServerOptions options) { public NettyServer() {} @NonNull @Override - public BufferedOutputFactory getOutputFactory() { + public OutputFactory getOutputFactory() { if (outputFactory == null) { - outputFactory = - new NettyBufferedOutputFactory(ByteBufAllocator.DEFAULT, getOptions().getBuffer()); + outputFactory = new NettyOutputFactory(ByteBufAllocator.DEFAULT, getOptions().getBuffer()); } return outputFactory; } @@ -135,7 +134,7 @@ public Server start(@NonNull Jooby... application) { this.dateLoop = Executors.newSingleThreadScheduledExecutor(); var dateService = new NettyDateService(dateLoop); - var outputFactory = (NettyBufferedOutputFactory) getOutputFactory(); + var outputFactory = (NettyOutputFactory) getOutputFactory(); var allocator = outputFactory.getAllocator(); var http2 = options.isHttp2() == Boolean.TRUE; /* Bootstrap: */ 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 65682bd18a..3ce2d3b0bc 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.BufferedOutput; +import io.jooby.output.Output; import io.pebbletemplates.pebble.PebbleEngine; class PebbleTemplateEngine implements TemplateEngine { @@ -34,9 +34,9 @@ public List extensions() { } @Override - public BufferedOutput render(Context ctx, ModelAndView modelAndView) throws Exception { + public Output render(Context ctx, ModelAndView modelAndView) throws Exception { if (modelAndView instanceof MapModelAndView mapModelAndView) { - var buffer = ctx.getOutputFactory().newBufferedOutput(); + var buffer = ctx.getOutputFactory().newOutput(); var template = engine.getTemplate(modelAndView.getView()); Map model = new HashMap<>(ctx.getAttributes()); model.putAll(mapModelAndView.getModel()); diff --git a/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java b/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java index a026dd1d38..d277a316af 100644 --- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java +++ b/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java @@ -10,8 +10,8 @@ import com.fizzed.rocker.ContentType; import com.fizzed.rocker.RockerOutput; import com.fizzed.rocker.RockerOutputFactory; -import io.jooby.buffer.BufferedOutput; -import io.jooby.buffer.BufferedOutputFactory; +import io.jooby.output.Output; +import io.jooby.output.OutputFactory; /** * Rocker output that uses a byte array to render the output. @@ -27,9 +27,9 @@ public class BufferedRockerOutput implements RockerOutput private final ContentType contentType; /** The buffer where data is stored. */ - protected BufferedOutput output; + protected Output output; - BufferedRockerOutput(Charset charset, ContentType contentType, BufferedOutput output) { + BufferedRockerOutput(Charset charset, ContentType contentType, Output output) { this.charset = charset; this.contentType = contentType; this.output = output; @@ -67,12 +67,11 @@ public int getByteLength() { * * @return Byte buffer. */ - public BufferedOutput asOutput() { + public Output asOutput() { return output; } - static RockerOutputFactory factory( - Charset charset, BufferedOutputFactory factory) { + static RockerOutputFactory factory(Charset charset, OutputFactory factory) { return (contentType, charsetName) -> new BufferedRockerOutput(charset, contentType, factory.newCompositeOutput()); } 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 6b50cae4e0..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,7 +11,7 @@ import io.jooby.Context; import io.jooby.MediaType; import io.jooby.MessageEncoder; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; class RockerMessageEncoder implements MessageEncoder { private final RockerOutputFactory factory; @@ -21,7 +21,7 @@ class RockerMessageEncoder implements MessageEncoder { } @Override - public BufferedOutput 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()); 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 6ba1c65b80..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 @@ -53,10 +53,10 @@ import io.jooby.Session; import io.jooby.StatusCode; import io.jooby.WebSocket; -import io.jooby.buffer.BufferOptions; -import io.jooby.buffer.BufferedOutput; -import io.jooby.buffer.BufferedOutputFactory; 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; @@ -119,7 +119,7 @@ public class MockContext implements DefaultContext { private int port = -1; - private BufferedOutputFactory outputFactory = BufferedOutputFactory.create(BufferOptions.small()); + private OutputFactory outputFactory = OutputFactory.create(OutputOptions.small()); @Override public String getMethod() { @@ -138,7 +138,7 @@ public int getPort() { } @Override - public BufferedOutputFactory getOutputFactory() { + public OutputFactory getOutputFactory() { return outputFactory; } @@ -571,7 +571,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull BufferedOutput output, @NonNull Callback callback) { + public Sender write(@NonNull Output output, @NonNull Callback callback) { response.setResult(output); callback.onComplete(MockContext.this, null); return this; @@ -670,7 +670,7 @@ public MockContext send(@NonNull ByteBuffer data) { } @Override - public Context send(@NonNull BufferedOutput output) { + public Context send(@NonNull Output output) { responseStarted = true; this.response.setResult(output).setContentLength(output.size()); listeners.run(this); 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 4a68821715..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.BufferedOutput; +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 BufferedOutput 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 BufferedOutput message, @NonNull WriteCallback callback) { + public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { return sendObject(message, callback); } 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 59e23a92da..70b791e137 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,7 +16,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.MapModelAndView; import io.jooby.ModelAndView; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; public class ThymeleafTemplateEngine implements io.jooby.TemplateEngine { private final TemplateEngine templateEngine; @@ -33,7 +33,7 @@ public List extensions() { } @Override - public @NonNull BufferedOutput 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()); @@ -43,7 +43,7 @@ public List extensions() { if (locale == null) { locale = ctx.locale(); } - var buffer = ctx.getOutputFactory().newBufferedOutput(); + var buffer = ctx.getOutputFactory().newOutput(); var context = new Context(locale, model); var templateName = modelAndView.getView(); if (!templateName.startsWith("/")) { 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 340a79d5eb..d5a7bc4455 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 @@ -56,7 +56,7 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.WebSocket; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import io.jooby.value.Value; import io.undertow.Handlers; import io.undertow.io.IoCallback; @@ -497,7 +497,7 @@ public Context send(@NonNull ByteBuffer data) { } @NonNull @Override - public Context send(@NonNull BufferedOutput output) { + public Context send(@NonNull Output output) { output.send(this); return this; } 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 index b027c6fdb5..0a2222f6cc 100644 --- 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 @@ -9,7 +9,7 @@ import java.nio.ByteBuffer; import java.util.Iterator; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import io.undertow.io.IoCallback; import io.undertow.io.Sender; import io.undertow.server.HttpServerExchange; @@ -19,7 +19,7 @@ public class UndertowOutputCallback implements IoCallback { private Iterator iterator; private IoCallback callback; - public UndertowOutputCallback(BufferedOutput buffer, IoCallback callback) { + public UndertowOutputCallback(Output buffer, IoCallback callback) { this.iterator = buffer.iterator(); this.callback = callback; } 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 ea28366918..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.BufferedOutput; +import io.jooby.output.Output; import io.undertow.io.IoCallback; import io.undertow.server.HttpServerExchange; @@ -30,7 +30,7 @@ public Sender write(@NonNull byte[] data, @NonNull Callback callback) { } @NonNull @Override - public Sender write(@NonNull BufferedOutput output, @NonNull Callback callback) { + 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 7b79daee9e..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 @@ -25,7 +25,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.ServerSentMessage; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import io.undertow.connector.PooledByteBuffer; import io.undertow.server.HttpServerExchange; @@ -169,7 +169,7 @@ private void fillBuffer() { * * @param dest Destination buffer. */ - private void transferTo(@NonNull BufferedOutput source, @NonNull ByteBuffer dest) { + private void transferTo(@NonNull Output source, @NonNull ByteBuffer dest) { transferTo(source, 0, dest, dest.position(), source.size()); } @@ -184,11 +184,7 @@ private void transferTo(@NonNull BufferedOutput source, @NonNull ByteBuffer dest * @param length the amount of data to copy */ private void transferTo( - @NonNull BufferedOutput source, - int srcPos, - @NonNull ByteBuffer dest, - int destPos, - int length) { + @NonNull Output source, int srcPos, @NonNull ByteBuffer dest, int destPos, int length) { dest = dest.duplicate().clear(); dest.put(destPos, source.asByteBuffer(), srcPos, length); } @@ -277,7 +273,7 @@ private static class SSEData { final ServerSentMessage message; final UndertowServerSentConnection.EventCallback callback; private int endBufferPosition = -1; - private BufferedOutput 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 30d02393cc..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 @@ -33,7 +33,7 @@ import io.jooby.WebSocketCloseStatus; import io.jooby.WebSocketConfigurer; import io.jooby.WebSocketMessage; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import io.undertow.websockets.core.AbstractReceiveListener; import io.undertow.websockets.core.BufferedBinaryMessage; import io.undertow.websockets.core.BufferedTextMessage; @@ -99,7 +99,7 @@ public WebSocketOutputCallback( WebSocketChannel channel, WriteCallback callback, boolean binary, - BufferedOutput buffer) { + Output buffer) { this.ws = ws; this.channel = channel; this.binary = binary; @@ -223,12 +223,12 @@ public WebSocket sendBinary(@NonNull String message, @NonNull WriteCallback call } @NonNull @Override - public WebSocket sendBinary(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { + public WebSocket sendBinary(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage(message, true, callback); } @NonNull @Override - public WebSocket send(@NonNull BufferedOutput message, @NonNull WriteCallback callback) { + public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage(message, false, callback); } @@ -237,7 +237,7 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback return sendMessage(message, true, callback); } - private WebSocket sendMessage(BufferedOutput buffer, boolean binary, WriteCallback callback) { + private WebSocket sendMessage(Output buffer, boolean binary, WriteCallback callback) { if (isOpen()) { try { new WebSocketOutputCallback(this, channel, callback, binary, buffer).send(); 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 685f0d316f..41507f63a8 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 @@ -21,10 +21,10 @@ import io.jooby.ServerOptions; import io.jooby.SneakyThrows; import io.jooby.SslOptions; -import io.jooby.buffer.BufferedOutputFactory; 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; @@ -59,7 +59,7 @@ public class UndertowServer extends Server.Base { private XnioWorker worker; - private BufferedOutputFactory outputFactory; + private OutputFactory outputFactory; public UndertowServer(@NonNull ServerOptions options) { setOptions(options); @@ -75,9 +75,9 @@ public UndertowServer setOptions(@NonNull ServerOptions options) { } @NonNull @Override - public BufferedOutputFactory getOutputFactory() { + public OutputFactory getOutputFactory() { if (outputFactory == null) { - outputFactory = BufferedOutputFactory.create(getOptions().getBuffer()); + outputFactory = OutputFactory.create(getOptions().getBuffer()); } return outputFactory; } 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 b038043ee8..b55fb0da02 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 @@ -20,7 +20,7 @@ import io.jooby.MessageDecoder; import io.jooby.MessageEncoder; import io.jooby.ServiceRegistry; -import io.jooby.buffer.BufferedOutput; +import io.jooby.output.Output; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; @@ -101,10 +101,10 @@ public Object decode(@NonNull final Context ctx, @NonNull final Type type) throw } @Nullable @Override - public BufferedOutput encode(@NonNull final Context ctx, @NonNull final Object value) { + public Output encode(@NonNull final Context ctx, @NonNull final Object value) { ctx.setDefaultResponseType(MediaType.json); var factory = ctx.getOutputFactory(); - var output = factory.newBufferedOutput(); + var output = factory.newOutput(); 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 40983950b2..625c027024 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,8 +19,8 @@ import io.jooby.Body; import io.jooby.Context; import io.jooby.MediaType; -import io.jooby.buffer.BufferOptions; -import io.jooby.buffer.BufferedOutputFactory; +import io.jooby.output.OutputFactory; +import io.jooby.output.OutputOptions; public class YassonModuleTest { @@ -40,7 +40,7 @@ public void render() { user.age = Integer.MAX_VALUE; Context ctx = mock(Context.class); - when(ctx.getOutputFactory()).thenReturn(BufferedOutputFactory.create(BufferOptions.small())); + when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small())); var buffer = YassonModule.encode(ctx, user); assertEquals( "{\"age\":2147483647,\"id\":-1,\"name\":\"Lorem €@!?\"}", diff --git a/tests/src/test/java/io/jooby/i2613/Issue2613.java b/tests/src/test/java/io/jooby/i2613/Issue2613.java index ee5a23fe88..ddd458e005 100644 --- a/tests/src/test/java/io/jooby/i2613/Issue2613.java +++ b/tests/src/test/java/io/jooby/i2613/Issue2613.java @@ -14,10 +14,10 @@ import io.jooby.Context; import io.jooby.MediaType; import io.jooby.MessageEncoder; -import io.jooby.buffer.BufferedOutput; import io.jooby.jackson.JacksonModule; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; +import io.jooby.output.Output; public class Issue2613 { @@ -30,7 +30,7 @@ public String html() { public static class ThemeResultEncoder implements MessageEncoder { @Override - public BufferedOutput encode(@NonNull Context ctx, @NonNull 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.getOutputFactory() diff --git a/tests/src/test/java/io/jooby/i2806/Issue2806.java b/tests/src/test/java/io/jooby/i2806/Issue2806.java index 20740f09df..592770fc0d 100644 --- a/tests/src/test/java/io/jooby/i2806/Issue2806.java +++ b/tests/src/test/java/io/jooby/i2806/Issue2806.java @@ -12,10 +12,10 @@ import com.google.common.collect.ImmutableMap; import io.jooby.ServerOptions; -import io.jooby.buffer.BufferOptions; 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; @@ -29,7 +29,7 @@ public void renderShouldWorkFromErrorHandlerWhenLargeRequestAreSent(ServerTestRu runner .options( new ServerOptions() - .setBuffer(new BufferOptions().setSize(ServerOptions._16KB / 2)) + .setBuffer(new OutputOptions().setSize(ServerOptions._16KB / 2)) .setMaxRequestSize(ServerOptions._16KB)) .define( app -> { diff --git a/tests/src/test/java/io/jooby/internal/ChiStaticPathsBench.java b/tests/src/test/java/io/jooby/internal/ChiStaticPathsBench.java deleted file mode 100644 index 05dacab34b..0000000000 --- a/tests/src/test/java/io/jooby/internal/ChiStaticPathsBench.java +++ /dev/null @@ -1,48 +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.util.concurrent.TimeUnit; - -import org.openjdk.jmh.annotations.*; - -import io.jooby.MessageEncoder; -import io.jooby.Route; - -@Fork(5) -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 10, time = 1) -@BenchmarkMode(Mode.Throughput) -@OutputTimeUnit(TimeUnit.SECONDS) -@State(Scope.Benchmark) -public class ChiStaticPathsBench { - - private Chi router; - - @Setup - public void setup() { - this.router = new Chi(); - router.insert(route("GET", "/plaintext", stringHandler("plaintext"))); - router.insert(route("GET", "/json", stringHandler("json"))); - router.insert(route("GET", "/fortune", stringHandler("fortune"))); - router.insert(route("GET", "/db", stringHandler("db"))); - router.insert(route("GET", "/updates", stringHandler("updates"))); - router.insert(route("GET", "/queries", stringHandler("queries"))); - } - - @Benchmark - public void plaintext() { - router.find("GET", "/plaintext").matches(); - } - - private Route.Handler stringHandler(String foo) { - return ctx -> foo; - } - - private Route route(String method, String pattern, Route.Handler handler) { - return new Route(method, pattern, handler).setEncoder(MessageEncoder.TO_STRING); - } -} diff --git a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index f6e60d00ee..4509a8faec 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -18,8 +18,8 @@ import org.junit.jupiter.api.condition.DisabledOnOs; import io.jooby.*; -import io.jooby.buffer.BufferedOutputFactory; import io.jooby.internal.MutedServer; +import io.jooby.output.OutputFactory; import io.jooby.test.WebClient; public class ServerTestRunner { @@ -62,7 +62,6 @@ public ServerTestRunner define(Consumer consumer) { // set default session app.setSessionStore(SessionStore.memory(Cookie.session("jooby.sid"))); } - app.setExecutionMode(executionMode); consumer.accept(app); return app; }); @@ -90,11 +89,11 @@ public void ready(SneakyThrows.Consumer2 onReady) { Server server = this.server.get(serverOptions); String applogger = null; try { - setBufferFactory(server.getOutputFactory()); + 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(); - Optional.ofNullable(executionMode).ifPresent(app::setExecutionMode); // Reduce log from maven build: var mavenBuild = System.getProperty("surefire.real.class.path", "").length() > 0; if (mavenBuild) { @@ -142,13 +141,24 @@ public void ready(SneakyThrows.Consumer2 onReady) { } else { MutedServer.mute(server).stop(); } - setBufferFactory(null); + setOutputFactory(null); + setExecutionMode(null); } } - private static void setBufferFactory(BufferedOutputFactory bufferedOutputFactory) { + private void setExecutionMode(ExecutionMode executionMode) { try { - var field = Jooby.class.getDeclaredField("BUFFER_FACTORY"); + 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) { diff --git a/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index 518fd406ca..0fe8c320d6 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -72,7 +72,6 @@ import io.jooby.ServiceKey; import io.jooby.ServiceRegistry; import io.jooby.StatusCode; -import io.jooby.buffer.BufferOptions; import io.jooby.handlebars.HandlebarsModule; import io.jooby.handler.AccessLogHandler; import io.jooby.handler.AssetHandler; @@ -87,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; @@ -1534,7 +1534,7 @@ public void maxRequestSize(ServerTestRunner runner) { runner .options( new ServerOptions() - .setBuffer(new BufferOptions().setSize(ServerOptions._16KB / 2)) + .setBuffer(new OutputOptions().setSize(ServerOptions._16KB / 2)) .setMaxRequestSize(ServerOptions._16KB)) .define( app -> { From 8c37006a06b0df6d219eaacdab075d5c1e3b1ec9 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 20 Jul 2025 12:38:38 -0300 Subject: [PATCH 54/60] server: make server options available on app initialization - code clean up --- jooby/src/main/java/io/jooby/Jooby.java | 10 +-- jooby/src/main/java/io/jooby/Router.java | 7 +- jooby/src/main/java/io/jooby/Server.java | 11 ++- .../src/main/java/io/jooby/ServerOptions.java | 67 +++++++------------ .../java/io/jooby/internal/RouterImpl.java | 12 +++- jooby/src/test/java/io/jooby/Issue3653.java | 1 - .../jooby/{buffer => output}/Issue3434.java | 4 +- .../jooby/{buffer => output}/OutputTest.java | 4 +- .../main/java/io/jooby/jetty/JettyServer.java | 26 +++---- .../main/java/io/jooby/netty/NettyServer.java | 20 +++--- .../io/jooby/undertow/UndertowServer.java | 21 +++--- tests/src/test/java/examples/Performance.java | 4 +- .../test/java/io/jooby/i2806/Issue2806.java | 2 +- .../io/jooby/junit/ServerExtensionImpl.java | 11 +-- .../java/io/jooby/junit/ServerTestRunner.java | 17 +++-- .../test/java/io/jooby/test/FeaturedTest.java | 2 +- 16 files changed, 95 insertions(+), 124 deletions(-) rename jooby/src/test/java/io/jooby/{buffer => output}/Issue3434.java (97%) rename jooby/src/test/java/io/jooby/{buffer => output}/OutputTest.java (97%) diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index f60c3b3d85..e608832348 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -33,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; @@ -89,6 +88,7 @@ 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; @@ -136,6 +136,7 @@ public Jooby() { // 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); } @@ -444,7 +445,7 @@ public String getContextPath() { return router; } - @Nullable @Override + @NonNull @Override public ServerOptions getServerOptions() { return router.getServerOptions(); } @@ -961,8 +962,7 @@ public Jooby setStartupSummary(List startupSummary) { 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); } @@ -1301,11 +1301,13 @@ public static Jooby createApp( Jooby app; try { Jooby.OUTPUT_FACTORY = server.getOutputFactory(); + Jooby.SERVER_OPTIONS = server.getOptions(); Jooby.BOOT_EXECUTION_MODE = executionMode; app = provider.get(); } finally { Jooby.BOOT_EXECUTION_MODE = executionMode; Jooby.OUTPUT_FACTORY = null; + Jooby.SERVER_OPTIONS = null; } return app; diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 35964fb3d8..a7dda31a51 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -182,12 +182,11 @@ default Object execute(@NonNull Context context) { @NonNull ServiceRegistry getServices(); /** - * Server options. They might be null during application initialization. Once deployed, they are - * never null (at runtime). + * Server options. * - * @return Server options or null. + * @return Server options. */ - @Nullable ServerOptions getServerOptions(); + @NonNull ServerOptions getServerOptions(); /** * Set application context path. Context path is the base path for all routes. Default is: / diff --git a/jooby/src/main/java/io/jooby/Server.java b/jooby/src/main/java/io/jooby/Server.java index e2b6e3d1d8..0a8222b3a1 100644 --- a/jooby/src/main/java/io/jooby/Server.java +++ b/jooby/src/main/java/io/jooby/Server.java @@ -156,7 +156,9 @@ public Server setOptions(@NonNull ServerOptions options) { return this; } - protected abstract ServerOptions defaultOptions(); + protected ServerOptions defaultOptions() { + return new ServerOptions(); + } } @NonNull OutputFactory getOutputFactory(); @@ -298,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 5cc2ba0608..6de52e5fd1 100644 --- a/jooby/src/main/java/io/jooby/ServerOptions.java +++ b/jooby/src/main/java/io/jooby/ServerOptions.java @@ -75,23 +75,28 @@ public class ServerOptions { public static final int IO_THREADS = Integer.parseInt( System.getProperty( - "jooby.server.ioThreads", - Integer.toString(Runtime.getRuntime().availableProcessors()))); + "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 @@ -100,9 +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 buffer = OutputOptions.defaults(); + private OutputOptions output = OutputOptions.defaults(); /** * Maximum request size in bytes. Request exceeding this value results in {@link @@ -150,11 +155,11 @@ public class ServerOptions { if (conf.hasPath("server.name")) { options.setServer(conf.getString("server.name")); } - if (conf.hasPath("server.buffer.size")) { - options.buffer.setSize(conf.getInt("server.buffer.size")); + if (conf.hasPath("server.output.size")) { + options.output.setSize(conf.getInt("server.output.size")); } - if (conf.hasPath("server.buffer.useDirectBuffers")) { - options.buffer.setDirectBuffers(conf.getBoolean("server.buffer.useDirectBuffers")); + 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")); @@ -197,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(", buffer: ").append(getBuffer()); + buff.append(", output: ").append(getOutput()); buff.append(", maxRequestSize: ").append(maxRequestSize); buff.append(", httpsOnly: ").append(httpsOnly); if (compressionLevel != null) { @@ -311,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; } /** @@ -343,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; } /** @@ -415,12 +396,12 @@ public boolean getDefaultHeaders() { return this; } - public OutputOptions getBuffer() { - return buffer; + public OutputOptions getOutput() { + return output; } - public ServerOptions setBuffer(@NonNull OutputOptions buffer) { - this.buffer = buffer; + public ServerOptions setOutput(@NonNull OutputOptions output) { + this.output = output; return this; } diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 98c0793544..0169a3c6a6 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -37,7 +37,6 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; import io.jooby.*; import io.jooby.exception.RegistryException; import io.jooby.exception.StatusCodeException; @@ -172,6 +171,8 @@ public Stack executor(Executor executor) { private OutputFactory outputFactory; + private ServerOptions serverOptions; + public RouterImpl() { stack.addLast(new Stack(chi, null)); } @@ -783,9 +784,14 @@ public Router setFlashCookie(@NonNull Cookie flashCookie) { return this; } - @Nullable @Override + @NonNull @Override public ServerOptions getServerOptions() { - return services.getOrNull(ServerOptions.class); + return serverOptions; + } + + public void setServerOptions(ServerOptions serverOptions) { + this.serverOptions = serverOptions; + services.put(ServerOptions.class, serverOptions); } @NonNull @Override diff --git a/jooby/src/test/java/io/jooby/Issue3653.java b/jooby/src/test/java/io/jooby/Issue3653.java index 4cc3af9bcc..822029bb18 100644 --- a/jooby/src/test/java/io/jooby/Issue3653.java +++ b/jooby/src/test/java/io/jooby/Issue3653.java @@ -26,7 +26,6 @@ public OutputFactory getOutputFactory() { return null; } - @Override protected ServerOptions defaultOptions() { return defaultOptions; } diff --git a/jooby/src/test/java/io/jooby/buffer/Issue3434.java b/jooby/src/test/java/io/jooby/output/Issue3434.java similarity index 97% rename from jooby/src/test/java/io/jooby/buffer/Issue3434.java rename to jooby/src/test/java/io/jooby/output/Issue3434.java index 713e73be49..7bf4ff1166 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; @@ -15,8 +15,6 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; -import io.jooby.output.OutputFactory; -import io.jooby.output.OutputOptions; public class Issue3434 { OutputFactory factory = OutputFactory.create(OutputOptions.small()); diff --git a/jooby/src/test/java/io/jooby/buffer/OutputTest.java b/jooby/src/test/java/io/jooby/output/OutputTest.java similarity index 97% rename from jooby/src/test/java/io/jooby/buffer/OutputTest.java rename to jooby/src/test/java/io/jooby/output/OutputTest.java index ca6f7dd7d1..4ac3de0726 100644 --- a/jooby/src/test/java/io/jooby/buffer/OutputTest.java +++ b/jooby/src/test/java/io/jooby/output/OutputTest.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,8 +17,6 @@ import io.jooby.SneakyThrows; import io.jooby.internal.output.ByteBufferWrappedOutput; import io.jooby.internal.output.CompsiteByteBufferOutput; -import io.jooby.output.ByteBufferOutput; -import io.jooby.output.Output; public class OutputTest { 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 f2ed292917..7132be3ced 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 @@ -47,12 +47,15 @@ */ 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("jooby.server.ioThreads", ioThreads + ""); + System.setProperty("server.ioThreads", ioThreads + ""); + System.setProperty("server.workerThreads", THREADS + ""); + System.setProperty("server.name", NAME); } private OutputFactory outputFactory; @@ -83,25 +86,14 @@ public JettyServer() {} @NonNull @Override public OutputFactory getOutputFactory() { if (outputFactory == null) { - this.outputFactory = OutputFactory.create(getOptions().getBuffer()); + this.outputFactory = OutputFactory.create(getOptions().getOutput()); } return outputFactory; } - @NonNull @Override - public JettyServer setOptions(@NonNull ServerOptions options) { - super.setOptions(options.setWorkerThreads(options.getWorkerThreads(THREADS))); - return this; - } - - @Override - protected ServerOptions defaultOptions() { - return new ServerOptions().setServer(getName()).setWorkerThreads(THREADS); - } - @NonNull @Override public String getName() { - return "jetty"; + return NAME; } /** @@ -150,8 +142,8 @@ public io.jooby.Server start(@NonNull Jooby... application) { var httpConf = new HttpConfiguration(); httpConf.setUriCompliance(UriCompliance.LEGACY); - httpConf.setOutputBufferSize(options.getBuffer().getSize()); - httpConf.setOutputAggregationSize(options.getBuffer().getSize()); + httpConf.setOutputBufferSize(options.getOutput().getSize()); + httpConf.setOutputAggregationSize(options.getOutput().getSize()); httpConf.setSendXPoweredBy(false); httpConf.setSendDateHeader(options.getDefaultHeaders()); httpConf.setSendServerVersion(false); @@ -302,7 +294,7 @@ private List> createHandler( new JettyHandler( invocationType, applications.get(0), - options.getBuffer().getSize(), + options.getOutput().getSize(), options.getMaxRequestSize(), options.getDefaultHeaders()); if (options.isExpectContinue() == Boolean.TRUE) { 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 72386c9663..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 @@ -45,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; @@ -86,19 +93,14 @@ public NettyServer() {} @NonNull @Override public OutputFactory getOutputFactory() { if (outputFactory == null) { - outputFactory = new NettyOutputFactory(ByteBufAllocator.DEFAULT, getOptions().getBuffer()); + outputFactory = new NettyOutputFactory(ByteBufAllocator.DEFAULT, getOptions().getOutput()); } return outputFactory; } - @Override - protected ServerOptions defaultOptions() { - return new ServerOptions().setServer(getName()); - } - @NonNull @Override public String getName() { - return "netty"; + return NAME; } @NonNull @Override @@ -198,7 +200,7 @@ private NettyPipeline newPipeline( new HttpDecoderConfig() .setMaxInitialLineLength(_4KB) .setMaxHeaderSize(options.getMaxHeaderSize()) - .setMaxChunkSize(options.getBuffer().getSize()) + .setMaxChunkSize(options.getOutput().getSize()) .setHeadersFactory(HEADERS) .setTrailersFactory(HEADERS); return new NettyPipeline( @@ -207,7 +209,7 @@ private NettyPipeline newPipeline( decoderConfig, applications, options.getMaxRequestSize(), - options.getBuffer().getSize(), + options.getOutput().getSize(), options.getDefaultHeaders(), http2, options.isExpectContinue() == Boolean.TRUE, 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 41507f63a8..7427d7e2c6 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 @@ -42,9 +42,11 @@ */ public class UndertowServer extends Server.Base { + private static final String NAME = "undertow"; + static { - System.setProperty( - "jooby.server.ioThreads", (Runtime.getRuntime().availableProcessors() * 2) + ""); + // Default values + System.setProperty("server.name", NAME); } private static final int BACKLOG = 8192; @@ -77,19 +79,14 @@ public UndertowServer setOptions(@NonNull ServerOptions options) { @NonNull @Override public OutputFactory getOutputFactory() { if (outputFactory == null) { - outputFactory = OutputFactory.create(getOptions().getBuffer()); + outputFactory = OutputFactory.create(getOptions().getOutput()); } return outputFactory; } - @Override - protected ServerOptions defaultOptions() { - return new ServerOptions().setIoThreads(ServerOptions.IO_THREADS).setServer(getName()); - } - @NonNull @Override public String getName() { - return "undertow"; + return NAME; } @Override @@ -110,7 +107,7 @@ public String getName() { HttpHandler handler = new UndertowHandler( this.applications, - getOptions().getBuffer().getSize(), + getOptions().getOutput().getSize(), options.getMaxRequestSize(), options.getDefaultHeaders()); @@ -144,8 +141,8 @@ public String getName() { Undertow.Builder builder = Undertow.builder() - .setBufferSize(options.getBuffer().getSize()) - .setDirectBuffers(options.getBuffer().isDirectBuffers()) + .setBufferSize(options.getOutput().getSize()) + .setDirectBuffers(options.getOutput().isDirectBuffers()) /* Socket : */ .setSocketOption(Options.BACKLOG, BACKLOG) /* Server: */ diff --git a/tests/src/test/java/examples/Performance.java b/tests/src/test/java/examples/Performance.java index 3f26c3dcfb..3dd6775250 100644 --- a/tests/src/test/java/examples/Performance.java +++ b/tests/src/test/java/examples/Performance.java @@ -12,7 +12,7 @@ import io.jooby.Jooby; import io.jooby.StatusCode; -import io.jooby.netty.NettyServer; +import io.jooby.undertow.UndertowServer; public class Performance extends Jooby { @@ -41,6 +41,6 @@ public class Performance extends Jooby { public static void main(final String[] args) { System.setProperty("io.netty.disableHttpHeadersValidation", "true"); - runApp(args, new NettyServer(), EVENT_LOOP, Performance::new); + runApp(args, new UndertowServer(), EVENT_LOOP, Performance::new); } } diff --git a/tests/src/test/java/io/jooby/i2806/Issue2806.java b/tests/src/test/java/io/jooby/i2806/Issue2806.java index 592770fc0d..c305b18e01 100644 --- a/tests/src/test/java/io/jooby/i2806/Issue2806.java +++ b/tests/src/test/java/io/jooby/i2806/Issue2806.java @@ -29,7 +29,7 @@ public void renderShouldWorkFromErrorHandlerWhenLargeRequestAreSent(ServerTestRu runner .options( new ServerOptions() - .setBuffer(new OutputOptions().setSize(ServerOptions._16KB / 2)) + .setOutput(new OutputOptions().setSize(ServerOptions._16KB / 2)) .setMaxRequestSize(ServerOptions._16KB)) .define( app -> { 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/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index 4509a8faec..61e4946cd6 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -54,10 +54,7 @@ public ServerTestRunner( public ServerTestRunner define(Consumer consumer) { use( () -> { - Jooby app = new Jooby(); - if (serverOptions != null) { - app.getServices().put(ServerOptions.class, serverOptions); - } + var app = new Jooby(); if (app.getSessionStore() == SessionStore.UNSUPPORTED) { // set default session app.setSessionStore(SessionStore.memory(Cookie.session("jooby.sid"))); @@ -89,6 +86,7 @@ public void ready(SneakyThrows.Consumer2 onReady) { 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); @@ -143,6 +141,17 @@ public void ready(SneakyThrows.Consumer2 onReady) { } 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); } } diff --git a/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index 0fe8c320d6..64508f6ba5 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -1534,7 +1534,7 @@ public void maxRequestSize(ServerTestRunner runner) { runner .options( new ServerOptions() - .setBuffer(new OutputOptions().setSize(ServerOptions._16KB / 2)) + .setOutput(new OutputOptions().setSize(ServerOptions._16KB / 2)) .setMaxRequestSize(ServerOptions._16KB)) .define( app -> { From 3dbbddd794e564ec76f089c606f481913ef79a7c Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 20 Jul 2025 13:05:51 -0300 Subject: [PATCH 55/60] router: move more options to corresponding class --- .../main/java/io/jooby/DefaultContext.java | 4 +- jooby/src/main/java/io/jooby/Jooby.java | 17 ----- jooby/src/main/java/io/jooby/Router.java | 67 +++--------------- .../src/main/java/io/jooby/RouterOptions.java | 69 +++++++++++++++++++ .../java/io/jooby/internal/RouterImpl.java | 35 ++-------- .../test/java/io/jooby/i1937/Issue1937.java | 37 ---------- .../test/java/io/jooby/test/FeaturedTest.java | 4 +- .../test/java/io/jooby/test/HttpsTest.java | 5 +- 8 files changed, 91 insertions(+), 147 deletions(-) diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 9668f3a74d..c380ef36c9 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -361,7 +361,9 @@ default long getRequestLength() { @Override default String getHostAndPort() { Optional header = - getRouter().isTrustProxy() ? header("X-Forwarded-Host").toOptional() : Optional.empty(); + getRouter().getRouterOptions().isTrustProxy() + ? header("X-Forwarded-Host").toOptional() + : Optional.empty(); var value = header.orElseGet( () -> diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index e608832348..c1eaeabe3f 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -450,11 +450,6 @@ public ServerOptions getServerOptions() { return router.getServerOptions(); } - @Override - public boolean isTrustProxy() { - return router.isTrustProxy(); - } - @Override public boolean isStarted() { return started.get(); @@ -465,12 +460,6 @@ public boolean isStopped() { return stopped.get(); } - @NonNull @Override - public Jooby setTrustProxy(boolean trustProxy) { - this.router.setTrustProxy(trustProxy); - return this; - } - @NonNull @Override public Route.Set domain(@NonNull String domain, @NonNull Router subrouter) { return this.router.domain(domain, subrouter); @@ -863,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); diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index a7dda31a51..fc2f749733 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -204,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: * @@ -280,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) * *********************************************************************************************** @@ -304,7 +252,8 @@ 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. * @@ -328,7 +277,8 @@ 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. @@ -352,7 +302,7 @@ 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. * @@ -379,7 +329,8 @@ 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. * @@ -852,9 +803,7 @@ default Object execute(@NonNull Context context) { @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. */ diff --git a/jooby/src/main/java/io/jooby/RouterOptions.java b/jooby/src/main/java/io/jooby/RouterOptions.java index 300026e57e..41d5a2eca3 100644 --- a/jooby/src/main/java/io/jooby/RouterOptions.java +++ b/jooby/src/main/java/io/jooby/RouterOptions.java @@ -5,6 +5,8 @@ */ package io.jooby; +import edu.umd.cs.findbugs.annotations.NonNull; + /** * Router options: * @@ -38,6 +40,25 @@ public class RouterOptions { /** 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). @@ -178,4 +199,52 @@ public static RouterOptions defaults() { 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/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 0169a3c6a6..e67d9c5a18 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -45,7 +45,6 @@ import io.jooby.output.OutputFactory; import io.jooby.problem.ProblemDetailsHandler; import io.jooby.value.ValueFactory; -import jakarta.inject.Provider; public class RouterImpl implements Router { @@ -159,10 +158,6 @@ public Stack executor(Executor executor) { private RouterOptions routerOptions = RouterOptions.defaults(); - private boolean trustProxy; - - private boolean contextAsService; - private boolean started; private boolean stopped; @@ -232,11 +227,6 @@ public List getRoutes() { return routes; } - @Override - public boolean isTrustProxy() { - return trustProxy; - } - @Override public boolean isStarted() { return started; @@ -247,15 +237,12 @@ 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 @@ -548,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; @@ -812,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; } 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/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java index 64508f6ba5..7ca636fc30 100644 --- a/tests/src/test/java/io/jooby/test/FeaturedTest.java +++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java @@ -3820,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()); }) @@ -3871,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/HttpsTest.java b/tests/src/test/java/io/jooby/test/HttpsTest.java index 6bda11db21..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; @@ -96,7 +97,7 @@ public void forceSSL(ServerTestRunner runner) { .options(new ServerOptions().setSecurePort(8443)) .define( app -> { - app.setTrustProxy(true); + app.setRouterOptions(new RouterOptions().setTrustProxy(true)); app.setContextPath("/secure"); app.before(new SSLHandler()); @@ -131,7 +132,7 @@ public void forceSSL2(ServerTestRunner runner) { .options(new ServerOptions().setSecurePort(8443)) .define( app -> { - app.setTrustProxy(true); + app.setRouterOptions(new RouterOptions().setTrustProxy(true)); app.before(new SSLHandler()); From 27a36501cc7e9d464019aa2aae099e809eb5b320 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Tue, 22 Jul 2025 16:27:03 -0300 Subject: [PATCH 56/60] open-api: javadoc --- .../main/java/io/jooby/DefaultContext.java | 2 +- .../main/java/io/jooby/ForwardingContext.java | 2 +- ...utput.java => ByteBufferOutputStatic.java} | 24 +++++++----- .../output/CompsiteByteBufferOutput.java | 3 +- .../io/jooby/output/ByteBufferOutput.java | 2 + .../jooby/output/ByteBufferOutputFactory.java | 15 +++++-- .../java/io/jooby/output/OutputFactory.java | 39 +++++-------------- .../test/java/io/jooby/output/OutputTest.java | 4 +- .../avaje/jsonb/AvajeJsonbEncoderBench.java | 12 ++++-- .../main/java/io/jooby/jetty/JettyServer.java | 5 +++ .../internal/netty/NettyOutputFactory.java | 22 +++++++++++ modules/jooby-openapi/pom.xml | 6 +++ .../io/jooby/undertow/UndertowServer.java | 5 +++ tests/src/test/java/examples/Performance.java | 13 +++++-- 14 files changed, 98 insertions(+), 56 deletions(-) rename jooby/src/main/java/io/jooby/internal/output/{ByteBufferWrappedOutput.java => ByteBufferOutputStatic.java} (69%) diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index c380ef36c9..7f5d7f980e 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -693,6 +693,6 @@ default Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { @Override default OutputFactory getOutputFactory() { - return getRouter().getOutputFactory(); + return getRouter().getOutputFactory().getContextOutputFactory(); } } diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index 9453d8eb45..6b5fe4693b 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -672,7 +672,7 @@ public Router getRouter() { @Override public OutputFactory getOutputFactory() { - return ctx.getOutputFactory(); + return ctx.getOutputFactory().getContextOutputFactory(); } @Override diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java b/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutputStatic.java similarity index 69% rename from jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java rename to jooby/src/main/java/io/jooby/internal/output/ByteBufferOutputStatic.java index 8c64efe090..c4518f89d9 100644 --- a/jooby/src/main/java/io/jooby/internal/output/ByteBufferWrappedOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutputStatic.java @@ -7,18 +7,25 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; +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 ByteBufferWrappedOutput implements Output { +public class ByteBufferOutputStatic implements Output { - private final ByteBuffer buffer; + private final int size; + private final Supplier provider; - public ByteBufferWrappedOutput(ByteBuffer buffer) { - this.buffer = buffer; + public ByteBufferOutputStatic(int size, Supplier provider) { + this.size = size; + this.provider = provider; + } + + public ByteBufferOutputStatic(ByteBuffer byteBuffer) { + this(byteBuffer.remaining(), () -> byteBuffer); } @Override @@ -38,23 +45,22 @@ public Output write(byte[] source, int offset, int length) { @Override public Output clear() { - buffer.clear(); return this; } @Override public int size() { - return buffer.remaining(); + return size; } @Override public void transferTo(@NonNull SneakyThrows.Consumer consumer) { - consumer.accept(buffer); + consumer.accept(asByteBuffer()); } @Override public ByteBuffer asByteBuffer() { - return buffer.duplicate(); + return provider.get(); } @Override @@ -69,6 +75,6 @@ public String toString() { @Override public void send(Context ctx) { - ctx.send(buffer); + ctx.send(asByteBuffer()); } } diff --git a/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java b/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java index 4562504d3e..2db2861e4c 100644 --- a/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java @@ -86,7 +86,6 @@ public void send(Context ctx) { private void addChunk(ByteBuffer chunk) { chunks.add(chunk); - int length = chunk.remaining(); - size += length; + size += chunk.remaining(); } } diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferOutput.java b/jooby/src/main/java/io/jooby/output/ByteBufferOutput.java index d0acea8f09..0932848d98 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/output/ByteBufferOutput.java @@ -108,6 +108,8 @@ public Output write(@NonNull ByteBuffer source) { @Override public Output clear() { + this.writePosition = 0; + this.readPosition = 0; this.buffer.clear(); return this; } diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java b/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java index e557ff7fe1..52710d070b 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java @@ -6,9 +6,10 @@ 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.ByteBufferWrappedOutput; +import io.jooby.internal.output.ByteBufferOutputStatic; import io.jooby.internal.output.CompsiteByteBufferOutput; /** @@ -45,18 +46,24 @@ public Output newCompositeOutput() { return new CompsiteByteBufferOutput(); } + @Override + public Output wrap(@NonNull String value, @NonNull Charset charset) { + return new ByteBufferOutputStatic(ByteBuffer.wrap(value.getBytes(charset))); + } + @Override public Output wrap(@NonNull ByteBuffer buffer) { - return new ByteBufferWrappedOutput(buffer); + return new ByteBufferOutputStatic(buffer.remaining(), () -> buffer); } @Override public Output wrap(@NonNull byte[] bytes) { - return new ByteBufferWrappedOutput(ByteBuffer.wrap(bytes)); + return new ByteBufferOutputStatic(bytes.length, () -> ByteBuffer.wrap(bytes)); } @Override public Output wrap(@NonNull byte[] bytes, int offset, int length) { - return new ByteBufferWrappedOutput(ByteBuffer.wrap(bytes, offset, length)); + return new ByteBufferOutputStatic( + length - offset, () -> ByteBuffer.wrap(bytes, offset, length)); } } diff --git a/jooby/src/main/java/io/jooby/output/OutputFactory.java b/jooby/src/main/java/io/jooby/output/OutputFactory.java index a3b9072b8b..3f08362642 100644 --- a/jooby/src/main/java/io/jooby/output/OutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/OutputFactory.java @@ -5,8 +5,6 @@ */ package io.jooby.output; -import static java.lang.ThreadLocal.withInitial; - import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -21,34 +19,6 @@ */ public interface OutputFactory { - /** - * Thread local for output buffer. Please note only store calls to {@link #newOutput()}, {@link - * #newCompositeOutput()} are not saved into thread local. - * - * @param factory Factory. - * @return Thread local factory. - */ - static OutputFactory threadLocal(OutputFactory factory) { - return new ForwardingOutputFactory(factory) { - private final ThreadLocal threadLocal = withInitial(factory::newOutput); - - @Override - public Output newOutput(boolean direct, int size) { - return threadLocal.get().clear(); - } - - @Override - public Output newOutput(int size) { - return threadLocal.get().clear(); - } - - @Override - public Output newOutput() { - return threadLocal.get().clear(); - } - }; - } - /** * Default output factory, backed by {@link ByteBuffer}. * @@ -148,4 +118,13 @@ default Output wrap(@NonNull String value, @NonNull Charset charset) { * @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. + */ + default OutputFactory getContextOutputFactory() { + return this; + } } diff --git a/jooby/src/test/java/io/jooby/output/OutputTest.java b/jooby/src/test/java/io/jooby/output/OutputTest.java index 4ac3de0726..771b6a5bde 100644 --- a/jooby/src/test/java/io/jooby/output/OutputTest.java +++ b/jooby/src/test/java/io/jooby/output/OutputTest.java @@ -15,7 +15,7 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; -import io.jooby.internal.output.ByteBufferWrappedOutput; +import io.jooby.internal.output.ByteBufferOutputStatic; import io.jooby.internal.output.CompsiteByteBufferOutput; public class OutputTest { @@ -104,7 +104,7 @@ public void chunkedOutput() { @Test public void wrapOutput() throws IOException { var bytes = "xxHello World!xx".getBytes(StandardCharsets.UTF_8); - var output = new ByteBufferWrappedOutput(ByteBuffer.wrap(bytes, 2, bytes.length - 4)); + var output = new ByteBufferOutputStatic(ByteBuffer.wrap(bytes, 2, bytes.length - 4)); assertEquals("Hello World!", output.asString(StandardCharsets.UTF_8)); assertEquals(12, output.size()); } diff --git a/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java b/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java index 15053ba5bd..c61a661eee 100644 --- a/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java +++ b/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java @@ -43,18 +43,24 @@ public void setup() { @Benchmark public void withJsonBuffer() { - jsonb.toJsonBytes(message); + factory.wrap(jsonb.toJsonBytes(message)); } @Benchmark - public void witCachedBufferedOutput() { + public void withCachedBufferedOutput() { var buffer = cache.get().clear(); jsonb.toJson(message, jsonb.writer(new BufferedJsonOutput(buffer))); } @Benchmark - public void witBufferedOutput() { + public void withBufferedOutput() { var buffer = factory.newOutput(1024); jsonb.toJson(message, jsonb.writer(new BufferedJsonOutput(buffer))); } + + @Benchmark + public void withCompositeOutput() { + var buffer = factory.newCompositeOutput(); + jsonb.toJson(message, jsonb.writer(new BufferedJsonOutput(buffer))); + } } 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 7132be3ced..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 @@ -91,6 +91,11 @@ public OutputFactory getOutputFactory() { return outputFactory; } + public JettyServer setOutputFactory(OutputFactory outputFactory) { + this.outputFactory = outputFactory; + return this; + } + @NonNull @Override public String getName() { return NAME; 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 index 43a8645d86..69dfa68feb 100644 --- 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 @@ -17,6 +17,23 @@ import io.netty.util.ResourceLeakDetector; public class NettyOutputFactory implements OutputFactory { + + private static class NettyContextOutputFactory extends NettyOutputFactory { + public NettyContextOutputFactory(ByteBufAllocator allocator, OutputOptions options) { + super(allocator, options); + } + + @Override + @NonNull public Output wrap(@NonNull byte[] bytes) { + return new NettyOutputDefault(Unpooled.wrappedBuffer(bytes)); + } + + @Override + @NonNull public Output wrap(@NonNull byte[] bytes, int offset, int length) { + return new NettyOutputDefault(Unpooled.wrappedBuffer(bytes, offset, length)); + } + } + private static final String LEAK_DETECTION = "io.netty.leakDetection.level"; static { @@ -81,4 +98,9 @@ public Output wrap(@NonNull String value, @NonNull Charset charset) { @NonNull public Output newCompositeOutput() { return new NettyOutputDefault(allocator.compositeBuffer(48)); } + + @Override + @NonNull public OutputFactory getContextOutputFactory() { + return new NettyContextOutputFactory(allocator, options); + } } diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 7c62c03cd6..0c19f82cc0 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -23,6 +23,12 @@ ${jooby.version} + + com.puppycrawl.tools + checkstyle + 10.26.1 + + io.jooby jooby-kotlin 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 7427d7e2c6..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 @@ -84,6 +84,11 @@ public OutputFactory getOutputFactory() { return outputFactory; } + public UndertowServer setOutputFactory(OutputFactory outputFactory) { + this.outputFactory = outputFactory; + return this; + } + @NonNull @Override public String getName() { return NAME; diff --git a/tests/src/test/java/examples/Performance.java b/tests/src/test/java/examples/Performance.java index 3dd6775250..290970a677 100644 --- a/tests/src/test/java/examples/Performance.java +++ b/tests/src/test/java/examples/Performance.java @@ -12,7 +12,7 @@ import io.jooby.Jooby; import io.jooby.StatusCode; -import io.jooby.undertow.UndertowServer; +import io.jooby.netty.NettyServer; public class Performance extends Jooby { @@ -21,10 +21,12 @@ public class Performance extends Jooby { private static final byte[] MESSAGE_BYTES = MESSAGE.getBytes(StandardCharsets.UTF_8); { - var message = getOutputFactory().wrap(MESSAGE_BYTES); + var outputFactory = getOutputFactory(); + var message = outputFactory.wrap(MESSAGE_BYTES); get( "/plaintext", ctx -> { + // return ctx.send(outputFactory.wrap(MESSAGE_BYTES)); return ctx.send(message); }); @@ -34,13 +36,16 @@ public class Performance extends Jooby { get("/queries", ctx -> ctx.send(StatusCode.OK)); - get("/fortuxnes", 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(), EVENT_LOOP, Performance::new); + // runApp(args, new + // UndertowServer().setOutputFactory(OutputFactory.threadLocal(OutputFactory.create())), + // EVENT_LOOP, Performance::new); + runApp(args, new NettyServer(), EVENT_LOOP, Performance::new); } } From 5ea74d6026d9c3ec8a7702806a152a23384f8e66 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 23 Jul 2025 12:36:27 -0300 Subject: [PATCH 57/60] output-api: refactor API - Make Output more simply, so it allows reuse and sharing - Added BufferedOutput for buffers --- .../main/java/io/jooby/DefaultContext.java | 2 +- .../main/java/io/jooby/ForwardingContext.java | 2 +- .../main/java/io/jooby/ServerSentMessage.java | 2 +- .../internal/handler/ChunkedSubscriber.java | 2 +- ...BufferOutput.java => CompositeOutput.java} | 30 ++-- .../internal/output/ContextOutputFactory.java | 40 +++++ .../internal/output/OutputOutputStream.java | 8 +- .../jooby/internal/output/OutputWriter.java | 7 +- ...ferOutputStatic.java => StaticOutput.java} | 37 +--- .../jooby/internal/output/WrappedOutput.java | 49 ++++++ .../java/io/jooby/output/BufferedOutput.java | 158 ++++++++++++++++++ .../jooby/output/ByteBufferOutputFactory.java | 69 -------- ...ferOutput.java => ByteBufferedOutput.java} | 66 ++++---- .../output/ByteBufferedOutputFactory.java | 74 ++++++++ .../jooby/output/ForwardingOutputFactory.java | 14 +- .../src/main/java/io/jooby/output/Output.java | 145 ++-------------- .../java/io/jooby/output/OutputFactory.java | 22 ++- .../java/io/jooby/ServerSentMessageTest.java | 10 +- .../jooby/output/ByteBufferedOutputTest.java | 39 +++++ .../test/java/io/jooby/output/Issue3434.java | 4 +- .../test/java/io/jooby/output/OutputTest.java | 34 ++-- .../jooby/avaje/jsonb/AvajeJsonbModule.java | 2 +- .../avaje/jsonb/BufferedJsonOutput.java | 6 +- .../avaje/jsonb/AvajeJsonbEncoderBench.java | 10 +- .../avaje/jsonb/AvajeJsonbModuleTest.java | 2 +- .../freemarker/FreemarkerTemplateEngine.java | 2 +- .../freemarker/FreemarkerModuleTest.java | 56 ++++--- .../main/java/io/jooby/gson/GsonModule.java | 2 +- .../test/java/io/jooby/gson/Issue3434.java | 4 +- .../handlebars/HandlebarsTemplateEngine.java | 2 +- .../handlebars/HandlebarsModuleTest.java | 11 +- .../java/io/jooby/jackson/JacksonModule.java | 2 +- .../java/io/jooby/jackson/JacksonBench.java | 4 +- .../jooby/jackson/JacksonJsonModuleTest.java | 6 +- .../jooby/internal/jetty/JettyWebSocket.java | 3 +- .../internal/jte/BufferedTemplateOutput.java | 6 +- .../jooby/internal/jte/JteModelEncoder.java | 2 +- .../java/io/jooby/jte/JteTemplateEngine.java | 2 +- .../jte/BufferedTemplateOutputTest.java | 12 +- .../src/test/java/io/jooby/jte/Issue3599.java | 10 +- .../src/test/java/io/jooby/jte/Issue3602.java | 6 +- ...utDefault.java => NettyByteBufOutput.java} | 32 ++-- ...utputByteBuf.java => NettyByteBufRef.java} | 24 +-- .../internal/netty/NettyOutputFactory.java | 53 +++--- .../internal/netty/NettyOutputStatic.java | 46 ++--- .../io/jooby/internal/netty/NettySender.java | 2 +- .../netty/NettyServerSentEmitter.java | 2 +- .../jooby/internal/netty/NettyWebSocket.java | 2 +- .../internal/netty/NettyWrappedOutput.java | 26 +++ .../io/jooby/pebble/PebbleTemplateEngine.java | 2 +- .../io/jooby/pebble/PebbleModuleTest.java | 40 +++-- .../io/jooby/rocker/BufferedRockerOutput.java | 7 +- .../java/io/jooby/rocker/RockerHandler.java | 34 ---- .../thymeleaf/ThymeleafTemplateEngine.java | 2 +- .../internal/undertow/UndertowContext.java | 1 + .../java/io/jooby/yasson/YassonModule.java | 2 +- .../io/jooby/yasson/YassonModuleTest.java | 7 +- 57 files changed, 689 insertions(+), 557 deletions(-) rename jooby/src/main/java/io/jooby/internal/output/{CompsiteByteBufferOutput.java => CompositeOutput.java} (74%) create mode 100644 jooby/src/main/java/io/jooby/internal/output/ContextOutputFactory.java rename jooby/src/main/java/io/jooby/internal/output/{ByteBufferOutputStatic.java => StaticOutput.java} (53%) create mode 100644 jooby/src/main/java/io/jooby/internal/output/WrappedOutput.java create mode 100644 jooby/src/main/java/io/jooby/output/BufferedOutput.java delete mode 100644 jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java rename jooby/src/main/java/io/jooby/output/{ByteBufferOutput.java => ByteBufferedOutput.java} (81%) create mode 100644 jooby/src/main/java/io/jooby/output/ByteBufferedOutputFactory.java create mode 100644 jooby/src/test/java/io/jooby/output/ByteBufferedOutputTest.java rename modules/jooby-netty/src/main/java/io/jooby/internal/netty/{NettyOutputDefault.java => NettyByteBufOutput.java} (52%) rename modules/jooby-netty/src/main/java/io/jooby/internal/netty/{NettyOutputByteBuf.java => NettyByteBufRef.java} (64%) create mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyWrappedOutput.java delete mode 100644 modules/jooby-rocker/src/main/java/io/jooby/rocker/RockerHandler.java diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 7f5d7f980e..34ac7811fd 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -693,6 +693,6 @@ default Context sendError(@NonNull Throwable cause, @NonNull StatusCode code) { @Override default OutputFactory getOutputFactory() { - return getRouter().getOutputFactory().getContextOutputFactory(); + return getRouter().getOutputFactory().getContextFactory(); } } diff --git a/jooby/src/main/java/io/jooby/ForwardingContext.java b/jooby/src/main/java/io/jooby/ForwardingContext.java index 6b5fe4693b..98f4cb92a9 100644 --- a/jooby/src/main/java/io/jooby/ForwardingContext.java +++ b/jooby/src/main/java/io/jooby/ForwardingContext.java @@ -672,7 +672,7 @@ public Router getRouter() { @Override public OutputFactory getOutputFactory() { - return ctx.getOutputFactory().getContextOutputFactory(); + return ctx.getOutputFactory().getContextFactory(); } @Override diff --git a/jooby/src/main/java/io/jooby/ServerSentMessage.java b/jooby/src/main/java/io/jooby/ServerSentMessage.java index 14ca5119ca..540f8c34f6 100644 --- a/jooby/src/main/java/io/jooby/ServerSentMessage.java +++ b/jooby/src/main/java/io/jooby/ServerSentMessage.java @@ -137,7 +137,7 @@ public ServerSentMessage(@NonNull Object data) { var route = ctx.getRoute(); var encoder = route.getEncoder(); var bufferFactory = ctx.getOutputFactory(); - var buffer = bufferFactory.newOutput(); + var buffer = bufferFactory.allocate(); if (id != null) { buffer.write(ID); 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 6867ec8e1f..e7e600c56d 100644 --- a/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java +++ b/jooby/src/main/java/io/jooby/internal/handler/ChunkedSubscriber.java @@ -117,7 +117,7 @@ public void onComplete() { } private static Output prepend(Context ctx, Output data, byte c) { - var buffer = ctx.getOutputFactory().newCompositeOutput(); + var buffer = ctx.getOutputFactory().newComposite(); buffer.write(c); data.transferTo(buffer::write); return buffer; diff --git a/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java b/jooby/src/main/java/io/jooby/internal/output/CompositeOutput.java similarity index 74% rename from jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java rename to jooby/src/main/java/io/jooby/internal/output/CompositeOutput.java index 2db2861e4c..dfd6241587 100644 --- a/jooby/src/main/java/io/jooby/internal/output/CompsiteByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/internal/output/CompositeOutput.java @@ -6,16 +6,21 @@ package io.jooby.internal.output; import java.nio.ByteBuffer; -import java.nio.charset.Charset; 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.Output; +import io.jooby.output.BufferedOutput; -public class CompsiteByteBufferOutput implements Output { +/** + * 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; @@ -25,25 +30,25 @@ public int size() { } @Override - public Output write(byte b) { + public BufferedOutput write(byte b) { addChunk(ByteBuffer.wrap(new byte[] {b})); return this; } @Override - public Output write(byte[] source) { + public BufferedOutput write(byte[] source) { addChunk(ByteBuffer.wrap(source)); return this; } @Override - public Output write(byte[] source, int offset, int length) { + public BufferedOutput write(byte[] source, int offset, int length) { addChunk(ByteBuffer.wrap(source, offset, length)); return this; } @Override - public Output clear() { + public BufferedOutput clear() { chunks.forEach(ByteBuffer::clear); chunks.clear(); return this; @@ -57,18 +62,13 @@ public Output clear() { @Override public ByteBuffer asByteBuffer() { var buf = ByteBuffer.allocate(size); - chunks.forEach(buf::put); + for (ByteBuffer chunk : chunks) { + buf.put(chunk.duplicate()); + } buf.flip(); return buf; } - @Override - public String asString(@NonNull Charset charset) { - var sb = new StringBuilder(); - chunks.forEach(bytes -> sb.append(charset.decode(bytes))); - return sb.toString(); - } - @Override public void transferTo(@NonNull SneakyThrows.Consumer consumer) { chunks.forEach(consumer); diff --git a/jooby/src/main/java/io/jooby/internal/output/ContextOutputFactory.java b/jooby/src/main/java/io/jooby/internal/output/ContextOutputFactory.java new file mode 100644 index 0000000000..7634431e95 --- /dev/null +++ b/jooby/src/main/java/io/jooby/internal/output/ContextOutputFactory.java @@ -0,0 +1,40 @@ +/* + * 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.nio.charset.Charset; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.output.ByteBufferedOutputFactory; +import io.jooby.output.Output; +import io.jooby.output.OutputOptions; + +public 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)); + } +} diff --git a/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java index eb38732151..aeea74deec 100644 --- a/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java +++ b/jooby/src/main/java/io/jooby/internal/output/OutputOutputStream.java @@ -9,20 +9,21 @@ 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 Output#asOutputStream() + * @see BufferedOutput#asOutputStream() */ public class OutputOutputStream extends OutputStream { - private final Output output; + private final BufferedOutput output; private boolean closed; - public OutputOutputStream(@NonNull Output output) { + public OutputOutputStream(@NonNull BufferedOutput output) { this.output = output; } @@ -46,7 +47,6 @@ public void close() throws IOException { return; } this.closed = true; - output.clear(); } private void checkClosed() throws IOException { diff --git a/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java b/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java index f49779657d..c0279a1eba 100644 --- a/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java +++ b/jooby/src/main/java/io/jooby/internal/output/OutputWriter.java @@ -11,14 +11,14 @@ import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; public class OutputWriter extends Writer { - private final Output output; + private final BufferedOutput output; private final Charset charset; private boolean closed; - public OutputWriter(@NonNull Output output, @NonNull Charset charset) { + public OutputWriter(@NonNull BufferedOutput output, @NonNull Charset charset) { this.output = output; this.charset = charset; } @@ -61,7 +61,6 @@ public void close() throws IOException { return; } this.closed = true; - output.clear(); } private void checkClosed() throws IOException { diff --git a/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutputStatic.java b/jooby/src/main/java/io/jooby/internal/output/StaticOutput.java similarity index 53% rename from jooby/src/main/java/io/jooby/internal/output/ByteBufferOutputStatic.java rename to jooby/src/main/java/io/jooby/internal/output/StaticOutput.java index c4518f89d9..2dc2620875 100644 --- a/jooby/src/main/java/io/jooby/internal/output/ByteBufferOutputStatic.java +++ b/jooby/src/main/java/io/jooby/internal/output/StaticOutput.java @@ -6,7 +6,6 @@ package io.jooby.internal.output; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.util.function.Supplier; import edu.umd.cs.findbugs.annotations.NonNull; @@ -14,40 +13,20 @@ import io.jooby.SneakyThrows; import io.jooby.output.Output; -public class ByteBufferOutputStatic implements Output { +public class StaticOutput implements Output { private final int size; private final Supplier provider; - public ByteBufferOutputStatic(int size, Supplier provider) { + public StaticOutput(int size, Supplier provider) { this.size = size; this.provider = provider; } - public ByteBufferOutputStatic(ByteBuffer byteBuffer) { + public StaticOutput(ByteBuffer byteBuffer) { this(byteBuffer.remaining(), () -> byteBuffer); } - @Override - public Output write(byte b) { - throw new UnsupportedOperationException(); - } - - @Override - public Output write(byte[] source) { - throw new UnsupportedOperationException(); - } - - @Override - public Output write(byte[] source, int offset, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public Output clear() { - return this; - } - @Override public int size() { return size; @@ -60,12 +39,8 @@ public void transferTo(@NonNull SneakyThrows.Consumer consumer) { @Override public ByteBuffer asByteBuffer() { - return provider.get(); - } - - @Override - public String asString(@NonNull Charset charset) { - return charset.decode(asByteBuffer()).toString(); + var buffer = provider.get(); + return buffer.slice().asReadOnlyBuffer(); } @Override @@ -75,6 +50,6 @@ public String toString() { @Override public void send(Context ctx) { - ctx.send(asByteBuffer()); + 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/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/ByteBufferOutputFactory.java b/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java deleted file mode 100644 index 52710d070b..0000000000 --- a/jooby/src/main/java/io/jooby/output/ByteBufferOutputFactory.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.ByteBufferOutputStatic; -import io.jooby.internal.output.CompsiteByteBufferOutput; - -/** - * An output factory backed by {@link ByteBuffer}. - * - * @author edgar - * @since 4.0.0 - */ -public class ByteBufferOutputFactory implements OutputFactory { - private OutputOptions options; - - public ByteBufferOutputFactory(OutputOptions options) { - this.options = options; - } - - @Override - public OutputOptions getOptions() { - return options; - } - - @Override - public OutputFactory setOptions(OutputOptions options) { - this.options = options; - return this; - } - - @Override - public Output newOutput(boolean direct, int size) { - return new ByteBufferOutput(direct, size); - } - - @Override - public Output newCompositeOutput() { - return new CompsiteByteBufferOutput(); - } - - @Override - public Output wrap(@NonNull String value, @NonNull Charset charset) { - return new ByteBufferOutputStatic(ByteBuffer.wrap(value.getBytes(charset))); - } - - @Override - public Output wrap(@NonNull ByteBuffer buffer) { - return new ByteBufferOutputStatic(buffer.remaining(), () -> buffer); - } - - @Override - public Output wrap(@NonNull byte[] bytes) { - return new ByteBufferOutputStatic(bytes.length, () -> ByteBuffer.wrap(bytes)); - } - - @Override - public Output wrap(@NonNull byte[] bytes, int offset, int length) { - return new ByteBufferOutputStatic( - length - offset, () -> ByteBuffer.wrap(bytes, offset, length)); - } -} diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferOutput.java b/jooby/src/main/java/io/jooby/output/ByteBufferedOutput.java similarity index 81% rename from jooby/src/main/java/io/jooby/output/ByteBufferOutput.java rename to jooby/src/main/java/io/jooby/output/ByteBufferedOutput.java index 0932848d98..e3448a8b4a 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferOutput.java +++ b/jooby/src/main/java/io/jooby/output/ByteBufferedOutput.java @@ -6,7 +6,6 @@ package io.jooby.output; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.util.Iterator; import java.util.List; @@ -14,22 +13,25 @@ import io.jooby.Context; import io.jooby.SneakyThrows; -public class ByteBufferOutput implements Output { +/** + * 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 capacity; - private int readPosition; private int writePosition; - public ByteBufferOutput(boolean direct, int capacity) { + public ByteBufferedOutput(boolean direct, int capacity) { this.buffer = allocate(capacity, direct); - this.capacity = this.buffer.remaining(); } @Override @@ -37,13 +39,6 @@ public int size() { return this.writePosition - this.readPosition; } - private void ensureWritable(int length) { - if (length > writableByteCount()) { - int newCapacity = calculateCapacity(this.writePosition + length); - setCapacity(newCapacity); - } - } - @Override public void transferTo(@NonNull SneakyThrows.Consumer consumer) { consumer.accept(asByteBuffer()); @@ -54,22 +49,13 @@ public void transferTo(@NonNull SneakyThrows.Consumer consumer) { return List.of(asByteBuffer()).iterator(); } - private int writableByteCount() { - return this.capacity - this.writePosition; - } - @Override public @NonNull ByteBuffer asByteBuffer() { - return this.buffer.duplicate().position(this.readPosition).limit(this.writePosition); + return this.buffer.slice(this.readPosition, size()).asReadOnlyBuffer(); } @Override - public String asString(@NonNull Charset charset) { - return charset.decode(asByteBuffer()).toString(); - } - - @Override - public Output write(byte b) { + public BufferedOutput write(byte b) { ensureWritable(1); this.buffer.put(this.writePosition, b); this.writePosition += 1; @@ -77,12 +63,12 @@ public Output write(byte b) { } @Override - public Output write(byte[] source) { + public BufferedOutput write(byte[] source) { return write(source, 0, source.length); } @Override - public Output write(byte[] source, int offset, int length) { + public BufferedOutput write(byte[] source, int offset, int length) { ensureWritable(length); var tmp = this.buffer.duplicate(); @@ -95,7 +81,7 @@ public Output write(byte[] source, int offset, int length) { } @Override - public Output write(@NonNull ByteBuffer source) { + public BufferedOutput write(@NonNull ByteBuffer source) { ensureWritable(source.remaining()); var length = source.remaining(); var tmp = this.buffer.duplicate(); @@ -107,7 +93,7 @@ public Output write(@NonNull ByteBuffer source) { } @Override - public Output clear() { + public BufferedOutput clear() { this.writePosition = 0; this.readPosition = 0; this.buffer.clear(); @@ -116,7 +102,7 @@ public Output clear() { @Override public void send(Context ctx) { - ctx.send(asByteBuffer()); + ctx.send(this.buffer.slice(this.readPosition, size())); } @Override @@ -128,7 +114,11 @@ public String toString() { + ", size=" + this.size() + ", capacity=" - + this.capacity; + + this.buffer.capacity(); + } + + private int writableByteCount() { + return this.buffer.capacity() - this.writePosition; } /** Calculate the capacity of the buffer. */ @@ -148,7 +138,7 @@ private int calculateCapacity(int neededCapacity) { while (newCapacity < neededCapacity) { newCapacity <<= 1; } - return Math.min(newCapacity, MAX_CAPACITY); + return newCapacity; } } @@ -159,7 +149,7 @@ private void setCapacity(int newCapacity) { } var readPosition = this.readPosition; var writePosition = this.writePosition; - var oldCapacity = this.capacity; + var oldCapacity = this.buffer.capacity(); if (newCapacity > oldCapacity) { var oldBuffer = this.buffer; @@ -168,7 +158,7 @@ private void setCapacity(int newCapacity) { newBuffer.position(0).limit(oldBuffer.capacity()); newBuffer.put(oldBuffer); newBuffer.clear(); - setNativeBuffer(newBuffer); + this.buffer = newBuffer; } else if (newCapacity < oldCapacity) { var oldBuffer = this.buffer; var newBuffer = allocate(newCapacity, oldBuffer.isDirect()); @@ -185,13 +175,15 @@ private void setCapacity(int newCapacity) { this.readPosition = newCapacity; this.writePosition = newCapacity; } - setNativeBuffer(newBuffer); + this.buffer = newBuffer; } } - private void setNativeBuffer(ByteBuffer buffer) { - this.buffer = buffer; - this.capacity = buffer.capacity(); + private void ensureWritable(int length) { + if (length > writableByteCount()) { + int newCapacity = calculateCapacity(this.writePosition + length); + setCapacity(newCapacity); + } } private static ByteBuffer allocate(int capacity, boolean direct) { 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..950b354f3c --- /dev/null +++ b/jooby/src/main/java/io/jooby/output/ByteBufferedOutputFactory.java @@ -0,0 +1,74 @@ +/* + * 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.ContextOutputFactory; +import io.jooby.internal.output.StaticOutput; + +/** + * An output factory backed by {@link ByteBuffer}. + * + * @author edgar + * @since 4.0.0 + */ +public class ByteBufferedOutputFactory implements OutputFactory { + 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 index f79e38f39a..d843f65f15 100644 --- a/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/ForwardingOutputFactory.java @@ -29,24 +29,24 @@ public OutputOptions getOptions() { } @Override - public OutputFactory setOptions(OutputOptions options) { + public OutputFactory setOptions(@NonNull OutputOptions options) { delegate.setOptions(options); return this; } @Override - public Output newOutput(int size) { - return delegate.newOutput(size); + public BufferedOutput allocate(int size) { + return delegate.allocate(size); } @Override - public Output newOutput(boolean direct, int size) { - return delegate.newOutput(direct, size); + public BufferedOutput allocate(boolean direct, int size) { + return delegate.allocate(direct, size); } @Override - public Output newCompositeOutput() { - return delegate.newCompositeOutput(); + public BufferedOutput newComposite() { + return delegate.newComposite(); } @Override diff --git a/jooby/src/main/java/io/jooby/output/Output.java b/jooby/src/main/java/io/jooby/output/Output.java index 57dd405ad4..c7fd3254a9 100644 --- a/jooby/src/main/java/io/jooby/output/Output.java +++ b/jooby/src/main/java/io/jooby/output/Output.java @@ -5,89 +5,40 @@ */ 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 java.util.ArrayList; import java.util.Iterator; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.SneakyThrows; -import io.jooby.internal.output.OutputOutputStream; -import io.jooby.internal.output.OutputWriter; /** - * Buffered output used to support multiple implementations like byte array, byte buffer, netty - * buffers. - * - *

There are two implementations of output one is backed by a {@link ByteBuffer} and the other is - * a view of multiple {@link ByteBuffer} byffers. See {@link OutputFactory#newOutput()} and {@link - * OutputFactory#newCompositeOutput()} + * Output used to support multiple implementations like byte array, byte buffer, netty buffers. * * @author edgar * @since 4.0.0 + * @see BufferedOutput */ public interface 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); - } - - /** - * A view of internal bytes as {@link byte buffer} changes made to the buffer are reflected in - * this output. + * A read-only view as {@link ByteBuffer}. * - * @return A byte byffer. + * @return A read-only byte buffer. */ ByteBuffer asByteBuffer(); /** - * A view of internal bytes as string. - * - * @param charset Charset to use. - * @return A string. - */ - String asString(@NonNull Charset charset); - - /** - * Transfers the entire buffered output (one or multiple buffers) to a consumer. + * 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 byte buffers. + * An iterator over read-only byte buffers. * - * @return An iterator over byte buffers. + * @return An iterator over read-only byte buffers. */ default Iterator iterator() { var list = new ArrayList(); @@ -98,88 +49,14 @@ default Iterator iterator() { /** * Total size in number of bytes of the output. * - * @return size + * @return Total size in number of bytes of the output. */ int size(); /** - * Write a single byte into this buffer at the current writing position. - * - * @param b the byte to be written - * @return this output - */ - Output write(byte b); - - /** - * Write the given source into this buffer, starting at the current writing position of this - * buffer. + * Send the output to the client. * - * @param source the bytes to be written into this buffer - * @return this output + * @param ctx Context. */ - Output 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 - */ - Output 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 Output 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 Output 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 Output 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); - } - } - - default Output write(@NonNull CharBuffer source, @NonNull Charset charset) { - if (!source.isEmpty()) { - return write(charset.encode(source)); - } - return this; - } - void send(io.jooby.Context ctx); - - Output clear(); } diff --git a/jooby/src/main/java/io/jooby/output/OutputFactory.java b/jooby/src/main/java/io/jooby/output/OutputFactory.java index 3f08362642..c7a60177ff 100644 --- a/jooby/src/main/java/io/jooby/output/OutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/OutputFactory.java @@ -24,8 +24,8 @@ public interface OutputFactory { * * @return Default output factory. */ - static OutputFactory create(OutputOptions options) { - return new ByteBufferOutputFactory(options); + static OutputFactory create(@NonNull OutputOptions options) { + return new ByteBufferedOutputFactory(options); } /** @@ -39,7 +39,7 @@ static OutputFactory create() { OutputOptions getOptions(); - OutputFactory setOptions(OutputOptions options); + OutputFactory setOptions(@NonNull OutputOptions options); /** * Creates a new byte buffered output. @@ -48,7 +48,7 @@ static OutputFactory create() { * @param size Output size. * @return A byte buffered output. */ - Output newOutput(boolean direct, int size); + BufferedOutput allocate(boolean direct, int size); /** * Creates a new byte buffered output. @@ -56,12 +56,12 @@ static OutputFactory create() { * @param size Output size. * @return A byte buffered output. */ - default Output newOutput(int size) { - return newOutput(getOptions().isDirectBuffers(), size); + default BufferedOutput allocate(int size) { + return allocate(getOptions().isDirectBuffers(), size); } - default Output newOutput() { - return newOutput(getOptions().isDirectBuffers(), getOptions().getSize()); + default BufferedOutput allocate() { + return allocate(getOptions().isDirectBuffers(), getOptions().getSize()); } /** @@ -70,7 +70,7 @@ default Output newOutput() { * * @return A new composite buffer. */ - Output newCompositeOutput(); + BufferedOutput newComposite(); /** * Readonly buffer created from string utf-8 bytes. @@ -124,7 +124,5 @@ default Output wrap(@NonNull String value, @NonNull Charset charset) { * * @return Same or custom implementation. */ - default OutputFactory getContextOutputFactory() { - return this; - } + OutputFactory getContextFactory(); } diff --git a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java index b705bd5865..bd04cfdb86 100644 --- a/jooby/src/test/java/io/jooby/ServerSentMessageTest.java +++ b/jooby/src/test/java/io/jooby/ServerSentMessageTest.java @@ -35,7 +35,9 @@ public void shouldFormatMessage() throws Exception { when(ctx.getRoute()).thenReturn(route); var message = new ServerSentMessage(data); - assertEquals("data: " + data + "\n\n", message.encode(ctx).asString(StandardCharsets.UTF_8)); + assertEquals( + "data: " + data + "\n\n", + StandardCharsets.UTF_8.decode(message.encode(ctx).asByteBuffer()).toString()); } @Test @@ -57,7 +59,7 @@ public void shouldFormatMultiLineMessage() throws Exception { var message = new ServerSentMessage(data); assertEquals( "data: line 1\ndata: line ,a .. 2\ndata: line ...abc 3\n\n", - message.encode(ctx).asString(StandardCharsets.UTF_8)); + StandardCharsets.UTF_8.decode(message.encode(ctx).asByteBuffer()).toString()); } @Test @@ -77,6 +79,8 @@ public void shouldFormatMessageEndingWithNL() throws Exception { when(ctx.getRoute()).thenReturn(route); var message = new ServerSentMessage(data); - assertEquals("data: " + data + "\n\n", message.encode(ctx).asString(StandardCharsets.UTF_8)); + assertEquals( + "data: " + data + "\n\n", + StandardCharsets.UTF_8.decode(message.encode(ctx).asByteBuffer()).toString()); } } 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/output/Issue3434.java b/jooby/src/test/java/io/jooby/output/Issue3434.java index 7bf4ff1166..047afd27ae 100644 --- a/jooby/src/test/java/io/jooby/output/Issue3434.java +++ b/jooby/src/test/java/io/jooby/output/Issue3434.java @@ -142,10 +142,10 @@ void shouldWriteCharBufferOnBufferWriter() throws IOException { private String writeCharSequence(Charset charset, SneakyThrows.Consumer writer) throws IOException { - var buffer = factory.newOutput(); + var buffer = factory.allocate(); try (var out = buffer.asWriter(charset)) { writer.accept(out); - return buffer.asString(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 index 771b6a5bde..bfad698a61 100644 --- a/jooby/src/test/java/io/jooby/output/OutputTest.java +++ b/jooby/src/test/java/io/jooby/output/OutputTest.java @@ -15,8 +15,8 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; -import io.jooby.internal.output.ByteBufferOutputStatic; -import io.jooby.internal.output.CompsiteByteBufferOutput; +import io.jooby.internal.output.CompositeOutput; +import io.jooby.internal.output.StaticOutput; public class OutputTest { @@ -28,7 +28,8 @@ public void bufferedOutput() { buffered.write("Hello".getBytes(StandardCharsets.UTF_8)); buffered.write(" "); buffered.write("World!"); - assertEquals("Hello World!", buffered.asString(StandardCharsets.UTF_8)); + assertEquals( + "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(12, buffered.size()); assertEquals( "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); @@ -38,13 +39,14 @@ public void bufferedOutput() { StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(30, buffered.size()); }, - new ByteBufferOutput(false, 3)); + 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!", buffered.asString(StandardCharsets.UTF_8)); + assertEquals( + "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(12, buffered.size()); assertEquals( "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); @@ -54,27 +56,28 @@ public void bufferedOutput() { StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(30, buffered.size()); }, - new ByteBufferOutput(false, 255)); + 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!", buffered.asString(StandardCharsets.UTF_8)); + assertEquals( + "Hello World!", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(12, buffered.size()); }, - new ByteBufferOutput(false, 255)); + new ByteBufferedOutput(false, 255)); output( buffered -> { buffered.write((byte) 'A'); - assertEquals("A", buffered.asString(StandardCharsets.UTF_8)); + assertEquals("A", StandardCharsets.UTF_8.decode(buffered.asByteBuffer()).toString()); assertEquals(1, buffered.size()); }, - new ByteBufferOutput(false, 255)); + new ByteBufferedOutput(false, 255)); } - private void output(SneakyThrows.Consumer consumer, Output... buffers) { + private void output(SneakyThrows.Consumer consumer, BufferedOutput... buffers) { Stream.of(buffers).forEach(consumer); } @@ -95,17 +98,18 @@ public void chunkedOutput() { assertEquals(8, chunked.size()); chunked.write(buffer); assertEquals(12, chunked.size()); - assertEquals("Hello World!", chunked.asString(StandardCharsets.UTF_8)); + assertEquals( + "Hello World!", StandardCharsets.UTF_8.decode(chunked.asByteBuffer()).toString()); assertEquals(12, chunked.size()); }, - new CompsiteByteBufferOutput()); + new CompositeOutput()); } @Test public void wrapOutput() throws IOException { var bytes = "xxHello World!xx".getBytes(StandardCharsets.UTF_8); - var output = new ByteBufferOutputStatic(ByteBuffer.wrap(bytes, 2, bytes.length - 4)); - assertEquals("Hello World!", output.asString(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/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 6ee5961284..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 @@ -108,7 +108,7 @@ public Object decode(@NonNull Context ctx, @NonNull Type type) throws Exception public Output encode(@NonNull Context ctx, @NonNull Object value) { ctx.setDefaultResponseType(MediaType.json); var factory = ctx.getOutputFactory(); - var buffer = factory.newOutput(); + var buffer = factory.allocate(); try (var writer = jsonb.writer(new BufferedJsonOutput(buffer))) { jsonb.toJson(value, writer); return buffer; diff --git a/modules/jooby-avaje-jsonb/src/main/java/io/jooby/internal/avaje/jsonb/BufferedJsonOutput.java b/modules/jooby-avaje-jsonb/src/main/java/io/jooby/internal/avaje/jsonb/BufferedJsonOutput.java index d10231c814..98943c2f86 100644 --- a/modules/jooby-avaje-jsonb/src/main/java/io/jooby/internal/avaje/jsonb/BufferedJsonOutput.java +++ b/modules/jooby-avaje-jsonb/src/main/java/io/jooby/internal/avaje/jsonb/BufferedJsonOutput.java @@ -9,12 +9,12 @@ import java.io.OutputStream; import io.avaje.json.stream.JsonOutput; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; public class BufferedJsonOutput implements JsonOutput { - private final Output output; + private final BufferedOutput output; - public BufferedJsonOutput(Output output) { + public BufferedJsonOutput(BufferedOutput output) { this.output = output; } diff --git a/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java b/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java index c61a661eee..f4305f1863 100644 --- a/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java +++ b/modules/jooby-avaje-jsonb/src/test/java/io/jooby/avaje/jsonb/AvajeJsonbEncoderBench.java @@ -12,7 +12,7 @@ import io.avaje.jsonb.Jsonb; import io.jooby.internal.avaje.jsonb.BufferedJsonOutput; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.jooby.output.OutputFactory; import io.jooby.output.OutputOptions; @@ -28,10 +28,10 @@ public class AvajeJsonbEncoderBench { private Map message; private OutputFactory factory; - private ThreadLocal cache = + private ThreadLocal cache = ThreadLocal.withInitial( () -> { - return factory.newOutput(1024); + return factory.allocate(1024); }); @Setup @@ -54,13 +54,13 @@ public void withCachedBufferedOutput() { @Benchmark public void withBufferedOutput() { - var buffer = factory.newOutput(1024); + var buffer = factory.allocate(1024); jsonb.toJson(message, jsonb.writer(new BufferedJsonOutput(buffer))); } @Benchmark public void withCompositeOutput() { - var buffer = factory.newCompositeOutput(); + 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 3f2a0d0563..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.asString(StandardCharsets.UTF_8)); + assertEquals("[1,2,3]", StandardCharsets.UTF_8.decode(json.asByteBuffer()).toString()); } } 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 9719be94c5..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 @@ -34,7 +34,7 @@ public List extensions() { @Override public Output render(Context ctx, ModelAndView modelAndView) throws Exception { - var buffer = ctx.getOutputFactory().newOutput(); + 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 b6d87579cd..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.asString(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)) - .asString(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"))) - .asString(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)) - .asString(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.asString(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.asString(StandardCharsets.UTF_8).trim()); + assertEquals("var", StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString().trim()); } } 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 c4f7ef3621..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 @@ -108,7 +108,7 @@ public Object decode(@NonNull Context ctx, @NonNull Type type) throws Exception @NonNull @Override public Output encode(@NonNull Context ctx, @NonNull Object value) { - var buffer = ctx.getOutputFactory().newOutput(); + 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 e20a1aa62e..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 @@ -146,6 +146,8 @@ void shouldEncodeUsingBufferWriter() { var encoder = new GsonModule(); var result = encoder.encode(ctx, new Bean3434(text)); - assertEquals(gson.toJson(new Bean3434(text)), result.asString(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 6285b79c0c..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 @@ -42,7 +42,7 @@ public Output render(Context ctx, ModelAndView modelAndView) throws Exception .resolver(resolvers) .build() .data(ctx.getAttributes()); - var buffer = ctx.getOutputFactory().newOutput(); + 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 0ebb761f16..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.asString(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.asString(StandardCharsets.UTF_8).trim()); + assertEquals( + "Hello foo bar var!", + StandardCharsets.UTF_8.decode(output.asByteBuffer()).toString().trim()); } } 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 ff50761dbc..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 @@ -156,7 +156,7 @@ public void install(@NonNull Jooby application) { public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { var factory = ctx.getOutputFactory(); ctx.setDefaultResponseType(mediaType); - // let jackson uses his own cache, so just wrap the bytes + // let jackson uses his own cache, so wrap the bytes return factory.wrap(mapper.writeValueAsBytes(value)); } 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 index 1baf3982bb..3c8f5dbba5 100644 --- a/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java +++ b/modules/jooby-jackson/src/test/java/io/jooby/jackson/JacksonBench.java @@ -13,7 +13,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.jooby.output.OutputFactory; import io.jooby.output.OutputOptions; @@ -28,7 +28,7 @@ public class JacksonBench { private Map message; private OutputFactory factory; - private ThreadLocal cache = ThreadLocal.withInitial(() -> factory.newOutput(1024)); + private ThreadLocal cache = ThreadLocal.withInitial(() -> factory.allocate(1024)); @Setup public void setup() { 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 65e571199a..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 @@ -34,7 +34,7 @@ public void renderJson() throws Exception { JacksonModule jackson = new JacksonModule(new ObjectMapper()); var buffer = jackson.encode(ctx, mapOf("k", "v")); - assertEquals("{\"k\":\"v\"}", buffer.asString(StandardCharsets.UTF_8)); + assertEquals("{\"k\":\"v\"}", StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); verify(ctx).setDefaultResponseType(MediaType.json); } @@ -63,7 +63,9 @@ public void renderXml() throws Exception { JacksonModule jackson = new JacksonModule(new XmlMapper()); var buffer = jackson.encode(ctx, mapOf("k", "v")); - assertEquals("v", buffer.asString(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/JettyWebSocket.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyWebSocket.java index d6ba8d3e25..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 @@ -289,7 +289,8 @@ public WebSocket sendBinary(@NonNull ByteBuffer message, @NonNull WriteCallback @NonNull @Override public WebSocket send(@NonNull Output message, @NonNull WriteCallback callback) { return sendMessage( - (remote, writeCallback) -> remote.sendText(message.asString(UTF_8), writeCallback), + (remote, writeCallback) -> + remote.sendText(UTF_8.decode(message.asByteBuffer()).toString(), writeCallback), new WriteCallbackAdaptor(this, callback)); } diff --git a/modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.java b/modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.java index 54f0713bd8..6129c812dc 100644 --- a/modules/jooby-jte/src/main/java/io/jooby/internal/jte/BufferedTemplateOutput.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.output.Output; +import io.jooby.output.BufferedOutput; public class BufferedTemplateOutput implements TemplateOutput { - private final Output buffer; + private final BufferedOutput buffer; private final Charset charset; - public BufferedTemplateOutput(Output 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 d85fc35a5e..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 @@ -17,7 +17,7 @@ public class JteModelEncoder implements io.jooby.MessageEncoder { @Nullable @Override public Output encode(@NonNull Context ctx, @NonNull Object value) throws Exception { if (value instanceof JteModel jte) { - var buffer = ctx.getOutputFactory().newOutput(); + var buffer = ctx.getOutputFactory().allocate(); jte.render(new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8)); return buffer; } 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 fb11f0ee0d..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 @@ -33,7 +33,7 @@ public List extensions() { @Override public Output render(Context ctx, ModelAndView modelAndView) { - var buffer = ctx.getOutputFactory().newOutput(); + var buffer = ctx.getOutputFactory().allocate(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); var attributes = ctx.getAttributes(); if (modelAndView instanceof MapModelAndView mapModelAndView) { 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 index 007e7114f1..16f80889a9 100644 --- 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 @@ -19,27 +19,27 @@ public class BufferedTemplateOutputTest { @Test public void checkWriteContent() { var factory = OutputFactory.create(OutputOptions.small()); - var buffer = factory.newOutput(); + var buffer = factory.allocate(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeContent("Hello"); - assertEquals("Hello", buffer.asString(StandardCharsets.UTF_8)); + assertEquals("Hello", StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); } @Test public void checkWriteContentSubstring() { var factory = OutputFactory.create(OutputOptions.small()); - var buffer = factory.newOutput(); + var buffer = factory.allocate(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeContent(" Hello World! ", 1, " Hello World! ".length() - 2); - assertEquals("Hello World", buffer.asString(StandardCharsets.UTF_8)); + assertEquals("Hello World", StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); } @Test public void checkWriteBinaryContent() { var factory = OutputFactory.create(OutputOptions.small()); - var buffer = factory.newOutput(); + var buffer = factory.allocate(); var output = new BufferedTemplateOutput(buffer, StandardCharsets.UTF_8); output.writeBinaryContent("Hello".getBytes(StandardCharsets.UTF_8)); - assertEquals("Hello", buffer.asString(StandardCharsets.UTF_8)); + assertEquals("Hello", StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); } } 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 9f2fe97fbc..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,7 +19,7 @@ import io.jooby.Context; import io.jooby.MapModelAndView; import io.jooby.ModelAndView; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.jooby.output.OutputFactory; public class Issue3599 { @@ -27,8 +27,8 @@ public class Issue3599 { @Test public void shouldNotCallObjectMethodOnMapModels() { var bufferFactory = mock(OutputFactory.class); - var buffer = mock(Output.class); - when(bufferFactory.newOutput()).thenReturn(buffer); + var buffer = mock(BufferedOutput.class); + when(bufferFactory.allocate()).thenReturn(buffer); var attributes = Map.of("foo", 1); var mapModel = new HashMap(); @@ -54,8 +54,8 @@ public void shouldNotCallObjectMethodOnMapModels() { @Test public void shouldCallObjectMethodOnObjectModels() { var bufferFactory = mock(OutputFactory.class); - var buffer = mock(Output.class); - when(bufferFactory.newOutput()).thenReturn(buffer); + var buffer = mock(BufferedOutput.class); + when(bufferFactory.allocate()).thenReturn(buffer); var attributes = Map.of("foo", 1); var model = new Issue3599(); 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 43afa02914..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 @@ -16,7 +16,7 @@ import gg.jte.models.runtime.JteModel; import io.jooby.Context; import io.jooby.internal.jte.JteModelEncoder; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.jooby.output.OutputFactory; public class Issue3602 { @@ -24,8 +24,8 @@ public class Issue3602 { @Test public void shouldRenderJteModel() throws Exception { var bufferFactory = mock(OutputFactory.class); - var buffer = mock(Output.class); - when(bufferFactory.newOutput()).thenReturn(buffer); + var buffer = mock(BufferedOutput.class); + when(bufferFactory.allocate()).thenReturn(buffer); var attributes = Map.of("foo", 1); var ctx = mock(Context.class); diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputDefault.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufOutput.java similarity index 52% rename from modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputDefault.java rename to modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufOutput.java index 55f3ca0f1c..c3cf434f43 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputDefault.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufOutput.java @@ -9,54 +9,60 @@ import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.output.Output; +import io.jooby.output.BufferedOutput; import io.netty.buffer.ByteBuf; -public class NettyOutputDefault implements NettyOutputByteBuf { +public class NettyByteBufOutput implements BufferedOutput, NettyByteBufRef { private final ByteBuf buffer; - protected NettyOutputDefault(ByteBuf buffer) { + protected NettyByteBufOutput(ByteBuf buffer) { this.buffer = buffer; } - @NonNull public ByteBuf byteBuf() { - return this.buffer; - } - @Override - @NonNull public Output write(byte b) { + @NonNull public BufferedOutput write(byte b) { buffer.writeByte(b); return this; } @Override - @NonNull public Output write(byte[] source) { + @NonNull public BufferedOutput write(byte[] source) { buffer.writeBytes(source); return this; } @Override - @NonNull public Output write(byte[] source, int offset, int length) { + @NonNull public BufferedOutput write(byte[] source, int offset, int length) { this.buffer.writeBytes(source, offset, length); return this; } @Override - @NonNull public Output write(@NonNull String source, @NonNull Charset charset) { + @NonNull public BufferedOutput write(@NonNull String source, @NonNull Charset charset) { this.buffer.writeBytes(source.getBytes(charset)); return this; } @Override - public Output write(@NonNull CharBuffer source, @NonNull Charset charset) { + @NonNull public BufferedOutput write(@NonNull CharBuffer source, @NonNull Charset charset) { this.buffer.writeBytes(charset.encode(source)); return this; } @Override - @NonNull public Output clear() { + @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/NettyOutputByteBuf.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufRef.java similarity index 64% rename from modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputByteBuf.java rename to modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufRef.java index 8bc2e835a4..fce4d73384 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputByteBuf.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyByteBufRef.java @@ -6,7 +6,6 @@ package io.jooby.internal.netty; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; @@ -15,41 +14,30 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -public interface NettyOutputByteBuf extends Output { +public interface NettyByteBufRef extends Output { @NonNull ByteBuf byteBuf(); - @Override - @NonNull default ByteBuffer asByteBuffer() { - return byteBuf().nioBuffer(); - } - - @Override - @NonNull default String asString(@NonNull Charset charset) { - return byteBuf().toString(charset); - } - @Override default void transferTo(@NonNull SneakyThrows.Consumer consumer) { consumer.accept(asByteBuffer()); } @Override - default int size() { - return byteBuf().readableBytes(); + @NonNull default ByteBuffer asByteBuffer() { + return byteBuf().slice().nioBuffer().asReadOnlyBuffer(); } - @Override default void send(Context ctx) { - if (ctx instanceof NettyContext netty) { + if (ctx.getClass() == NettyContext.class) { var buf = byteBuf(); - netty.send(buf, Integer.toString(buf.readableBytes())); + ((NettyContext) ctx).send(buf, Integer.toString(buf.readableBytes())); } else { ctx.send(asByteBuffer()); } } static ByteBuf byteBuf(Output output) { - if (output instanceof NettyOutputByteBuf netty) { + 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/NettyOutputFactory.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyOutputFactory.java index 69dfa68feb..e03b5a78b2 100644 --- 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 @@ -9,6 +9,7 @@ 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; @@ -18,30 +19,40 @@ 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 byte[] bytes) { - return new NettyOutputDefault(Unpooled.wrappedBuffer(bytes)); + @NonNull public Output wrap(@NonNull String value, @NonNull Charset charset) { + return new NettyWrappedOutput(Unpooled.wrappedBuffer(value.getBytes(charset))); } @Override - @NonNull public Output wrap(@NonNull byte[] bytes, int offset, int length) { - return new NettyOutputDefault(Unpooled.wrappedBuffer(bytes, offset, length)); + @NonNull public Output wrap(@NonNull ByteBuffer buffer) { + return new NettyWrappedOutput(Unpooled.wrappedBuffer(buffer)); } - } - private static final String LEAK_DETECTION = "io.netty.leakDetection.level"; + @Override + @NonNull public Output wrap(@NonNull byte[] bytes) { + return new NettyWrappedOutput(Unpooled.wrappedBuffer(bytes)); + } - static { - System.setProperty( - LEAK_DETECTION, - System.getProperty(LEAK_DETECTION, ResourceLeakDetector.Level.DISABLED.name())); - ResourceLeakDetector.setLevel( - ResourceLeakDetector.Level.valueOf(System.getProperty(LEAK_DETECTION))); + @Override + @NonNull public Output wrap(@NonNull byte[] bytes, int offset, int length) { + return new NettyWrappedOutput(Unpooled.wrappedBuffer(bytes, offset, length)); + } } private final ByteBufAllocator allocator; @@ -57,19 +68,19 @@ public ByteBufAllocator getAllocator() { } @Override - public OutputOptions getOptions() { + @NonNull public OutputOptions getOptions() { return options; } @Override - public OutputFactory setOptions(OutputOptions options) { + @NonNull public OutputFactory setOptions(@NonNull OutputOptions options) { this.options = options; return this; } @Override - public @NonNull Output newOutput(boolean direct, int size) { - return new NettyOutputDefault( + public @NonNull BufferedOutput allocate(boolean direct, int size) { + return new NettyByteBufOutput( direct ? this.allocator.directBuffer(size) : this.allocator.heapBuffer(size)); } @@ -79,8 +90,8 @@ public OutputFactory setOptions(OutputOptions options) { } @Override - public Output wrap(@NonNull String value, @NonNull Charset charset) { - return new NettyOutputDefault(Unpooled.wrappedBuffer(value.getBytes(charset))); + @NonNull public Output wrap(@NonNull String value, @NonNull Charset charset) { + return new NettyByteBufOutput(Unpooled.wrappedBuffer(value.getBytes(charset))); } @Override @@ -95,12 +106,12 @@ public Output wrap(@NonNull String value, @NonNull Charset charset) { } @Override - @NonNull public Output newCompositeOutput() { - return new NettyOutputDefault(allocator.compositeBuffer(48)); + @NonNull public BufferedOutput newComposite() { + return new NettyByteBufOutput(allocator.compositeBuffer(48)); } @Override - @NonNull public OutputFactory getContextOutputFactory() { + @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 index 769ace53e9..d1f5b1c2c4 100644 --- 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 @@ -5,65 +5,39 @@ */ package io.jooby.internal.netty; -import java.nio.CharBuffer; -import java.nio.charset.Charset; import java.util.function.Supplier; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; -import io.jooby.output.Output; import io.netty.buffer.ByteBuf; import io.netty.util.AsciiString; -public class NettyOutputStatic implements NettyOutputByteBuf { +public class NettyOutputStatic implements NettyByteBufRef { private final Supplier provider; + private final int size; private final AsciiString contentLength; protected NettyOutputStatic(int length, Supplier provider) { this.provider = provider; - this.contentLength = AsciiString.of(Integer.toString(length)); - } - - @NonNull public ByteBuf byteBuf() { - return provider.get(); - } - - @Override - @NonNull public Output write(byte b) { - throw new UnsupportedOperationException(); - } - - @Override - @NonNull public Output write(byte[] source) { - throw new UnsupportedOperationException(); + this.size = length; + this.contentLength = AsciiString.cached(Integer.toString(length)); } @Override - @NonNull public Output write(byte[] source, int offset, int length) { - throw new UnsupportedOperationException(); + public int size() { + return size; } - @Override - @NonNull public Output write(@NonNull String source, @NonNull Charset charset) { - throw new UnsupportedOperationException(); + @NonNull public ByteBuf byteBuf() { + return provider.get(); } @Override public void send(Context ctx) { - if (ctx instanceof NettyContext netty) { - netty.send(provider.get(), contentLength); + if (ctx.getClass() == NettyContext.class) { + ((NettyContext) ctx).send(provider.get(), contentLength); } else { ctx.send(asByteBuffer()); } } - - @Override - public Output write(@NonNull CharBuffer source, @NonNull Charset charset) { - throw new UnsupportedOperationException(); - } - - @Override - @NonNull public Output clear() { - return this; - } } 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 8a9f9f4029..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,7 +5,7 @@ */ package io.jooby.internal.netty; -import static io.jooby.internal.netty.NettyOutputByteBuf.byteBuf; +import static io.jooby.internal.netty.NettyByteBufRef.byteBuf; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Sender; 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 a8e50f1697..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,7 +5,7 @@ */ package io.jooby.internal.netty; -import static io.jooby.internal.netty.NettyOutputByteBuf.byteBuf; +import static io.jooby.internal.netty.NettyByteBufRef.byteBuf; import java.util.UUID; import java.util.concurrent.TimeUnit; 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 05f9f8e27f..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,7 +5,7 @@ */ package io.jooby.internal.netty; -import static io.jooby.internal.netty.NettyOutputByteBuf.byteBuf; +import static io.jooby.internal.netty.NettyByteBufRef.byteBuf; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; 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-pebble/src/main/java/io/jooby/pebble/PebbleTemplateEngine.java b/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleTemplateEngine.java index 3ce2d3b0bc..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 @@ -36,7 +36,7 @@ public List extensions() { @Override public Output render(Context ctx, ModelAndView modelAndView) throws Exception { if (modelAndView instanceof MapModelAndView mapModelAndView) { - var buffer = ctx.getOutputFactory().newOutput(); + 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 f5b7b2b6e8..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.asString(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.asString(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")).asString(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"))) - .asString(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)) - .asString(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)) - .asString(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"))) - .asString(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-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java b/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java index d277a316af..ab31ef13fa 100644 --- a/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java +++ b/modules/jooby-rocker/src/main/java/io/jooby/rocker/BufferedRockerOutput.java @@ -10,6 +10,7 @@ import com.fizzed.rocker.ContentType; import com.fizzed.rocker.RockerOutput; import com.fizzed.rocker.RockerOutputFactory; +import io.jooby.output.BufferedOutput; import io.jooby.output.Output; import io.jooby.output.OutputFactory; @@ -27,9 +28,9 @@ public class BufferedRockerOutput implements RockerOutput private final ContentType contentType; /** The buffer where data is stored. */ - protected Output output; + protected BufferedOutput output; - BufferedRockerOutput(Charset charset, ContentType contentType, Output output) { + BufferedRockerOutput(Charset charset, ContentType contentType, BufferedOutput output) { this.charset = charset; this.contentType = contentType; this.output = output; @@ -73,6 +74,6 @@ public Output asOutput() { static RockerOutputFactory factory(Charset charset, OutputFactory factory) { return (contentType, charsetName) -> - new BufferedRockerOutput(charset, contentType, factory.newCompositeOutput()); + 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 a963904acd..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).asOutput()); - } catch (Throwable x) { - ctx.sendError(x); - return x; - } - }; - } -} 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 70b791e137..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 @@ -43,7 +43,7 @@ public List extensions() { if (locale == null) { locale = ctx.locale(); } - var buffer = ctx.getOutputFactory().newOutput(); + var buffer = ctx.getOutputFactory().allocate(); var context = new Context(locale, model); var templateName = modelAndView.getView(); if (!templateName.startsWith("/")) { 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 d5a7bc4455..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 @@ -491,6 +491,7 @@ 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; 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 b55fb0da02..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 @@ -104,7 +104,7 @@ public Object decode(@NonNull final Context ctx, @NonNull final Type type) throw public Output encode(@NonNull final Context ctx, @NonNull final Object value) { ctx.setDefaultResponseType(MediaType.json); var factory = ctx.getOutputFactory(); - var output = factory.newOutput(); + 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 625c027024..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 @@ -32,8 +32,7 @@ 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 €@!?"; @@ -41,10 +40,10 @@ public void render() { Context ctx = mock(Context.class); when(ctx.getOutputFactory()).thenReturn(OutputFactory.create(OutputOptions.small())); - var buffer = YassonModule.encode(ctx, user); + var buffer = yasson.encode(ctx, user); assertEquals( "{\"age\":2147483647,\"id\":-1,\"name\":\"Lorem €@!?\"}", - buffer.asString(StandardCharsets.UTF_8)); + StandardCharsets.UTF_8.decode(buffer.asByteBuffer()).toString()); verify(ctx).setDefaultResponseType(MediaType.json); } From bc0d8366fad0098807f8af0fb5453014b3e85302 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 23 Jul 2025 14:50:24 -0300 Subject: [PATCH 58/60] performance: netty optimize response headers --- .../internal/output/ContextOutputFactory.java | 40 ----------------- .../output/ByteBufferedOutputFactory.java | 29 +++++++++++- .../src/main/java/io/jooby/output/Output.java | 1 + .../jooby/internal/netty/HeadersMultiMap.java | 4 +- .../internal/netty/NettyDateService.java | 8 ++-- .../io/jooby/internal/netty/NettyHandler.java | 13 +++--- .../internal/netty/NettyOutputFactory.java | 5 --- .../internal/netty/NettyOutputStatic.java | 5 +-- .../io/jooby/internal/netty/NettyString.java | 45 +++++++++++++++++++ tests/src/test/java/examples/Performance.java | 7 +-- 10 files changed, 91 insertions(+), 66 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/internal/output/ContextOutputFactory.java create mode 100644 modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyString.java diff --git a/jooby/src/main/java/io/jooby/internal/output/ContextOutputFactory.java b/jooby/src/main/java/io/jooby/internal/output/ContextOutputFactory.java deleted file mode 100644 index 7634431e95..0000000000 --- a/jooby/src/main/java/io/jooby/internal/output/ContextOutputFactory.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.internal.output; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; - -import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.output.ByteBufferedOutputFactory; -import io.jooby.output.Output; -import io.jooby.output.OutputOptions; - -public 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)); - } -} diff --git a/jooby/src/main/java/io/jooby/output/ByteBufferedOutputFactory.java b/jooby/src/main/java/io/jooby/output/ByteBufferedOutputFactory.java index 950b354f3c..d967cbe845 100644 --- a/jooby/src/main/java/io/jooby/output/ByteBufferedOutputFactory.java +++ b/jooby/src/main/java/io/jooby/output/ByteBufferedOutputFactory.java @@ -10,8 +10,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.internal.output.CompositeOutput; -import io.jooby.internal.output.ContextOutputFactory; import io.jooby.internal.output.StaticOutput; +import io.jooby.internal.output.WrappedOutput; /** * An output factory backed by {@link ByteBuffer}. @@ -20,6 +20,33 @@ * @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) { diff --git a/jooby/src/main/java/io/jooby/output/Output.java b/jooby/src/main/java/io/jooby/output/Output.java index c7fd3254a9..3bee9efb5b 100644 --- a/jooby/src/main/java/io/jooby/output/Output.java +++ b/jooby/src/main/java/io/jooby/output/Output.java @@ -20,6 +20,7 @@ * @see BufferedOutput */ public interface Output { + /** * A read-only view as {@link ByteBuffer}. * 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 7e5c4d8735..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 @@ -490,7 +490,9 @@ static void encoderHeader(CharSequence name, CharSequence value, ByteBuf buf) { } private static void writeAscii(ByteBuf buf, int offset, CharSequence value) { - if (value instanceof AsciiString ascii) { + 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/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 5b0965354a..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,10 +69,10 @@ 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() == HttpMethod.GET) { router.match(context).execute(context); 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 index e03b5a78b2..da789b1fa3 100644 --- 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 @@ -89,11 +89,6 @@ public ByteBufAllocator getAllocator() { return new NettyOutputStatic(buffer.remaining(), () -> Unpooled.wrappedBuffer(buffer)); } - @Override - @NonNull public Output wrap(@NonNull String value, @NonNull Charset charset) { - return new NettyByteBufOutput(Unpooled.wrappedBuffer(value.getBytes(charset))); - } - @Override @NonNull public Output wrap(@NonNull byte[] bytes) { return new NettyOutputStatic(bytes.length, () -> Unpooled.wrappedBuffer(bytes)); 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 index d1f5b1c2c4..337b9c8d44 100644 --- 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 @@ -10,17 +10,16 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.netty.buffer.ByteBuf; -import io.netty.util.AsciiString; public class NettyOutputStatic implements NettyByteBufRef { private final Supplier provider; private final int size; - private final AsciiString contentLength; + private final NettyString contentLength; protected NettyOutputStatic(int length, Supplier provider) { this.provider = provider; this.size = length; - this.contentLength = AsciiString.cached(Integer.toString(length)); + this.contentLength = new NettyString(Integer.toString(length)); } @Override 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/tests/src/test/java/examples/Performance.java b/tests/src/test/java/examples/Performance.java index 290970a677..95206a21f9 100644 --- a/tests/src/test/java/examples/Performance.java +++ b/tests/src/test/java/examples/Performance.java @@ -8,8 +8,6 @@ import static io.jooby.ExecutionMode.EVENT_LOOP; import static io.jooby.MediaType.JSON; -import java.nio.charset.StandardCharsets; - import io.jooby.Jooby; import io.jooby.StatusCode; import io.jooby.netty.NettyServer; @@ -18,15 +16,12 @@ public class Performance extends Jooby { private static final String MESSAGE = "Hello, World!"; - private static final byte[] MESSAGE_BYTES = MESSAGE.getBytes(StandardCharsets.UTF_8); - { var outputFactory = getOutputFactory(); - var message = outputFactory.wrap(MESSAGE_BYTES); + var message = outputFactory.wrap(MESSAGE); get( "/plaintext", ctx -> { - // return ctx.send(outputFactory.wrap(MESSAGE_BYTES)); return ctx.send(message); }); From 2807a2367d554196d70ec891e0034aaf5c82406a Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 23 Jul 2025 20:45:28 -0300 Subject: [PATCH 59/60] open-api: mock of javadoc --- modules/jooby-openapi/pom.xml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 0c19f82cc0..8954921a5c 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -23,12 +23,6 @@ ${jooby.version} - - com.puppycrawl.tools - checkstyle - 10.26.1 - - io.jooby jooby-kotlin @@ -140,6 +134,13 @@ 1.17.5 test + + + com.puppycrawl.tools + checkstyle + 10.26.1 + test + From 8b3ee28212f6f321226412e4241403b60a51733d Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 23 Jul 2025 21:44:03 -0300 Subject: [PATCH 60/60] doc: migration notes for 4.x --- docs/asciidoc/body.adoc | 2 +- docs/asciidoc/index.adoc | 1 + docs/asciidoc/migration.adoc | 1 + docs/asciidoc/migration/4.x.adoc | 80 ++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 docs/asciidoc/migration/4.x.adoc 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` +|===