{
- @Nullable
- T extract(JsonObject json, String key) throws Exception;
-}
-
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/FilterException.java b/src/main/java/com/zepben/vertxutils/json/filter/FilterException.java
deleted file mode 100644
index 1825923..0000000
--- a/src/main/java/com/zepben/vertxutils/json/filter/FilterException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.json.filter;
-
-import com.zepben.vertxutils.json.filter.parser.Token;
-import org.apache.commons.lang3.StringUtils;
-
-public class FilterException extends Exception {
-
- public FilterException(String specification, int from, Token... expected) {
- super(String.format("Error parsing [%s]. After [%s] expected one of [%s] but found [%s]",
- specification, specification.substring(0, from), StringUtils.join(expected, ","), specification.substring(from)));
- }
-
- FilterException(String message) {
- super(message);
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/FilterException.kt b/src/main/java/com/zepben/vertxutils/json/filter/FilterException.kt
new file mode 100644
index 0000000..4c476a6
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/json/filter/FilterException.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.json.filter
+
+import com.zepben.vertxutils.json.filter.parser.Token
+
+class FilterException(specification: String, from: Int, vararg expected: Token) : Exception(
+ "Error parsing [$specification]. After [${specification.substring(0, from)}] expected one of [${
+ expected.joinToString(",")
+ }] but found [${specification.substring(from)}]",
+)
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.java b/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.java
deleted file mode 100644
index 2c6a993..0000000
--- a/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.json.filter;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import com.zepben.vertxutils.json.filter.parser.Node;
-import com.zepben.vertxutils.json.filter.parser.Parser;
-
-import java.util.Optional;
-
-/**
- * FilterSpecification
- *
- * Specifies json fields to filter. Can be used to specify fields to exclude or to specify fields to include (i.e., exclude everything but).
- *
- * For example: a.b.c or a.b.c,x.y.x or a(b,c(d)) or -a.b.c or -a.b.c,-x.y.x or -a(b,c(d))
- *
- */
-@EverythingIsNonnullByDefault
-@SuppressWarnings({"WeakerAccess"})
-public class FilterSpecification {
-
- private Node root;
-
- public FilterSpecification() {
- root = new Node();
- }
-
- public FilterSpecification(String filter) throws FilterException {
- root = Parser.parse(filter);
- }
-
- private FilterSpecification(Node root) {
- this.root = root;
- }
-
- public Node getRoot() {
- return root;
- }
-
- public void setRoot(Node root) {
- this.root = root;
- }
-
- public String getFilter() {
- return root.toString();
- }
-
- @Override
- public String toString() {
- return root.toString();
- }
-
- public Optional getSubfilter(String location) {
- Node descendant = root.getDescendent(location);
- if (descendant == null)
- return Optional.empty();
- else
- return Optional.of(new FilterSpecification(descendant));
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.kt b/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.kt
new file mode 100644
index 0000000..3eb2cb3
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/json/filter/FilterSpecification.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.json.filter
+
+import com.zepben.vertxutils.json.filter.parser.Node
+import com.zepben.vertxutils.json.filter.parser.Parser.parse
+
+/**
+ * **FilterSpecification**
+ *
+ * Specifies json fields to filter. Can be used to specify fields to exclude or to specify fields to include (i.e., exclude everything but).
+ *
+ * For example: a.b.c or a.b.c,x.y.x or a(b,c(d)) or -a.b.c or -a.b.c,-x.y.x or -a(b,c(d))
+ */
+class FilterSpecification(
+ val root: Node = Node(),
+) {
+
+ constructor(filter: String) : this(root = parse(filter))
+
+ val filter: String
+ get() = root.toString()
+
+ override fun toString(): String = root.toString()
+
+ fun getSubfilter(location: String): FilterSpecification? =
+ root.getDescendent(location)?.let { FilterSpecification(it) }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.java b/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.java
deleted file mode 100644
index 646d516..0000000
--- a/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.json.filter;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import com.zepben.vertxutils.json.filter.parser.Node;
-import io.vertx.core.json.JsonArray;
-import io.vertx.core.json.JsonObject;
-
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-@EverythingIsNonnullByDefault
-@SuppressWarnings("WeakerAccess")
-public class JsonObjectFilter implements JsonFilter {
-
- static public JsonObject applyFilter(JsonObject object, FilterSpecification fs) {
- return new JsonObjectFilter().apply(object, fs);
- }
-
- /*
- * Applies a filter to a JsonObject. Note that this mutates the given object and returns it.
- */
- @Override
- public JsonObject apply(JsonObject object, FilterSpecification fs) {
- apply(fs.getRoot(), object);
- return object;
- }
-
- private void apply(Node node, Object object) {
- switch (node.filterType()) {
- case INCLUDE:
- includeSpecified(node, object);
- break;
- case EXCLUDE:
- excludeSpecified(node, object);
- break;
- case PASSTHROUGH:
- // Do nothing
- break;
- }
- }
-
- private void excludeSpecified(Node node, Object object) {
- if (object instanceof JsonArray) {
- for (Object value : ((JsonArray) object)) {
- apply(node, value);
- }
- } else if (object instanceof JsonObject) {
- JsonObject jsonObject = (JsonObject) object;
- for (Node child : node.children()) {
- if (child.children().isEmpty()) {
- jsonObject.remove(child.content());
- } else {
- apply(child, jsonObject.getValue(child.content()));
- }
- }
- }
- }
-
- private void includeSpecified(Node node, Object object) {
- if (node.children().isEmpty()) {
- return; // Nothing to do
- }
- if (object instanceof JsonArray) {
- for (Object value : ((JsonArray) object)) {
- apply(node, value);
- }
- } else if (object instanceof JsonObject) {
- JsonObject jsonObject = (JsonObject) object;
-
- Set fieldsToInclude = node
- .children()
- .stream()
- .map(Node::content)
- .filter(Objects::nonNull)
- .collect(Collectors.toSet());
-
- Set fieldsToRemove = new HashSet<>(jsonObject.fieldNames());
- fieldsToRemove.removeAll(fieldsToInclude);
-
- for (String fieldName : fieldsToRemove) {
- jsonObject.remove(fieldName);
- }
-
- for (Node child : node.children()) {
- String fieldName = child.content();
- Object value = jsonObject.getValue(fieldName);
- if (value != null) {
- apply(child, value);
- }
- }
- }
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.kt b/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.kt
new file mode 100644
index 0000000..12a7d53
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/json/filter/JsonObjectFilter.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.json.filter
+
+import com.zepben.vertxutils.json.filter.parser.FilterType
+import com.zepben.vertxutils.json.filter.parser.Node
+import io.vertx.core.json.JsonArray
+import io.vertx.core.json.JsonObject
+
+class JsonObjectFilter {
+
+ /**
+ * Applies a filter to a JsonObject. Note that this mutates the given object and returns it.
+ */
+ fun apply(json: JsonObject, fs: FilterSpecification): JsonObject {
+ apply(fs.root, json)
+ return json
+ }
+
+ private fun apply(node: Node, obj: Any) {
+ when (node.filterType) {
+ FilterType.INCLUDE -> includeSpecified(node, obj)
+ FilterType.EXCLUDE -> excludeSpecified(node, obj)
+ FilterType.PASSTHROUGH -> {}
+ }
+ }
+
+ private fun includeSpecified(node: Node, obj: Any) {
+ if (node.getChildren().isEmpty())
+ return // Nothing to do
+
+ when (obj) {
+ is JsonArray -> obj.forEach { value -> apply(node, value) }
+
+ is JsonObject -> {
+ val fieldsToInclude = node
+ .getChildren()
+ .asSequence()
+ .mapNotNull(Node::content)
+ .toSet()
+
+ val fieldsToRemove = obj.fieldNames() - fieldsToInclude
+ fieldsToRemove.forEach { obj.remove(it) }
+
+ node.getChildren().forEach { child ->
+ obj.getValue(child.content)?.also {
+ apply(child, it)
+ }
+ }
+ }
+ }
+ }
+
+ private fun excludeSpecified(node: Node, obj: Any) {
+ when (obj) {
+ is JsonArray -> obj.forEach { value -> apply(node, value) }
+
+ is JsonObject -> {
+ node.getChildren().forEach { child ->
+ if (child.getChildren().isEmpty()) {
+ obj.remove(child.content)
+ } else {
+ apply(child, obj.getValue(child.content))
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+
+ fun applyFilter(json: JsonObject, fs: FilterSpecification): JsonObject =
+ JsonObjectFilter().apply(json, fs)
+
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.java b/src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.kt
similarity index 57%
rename from src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.java
rename to src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.kt
index 44f9433..03e0721 100644
--- a/src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.java
+++ b/src/main/java/com/zepben/vertxutils/json/filter/parser/FilterType.kt
@@ -1,15 +1,18 @@
/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
+ * Copyright 2026 Zeppelin Bend Pty Ltd
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
+package com.zepben.vertxutils.json.filter.parser
-package com.zepben.vertxutils.json.filter.parser;
+enum class FilterType(
+ val denotedBy: String = "",
+) {
-public enum FilterType {
INCLUDE,
- EXCLUDE,
+ EXCLUDE(denotedBy = "-"),
PASSTHROUGH
+
}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.java b/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.java
deleted file mode 100644
index 75729d2..0000000
--- a/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.json.filter.parser;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import com.zepben.vertxutils.json.filter.FilterException;
-
-import javax.annotation.Nullable;
-import java.util.regex.Matcher;
-
-@EverythingIsNonnullByDefault
-class Lexer {
-
- // The string to be tokenised
- private final String specification;
-
- // Where in the string the tokenising is up to
- private int currentPosition = 0;
-
- // If a token has matched, this is the content of the token
- @Nullable
- private String currentContent;
-
- // If a token has matched, this is the token, otherwise NONE
- private Token currentToken = Token.NONE;
-
- Lexer(String specification) {
- this.specification = specification;
- }
-
- void nextToken(Token... lookingFor) throws FilterException {
-
- skipWhitespace();
-
- for (Token t : lookingFor) {
- Matcher m = t.compiledPattern
- .matcher(specification)
- .region(currentPosition, specification.length());
- if (m.find()) {
- currentContent = specification.substring(currentPosition, m.end());
- currentPosition = m.end();
- currentToken = t;
- return;
- }
- }
- throw new FilterException(specification, currentPosition, lookingFor);
- }
-
- @Nullable
- String currentContent() {
- return currentContent;
- }
-
- @Nullable
- Token currentToken() {
- return currentToken;
- }
-
- private void skipWhitespace() {
- while (currentPosition < specification.length()
- && Character.isWhitespace(specification.charAt(currentPosition))) {
- currentPosition++;
- }
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.kt b/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.kt
new file mode 100644
index 0000000..90cf728
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/json/filter/parser/Lexer.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.json.filter.parser
+
+import com.zepben.vertxutils.json.filter.FilterException
+
+/**
+ * @param specification The string to be tokenised.
+ */
+internal class Lexer(
+ private val specification: String,
+) {
+
+ /**
+ * If a token has matched, this is the content of the token.
+ */
+ var currentContent: String? = null
+ private set
+
+ /**
+ * If a token has matched, this is the token, otherwise NONE
+ */
+ var currentToken = Token.NONE
+ private set
+
+ /**
+ * Where in the string the tokenising is up to
+ */
+ private var currentPosition = 0
+
+ @Throws(FilterException::class)
+ fun nextToken(vararg lookingFor: Token) {
+ skipWhitespace()
+
+ lookingFor.forEach { t ->
+ val m = t.compiledPattern
+ .matcher(specification)
+ .region(currentPosition, specification.length)
+ if (m.find()) {
+ currentContent = specification.substring(currentPosition, m.end())
+ currentPosition = m.end()
+ currentToken = t
+ return
+ }
+ }
+ throw FilterException(specification, currentPosition, *lookingFor)
+ }
+
+ private fun skipWhitespace() {
+ while ((currentPosition < specification.length) && Character.isWhitespace(specification[currentPosition]))
+ currentPosition++
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.java b/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.java
deleted file mode 100644
index ca0f3ce..0000000
--- a/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.json.filter.parser;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.util.Collection;
-import java.util.TreeMap;
-
-import static com.zepben.vertxutils.json.filter.parser.FilterType.EXCLUDE;
-import static com.zepben.vertxutils.json.filter.parser.FilterType.PASSTHROUGH;
-
-@SuppressWarnings({"WeakerAccess"})
-public class Node {
-
- // The children of this Node (might be empty)
- private final TreeMap children = new TreeMap<>();
-
- // The string content of this node if there is any
- private String content;
-
- // Is this an exclude or include node?
- private FilterType filterType;
-
- public Node() {
- filterType = PASSTHROUGH;
- }
-
- public Node(String content) {
- this.content = content;
- }
-
- public String content() {
- return content;
- }
-
- public void setContent(String content) {
- this.content = content;
- }
-
- public FilterType filterType() {
- return filterType;
- }
-
- public void setFilterType(FilterType filterType) {
- this.filterType = filterType;
- }
-
- public Collection children() {
- return children.values();
- }
-
- public Node getChild(String name) {
- return children.get(name);
- }
-
- public Node getDescendent(String name) {
- Node descendant = this;
- for (String n : name.split("\\.")) {
- if (descendant == null) {
- return null;
- }
- descendant = descendant.getChild(n);
- }
- return descendant;
- }
-
- public Node addOrGetChild(String content) {
- Node child = children.get(content);
- if (child == null) {
- child = new Node(content);
- child.setFilterType(filterType);
- children.put(content, child);
- }
- return child;
- }
-
- public int countAllNodes() {
- int n = 1;
- for (Node child : children.values()) {
- n += child.countAllNodes();
- }
- return n;
- }
-
- @Override
- public String toString() {
- String childrenString = StringUtils.join(children.values(), ",").replaceAll("-", "");
- if (content == null) {
- return (filterType == EXCLUDE ? "-" : "") + childrenString;
- }
- if (children.isEmpty()) {
- return content;
- } else {
- return String.format(children.size() == 1 ? "%s.%s" : "%s(%s)", content, childrenString);
- }
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.kt b/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.kt
new file mode 100644
index 0000000..ee1a6a3
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/json/filter/parser/Node.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.json.filter.parser
+
+import java.util.*
+
+/**
+ * A Node in the filter tree.
+ *
+ * @property content The string content of this node if there is any.
+ * @property filterType Is this an exclude or include node?
+ */
+data class Node(
+ val content: String? = null,
+ val filterType: FilterType = FilterType.PASSTHROUGH,
+) {
+
+ // The children of this Node (might be empty)
+ private val children = TreeMap()
+
+ fun getChildren(): Collection = children.values
+
+ fun getChild(name: String): Node? = children[name]
+
+ fun getDescendent(name: String): Node? {
+ var descendant: Node? = this
+ name.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray().forEach { n ->
+ descendant ?: return null
+ descendant = descendant.getChild(n)
+ }
+ return descendant
+ }
+
+ fun addOrGetChild(content: String): Node =
+ children.getOrPut(content) { Node(content, filterType) }
+
+ fun countAllNodes(): Int =
+ children.values.sumOf { it.countAllNodes() } + 1
+
+ override fun toString(): String {
+ val childrenString = children.values.joinToString(separator = ",") { it.toString().replace("-", "") }
+ return when {
+ content == null -> "${filterType.denotedBy}$childrenString"
+ children.isEmpty() -> content
+ children.size == 1 -> "$content.$childrenString"
+ else -> "$content($childrenString)"
+ }
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.java b/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.java
deleted file mode 100644
index b5a2779..0000000
--- a/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.json.filter.parser;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import com.zepben.vertxutils.json.filter.FilterException;
-
-import static com.zepben.vertxutils.json.filter.parser.FilterType.EXCLUDE;
-import static com.zepben.vertxutils.json.filter.parser.FilterType.INCLUDE;
-import static com.zepben.vertxutils.json.filter.parser.Token.*;
-
-@EverythingIsNonnullByDefault
-public class Parser {
-
- public static Node parse(String specification)
- throws FilterException {
- Node root = new Node();
- Lexer lexer = new Lexer(specification);
-
- lexer.nextToken(IDENTIFIER, DASH);
- if (lexer.currentToken() == DASH) {
- root.setFilterType(EXCLUDE);
- lexer.nextToken(IDENTIFIER);
- } else {
- root.setFilterType(INCLUDE);
- }
-
- parseNode(root, lexer, END);
-
- return root;
- }
-
- private static void parseNode(Node node, Lexer lexer, Token endingToken) throws FilterException {
- Node root = node;
- while (lexer.currentToken() != endingToken) {
- node = node.addOrGetChild(lexer.currentContent());
- lexer.nextToken(OPEN, COMMA, DOT, endingToken);
- if (lexer.currentToken() == DOT) {
- lexer.nextToken(IDENTIFIER);
- }
- if (lexer.currentToken() == OPEN) {
- lexer.nextToken(IDENTIFIER);
- parseNode(node, lexer, CLOSE);
- lexer.nextToken(COMMA, endingToken);
- }
- if (lexer.currentToken() == COMMA) {
- lexer.nextToken(IDENTIFIER);
- parseNode(root, lexer, endingToken);
- }
- }
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.kt b/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.kt
new file mode 100644
index 0000000..475a986
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/json/filter/parser/Parser.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.json.filter.parser
+
+import com.zepben.vertxutils.json.filter.FilterException
+
+object Parser {
+
+ @Throws(FilterException::class)
+ fun parse(specification: String): Node {
+ val lexer = Lexer(specification)
+
+ lexer.nextToken(Token.IDENTIFIER, Token.DASH)
+ val root = Node(
+ filterType = if (lexer.currentToken == Token.DASH) {
+ lexer.nextToken(Token.IDENTIFIER)
+ FilterType.EXCLUDE
+ } else {
+ FilterType.INCLUDE
+ },
+ )
+
+ parseNode(root, lexer, Token.END)
+
+ return root
+ }
+
+ @Throws(FilterException::class)
+ private fun parseNode(node: Node, lexer: Lexer, endingToken: Token) {
+ var node = node
+ val root = node
+ while (lexer.currentToken != endingToken) {
+ node = node.addOrGetChild(lexer.currentContent!!)
+ lexer.nextToken(Token.OPEN, Token.COMMA, Token.DOT, endingToken)
+ if (lexer.currentToken == Token.DOT) {
+ lexer.nextToken(Token.IDENTIFIER)
+ }
+ if (lexer.currentToken == Token.OPEN) {
+ lexer.nextToken(Token.IDENTIFIER)
+ parseNode(node, lexer, Token.CLOSE)
+ lexer.nextToken(Token.COMMA, endingToken)
+ }
+ if (lexer.currentToken == Token.COMMA) {
+ lexer.nextToken(Token.IDENTIFIER)
+ parseNode(root, lexer, endingToken)
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.java b/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.java
deleted file mode 100644
index c4bfda9..0000000
--- a/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.json.filter.parser;
-
-import java.util.regex.Pattern;
-
-// The tokens that the filter specification can be tokenised into
-public enum Token {
- IDENTIFIER("[a-zA-Z][a-zA-Z0-9]*"),
- OPEN("\\("),
- CLOSE("\\)"),
- COMMA(","),
- DOT("\\."),
- END("$"),
- DASH("-"),
- NONE("");
-
- public final String pattern;
- public final Pattern compiledPattern;
-
- Token(String pattern) {
- this.pattern = pattern;
- compiledPattern = Pattern.compile("^" + pattern);
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.kt b/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.kt
new file mode 100644
index 0000000..966c58d
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/json/filter/parser/Token.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.json.filter.parser
+
+import java.util.regex.Pattern
+
+/**
+ * The tokens that the filter specification can be tokenised into.
+ */
+enum class Token(pattern: String) {
+
+ IDENTIFIER("[a-zA-Z][a-zA-Z0-9]*"),
+ OPEN("\\("),
+ CLOSE("\\)"),
+ COMMA(","),
+ DOT("\\."),
+ END("$"),
+ DASH("-"),
+ NONE("");
+
+ val compiledPattern: Pattern = Pattern.compile("^$pattern")
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/CaptureChunkedJsonResponse.java b/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/CaptureChunkedJsonResponse.java
deleted file mode 100644
index 136f7c5..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/CaptureChunkedJsonResponse.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing.ChunkedResponse;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-
-@SuppressWarnings("WeakerAccess")
-@EverythingIsNonnullByDefault
-public class CaptureChunkedJsonResponse extends ChunkedJsonResponse {
-
- private String captured = "";
-
- public CaptureChunkedJsonResponse() {
- this(DEFAULT_BUFFER_SIZE);
- }
-
- public CaptureChunkedJsonResponse(int bufferSize) {
- super(bufferSize);
- }
-
- @Override
- protected void end(StringBuilder sb) {
- captured = sb.toString();
- }
-
- @Override
- protected void send(boolean force, StringBuilder sb) {
- captured = sb.toString();
- }
-
- @Override
- public String toString() {
- return captured;
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/ChunkedJsonResponse.java b/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/ChunkedJsonResponse.java
deleted file mode 100644
index 1809faa..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/ChunkedJsonResponse.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing.ChunkedResponse;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-
-import java.util.ArrayDeque;
-import java.util.Deque;
-
-/**
- * Fluent Helper to send a JSON response in chunks.
- */
-@SuppressWarnings("WeakerAccess")
-@EverythingIsNonnullByDefault
-public abstract class ChunkedJsonResponse {
-
- public final static int DEFAULT_BUFFER_SIZE = 1 << 21;
-
- public class JsonObject {
-
- private JsonObject() {
- }
-
- public JsonArray beginArray(String key) {
- if (isNotObject())
- throw new IllegalStateException("INTERNAL ERROR: You must have an open object to begin an array.");
-
- return doAddKey(key).doBeginArray();
- }
-
- public JsonObject beginObject(String key) {
- if (isNotObject())
- throw new IllegalStateException("INTERNAL ERROR: You must have an open object to begin another object.");
-
- return doAddKey(key).doBeginObject();
- }
-
- public JsonObject addJson(String key, String json) {
- if (isNotObject())
- throw new IllegalStateException("INTERNAL ERROR: You must have an open object to add json.");
-
- return doAddKey(key).doAddJson(json);
- }
-
- public JsonObject endObject() {
- if (isNotObject())
- throw new IllegalStateException("INTERNAL ERROR: You must have an open object to end.");
-
- doEndObject(JsonObjectType.OBJECT);
- return object;
- }
-
- public JsonArray endObjectInArray() {
- if (isNotObject())
- throw new IllegalStateException("INTERNAL ERROR: You must have an open object to end.");
-
- doEndObject(JsonObjectType.ARRAY);
- return array;
- }
-
- @SuppressWarnings("UnusedReturnValue")
- public JsonObject send(boolean force) {
- ChunkedJsonResponse.this.send(force, sb);
- return this;
- }
-
- private JsonArray doBeginArray() {
- openItems.push(JsonObjectType.ARRAY);
- isFirst = true;
-
- sb.append("[");
-
- return array;
- }
-
- private JsonObject doBeginObject() {
- openItems.push(JsonObjectType.OBJECT);
- isFirst = true;
-
- sb.append("{");
-
- return object;
- }
-
- private JsonObject doAddKey(String key) {
- if (isFirst)
- isFirst = false;
- else
- sb.append(",");
-
- sb.append("\"").append(key).append("\":");
-
- return this;
- }
-
- private JsonObject doAddJson(String json) {
- sb.append(json);
- send(false);
-
- return this;
- }
-
- private void doEndObject(JsonObjectType expectedParentType) {
- openItems.pop();
- if (!openItems.isEmpty() && (openItems.peek() != expectedParentType)) {
- openItems.push(JsonObjectType.OBJECT);
- throw new IllegalStateException("INTERNAL ERROR: Incorrect end object method called, the parent is not of the expected type.");
- }
-
- sb.append("}");
- isFirst = false;
-
- if (openItems.isEmpty())
- end(sb);
- else
- send(false);
- }
-
- }
-
- public class JsonArray {
-
- private JsonArray() {
- }
-
- public JsonArray addArrayItem(String json) {
- if (isNotArray())
- throw new IllegalStateException("INTERNAL ERROR: You must have an open array to add json.");
-
- if (isFirst)
- isFirst = false;
- else
- sb.append(",");
-
- sb.append(json);
- send(false);
-
- return this;
- }
-
- public JsonObject beginObject() {
- if (isNotArray())
- throw new IllegalStateException("INTERNAL ERROR: You must have an open array to begin an object.");
-
- openItems.push(JsonObjectType.OBJECT);
-
- if (!isFirst)
- sb.append(",");
-
- isFirst = true;
- sb.append("{");
-
- return object;
- }
-
- public JsonArray beginArray() {
- if (isNotArray())
- throw new IllegalStateException("INTERNAL ERROR: You must have an open array to begin an array.");
-
- openItems.push(JsonObjectType.ARRAY);
-
- if (!isFirst)
- sb.append(",");
-
- isFirst = true;
- sb.append("[");
-
- return array;
- }
-
- public JsonObject endArray() {
- if (isNotArray())
- throw new IllegalStateException("INTERNAL ERROR: You must have an open array to end.");
-
- doEndArray(JsonObjectType.OBJECT);
- return object;
- }
-
- public JsonArray endArrayInArray() {
- if (isNotArray())
- throw new IllegalStateException("INTERNAL ERROR: You must have an open array to end.");
-
- doEndArray(JsonObjectType.ARRAY);
- return array;
- }
-
- @SuppressWarnings("UnusedReturnValue")
- public JsonArray send(boolean force) {
- ChunkedJsonResponse.this.send(force, sb);
- return this;
- }
-
- private void doEndArray(JsonObjectType expectedParentType) {
- openItems.pop();
- if (!openItems.isEmpty() && (openItems.peek() != expectedParentType)) {
- openItems.push(JsonObjectType.ARRAY);
- throw new IllegalStateException("INTERNAL ERROR: Incorrect end array method called, the parent is not of the expected type.");
- }
-
- sb.append("]");
- isFirst = false;
-
- if (openItems.isEmpty())
- end(sb);
- else
- send(false);
- }
-
- }
-
- private enum JsonObjectType {OBJECT, ARRAY}
-
-
- private final StringBuilder sb;
- private final JsonObject object = new JsonObject();
- private final JsonArray array = new JsonArray();
-
- boolean isFirst = true;
- private final Deque openItems = new ArrayDeque<>();
-
- public ChunkedJsonResponse(int bufferSize) {
- sb = new StringBuilder(bufferSize);
- }
-
- public JsonArray ofArray() {
- if (!openItems.isEmpty())
- throw new IllegalStateException("INTERNAL ERROR: You can only start one object or array for a response.");
- return object.doBeginArray();
- }
-
- public JsonObject ofObject() {
- if (!openItems.isEmpty())
- throw new IllegalStateException("INTERNAL ERROR: You can only start one object or array for a response.");
- return object.doBeginObject();
- }
-
- private boolean isNotArray() {
- return openItems.peek() != JsonObjectType.ARRAY;
- }
-
- private boolean isNotObject() {
- return openItems.peek() != JsonObjectType.OBJECT;
- }
-
- protected abstract void end(StringBuilder sb);
-
- protected abstract void send(boolean force, StringBuilder sb);
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/HttpChunkedJsonResponse.java b/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/HttpChunkedJsonResponse.java
deleted file mode 100644
index a833e92..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/ChunkedResponse/HttpChunkedJsonResponse.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing.ChunkedResponse;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import io.vertx.core.http.HttpServerResponse;
-
-@SuppressWarnings("WeakerAccess")
-@EverythingIsNonnullByDefault
-public class HttpChunkedJsonResponse extends ChunkedJsonResponse {
-
- private final HttpServerResponse response;
- private final int bufferSize;
-
- public HttpChunkedJsonResponse(HttpServerResponse response) {
- this(response, DEFAULT_BUFFER_SIZE);
- }
-
- public HttpChunkedJsonResponse(HttpServerResponse response, int bufferSize) {
- super(bufferSize);
-
- this.response = response;
- this.bufferSize = bufferSize;
- }
-
- @Override
- protected void end(StringBuilder sb) {
- if (!response.closed()){
- response.end(sb.toString());
- }
- }
-
- @Override
- protected void send(boolean force, StringBuilder sb) {
- if ((force || sb.length() >= bufferSize) && !response.closed()) {
- response.write(sb.toString());
- sb.setLength(0);
- }
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.java b/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.java
deleted file mode 100644
index 6617e47..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import io.vertx.core.json.JsonObject;
-
-import java.util.Collections;
-import java.util.List;
-
-/**
- * A utility class to help with formatting error messages so they can be consistent across routes.
- */
-@SuppressWarnings("WeakerAccess")
-@EverythingIsNonnullByDefault
-public class ErrorFormatter {
-
- /**
- * Helper method for {@link ErrorFormatter#asJson(List)} that can be called with a single error string.
- *
- * @param error The error message
- * @return The JSON string
- */
- public static String asJson(String error) {
- return asJson(Collections.singletonList(error));
- }
-
- /**
- * Method takes a list of strings and puts in in a JSON object.
- * This allows route handlers to return errors in a consistent fashion.
- * The JSON object is constructed as follows: {@code {"errors": ["msg1", "msg2", ...]}}
- *
- * @param errors The errors to be included
- * @return The string representation of the JSON object.
- */
- public static String asJson(List errors) {
- return new JsonObject().put("errors", errors).encode();
- }
-
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.kt b/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.kt
new file mode 100644
index 0000000..7edccf2
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/ErrorFormatter.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+import io.vertx.core.json.JsonObject
+
+/**
+ * A utility class to help with formatting error messages so they can be consistent across routes.
+ */
+object ErrorFormatter {
+
+ /**
+ * Helper method for [ErrorFormatter.asJson] that can be called with a single error string.
+ *
+ * @param error The error message
+ * @return The JSON string
+ */
+ fun asJson(error: String?): String = asJson(listOf(error))
+
+ /**
+ * Method takes a list of strings and puts it in a JSON object.
+ * This allows route handlers to return errors in a consistent fashion.
+ * The JSON object is constructed as follows: `{"errors": ["msg1", "msg2", ...]}`
+ *
+ * @param errors The errors to be included
+ * @return The string representation of the JSON object.
+ */
+ fun asJson(errors: List): String = JsonObject().put("errors", errors).encode()
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.java b/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.java
deleted file mode 100644
index 81377a3..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import io.vertx.core.Handler;
-import io.vertx.ext.web.RoutingContext;
-
-import java.util.function.BiConsumer;
-
-
-public class ExceptionHandler implements Handler {
-
- private final Class tClass;
- private final BiConsumer handler;
-
- ExceptionHandler(Class tClass, BiConsumer handler) {
- this.tClass = tClass;
- this.handler = handler;
- }
-
- @Override
- public void handle(RoutingContext context) {
- if (!tClass.isInstance(context.failure())) {
- context.next();
- return;
- }
-
- handle(tClass.cast(context.failure()), context);
- }
-
- private void handle(T throwable, RoutingContext handler) {
- this.handler.accept(tClass.cast(throwable), handler);
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.kt b/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.kt
new file mode 100644
index 0000000..2b2bd7a
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/ExceptionHandler.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+import io.vertx.core.Handler
+import io.vertx.ext.web.RoutingContext
+
+class ExceptionHandler internal constructor(
+ private val tClass: Class,
+ private val handler: (T, RoutingContext) -> Unit,
+) : Handler {
+
+ override fun handle(context: RoutingContext) {
+ if (!tClass.isInstance(context.failure())) {
+ context.next()
+ return
+ }
+
+ handler(tClass.cast(context.failure()), context)
+ }
+
+ companion object {
+
+ internal inline fun of(noinline handler: (T, RoutingContext) -> Unit): ExceptionHandler =
+ ExceptionHandler(T::class.java, handler)
+
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.java b/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.java
deleted file mode 100644
index 08a8931..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import io.vertx.core.json.JsonArray;
-import io.vertx.core.json.JsonObject;
-
-import javax.annotation.Nullable;
-import java.util.ArrayList;
-import java.util.List;
-
-@EverythingIsNonnullByDefault
-public interface JsonBodyRequest {
-
- default T extract(JsonObject json, String key, GetValue valueSupplier) throws IllegalArgumentException {
- try {
- @Nullable T value = valueSupplier.get(json, key);
- if (value == null)
- throw new IllegalArgumentException(String.format("Required key '%s' must be specified", key));
- return value;
- } catch (ClassCastException ex) {
- throw new IllegalArgumentException(String.format("Error reading required key '%s'", key), ex);
- }
- }
-
- default List extractList(JsonObject json, String key, int minValues, ValueConverter valueConverter) throws IllegalArgumentException {
- try {
- @Nullable JsonArray values = json.getJsonArray(key);
- if (values == null)
- throw new IllegalArgumentException(String.format("Required key '%s' must be specified", key));
-
- List result = new ArrayList<>();
- for (int i = 0; i < values.size(); ++i)
- result.add(valueConverter.convert(values.getJsonObject(i)));
-
- if (result.size() < minValues) {
- if (minValues == 1)
- throw new IllegalArgumentException(String.format("Required key '%s' must have at least 1 value", key));
- else
- throw new IllegalArgumentException(String.format("Required key '%s' must have at least %d values", key, minValues));
- }
-
- return result;
- } catch (ClassCastException ex) {
- throw new IllegalArgumentException(String.format("Error reading required key '%s'", key), ex);
- }
- }
-
- @FunctionalInterface
- interface GetValue {
- @Nullable
- T get(JsonObject json, String key);
- }
-
- @FunctionalInterface
- interface ValueConverter {
- @Nullable
- T convert(JsonObject json);
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.kt b/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.kt
new file mode 100644
index 0000000..ae0e47a
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/JsonBodyRequest.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+import io.vertx.core.json.JsonObject
+
+interface JsonBodyRequest {
+
+ @Throws(IllegalArgumentException::class)
+ fun extract(json: JsonObject, key: String, valueSupplier: (json: JsonObject, key: String) -> T?): T =
+ try {
+ val value = valueSupplier(json, key)
+ requireNotNull(value) { "Required key '$key' must be specified" }
+ value
+ } catch (ex: ClassCastException) {
+ throw IllegalArgumentException("Error reading required key '$key'", ex)
+ }
+
+ @Throws(IllegalArgumentException::class)
+ fun extractList(json: JsonObject, key: String, minValues: Int, valueConverter: (json: JsonObject) -> T): List {
+ try {
+ val values = json.getJsonArray(key)
+ requireNotNull(values) { "Required key '$key' must be specified" }
+
+ val converted = (0..
+ valueConverter(values.getJsonObject(i))
+ }
+
+ if (converted.size < minValues) {
+ require(minValues != 1) { "Required key '$key' must have at least 1 value" }
+ throw IllegalArgumentException("Required key '$key' must have at least $minValues values")
+ }
+
+ return converted
+ } catch (ex: ClassCastException) {
+ throw IllegalArgumentException("Error reading required key '$key'", ex)
+ }
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/Respond.kt b/src/main/java/com/zepben/vertxutils/routing/Respond.kt
index 9e34eb9..855b601 100644
--- a/src/main/java/com/zepben/vertxutils/routing/Respond.kt
+++ b/src/main/java/com/zepben/vertxutils/routing/Respond.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 Zeppelin Bend Pty Ltd
+ * Copyright 2026 Zeppelin Bend Pty Ltd
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -9,7 +9,6 @@ package com.zepben.vertxutils.routing
import com.google.common.net.HttpHeaders
import com.google.common.net.MediaType
-import com.zepben.annotations.EverythingIsNonnullByDefault
import com.zepben.vertxutils.json.filter.FilterSpecification
import com.zepben.vertxutils.json.filter.JsonObjectFilter
import io.netty.handler.codec.http.HttpResponseStatus
@@ -20,50 +19,38 @@ import io.vertx.ext.web.RoutingContext
/**
* Class that contains a bunch of helper functions for handling HTTP responses.
*/
-@EverythingIsNonnullByDefault
object Respond {
- @JvmStatic
- @JvmOverloads
+
fun with(
context: RoutingContext,
status: HttpResponseStatus,
- addHeaders: Map = emptyMap()
+ addHeaders: Map = emptyMap(),
+ withEmptyContentLengthHeader: Boolean = false,
) {
context
.response()
.setStatusCode(status.code())
- .apply { if (addHeaders.isNotEmpty()) headers().addAll(addHeaders) }
+ .apply {
+ if (addHeaders.isNotEmpty()) headers().addAll(addHeaders)
+ if (withEmptyContentLengthHeader) headers()[HttpHeaders.CONTENT_LENGTH] = "0"
+ }
.end()
}
- @JvmStatic
- fun with(
- context: RoutingContext,
- status: HttpResponseStatus,
- withEmptyContentLengthHeader: Boolean = false
- ) = with(
- context,
- status,
- if (withEmptyContentLengthHeader) mapOf(HttpHeaders.CONTENT_LENGTH to "0") else emptyMap()
- )
-
- @JvmStatic
fun with(context: RoutingContext, response: Response) {
context
.response()
- .setStatusCode(response.status().code())
- .setStatusMessage(response.status().reasonPhrase())
- .apply { if (response.hasHeaders()) headers().addAll(response.headers()) }
- .end(response.body())
+ .setStatusCode(response.status.code())
+ .setStatusMessage(response.status.reasonPhrase())
+ .apply { if (response.hasHeaders()) headers().addAll(response.headers) }
+ .end(response.body)
}
- @JvmStatic
- @JvmOverloads
fun withJson(
context: RoutingContext,
status: HttpResponseStatus,
json: String,
- addHeaders: Map = emptyMap()
+ addHeaders: Map = emptyMap(),
) {
context.response()
.setStatusCode(status.code())
@@ -73,14 +60,12 @@ object Respond {
.end(json)
}
- @JvmStatic
- @JvmOverloads
fun withJson(
context: RoutingContext,
status: HttpResponseStatus,
json: JsonObject,
filterSpecification: FilterSpecification,
- addHeaders: Map = emptyMap()
+ addHeaders: Map = emptyMap(),
) {
context.response()
.setStatusCode(status.code())
@@ -92,12 +77,10 @@ object Respond {
// This function breaks the pattern and doesn't actually send the response, it just returns the unsent response.
// This is because EWB Network Routes needs it to behave this way and does further manipulation to it. Leave as is.
- @JvmStatic
- @JvmOverloads
fun withJsonChunked(
context: RoutingContext,
status: HttpResponseStatus,
- addHeaders: Map = emptyMap()
+ addHeaders: Map = emptyMap(),
): HttpServerResponse {
return context.response()
.setStatusCode(status.code())
diff --git a/src/main/java/com/zepben/vertxutils/routing/Response.java b/src/main/java/com/zepben/vertxutils/routing/Response.java
deleted file mode 100644
index 8995b81..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/Response.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import com.google.common.net.HttpHeaders;
-import com.google.common.net.MediaType;
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import io.netty.handler.codec.http.HttpResponseStatus;
-import io.vertx.core.buffer.Buffer;
-
-import javax.annotation.Nullable;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Class to hold things for a response to an HTTP request.
- */
-@SuppressWarnings({"WeakerAccess", "UnstableApiUsage"})
-@EverythingIsNonnullByDefault
-public class Response {
- private HttpResponseStatus status;
- private Buffer body;
- @Nullable private Map headers = null;
-
- public Response(HttpResponseStatus httpStatus) {
- this(httpStatus, Buffer.buffer());
- }
-
- public Response(HttpResponseStatus status, Buffer body) {
- this.status = status;
- this.body = body;
- }
-
- public static Response ofJson(HttpResponseStatus status, String json) {
- Response response = new Response(status, Buffer.buffer(json));
- response.headers().put(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString());
- return response;
- }
-
- public static Response ofText(HttpResponseStatus status, String body) {
- Response response = new Response(status, Buffer.buffer(body));
- response.headers().put(HttpHeaders.CONTENT_TYPE, MediaType.PLAIN_TEXT_UTF_8.toString());
- return response;
- }
-
- public HttpResponseStatus status() {
- return status;
- }
-
- public Buffer body() {
- return body;
- }
-
- public Response setStatus(HttpResponseStatus status) {
- this.status = status;
- return this;
- }
-
- public Response setBody(Buffer body) {
- this.body = body;
- return this;
- }
-
- public boolean hasHeaders() {
- return headers != null && !headers.isEmpty();
- }
-
- public Map headers() {
- if (headers == null)
- headers = new HashMap<>();
-
- return headers;
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/Response.kt b/src/main/java/com/zepben/vertxutils/routing/Response.kt
new file mode 100644
index 0000000..c2b7871
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/Response.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+import com.google.common.net.HttpHeaders
+import com.google.common.net.MediaType
+import io.netty.handler.codec.http.HttpResponseStatus
+import io.vertx.core.buffer.Buffer
+
+/**
+ * Class to hold things for a response to an HTTP request.
+ */
+class Response(
+ val status: HttpResponseStatus,
+ val body: Buffer = Buffer.buffer(),
+ val headers: Map = emptyMap(),
+) {
+
+ fun hasHeaders(): Boolean = !headers.isEmpty()
+
+ companion object {
+
+ fun ofJson(status: HttpResponseStatus, json: String): Response =
+ Response(
+ status,
+ Buffer.buffer(json),
+ mapOf(HttpHeaders.CONTENT_TYPE to MediaType.JSON_UTF_8.toString()),
+ )
+
+ fun ofText(status: HttpResponseStatus, body: String): Response =
+ Response(
+ status,
+ Buffer.buffer(body),
+ mapOf(HttpHeaders.CONTENT_TYPE to MediaType.PLAIN_TEXT_UTF_8.toString()),
+ )
+
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/Route.java b/src/main/java/com/zepben/vertxutils/routing/Route.java
deleted file mode 100644
index 6fb520a..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/Route.java
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import com.zepben.vertxutils.routing.handlers.DecodeBodyHandler;
-import com.zepben.vertxutils.routing.handlers.PathParamsHandler;
-import com.zepben.vertxutils.routing.handlers.QueryParamsHandler;
-import com.zepben.vertxutils.routing.handlers.params.*;
-import io.vertx.core.Handler;
-import io.vertx.core.buffer.Buffer;
-import io.vertx.core.http.HttpMethod;
-import io.vertx.ext.web.RoutingContext;
-import io.vertx.ext.web.handler.BodyHandler;
-
-import javax.annotation.Nullable;
-import java.util.Arrays;
-import java.util.List;
-import java.util.function.BiConsumer;
-
-@SuppressWarnings("WeakerAccess")
-@EverythingIsNonnullByDefault
-public class Route {
-
- @Nullable private final String path;
- private final boolean hasRegexPath;
- private final ImmutableSet methods;
- private final ImmutableList handlers;
- private final ImmutableList> failureHandlers;
- private final boolean isPublic;
-
- public static Builder builder() {
- return new Builder();
- }
-
- private Route(@Nullable String path,
- boolean hasRegexPath,
- ImmutableSet methods,
- ImmutableList handlers,
- ImmutableList> failureHandlers,
- boolean isPublic) {
- this.path = path;
- this.hasRegexPath = hasRegexPath;
- this.methods = methods;
- this.handlers = handlers;
- this.failureHandlers = failureHandlers;
- this.isPublic = isPublic;
- }
-
- /**
- * The path of the route.
- *
- * @return The path of the route.
- */
- @Nullable
- public String path() {
- return path;
- }
-
- /**
- * Set to true if the path uses regular expressions. Defaults to false.
- *
- * @return true is the path uses regular expressions.
- */
- public boolean hasRegexPath() {
- return hasRegexPath;
- }
-
- /**
- * The HTTP method for the route.
- *
- * @return The HTTP method for the route.
- */
- public Iterable methods() {
- return methods;
- }
-
- /**
- * Return a list of handlers for this route.
- *
- * Remember to always call {@link RoutingContext#next()} to chain to your next handler if you have more than one.
- *
- * @return A list of handlers for this route.
- */
- public List handlers() {
- return handlers;
- }
-
- /**
- * The failure handler for the route.
- *
- * @return The failure handler for the route.
- */
- public List> failureHandlers() {
- return failureHandlers;
- }
-
- /**
- * Indicates if the route should be documented as a public route.
- *
- * @return true if the route is a publicly documented route.
- */
- public boolean isPublic() {
- return isPublic;
- }
-
- @SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
- public static class Builder {
- @Nullable private String path = null;
- private boolean hasRegexPath = false;
- private final ImmutableSet.Builder methods = ImmutableSet.builder();
- @Nullable private PathParamsHandler pathParamsHandler = null;
- @Nullable private QueryParamsHandler queryParamsHandler = null;
- @Nullable private BodyHandler bodyHandler = null;
- @Nullable private DecodeBodyHandler decodeBodyHandler = null;
- private final ImmutableList.Builder handlers = ImmutableList.builder();
- private final ImmutableList.Builder> failureHandlers = ImmutableList.builder();
- private boolean isPublic = true;
-
- private Builder() {
- }
-
- public Builder path(String path) {
- if (path.isEmpty())
- throw new IllegalArgumentException("path must not be empty");
-
- if (path.indexOf('%') >= 0)
- throw new IllegalArgumentException("formatted path must not contain a '%'");
-
- this.path = path;
- return this;
- }
-
- public Builder path(String pathFormat, PathParamRule>... rules) {
- int count = 0;
- for (int index = pathFormat.indexOf('%'); index >= 0; index = pathFormat.indexOf('%', index + 1)) {
- ++count;
- if ((index == 0)
- || (index >= pathFormat.length() - 1)
- || (pathFormat.charAt(index - 1) != ':')
- || (pathFormat.charAt(index + 1) != 's')) {
- throw new IllegalArgumentException("invalid use of % in path format string");
- }
- }
-
- if (count < rules.length)
- throw new IllegalArgumentException("too many path params");
- else if (count > rules.length)
- throw new IllegalArgumentException("missing path params");
-
- path(String.format(pathFormat, Arrays.stream(rules).map(ParamRule::name).toArray()));
- pathParamsHandler = new PathParamsHandler(rules);
- return this;
- }
-
- public Builder hasRegexPath(boolean hasRegexPath) {
- this.hasRegexPath = hasRegexPath;
- return this;
- }
-
- public Builder method(HttpMethod method) {
- methods.add(method);
- return this;
- }
-
- public Builder methods(HttpMethod... methods) {
- for (HttpMethod method : methods)
- method(method);
-
- return this;
- }
-
- public Builder queryParams(QueryParamRule>... rules) {
- queryParamsHandler = new QueryParamsHandler(rules);
- return this;
- }
-
- public Builder bodySizeLimit(long size) {
- if (bodyHandler == null)
- bodyHandler(BodyHandler.create());
-
- bodyHandler.setBodyLimit(size);
- return this;
- }
-
- public Builder uploadsDirectory(String path) {
- if (bodyHandler == null)
- bodyHandler(BodyHandler.create());
-
- bodyHandler.setUploadsDirectory(path);
- return this;
- }
-
- public Builder decodeBody(RequestValueConverter bodyConverter) {
- return decodeBody(bodyConverter, true);
- }
-
- public Builder decodeBody(RequestValueConverter bodyConverter, boolean bodyRequired) {
- if (bodyHandler == null)
- bodyHandler(BodyHandler.create());
-
- decodeBodyHandler(new DecodeBodyHandler(new BodyRule<>(bodyConverter, bodyRequired)));
- return this;
- }
-
- public Builder bodyHandler(BodyHandler handler) {
- bodyHandler = handler;
- return this;
- }
-
- public Builder decodeBodyHandler(DecodeBodyHandler handler) {
- decodeBodyHandler = handler;
- return this;
- }
-
- public Builder addHandler(RouteHandler handler) {
- handlers.add(handler);
- return this;
- }
-
- public Builder addHandler(Handler handler) {
- return addHandler(new RouteHandler(handler, false));
- }
-
- /**
- * Registers a blocking handler.
- * This makes the handler equivalent to being registered with {@link io.vertx.ext.web.Route#blockingHandler(Handler, boolean)}.
- * on the {@link RouteRegister} however the boolean ordered flag is set by the argument given to the route register.
- *
- * @param blockingHandler The handler that contains blocking code.
- * @return This builder.
- */
- public final Builder addBlockingHandler(Handler blockingHandler) {
- return addHandler(new RouteHandler(blockingHandler, true, null));
- }
-
- /**
- * Registers a blocking handler.
- * This makes the handler equivalent to being registered with {@link io.vertx.ext.web.Route#blockingHandler(Handler, boolean)}
- * on the {@link RouteRegister}.
- *
- * @param blockingHandler The handler that contains blocking code.
- * @return This builder.
- */
- public final Builder addBlockingHandler(Handler blockingHandler, boolean ordered) {
- return addHandler(new RouteHandler(blockingHandler, true, ordered));
- }
-
- public final Builder addFailureHandler(Handler failureHandler) {
- this.failureHandlers.add(failureHandler);
- return this;
- }
-
- public final Builder addFailureHandler(Class throwableClass, BiConsumer handler) {
- failureHandlers.add(new ExceptionHandler<>(throwableClass, handler));
- return this;
- }
-
- public Builder isPublic(boolean isPublic) {
- this.isPublic = isPublic;
- return this;
- }
-
- public Route build() {
- if (path != null && !hasRegexPath && path.charAt(0) != '/')
- throw new IllegalStateException("path must start with a /");
-
- ImmutableList.Builder allHandlers = ImmutableList.builder();
- if (bodyHandler != null) {
- allHandlers.add(new RouteHandler(bodyHandler, false));
- if (decodeBodyHandler != null)
- allHandlers.add(new RouteHandler(decodeBodyHandler, false));
- }
-
- if (pathParamsHandler != null)
- allHandlers.add(new RouteHandler(pathParamsHandler, false));
-
- if (queryParamsHandler != null)
- allHandlers.add(new RouteHandler(queryParamsHandler, false));
-
- allHandlers.addAll(handlers.build());
-
- return new Route(
- path,
- hasRegexPath,
- methods.build(),
- allHandlers.build(),
- failureHandlers.build(),
- isPublic);
- }
- }
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/Route.kt b/src/main/java/com/zepben/vertxutils/routing/Route.kt
new file mode 100644
index 0000000..d5c5962
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/Route.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+import com.zepben.vertxutils.routing.handlers.DecodeBodyHandler
+import com.zepben.vertxutils.routing.handlers.PathParamsHandler
+import com.zepben.vertxutils.routing.handlers.QueryParamsHandler
+import com.zepben.vertxutils.routing.handlers.params.BodyRule
+import com.zepben.vertxutils.routing.handlers.params.PathParamRule
+import com.zepben.vertxutils.routing.handlers.params.QueryParamRule
+import com.zepben.vertxutils.routing.handlers.params.RequestValueConverter
+import io.vertx.core.Handler
+import io.vertx.core.http.HttpMethod
+import io.vertx.ext.web.RequestBody
+import io.vertx.ext.web.RoutingContext
+import io.vertx.ext.web.handler.BodyHandler
+
+/**
+ * @property path The path of the route.
+ * @property hasRegexPath Set to true if the path uses regular expressions. Defaults to false.
+ * @property methods The HTTP method for the route.
+ * @property handlers A list of handlers for this route. Remember to always call [RoutingContext.next] to chain to your next handler if you have more than one.
+ * @property failureHandlers The failure handlers for the route.
+ * @property isPublic Indicates if the route should be documented as a public route. True if the route is a publicly documented route.
+ */
+class Route private constructor(
+ val path: String?,
+ val hasRegexPath: Boolean,
+ val methods: Set,
+ val handlers: List,
+ val failureHandlers: List>,
+ val isPublic: Boolean,
+) {
+
+ class Builder internal constructor() {
+
+ private var path: String? = null
+ private var hasRegexPath = false
+ private val methods = mutableSetOf()
+ private var pathParamsHandler: PathParamsHandler? = null
+ private var queryParamsHandler: QueryParamsHandler? = null
+ private var bodyHandler: BodyHandler? = null
+ private var decodeBodyHandler: DecodeBodyHandler? = null
+ private val handlers = mutableListOf()
+ private val failureHandlers = mutableListOf>()
+ private var isPublic = true
+
+ fun path(path: String): Builder = also { builder ->
+ require(!path.isEmpty()) { "path must not be empty" }
+ require(path.indexOf('%') < 0) { "formatted path must not contain a '%'" }
+
+ builder.path = path
+ }
+
+ fun path(pathFormat: String, vararg rules: PathParamRule<*>): Builder = apply {
+ var count = 0
+ var index = pathFormat.indexOf('%')
+ while (index >= 0) {
+ ++count
+ require((index != 0) && (index < (pathFormat.length - 1)) && (pathFormat[index - 1] == ':') && (pathFormat[index + 1] == 's')) {
+ "invalid use of % in path format string"
+ }
+ index = pathFormat.indexOf('%', index + 1)
+ }
+
+ require(count >= rules.size) { "too many path params" }
+ require(count <= rules.size) { "missing path params" }
+
+ path(String.format(pathFormat, *rules.map { it.name }.toTypedArray()))
+ pathParamsHandler = PathParamsHandler(*rules)
+ }
+
+ fun hasRegexPath(hasRegexPath: Boolean): Builder = also { builder ->
+ builder.hasRegexPath = hasRegexPath
+ }
+
+ fun method(method: HttpMethod): Builder = apply {
+ methods.add(method)
+ }
+
+ fun methods(vararg methods: HttpMethod): Builder = apply {
+ for (method in methods)
+ method(method)
+ }
+
+ fun queryParams(vararg rules: QueryParamRule<*>): Builder = apply {
+ queryParamsHandler = QueryParamsHandler(*rules)
+ }
+
+ fun bodySizeLimit(size: Long): Builder = apply {
+ if (bodyHandler == null)
+ bodyHandler(BodyHandler.create())
+
+ bodyHandler!!.setBodyLimit(size)
+ }
+
+ fun uploadsDirectory(path: String): Builder = apply {
+ if (bodyHandler == null)
+ bodyHandler(BodyHandler.create())
+
+ bodyHandler!!.setUploadsDirectory(path)
+ }
+
+ fun decodeBody(bodyConverter: RequestValueConverter, bodyRequired: Boolean = true): Builder = apply {
+ if (bodyHandler == null)
+ bodyHandler(BodyHandler.create())
+
+ decodeBodyHandler(DecodeBodyHandler(BodyRule(bodyConverter, bodyRequired)))
+ }
+
+ fun bodyHandler(handler: BodyHandler): Builder = apply {
+ bodyHandler = handler
+ }
+
+ fun decodeBodyHandler(handler: DecodeBodyHandler): Builder = apply {
+ decodeBodyHandler = handler
+ }
+
+ fun addHandler(handler: RouteHandler): Builder = apply {
+ handlers.add(handler)
+ }
+
+ fun addHandler(handler: Handler): Builder =
+ addHandler(RouteHandler(handler, false))
+
+ /**
+ * Registers a blocking handler.
+ * This makes the handler equivalent to being registered with [io.vertx.ext.web.Route.blockingHandler].
+ * on the [RouteRegister] however the boolean ordered flag is set by the argument given to the route register.
+ *
+ * @param blockingHandler The handler that contains blocking code.
+ * @return This builder.
+ */
+ fun addBlockingHandler(blockingHandler: Handler): Builder =
+ addHandler(RouteHandler(blockingHandler, true, null))
+
+ /**
+ * Registers a blocking handler.
+ * This makes the handler equivalent to being registered with [io.vertx.ext.web.Route.blockingHandler]
+ * on the [RouteRegister].
+ *
+ * @param blockingHandler The handler that contains blocking code.
+ * @return This builder.
+ */
+ fun addBlockingHandler(blockingHandler: Handler, ordered: Boolean): Builder =
+ addHandler(RouteHandler(blockingHandler, true, ordered))
+
+ fun addFailureHandler(failureHandler: Handler): Builder = apply {
+ failureHandlers.add(failureHandler)
+ }
+
+ fun addFailureHandler(throwableClass: Class, handler: (T, RoutingContext?) -> Unit): Builder = apply {
+ failureHandlers.add(ExceptionHandler(throwableClass, handler))
+ }
+
+ fun isPublic(isPublic: Boolean): Builder = also { builder ->
+ builder.isPublic = isPublic
+ }
+
+ fun build(): Route {
+ check((path == null) || hasRegexPath || (path!![0] == '/')) { "path must start with a /" }
+
+ val allHandlers = listOfNotNull(
+ bodyHandler,
+ decodeBodyHandler.takeIf { bodyHandler != null },
+ pathParamsHandler,
+ queryParamsHandler,
+ ).map { RouteHandler(it, false) } +
+ handlers
+
+ return Route(
+ path,
+ hasRegexPath,
+ methods,
+ allHandlers,
+ failureHandlers,
+ isPublic,
+ )
+ }
+ }
+
+ companion object {
+
+ fun builder(): Builder = Builder()
+
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteGroup.java b/src/main/java/com/zepben/vertxutils/routing/RouteGroup.java
deleted file mode 100644
index c06bee6..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/RouteGroup.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-
-import java.util.List;
-
-@EverythingIsNonnullByDefault
-public interface RouteGroup {
-
- String mountPath();
-
- List routes();
-
- static RouteGroup create(String mountPath, List routes) {
- return new RouteGroup() {
- @Override
- public String mountPath() {
- return mountPath;
- }
-
- @Override
- public List routes() {
- return routes;
- }
- };
- }
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteGroup.kt b/src/main/java/com/zepben/vertxutils/routing/RouteGroup.kt
new file mode 100644
index 0000000..6cc9d2c
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/RouteGroup.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+interface RouteGroup {
+
+ val mountPath: String
+ val routes: List
+
+ companion object {
+
+ fun create(mountPath: String, routes: List): RouteGroup =
+ object : RouteGroup {
+ override val mountPath: String = mountPath
+
+ override val routes: List = routes
+ }
+
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteHandler.java b/src/main/java/com/zepben/vertxutils/routing/RouteHandler.java
deleted file mode 100644
index d220cc4..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/RouteHandler.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import io.vertx.core.Handler;
-import io.vertx.ext.web.RoutingContext;
-
-import javax.annotation.Nullable;
-import java.util.Optional;
-
-@SuppressWarnings("WeakerAccess")
-@EverythingIsNonnullByDefault
-public class RouteHandler {
-
- private final Handler handler;
- private final boolean isBlocking;
- @Nullable private final Boolean blockingOrdered;
-
- public RouteHandler(Handler handler, boolean isBlocking) {
- this(handler, isBlocking, null);
- }
-
- public RouteHandler(Handler handler, boolean isBlocking, @Nullable Boolean blockingOrdered) {
- this.handler = handler;
- this.isBlocking = isBlocking;
- this.blockingOrdered = blockingOrdered;
- }
-
- public Handler handler() {
- return handler;
- }
-
- public boolean isBlocking() {
- return isBlocking;
- }
-
- public Optional blockingOrdered() {
- return Optional.ofNullable(blockingOrdered);
- }
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteHandler.kt b/src/main/java/com/zepben/vertxutils/routing/RouteHandler.kt
new file mode 100644
index 0000000..754326a
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/RouteHandler.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+import io.vertx.core.Handler
+import io.vertx.ext.web.RoutingContext
+
+class RouteHandler(
+ val handler: Handler,
+ val isBlocking: Boolean,
+ val blockingOrdered: Boolean? = null,
+)
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteRegister.java b/src/main/java/com/zepben/vertxutils/routing/RouteRegister.java
deleted file mode 100644
index 2fbf46e..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/RouteRegister.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import io.vertx.core.Handler;
-import io.vertx.core.http.HttpMethod;
-import io.vertx.ext.web.Router;
-import io.vertx.ext.web.RoutingContext;
-
-import javax.annotation.Nullable;
-import java.util.function.BiConsumer;
-
-@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
-@EverythingIsNonnullByDefault
-public class RouteRegister {
- private final Router router;
- private final String rootMount;
- private final boolean defaultOrderedBlockingRoutes;
- private BiConsumer onAdd = (p, r) -> {
- };
-
- public RouteRegister(Router router, boolean defaultOrderedBlockingRoutes) {
- this(router, "", defaultOrderedBlockingRoutes);
- }
-
- public RouteRegister(Router router, String rootMount, boolean defaultOrderedBlockingRoutes) {
- this.router = router;
- this.rootMount = rootMount;
- this.defaultOrderedBlockingRoutes = defaultOrderedBlockingRoutes;
- }
-
- public Router router() {
- return router;
- }
-
- public RouteRegister onAdd(BiConsumer onAdd) {
- this.onAdd = onAdd;
- return this;
- }
-
- public RouteRegister add(Route route, String mountPath) {
- String path;
- io.vertx.ext.web.Route vertxRoute;
- if (route.path() == null) {
- path = "";
- vertxRoute = router.route();
- } else {
- path = buildPath(rootMount, mountPath, route.path());
- if (route.hasRegexPath())
- vertxRoute = router.routeWithRegex(path);
- else
- vertxRoute = router.route(path);
- }
-
- for (HttpMethod method : route.methods())
- vertxRoute.method(method);
-
- for (RouteHandler handler : route.handlers()) {
- if (handler.isBlocking()) {
- vertxRoute.blockingHandler(handler.handler(), handler.blockingOrdered().orElse(defaultOrderedBlockingRoutes));
- } else {
- vertxRoute.handler(handler.handler());
- }
- }
-
- for (Handler handler : route.failureHandlers())
- vertxRoute.failureHandler(handler);
-
- onAdd.accept(path, route);
- return this;
- }
-
- public RouteRegister add(Route route) {
- return add(route, "");
- }
-
- public RouteRegister add(RouteGroup group) {
- group.routes().forEach(route -> add(route, group.mountPath()));
- return this;
- }
-
- public RouteRegister add(Iterable routes) {
- routes.forEach(this::add);
- return this;
- }
-
- /**
- * Register a collection of route groups with this RouteRegister. NOTE: This function can't be named `add` like the others due
- * to type erasure making it have the same signature at the iterable for routes.
- *
- * @param routeGroups The collection of routes to register.
- * @return This RouteRegister for fluent use.
- */
- public RouteRegister addGroups(Iterable routeGroups) {
- routeGroups.forEach(this::add);
- return this;
- }
-
- private String buildPath(String rootMount, String mountPath, @Nullable String routePath) {
- String path = rootMount;
- if (!mountPath.isEmpty())
- path += "/" + mountPath;
-
- if (routePath == null) {
- // If the route path was null, it means match all paths, but now we are mounting it we need it to match
- // all paths below the mount point.
- if (!path.isEmpty())
- path += "/*";
- } else if (!routePath.isEmpty()) {
- if (!path.isEmpty() && !routePath.equals("$"))
- path += "/";
-
- path += routePath;
- }
-
- return path.replace("///", "/").replace("//", "/");
- }
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteRegister.kt b/src/main/java/com/zepben/vertxutils/routing/RouteRegister.kt
new file mode 100644
index 0000000..f124ed5
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/RouteRegister.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+import io.vertx.ext.web.Router
+
+class RouteRegister(
+ val router: Router,
+ val rootMount: String = "",
+ val defaultOrderedBlockingRoutes: Boolean,
+) {
+
+ var onAdd: (path: String, route: Route) -> Unit = { _, _ -> }
+
+ fun add(route: Route, mountPath: String = ""): RouteRegister = apply {
+ val path = route.path?.let { buildPath(rootMount, mountPath, route.path) }
+
+ val vertxRoute = when {
+ path == null -> router.route()
+ route.hasRegexPath -> router.routeWithRegex(path)
+ else -> router.route(path)
+ }
+
+ route.methods.forEach { vertxRoute.method(it) }
+
+ route.handlers.forEach {
+ when {
+ it.isBlocking -> vertxRoute.blockingHandler(it.handler, it.blockingOrdered ?: defaultOrderedBlockingRoutes)
+ else -> vertxRoute.handler(it.handler)
+ }
+ }
+
+ route.failureHandlers.forEach { vertxRoute.failureHandler(it) }
+
+ onAdd(path ?: "", route)
+ }
+
+ fun add(group: RouteGroup): RouteRegister = apply {
+ group.routes.forEach { route -> add(route, group.mountPath) }
+ }
+
+ fun add(routes: Iterable): RouteRegister = apply {
+ routes.forEach { route -> add(route) }
+ }
+
+ /**
+ * Register a collection of route groups with this RouteRegister. NOTE: This function can't be named `add` like the others due
+ * to type erasure making it have the same signature at the iterable for routes.
+ *
+ * @param routeGroups The collection of routes to register.
+ * @return This RouteRegister for fluent use.
+ */
+ fun addGroups(routeGroups: Iterable): RouteRegister = apply {
+ routeGroups.forEach { group -> add(group) }
+ }
+
+ private fun buildPath(rootMount: String, mountPath: String, routePath: String?): String {
+ var path = rootMount
+ if (!mountPath.isEmpty()) path += "/$mountPath"
+
+ if (routePath == null) {
+ // If the route path was null, it means match all paths, but now we are mounting it we need it to match
+ // all paths below the mount point.
+ if (!path.isEmpty())
+ path += "/*"
+ } else if (routePath.isNotEmpty()) {
+ if (path.isNotEmpty() && routePath != "$")
+ path += "/"
+
+ path += routePath
+ }
+
+ return path.replace("///", "/").replace("//", "/")
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.java b/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.java
deleted file mode 100644
index f90ab18..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import org.slf4j.Logger;
-
-import java.util.function.BiConsumer;
-
-@SuppressWarnings("WeakerAccess")
-public class RouteRegisterLogger implements BiConsumer {
-
- private final Logger logger;
-
- public RouteRegisterLogger(Logger logger) {
- this.logger = logger;
- }
-
- @Override
- public void accept(String path, Route route) {
- route.methods().forEach(method -> logger.info(method + ": " + path));
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.kt b/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.kt
new file mode 100644
index 0000000..413c72a
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/RouteRegisterLogger.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+import org.slf4j.Logger
+
+fun logRegisteredRoutes(logger: Logger): (String, Route) -> Unit = { path: String, route: Route ->
+ route.methods.forEach { method -> logger.info("$method: $path") }
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteVersion.java b/src/main/java/com/zepben/vertxutils/routing/RouteVersion.java
deleted file mode 100644
index 3a59ed4..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/RouteVersion.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import com.google.errorprone.annotations.Immutable;
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-
-@EverythingIsNonnullByDefault
-@Immutable
-@SuppressWarnings("WeakerAccess")
-public class RouteVersion {
-
- private final int first;
- private final int last;
-
- public static RouteVersion since(int first) {
- return new RouteVersion(first, Integer.MAX_VALUE);
- }
-
- public static RouteVersion between(int first, int last) {
- return new RouteVersion(first, last);
- }
-
- public boolean includes(int version) {
- return (first <= version) && (last >= version);
- }
-
- private RouteVersion(int first, int last) {
- this.first = first;
- this.last = last;
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteVersion.kt b/src/main/java/com/zepben/vertxutils/routing/RouteVersion.kt
new file mode 100644
index 0000000..f63feeb
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/RouteVersion.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+class RouteVersion private constructor(
+ private val first: Int,
+ private val last: Int,
+) {
+
+ operator fun contains(version: Int): Boolean = (first <= version) && (last >= version)
+
+ companion object {
+
+ fun since(first: Int): RouteVersion = RouteVersion(first, Int.MAX_VALUE)
+ fun between(first: Int, last: Int): RouteVersion = RouteVersion(first, last)
+
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.java b/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.java
deleted file mode 100644
index 863d412..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-
-import java.util.List;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-@EverythingIsNonnullByDefault
-@SuppressWarnings("WeakerAccess")
-public class RouteVersionUtils {
-
- public static List forVersion(T[] availableRoutes, int version, Function routeFactory) {
- return Stream.of(availableRoutes)
- .filter(rv -> rv.routeVersion().includes(version))
- .map(routeFactory)
- .collect(Collectors.toList());
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.kt b/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.kt
new file mode 100644
index 0000000..0ff38a2
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/RouteVersionUtils.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+object RouteVersionUtils {
+
+ inline fun ((T) -> Route?).forVersion(version: Int): List where T : Enum, T : VersionableRoute =
+ enumValues()
+ .asSequence()
+ .filter { version in it.routeVersion }
+ .mapNotNull { this(it) }
+ .toList()
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.java b/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.java
deleted file mode 100644
index 5218232..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import com.zepben.vertxutils.routing.handlers.DecodeBodyHandler;
-import com.zepben.vertxutils.routing.handlers.PathParamsHandler;
-import com.zepben.vertxutils.routing.handlers.QueryParamsHandler;
-import com.zepben.vertxutils.routing.handlers.params.BadParamException;
-import com.zepben.vertxutils.routing.handlers.params.PathParams;
-import com.zepben.vertxutils.routing.handlers.params.QueryParams;
-import io.vertx.ext.web.RoutingContext;
-
-import java.util.Optional;
-
-/**
- * These would ideally be extension methods for {@link io.vertx.ext.web.RoutingContext} but stupid Java doesn't have them.
- */
-@EverythingIsNonnullByDefault
-public class RoutingContextEx {
-
- public static final String PATH_PARAMS_KEY = PathParamsHandler.class.getSimpleName();
- public static final String QUERY_PARAMS_KEY = QueryParamsHandler.class.getSimpleName();
- public static final String BODY_KEY = DecodeBodyHandler.class.getSimpleName();
-
- public static PathParams getPathParams(RoutingContext context) {
- PathParams params = context.get(PATH_PARAMS_KEY);
- if (params == null)
- throw new IllegalStateException("PathParamsHandler must be called before you can use RoutingContextEx.getPathParams");
-
- return params;
- }
-
- public static void putPathParams(RoutingContext context, PathParams params) {
- context.put(PATH_PARAMS_KEY, params);
- }
-
- public static QueryParams getQueryParams(RoutingContext context) {
- QueryParams params = context.get(QUERY_PARAMS_KEY);
- if (params == null)
- throw new IllegalStateException("QueryParamsHandler must be called before you can use RoutingContextEx.getQueryParams");
-
- return params;
- }
-
- public static void putQueryParams(RoutingContext context, QueryParams params) {
- context.put(QUERY_PARAMS_KEY, params);
- }
-
- public static T getDecodedBody(RoutingContext context) {
- T body = context.get(BODY_KEY);
- if (body == null)
- throw BadParamException.missingBody();
-
- return body;
- }
-
- public static Optional getOptionalDecodedBody(RoutingContext context) {
- return Optional.ofNullable(context.get(BODY_KEY));
- }
-
- public static void putRequestBody(RoutingContext context, Object body) {
- context.put(BODY_KEY, body);
- }
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.kt b/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.kt
new file mode 100644
index 0000000..6ab7c7f
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/RoutingContextEx.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+import com.zepben.vertxutils.routing.handlers.DecodeBodyHandler
+import com.zepben.vertxutils.routing.handlers.PathParamsHandler
+import com.zepben.vertxutils.routing.handlers.QueryParamsHandler
+import com.zepben.vertxutils.routing.handlers.params.BadParamException
+import com.zepben.vertxutils.routing.handlers.params.PathParams
+import com.zepben.vertxutils.routing.handlers.params.QueryParams
+import io.vertx.ext.web.RoutingContext
+
+/**
+ * These would ideally be extension methods for [RoutingContext] but stupid Java doesn't have them.
+ */
+object RoutingContextEx {
+
+ val PATH_PARAMS_KEY: String = PathParamsHandler::class.java.getSimpleName()
+ val QUERY_PARAMS_KEY: String = QueryParamsHandler::class.java.getSimpleName()
+ val BODY_KEY: String = DecodeBodyHandler::class.java.getSimpleName()
+
+ fun getPathParams(context: RoutingContext): PathParams {
+ val params = context.get(PATH_PARAMS_KEY)
+ checkNotNull(params) { "PathParamsHandler must be called before you can use RoutingContextEx.getPathParams" }
+
+ return params
+ }
+
+ fun putPathParams(context: RoutingContext, params: PathParams) {
+ context.put(PATH_PARAMS_KEY, params)
+ }
+
+ fun getQueryParams(context: RoutingContext): QueryParams {
+ val params = context.get(QUERY_PARAMS_KEY)
+ checkNotNull(params) { "QueryParamsHandler must be called before you can use RoutingContextEx.getQueryParams" }
+
+ return params
+ }
+
+ fun putQueryParams(context: RoutingContext, params: QueryParams) {
+ context.put(QUERY_PARAMS_KEY, params)
+ }
+
+ fun getDecodedBody(context: RoutingContext): T =
+ context.get(BODY_KEY) ?: throw BadParamException.missingBody()
+
+ fun getOptionalDecodedBody(context: RoutingContext): T? =
+ context.get(BODY_KEY)
+
+ fun putRequestBody(context: RoutingContext, body: Any) {
+ context.put(BODY_KEY, body)
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.java b/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.java
deleted file mode 100644
index d79bcb9..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import com.zepben.vertxutils.routing.handlers.FaviconHandler;
-import com.zepben.vertxutils.routing.handlers.UtilHandlers;
-import io.vertx.core.http.HttpMethod;
-import io.vertx.ext.web.handler.StaticHandler;
-
-import javax.annotation.Nullable;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
-@EverythingIsNonnullByDefault
-public class StaticAssetRoutes {
-
- private final String baseUrlPath;
- @Nullable private String indexPage = null;
- private final List subdirs = new ArrayList<>();
- private final String filePath;
- @Nullable private String faviconUrlPath = null;
- @Nullable private String faviconFilePath = null;
- private boolean cachingEnabled = true;
- private String defaultCharacterEncoding = StandardCharsets.UTF_8.name();
-
- public StaticAssetRoutes(String baseUrlPath, String filePath, String... subdirs) {
- this.baseUrlPath = addTrailingSlash(baseUrlPath);
-
- // Make sure we do not have a trailing slash on the base file path. This is required as vertx leaves the last
- // character that matches the path (i.e. the slash) so we do not want to have a double slash.
- if (filePath.endsWith("/"))
- this.filePath = filePath.substring(0, filePath.length() - 1);
- else
- this.filePath = filePath;
-
- subDirs(subdirs);
- }
-
- public StaticAssetRoutes indexPage(String indexPage) {
- this.indexPage = indexPage;
- return this;
- }
-
- public StaticAssetRoutes subDirs(String... subdirs) {
- this.subdirs.addAll(Arrays.asList(subdirs));
- return this;
- }
-
- public StaticAssetRoutes favicon(String subUrlPath, String subFilePath) {
- this.faviconUrlPath = String.format("%s%s", baseUrlPath, subUrlPath);
- this.faviconFilePath = String.format("%s/%s", filePath, subFilePath);
- return this;
- }
-
- public StaticAssetRoutes cachingEnabled(boolean cachingEnabled) {
- this.cachingEnabled = cachingEnabled;
- return this;
- }
-
- public StaticAssetRoutes defaultCharacterEncoding(String defaultCharacterEncoding) {
- this.defaultCharacterEncoding = defaultCharacterEncoding;
- return this;
- }
-
- public List buildRoutes() {
- List routes = new ArrayList<>();
-
- indexPageRoutes(routes);
- faviconRoute(routes);
- subdirRoutes(routes);
-
- return routes;
- }
-
- private void subdirRoutes(List routes) {
- for (String subdir : subdirs) {
- routes.add(Route.builder()
- .path(baseUrlPath + subdir + "/*")
- .method(HttpMethod.GET)
- .addHandler(newStaticHandler(filePath + "/" + subdir))
- .build());
- }
- }
-
- private void faviconRoute(List routes) {
- if (faviconUrlPath != null && faviconFilePath != null) {
- routes.add(Route.builder()
- .path(faviconUrlPath)
- .method(HttpMethod.GET)
- .addHandler(new FaviconHandler(faviconFilePath, 86400))
- .build());
- }
- }
-
- private void indexPageRoutes(List routes) {
- if (indexPage != null) {
- // Vert.x has a bug where it matches URLs with and without a trailing '/' as the same route.
- // When no trailing / is on the URL, it causes issues with relative URLs in the returned html page.
- // Workaround by providing an exact regex match for a path with no / and redirecting to path with a /
- // TODO This has been raised at vertx github, but they can't decide what to do: https://github.com/vert-x3/vertx-web/issues/85
- routes.add(Route.builder()
- .path(baseUrlPath.substring(0, baseUrlPath.length() - 1) + "$")
- .hasRegexPath(true)
- .method(HttpMethod.GET)
- .addHandler(UtilHandlers.REDIRECT_NO_TRAILING_SLASH_TO_TRAILING_SLASH_HANDLER)
- .isPublic(false)
- .build());
-
- // Register the index page.
- routes.add(Route.builder()
- .path(baseUrlPath)
- .method(HttpMethod.GET)
- .addHandler(newStaticHandler(filePath).setIndexPage(indexPage))
- .build());
- }
- }
-
- private String addTrailingSlash(String str) {
- if (str.endsWith("/")) {
- return str;
- } else {
- return str + "/";
- }
- }
-
- private StaticHandler newStaticHandler(String path) {
- return StaticHandler.create()
- .setCachingEnabled(cachingEnabled)
- .setAllowRootFileSystemAccess(true)
- .setWebRoot(path)
- .setDefaultContentEncoding(defaultCharacterEncoding);
- }
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.kt b/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.kt
new file mode 100644
index 0000000..0d36ed9
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/StaticAssetRoutes.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+import com.zepben.vertxutils.routing.handlers.FaviconHandler
+import com.zepben.vertxutils.routing.handlers.UtilHandlers
+import io.vertx.core.http.HttpMethod
+import io.vertx.ext.web.handler.FileSystemAccess
+import io.vertx.ext.web.handler.StaticHandler
+import java.nio.charset.StandardCharsets
+
+class StaticAssetRoutes(
+ baseUrlPath: String,
+ filePath: String,
+ vararg subDirs: String,
+) {
+
+ private val baseUrlPath: String
+ private var indexPage: String? = null
+ private val subDirs = mutableListOf()
+ private val filePath: String
+ private var faviconUrlPath: String? = null
+ private var faviconFilePath: String? = null
+ private var cachingEnabled = true
+ private var defaultCharacterEncoding: String = StandardCharsets.UTF_8.name()
+
+ init {
+ this.baseUrlPath = addTrailingSlash(baseUrlPath)
+
+ // Make sure we do not have a trailing slash on the base file path. This is required as vertx leaves the last
+ // character that matches the path (i.e. the slash) so we do not want to have a double slash.
+ this.filePath = if (filePath.endsWith("/"))
+ filePath.substring(0, filePath.length - 1)
+ else
+ filePath
+
+ subDirs(*subDirs)
+ }
+
+ fun indexPage(indexPage: String): StaticAssetRoutes = also {
+ it.indexPage = indexPage
+ }
+
+ fun subDirs(vararg subDirs: String): StaticAssetRoutes = also {
+ it.subDirs.addAll(subDirs)
+ }
+
+ fun favicon(subUrlPath: String, subFilePath: String): StaticAssetRoutes = apply {
+ faviconUrlPath = String.format("%s%s", baseUrlPath, subUrlPath)
+ faviconFilePath = String.format("%s/%s", filePath, subFilePath)
+ }
+
+ fun cachingEnabled(cachingEnabled: Boolean): StaticAssetRoutes = also {
+ it.cachingEnabled = cachingEnabled
+ }
+
+ fun defaultCharacterEncoding(defaultCharacterEncoding: String): StaticAssetRoutes = also {
+ it.defaultCharacterEncoding = defaultCharacterEncoding
+ }
+
+ fun buildRoutes(): List = buildList {
+ addIndexPageRoutes()
+ addFaviconRoute()
+ addSubdirRoutes()
+ }
+
+ private fun MutableList.addSubdirRoutes() {
+ for (subdir in subDirs) {
+ add(
+ Route.builder()
+ .path("$baseUrlPath$subdir/*")
+ .method(HttpMethod.GET)
+ .addHandler(newStaticHandler("$filePath/$subdir"))
+ .build(),
+ )
+ }
+ }
+
+ private fun MutableList.addFaviconRoute() {
+ if (faviconUrlPath != null && faviconFilePath != null) {
+ add(
+ Route.builder()
+ .path(faviconUrlPath!!)
+ .method(HttpMethod.GET)
+ .addHandler(FaviconHandler(faviconFilePath!!, 86400))
+ .build(),
+ )
+ }
+ }
+
+ private fun MutableList.addIndexPageRoutes() {
+ if (indexPage != null) {
+ // Vert.x has a bug where it matches URLs with and without a trailing '/' as the same route.
+ // When no trailing / is on the URL, it causes issues with relative URLs in the returned html page.
+ // Workaround by providing an exact regex match for a path with no / and redirecting to path with a /
+ // TODO This has been raised at vertx github, but they can't decide what to do: https://github.com/vert-x3/vertx-web/issues/85
+ add(
+ Route.builder()
+ .path(baseUrlPath.substring(0, baseUrlPath.length - 1) + "$")
+ .hasRegexPath(true)
+ .method(HttpMethod.GET)
+ .addHandler(UtilHandlers.REDIRECT_NO_TRAILING_SLASH_TO_TRAILING_SLASH_HANDLER)
+ .isPublic(false)
+ .build(),
+ )
+
+ // Register the index page.
+ add(
+ Route.builder()
+ .path(baseUrlPath)
+ .method(HttpMethod.GET)
+ .addHandler(newStaticHandler(filePath).setIndexPage(indexPage))
+ .build(),
+ )
+ }
+ }
+
+ private fun addTrailingSlash(str: String): String =
+ when {
+ str.endsWith("/") -> str
+ else -> "$str/"
+ }
+
+ private fun newStaticHandler(path: String): StaticHandler {
+ return StaticHandler.create(FileSystemAccess.ROOT, path)
+ .setCachingEnabled(cachingEnabled)
+ .setDefaultContentEncoding(defaultCharacterEncoding)
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.java b/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.java
deleted file mode 100644
index 69d61a3..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-
-@EverythingIsNonnullByDefault
-@SuppressWarnings("WeakerAccess")
-public class StaticAssetsRouteConfig {
-
- private final String webRoot;
- private final boolean isCaching;
-
- public static StaticAssetsRouteConfig of(String webRoot, boolean isCaching) {
- return new StaticAssetsRouteConfig(webRoot, isCaching);
- }
-
- public String webRoot() {
- return webRoot;
- }
-
- public boolean isCaching() {
- return isCaching;
- }
-
- private StaticAssetsRouteConfig(String webRoot, boolean isCaching) {
- this.webRoot = webRoot;
- this.isCaching = isCaching;
- }
-
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.kt b/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.kt
new file mode 100644
index 0000000..ef01765
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/StaticAssetsRouteConfig.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing
+
+class StaticAssetsRouteConfig private constructor(
+ val webRoot: String,
+ val isCaching: Boolean,
+) {
+
+ companion object {
+
+ fun of(webRoot: String, isCaching: Boolean): StaticAssetsRouteConfig =
+ StaticAssetsRouteConfig(webRoot, isCaching)
+
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/VersionableRoute.kt b/src/main/java/com/zepben/vertxutils/routing/VersionableRoute.kt
index fa13dca..b9beca2 100644
--- a/src/main/java/com/zepben/vertxutils/routing/VersionableRoute.kt
+++ b/src/main/java/com/zepben/vertxutils/routing/VersionableRoute.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
+ * Copyright 2026 Zeppelin Bend Pty Ltd
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -12,5 +12,6 @@ package com.zepben.vertxutils.routing
*/
interface VersionableRoute {
- fun routeVersion(): RouteVersion
+ val routeVersion: RouteVersion
+
}
diff --git a/src/main/java/com/zepben/vertxutils/routing/chunked/CaptureChunkedJsonResponse.kt b/src/main/java/com/zepben/vertxutils/routing/chunked/CaptureChunkedJsonResponse.kt
new file mode 100644
index 0000000..e49e44e
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/chunked/CaptureChunkedJsonResponse.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing.chunked
+
+class CaptureChunkedJsonResponse(
+ bufferSize: Int = DEFAULT_BUFFER_SIZE,
+) : ChunkedJsonResponse(bufferSize) {
+
+ private var captured = ""
+
+ override fun onResponseCompleted(sb: StringBuilder) {
+ captured = sb.toString()
+ }
+
+ override fun checkWrite(sb: StringBuilder, force: Boolean) {
+ captured = sb.toString()
+ }
+
+ override fun toString(): String {
+ return captured
+ }
+
+ fun clear() {
+ captured = ""
+ sb.setLength(0)
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/chunked/ChunkedJsonResponse.kt b/src/main/java/com/zepben/vertxutils/routing/chunked/ChunkedJsonResponse.kt
new file mode 100644
index 0000000..e771711
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/chunked/ChunkedJsonResponse.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package com.zepben.vertxutils.routing.chunked
+
+import io.vertx.core.json.Json
+import io.vertx.core.json.JsonArray
+import io.vertx.core.json.JsonObject
+
+/**
+ * The base class for building chunked JSON responses.
+ *
+ * @param bufferSize The initial capacity of the underlying string builder.
+ */
+abstract class ChunkedJsonResponse(
+ bufferSize: Int = DEFAULT_BUFFER_SIZE,
+) {
+
+ companion object {
+
+ const val DEFAULT_BUFFER_SIZE = 1 shl 21
+
+ }
+
+ protected val sb = StringBuilder(bufferSize)
+ private val needsSeparatorStack = ArrayDeque()
+
+ /**
+ * DSL entry point for building a JSON object response.
+ *
+ * @param block The DSL block used to populate the object contents.
+ */
+ fun ofObject(block: JsonObjectBuilder.() -> Unit) {
+ check(sb.isEmpty()) { "Can't reuse a non-clean response builder" }
+ JsonObjectBuilder().build(block)
+ }
+
+ /**
+ * DSL entry point for building a JSON array response.
+ *
+ * @param block The DSL block used to populate the array contents.
+ */
+ fun ofArray(block: JsonArrayBuilder.() -> Unit) {
+ check(sb.isEmpty()) { "Can't reuse a non-clean response builder" }
+ JsonArrayBuilder().build(block)
+ }
+
+ /**
+ * A DSL builder for a JSON object.
+ */
+ inner class JsonObjectBuilder : JsonBuilder("{", "}") {
+
+ /**
+ * Add a scalar field to the JSON object being built, with appropriate escaping.
+ *
+ * NOTE: This deliberately removes support for adding JsonObject and JsonArray values directly, they should be provided via DSL builders.
+ *
+ * @param key The key for the filed in the object.
+ * @param value The value to associate with the [key].
+ * @throws IllegalArgumentException for any unsupported value types.
+ */
+ fun field(key: String, value: Any?) {
+ sb.writeKey(key).appendJsonValue(value)
+ checkWrite(sb)
+ }
+
+ /**
+ * Add a nested object to the JSON object being built.
+ *
+ * @param key The key for the nested object in the object.
+ * @param block The DSL block used to build the nested object.
+ */
+ fun obj(key: String, block: JsonObjectBuilder.() -> Unit) {
+ sb.writeKey(key)
+ JsonObjectBuilder().build(block)
+ }
+
+ /**
+ * Add a nested array to the JSON object being built.
+ *
+ * @param key The key for the nested array in the object.
+ * @param block The DSL block used to build the nested array.
+ */
+ fun array(key: String, block: JsonArrayBuilder.() -> Unit) {
+ sb.writeKey(key)
+ JsonArrayBuilder().build(block)
+ }
+
+ private fun StringBuilder.writeKey(key: String): StringBuilder =
+ maybeAppendSeparator()
+ .append(Json.encode(key))
+ .append(":")
+
+ }
+
+ /**
+ * A DSL builder for a JSON array.
+ */
+ inner class JsonArrayBuilder : JsonBuilder("[", "]") {
+
+ /**
+ * Add a scalar item to the JSON array being built, with appropriate escaping.
+ *
+ * NOTE: This deliberately removes support for adding JsonObject and JsonArray values directly, they should be provided via DSL builders.
+ *
+ * @param value The value to add to the array.
+ * @throws IllegalArgumentException for any unsupported value types.
+ */
+ fun item(value: Any?) {
+ sb.maybeAppendSeparator().appendJsonValue(value)
+ checkWrite(sb)
+ }
+
+ /**
+ * Add a nested object to the JSON array being built.
+ *
+ * @param block The DSL block used to build the nested object.
+ */
+ fun obj(block: JsonObjectBuilder.() -> Unit) {
+ sb.maybeAppendSeparator()
+ JsonObjectBuilder().build(block)
+ }
+
+ /**
+ * Add a nested array to the JSON array being built.
+ *
+ * @param block The DSL block used to build the nested array.
+ */
+ fun array(block: JsonArrayBuilder.() -> Unit) {
+ sb.maybeAppendSeparator()
+ JsonArrayBuilder().build(block)
+ }
+
+ }
+
+ /**
+ * A base class for the DSL builders.
+ *
+ * @param openToken The token used to open the element being built by this builder.
+ * @param openToken The token used to close the element being built by this builder.
+ */
+ abstract inner class JsonBuilder>(
+ private val openToken: String,
+ private val closeToken: String,
+ ) {
+
+ /**
+ * Write the current buffer to the underlying destination if it is appropriate (e.g. exceeds minimum size requirements), or if it is forced.
+ *
+ * @param force Flag to indicate that writing should occur without performing any other checks.
+ */
+ fun checkWrite(force: Boolean = false) = checkWrite(sb, force)
+
+ /**
+ * Build the element with the given DSL block.
+ *
+ * @param block The DSL block used to populate this element.
+ */
+ internal fun build(block: B.() -> Unit) {
+ sb.append(openToken)
+ needsSeparatorStack.addLast(false)
+
+ @Suppress("UNCHECKED_CAST")
+ (this as B).block()
+
+ sb.append(closeToken)
+ needsSeparatorStack.removeLast()
+
+ if (needsSeparatorStack.isEmpty())
+ onResponseCompleted(sb)
+ else
+ checkWrite(sb)
+ }
+
+ /**
+ * Append a separator if it is required before adding the next element.
+ *
+ * The first item added to any element will suppress the separator, with any subsequent elements inserting it.
+ */
+ protected fun StringBuilder.maybeAppendSeparator(): StringBuilder {
+ if (needsSeparatorStack.isNotEmpty()) {
+ if (needsSeparatorStack.removeLast())
+ append(",")
+ needsSeparatorStack.addLast(true)
+ }
+ return this
+ }
+
+ /**
+ * Append a JSON value to the element. This will provide required escaping.
+ *
+ * @param value The scalar value to add to this element.
+ * @throws IllegalArgumentException for any unsupported value types.
+ */
+ protected fun StringBuilder.appendJsonValue(value: Any?): StringBuilder =
+ when (value) {
+ null -> append("null")
+ is String -> append(Json.encode(value))
+ is Number, is Boolean -> append(value.toString())
+ is JsonObject -> append(value.encode())
+ is JsonArray -> append(value.encode())
+ else -> throw IllegalArgumentException("Unsupported JSON value type: ${value::class}")
+ }
+
+ }
+
+ /**
+ * Write the current buffer to the underlying destination if it is appropriate (e.g. exceeds minimum size requirements), or if it is forced.
+ *
+ * NOTE: The [sb] buffer won't be reset by the base class, so this should be done in this function if required for the implementation.
+ *
+ * @param sb The buffer to write if required.
+ * @param force Flag to indicate that writing should occur without performing any other checks.
+ */
+ protected abstract fun checkWrite(sb: StringBuilder, force: Boolean = false)
+
+ /**
+ * Notification the response has been completed.
+ *
+ * This should handle the reaming buffer, then reset it if the response can be reused.
+ *
+ * @param sb The remaining buffer when the response was completed.
+ */
+ protected abstract fun onResponseCompleted(sb: StringBuilder)
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/chunked/HttpChunkedJsonResponse.kt b/src/main/java/com/zepben/vertxutils/routing/chunked/HttpChunkedJsonResponse.kt
new file mode 100644
index 0000000..b4a69ed
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/chunked/HttpChunkedJsonResponse.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing.chunked
+
+import io.netty.handler.codec.http.HttpResponseStatus
+import io.vertx.core.http.HttpServerResponse
+
+class HttpChunkedJsonResponse(
+ private val response: HttpServerResponse,
+ private val bufferSize: Int = DEFAULT_BUFFER_SIZE,
+) : ChunkedJsonResponse(bufferSize) {
+
+ private var canSetStatus = true
+
+ override fun onResponseCompleted(sb: StringBuilder) {
+ canSetStatus = false
+ if (!response.closed()) {
+ response.end(sb.toString())
+ }
+ }
+
+ override fun checkWrite(sb: StringBuilder, force: Boolean) {
+ canSetStatus = false
+ if ((force || (sb.length >= bufferSize)) && !response.closed()) {
+ response.write(sb.toString())
+ sb.setLength(0)
+ }
+ }
+
+ var statusCode: HttpResponseStatus
+ get() = HttpResponseStatus.valueOf(response.statusCode)
+ set(status) {
+ // Once a response is committed (first write or end), it is too late to change the status.
+ check(canSetStatus) { "You can't set the status after the response has been committed" }
+ response.statusCode = status.code()
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.java b/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.java
deleted file mode 100644
index 449d12c..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing.handlers;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import com.zepben.vertxutils.routing.ErrorFormatter;
-import com.zepben.vertxutils.routing.Respond;
-import com.zepben.vertxutils.routing.RoutingContextEx;
-import com.zepben.vertxutils.routing.handlers.params.BadParamException;
-import com.zepben.vertxutils.routing.handlers.params.BodyRule;
-import com.zepben.vertxutils.routing.handlers.params.ValueConversionException;
-import io.netty.handler.codec.http.HttpResponseStatus;
-import io.vertx.core.Handler;
-import io.vertx.core.buffer.Buffer;
-import io.vertx.ext.web.RoutingContext;
-
-import javax.annotation.Nullable;
-
-@EverythingIsNonnullByDefault
-public class DecodeBodyHandler implements Handler {
-
- private final BodyRule> bodyRule;
-
- public DecodeBodyHandler(BodyRule> bodyRule) {
- this.bodyRule = bodyRule;
- }
-
- @Override
- public void handle(RoutingContext context) {
- try {
- Object decodedBody = handleBody(context);
- if (decodedBody != null)
- RoutingContextEx.putRequestBody(context, decodedBody);
-
- context.next();
- } catch (BadParamException ex) {
- Respond.withJson(context, HttpResponseStatus.BAD_REQUEST, ErrorFormatter.asJson(ex.getMessage()));
- }
- }
-
- @SuppressWarnings("ConstantConditions")
- @Nullable
- private Object handleBody(RoutingContext context) {
- Buffer rawBody = context.getBody();
- if (rawBody == null || rawBody.length() == 0) {
- if (bodyRule.isRequired())
- throw BadParamException.missingBody();
-
- return null;
- }
-
- try {
- Object body = bodyRule.converter().convert(rawBody);
- if (body == null)
- throw BadParamException.invalidBody(bodyRule, "value was converted into null value");
-
- return body;
- } catch (ValueConversionException ex) {
- throw BadParamException.invalidBody(bodyRule, ex.getMessage());
- }
- }
-
- public BodyRule> bodyRule() {
- return bodyRule;
- }
-}
diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.kt
new file mode 100644
index 0000000..192dfd0
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/handlers/DecodeBodyHandler.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing.handlers
+
+import com.zepben.vertxutils.routing.ErrorFormatter
+import com.zepben.vertxutils.routing.Respond
+import com.zepben.vertxutils.routing.RoutingContextEx
+import com.zepben.vertxutils.routing.handlers.params.BadParamException
+import com.zepben.vertxutils.routing.handlers.params.BodyRule
+import com.zepben.vertxutils.routing.handlers.params.ValueConversionException
+import io.netty.handler.codec.http.HttpResponseStatus
+import io.vertx.core.Handler
+import io.vertx.ext.web.RoutingContext
+
+class DecodeBodyHandler(
+ val bodyRule: BodyRule<*>,
+) : Handler {
+
+ override fun handle(context: RoutingContext) {
+ try {
+ handleBody(context)?.also {
+ RoutingContextEx.putRequestBody(context, it)
+ }
+
+ context.next()
+ } catch (ex: BadParamException) {
+ Respond.withJson(context, HttpResponseStatus.BAD_REQUEST, ErrorFormatter.asJson(ex.message))
+ }
+ }
+
+ private fun handleBody(context: RoutingContext): Any? {
+ val rawBody = context.body()
+ if (rawBody == null || rawBody.length() <= 0) {
+ if (bodyRule.isRequired)
+ throw BadParamException.missingBody()
+
+ return null
+ }
+
+ return try {
+ bodyRule.converter.convert(rawBody)
+ ?: throw BadParamException.invalidBody(bodyRule, "value was converted into null value")
+ } catch (ex: ValueConversionException) {
+ throw BadParamException.invalidBody(bodyRule, ex.message)
+ }
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.java b/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.java
deleted file mode 100644
index 875610a..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing.handlers;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import com.zepben.vertxutils.routing.Respond;
-import io.netty.handler.codec.http.HttpResponseStatus;
-import io.vertx.core.Handler;
-import io.vertx.core.buffer.Buffer;
-import io.vertx.core.file.FileSystemException;
-import io.vertx.ext.web.RoutingContext;
-
-import javax.annotation.Nullable;
-
-/**
- * Rework on the vertx {@link io.vertx.ext.web.handler.impl.FaviconHandlerImpl}:
- *
- *
- * - Allows you to register a favicon at any url path, not just /favicon.ico
- * - Removes the ability to load from the classpath
- *
- */
-@EverythingIsNonnullByDefault
-public class FaviconHandler implements Handler {
-
- private final String filePath;
- @Nullable private Buffer icon;
- private final long maxAgeSeconds;
-
- /**
- * Create a new Favicon instance using a file in the file system and customizable cache period
- *
- *
- * Router router = Router.router(vertx);
- * router.route().handler(FaviconHandler.create("/icons/icon.ico", 1000));
- *
- *
- * @param filePath file path to icon
- * @param maxAgeSeconds max age in http cache header
- */
- @SuppressWarnings("WeakerAccess")
- public FaviconHandler(String filePath, long maxAgeSeconds) {
- this.filePath = filePath;
- this.maxAgeSeconds = maxAgeSeconds;
- if (maxAgeSeconds < 0) {
- throw new IllegalArgumentException("maxAgeSeconds must be > 0");
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- public String faviconPath() {
- return filePath;
- }
-
- public void handle(RoutingContext ctx) {
- if (icon == null) {
- icon = loadIcon(ctx);
- }
-
- if (icon.length() > 0) {
- ctx.response().putHeader("Content-Type", "image/x-icon");
- ctx.response().putHeader("Content-Length", Integer.toString(icon.length()));
- ctx.response().putHeader("Cache-Control", "public, max-age=" + maxAgeSeconds);
- ctx.response().end(icon);
- } else {
- Respond.with(ctx, HttpResponseStatus.NOT_FOUND);
- }
- }
-
- private Buffer loadIcon(RoutingContext ctx) {
- try {
- return ctx.vertx().fileSystem().readFileBlocking(filePath);
- } catch (FileSystemException ex) {
- return Buffer.buffer();
- }
- }
-}
-
diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.kt b/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.kt
new file mode 100644
index 0000000..8c21ce2
--- /dev/null
+++ b/src/main/java/com/zepben/vertxutils/routing/handlers/FaviconHandler.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2026 Zeppelin Bend Pty Ltd
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+package com.zepben.vertxutils.routing.handlers
+
+import com.zepben.vertxutils.routing.Respond
+import io.netty.handler.codec.http.HttpResponseStatus
+import io.vertx.core.Handler
+import io.vertx.core.buffer.Buffer
+import io.vertx.core.file.FileSystemException
+import io.vertx.ext.web.RoutingContext
+
+/**
+ * Rework on the vertx [io.vertx.ext.web.handler.impl.FaviconHandlerImpl]:
+ *
+ * * Allows you to register a favicon at any url path, not just /favicon.ico
+ * * Removes the ability to load from the classpath
+ *
+ * @param filePath file path to icon
+ * @param maxAgeSeconds max age in http cache header
+ */
+class FaviconHandler(
+ private val filePath: String,
+ private val maxAgeSeconds: Long,
+) : Handler {
+
+ private var cachedIcon: Buffer? = null
+
+ init {
+ require(maxAgeSeconds >= 0) { "maxAgeSeconds must be > 0" }
+ }
+
+ fun faviconPath(): String {
+ return filePath
+ }
+
+ override fun handle(ctx: RoutingContext) {
+ val icon = cachedIcon ?: loadIcon(ctx).also { cachedIcon = it }
+
+ if (icon.length() > 0) {
+ ctx.response().putHeader("Content-Type", "image/x-icon")
+ ctx.response().putHeader("Content-Length", icon.length().toString())
+ ctx.response().putHeader("Cache-Control", "public, max-age=$maxAgeSeconds")
+ ctx.response().end(icon)
+ } else {
+ Respond.with(ctx, HttpResponseStatus.NOT_FOUND)
+ }
+ }
+
+ private fun loadIcon(ctx: RoutingContext): Buffer =
+ try {
+ ctx.vertx().fileSystem().readFileBlocking(filePath)
+ } catch (_: FileSystemException) {
+ Buffer.buffer()
+ }
+
+}
diff --git a/src/main/java/com/zepben/vertxutils/routing/handlers/PathParamsHandler.java b/src/main/java/com/zepben/vertxutils/routing/handlers/PathParamsHandler.java
deleted file mode 100644
index 5543d69..0000000
--- a/src/main/java/com/zepben/vertxutils/routing/handlers/PathParamsHandler.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2020 Zeppelin Bend Pty Ltd
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package com.zepben.vertxutils.routing.handlers;
-
-import com.zepben.annotations.EverythingIsNonnullByDefault;
-import com.zepben.vertxutils.routing.ErrorFormatter;
-import com.zepben.vertxutils.routing.Respond;
-import com.zepben.vertxutils.routing.RoutingContextEx;
-import com.zepben.vertxutils.routing.handlers.params.*;
-import io.netty.handler.codec.http.HttpResponseStatus;
-import io.vertx.core.Handler;
-import io.vertx.ext.web.RoutingContext;
-
-import java.util.*;
-
-import static java.util.stream.Collectors.toMap;
-
-@EverythingIsNonnullByDefault
-public class PathParamsHandler implements Handler {
-
-
- private final Map> rules;
-
- @SuppressWarnings("WeakerAccess")
- public PathParamsHandler(PathParamRule>... rules) {
- this(Arrays.asList(rules));
- }
-
- @SuppressWarnings("WeakerAccess")
- public PathParamsHandler(Collection